UNPKG

safe-apps-sdk

Version:

SDK developed to integrate third-party apps with Safe app.

451 lines (397 loc) 18 kB
import SDK from '../sdk'; import { SafeInfo } from '../types'; import { Methods } from '../communication/methods'; import { PostMessageOptions } from '../types'; import { PermissionsError } from '../types/permissions'; import { Wallet } from '../wallet'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe('Safe Apps SDK safe methods', () => { const sdkInstance = new SDK(); let postMessageSpy: jest.SpyInstance<void, [message: any, options?: PostMessageOptions]>; beforeEach(() => { postMessageSpy = jest.spyOn(window.parent, 'postMessage'); }); afterEach(() => { jest.clearAllMocks(); }); describe('SDK.safe.getInfo', () => { test('Should send a valid message to the interface', () => { sdkInstance.safe.getInfo(); expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ method: Methods.getSafeInfo, params: undefined }), '*', ); }); }); describe('SDK.safe.calculateMessageHash', () => { test('Should generate correct EIP-191 message hash', () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); // to test message/hash I signed a test message on rinkeby // https://dashboard.tenderly.co/tx/rinkeby/0x9308fb61d9f4282080334e3f35b357fc689e06808b8ad2817536813948e3720d const message = 'approve rugpull'; const expectedHash = '0xe32c44147e358bc973757518210c3baec92de66115c513ea1146d61ad4fd93af'; const hash = sdkInstance.safe.calculateMessageHash(message); expect(hash).toEqual(expectedHash); }); }); describe('SDK.safe.check1271Signature', () => { test('Should send a valid message to the interface', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull') // @ts-expect-error method is private but we are testing it sdkInstance.safe.check1271Signature(message); await sleep(200); expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ method: Methods.rpcCall, params: { call: 'eth_call', params: [ { to: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', data: '0x1626ba7e617070726f76652072756770756c6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000', }, 'latest', ], }, }), '*', ); }); test('Should return true if the message is signed and magic value is returned', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); rpcCallSpy.mockImplementationOnce(() => Promise.resolve({ id: '1', success: true, data: '0x1626ba7e00000000000000000000000000000000000000000000000000000000', }), ); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull') // @ts-expect-error method is private but we are testing it expect(await sdkInstance.safe.check1271Signature(message)).toEqual(true); }); test('Should return false if the message isnt signed and underlying call reverts', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); rpcCallSpy.mockImplementationOnce(() => Promise.reject(new Error('Hash not approved'))); const message = '0x68616c6c6f000000000000000000000000000000000000000000000000000000'; // ethers.utils.formatBytes32String('hallo') // @ts-expect-error method is private but we are testing it expect(await sdkInstance.safe.check1271Signature(message)).toEqual(false); }); }); describe('SDK.safe.check1271SignatureBytes', () => { test('Should send a valid message to the interface', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull') // @ts-expect-error method is private but we are testing it sdkInstance.safe.check1271SignatureBytes(message); await sleep(200); expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ method: Methods.rpcCall, params: { call: 'eth_call', params: [ { to: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', data: '0x20c13b0b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020617070726f76652072756770756c6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', }, 'latest', ], }, }), '*', ); }); test('Should return true if the message is signed and magic value is returned', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); rpcCallSpy.mockImplementationOnce(() => Promise.resolve({ id: '1', success: true, data: '0x20c13b0b00000000000000000000000000000000000000000000000000000000', }), ); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull') // @ts-expect-error method is private but we are testing it expect(await sdkInstance.safe.check1271SignatureBytes(message)).toEqual(true); }); test('Should return false if the message isnt signed and underlying call reverts', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); rpcCallSpy.mockImplementationOnce(() => Promise.reject(new Error('Hash not approved'))); const message = '0x68616c6c6f000000000000000000000000000000000000000000000000000000'; // ethers.utils.formatBytes32String('hallo') // @ts-expect-error method is private but we are testing it expect(await sdkInstance.safe.check1271SignatureBytes(message)).toEqual(false); }); }); describe('SDK.safe.isMessageSigned', () => { test('Should call SDK.safe.isMessageHashSigned with a hash of the message', () => { const isMessageHashSignedSpy = jest.spyOn(sdkInstance.safe, 'isMessageHashSigned'); // ethers.utils.formatBytes32String('approve rugpull') const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; const expectedHash = '0xaae9257b8ff1c926ac3cdf36923661de4e81bf934e38958beeede3519aa18b08'; sdkInstance.safe.isMessageSigned(message); expect(isMessageHashSignedSpy).toHaveBeenCalledWith(expectedHash, '0x'); }); }); describe('SDK.safe.isMessageHashSigned', () => { test('Should send call messages to check the message the interface', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); safeInfoSpy.mockImplementation( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull') sdkInstance.safe.isMessageHashSigned(message); await sleep(200); // calling first check1271Signature method expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ method: Methods.rpcCall, params: { call: 'eth_call', params: [ { to: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', data: '0x1626ba7e617070726f76652072756770756c6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000', }, 'latest', ], }, }), '*', ); }); test('Should return true if check1271Signature return true', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const check1271SignatureSpy = jest.spyOn(sdkInstance.safe, 'check1271Signature'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); // @ts-expect-error ts fails to infer the return type because of a private method check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(true)); // ethers.utils.formatBytes32String('approve rugpull') const message = sdkInstance.safe.calculateMessageHash( '0x617070726f76652072756770756c6c0000000000000000000000000000000000', ); const signed = await sdkInstance.safe.isMessageHashSigned(message); expect(signed).toEqual(true); }); test('Should return true if check1271SignatureBytes return true', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const check1271SignatureSpy = jest.spyOn(sdkInstance.safe, 'check1271Signature'); // @ts-expect-error method is private but we are testing it const check1271SignatureBytesSpy = jest.spyOn(sdkInstance.safe, 'check1271SignatureBytes'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); // @ts-expect-error ts fails to infer the return type because of a private method check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(false)); // @ts-expect-error ts fails to infer the return type because of a private method check1271SignatureBytesSpy.mockImplementationOnce(() => Promise.resolve(true)); // ethers.utils.formatBytes32String('approve rugpull') const message = sdkInstance.safe.calculateMessageHash( '0x617070726f76652072756770756c6c0000000000000000000000000000000000', ); const signed = await sdkInstance.safe.isMessageHashSigned(message); expect(signed).toEqual(true); }); test('Should return false if all of the underlying checks return false', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); // @ts-expect-error method is private but we are testing it const check1271SignatureSpy = jest.spyOn(sdkInstance.safe, 'check1271Signature'); // @ts-expect-error method is private but we are testing it const check1271SignatureBytesSpy = jest.spyOn(sdkInstance.safe, 'check1271SignatureBytes'); safeInfoSpy.mockImplementationOnce( (): Promise<SafeInfo> => Promise.resolve({ chainId: 4, safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', owners: [], threshold: 1, isReadOnly: false, }), ); // @ts-expect-error ts fails to infer the return type because of a private method check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(false)); // @ts-expect-error ts fails to infer the return type because of a private method check1271SignatureBytesSpy.mockImplementationOnce(() => Promise.resolve(false)); // ethers.utils.formatBytes32String('approve rugpull') const message = sdkInstance.safe.calculateMessageHash( '0x617070726f76652072756770756c6c0000000000000000000000000000000000', ); const signed = await sdkInstance.safe.isMessageHashSigned(message); expect(signed).toEqual(false); }); }); describe('SDK.safe.getBalances', () => { test('Should send a valid message to the interface', () => { sdkInstance.safe.experimental_getBalances({ currency: 'eur' }); expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ method: Methods.getSafeBalances, params: { currency: 'eur' } }), '*', ); }); }); describe('SDK.safe.getChainInfo', () => { test('Should send a valid message to the interface', () => { sdkInstance.safe.getChainInfo(); expect(postMessageSpy).toHaveBeenCalledWith(expect.objectContaining({ method: Methods.getChainInfo }), '*'); }); }); describe('SDK.safe.getOffChainSignature', () => { test('Should request the off-chain signature from the interface', () => { sdkInstance.safe.getOffChainSignature('0x123'); expect(postMessageSpy).toHaveBeenCalledWith( expect.objectContaining({ method: Methods.getOffChainSignature, params: '0x123' }), '*', ); }); }); describe('SDK.safe.requestAddressBook', () => { const wrongPermissions = [ { date: 1657713994059, invoker: 'https://test.eth', parentCapability: 'wrongPermission', }, ]; const correctPermissions = [ { date: 1657713994059, invoker: 'https://test.eth', parentCapability: 'requestAddressBook', }, ]; test('Should call requestAddressBook when the permissions are ok', async () => { jest .spyOn(Wallet.prototype, 'getPermissions') .mockImplementationOnce(jest.fn().mockResolvedValue(correctPermissions)); sdkInstance.safe.requestAddressBook(); await sleep(200); expect(postMessageSpy).toHaveBeenCalledWith(expect.objectContaining({ method: Methods.requestAddressBook }), '*'); }); test('Should call requestPermissions when the current permissions are not ok', async () => { jest .spyOn(Wallet.prototype, 'getPermissions') .mockImplementationOnce(jest.fn().mockResolvedValue(wrongPermissions)); const requestPermissionsSpy = jest.spyOn(Wallet.prototype, 'requestPermissions'); sdkInstance.safe.requestAddressBook(); await sleep(200); expect(requestPermissionsSpy).toHaveBeenCalledWith([{ requestAddressBook: {} }]); }); test('Should throw PermissionError when the permissions are not ok', async () => { jest .spyOn(Wallet.prototype, 'getPermissions') .mockImplementationOnce(jest.fn().mockResolvedValue(wrongPermissions)); jest .spyOn(Wallet.prototype, 'requestPermissions') .mockImplementationOnce(jest.fn().mockResolvedValue(wrongPermissions)); try { await sdkInstance.safe.requestAddressBook(); } catch (e) { expect(e).toBeInstanceOf(PermissionsError); } }); }); });