From 2c0b20662106833699b895350cabb787c8e3f987 Mon Sep 17 00:00:00 2001 From: iamacook Date: Tue, 10 Oct 2023 13:09:49 +0200 Subject: [PATCH] fix: extend Safe wallet provider tests --- .../safe-wallet-provider/index.test.ts | 252 +++++++++++++---- .../useSafeWalletProvider.test.ts | 80 ------ .../useSafeWalletProvider.test.tsx | 265 ++++++++++++++++++ 3 files changed, 462 insertions(+), 135 deletions(-) delete mode 100644 src/services/safe-wallet-provider/useSafeWalletProvider.test.ts create mode 100644 src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx diff --git a/src/services/safe-wallet-provider/index.test.ts b/src/services/safe-wallet-provider/index.test.ts index a4c54a2f4a..91bd39b066 100644 --- a/src/services/safe-wallet-provider/index.test.ts +++ b/src/services/safe-wallet-provider/index.test.ts @@ -72,42 +72,24 @@ describe('SafeWalletProvider', () => { }) }) }) - - describe('eth_chainId', () => { - it('should return the chain id when the method is eth_chainId', async () => { - const sdk = {} - const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) - - const result = await safeWalletProvider.request(1, { method: 'eth_chainId' } as any, {} as any) - - expect(result).toEqual({ - id: 1, - jsonrpc: '2.0', - result: '0x1', + ;['net_version', 'eth_chainId'].forEach((method) => { + describe(method, () => { + it(`should return the chain id when the method is ${method}`, async () => { + const sdk = {} + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request(1, { method } as any, {} as any) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x1', + }) }) }) }) - describe('eth_sign', () => { - it('should return the signature when the method is eth_sign', async () => { - const sdk = { - signMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), - } - const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) - - const result = await safeWalletProvider.request( - 1, - { method: 'eth_sign', params: ['0x123', '0x123'] } as any, - {} as any, - ) - - expect(result).toEqual({ - id: 1, - jsonrpc: '2.0', - result: '0x123', - }) - }) - + describe('personal_sign', () => { it('should throw an error when the address is invalid', async () => { const sdk = { signMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), @@ -165,31 +147,16 @@ describe('SafeWalletProvider', () => { }) }) - describe('eth_signTypedData', () => { - it('should return the signature when the method is eth_signTypedData', async () => { + describe('eth_sign', () => { + it('should return the signature when the method is eth_sign', async () => { const sdk = { - signTypedMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), + signMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), } const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) const result = await safeWalletProvider.request( 1, - { - method: 'eth_signTypedData', - params: [ - '0x123', - { - domain: { - chainId: 1, - name: 'test', - version: '1', - }, - message: { - test: 'test', - }, - }, - ], - } as any, + { method: 'eth_sign', params: ['0x123', '0x123'] } as any, {} as any, ) @@ -202,25 +169,153 @@ describe('SafeWalletProvider', () => { it('should throw an error when the address is invalid', async () => { const sdk = { - signTypedMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), + signMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) await expect( - safeWalletProvider.request(1, { method: 'eth_signTypedData', params: ['0x456', {}] } as any, {} as any), + safeWalletProvider.request(1, { method: 'eth_sign', params: ['0x456', '0x456'] } as any, {} as any), ).resolves.toEqual({ id: 1, jsonrpc: '2.0', error: { code: -32000, - message: 'The address is invalid', + message: 'The address or message hash is invalid', }, }) }) + + it('should throw an error when the message hash is invalid', async () => { + const sdk = { + signMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), + } + + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + await expect( + safeWalletProvider.request(1, { method: 'eth_sign', params: ['0x123', 'messageHash'] } as any, {} as any), + ).resolves.toEqual({ + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'The address or message hash is invalid', + }, + }) + }) + + it('should return an empty string when the signature is undefined', async () => { + const sdk = { + signMessage: jest.fn().mockResolvedValue({}), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request( + 1, + { method: 'personal_sign', params: ['0x123', '0x123'] } as any, + {} as any, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x', + }) + }) + }) + ;['eth_signTypedData', 'eth_signTypedData_v4'].forEach((method) => { + describe(method, () => { + it(`should return the signature when the method is ${method}`, async () => { + const sdk = { + signTypedMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request( + 1, + { + method, + params: [ + '0x123', + { + domain: { + chainId: 1, + name: 'test', + version: '1', + }, + message: { + test: 'test', + }, + }, + ], + } as any, + {} as any, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x123', + }) + }) + + it('should throw an error when the address is invalid', async () => { + const sdk = { + signTypedMessage: jest.fn().mockResolvedValue({ signature: '0x123' }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + await expect( + safeWalletProvider.request(1, { method, params: ['0x456', {}] } as any, {} as any), + ).resolves.toEqual({ + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'The address is invalid', + }, + }) + }) + + it('should return an empty string when the signature is undefined', async () => { + const sdk = { + signTypedMessage: jest.fn().mockResolvedValue({}), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request( + 1, + { + method, + params: [ + '0x123', + { + domain: { + chainId: 1, + name: 'test', + version: '1', + }, + message: { + test: 'test', + }, + }, + ], + } as any, + {} as any, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x', + }) + }) + }) }) describe('eth_sendTransaction', () => { - it('should return the transaction hash when the method is eth_sendTransaction', async () => { + it('should return the transaction safeTxHash when the method is eth_sendTransaction', async () => { const sdk = { send: jest.fn().mockResolvedValue({ safeTxHash: '0x456' }), } @@ -265,6 +360,40 @@ describe('SafeWalletProvider', () => { }, }) }) + + it('should format the gas when it is passed as a hex-encoded string', async () => { + const sdk = { + send: jest.fn().mockResolvedValue({ safeTxHash: '0x456' }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request( + 1, + { + method: 'eth_sendTransaction', + params: [ + { + from: '0x123', + to: '0x123', + value: '0x123', + gas: 0x3e8, // 1000 + }, + ], + } as any, + appInfo, + ) + + expect(sdk.send).toHaveBeenCalledWith( + { txs: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000, data: '0x' }], params: { safeTxGas: 1000 } }, + appInfo, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x456', + }) + }) }) describe('eth_getTransactionByHash', () => { @@ -350,4 +479,17 @@ describe('SafeWalletProvider', () => { }) }) }) + + describe('proxy', () => { + it('should default to using the proxy if the method is not supported by the provider', async () => { + const sdk = { + proxy: jest.fn(), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + await safeWalletProvider.request(1, { method: 'web3_clientVersion', params: [''] } as any, appInfo) + + expect(sdk.proxy).toHaveBeenCalledWith('web3_clientVersion', ['']) + }) + }) }) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.ts b/src/services/safe-wallet-provider/useSafeWalletProvider.test.ts deleted file mode 100644 index 62b91e3bfa..0000000000 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { renderHook, act } from '@/tests/test-utils' -import useSafeWalletProvider, { _useTxFlowApi } from './useSafeWalletProvider' -import * as web3 from '@/hooks/wallets/web3' -import * as gateway from '@safe-global/safe-gateway-typescript-sdk' - -describe('useSafeWalletProvider', () => { - it('should return a provider', async () => { - const { result } = renderHook(() => useSafeWalletProvider()) - await act(() => Promise.resolve()) - expect(result.current).toBeDefined() - }) - - describe.only('_useTxFlowApi', () => { - it('should return a provider', async () => { - const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) - await act(() => Promise.resolve()) - expect(result.current?.getBySafeTxHash).toBeDefined() - expect(result.current?.proxy).toBeDefined() - expect(result.current?.send).toBeDefined() - expect(result.current?.signMessage).toBeDefined() - expect(result.current?.signTypedMessage).toBeDefined() - expect(result.current?.switchChain).toBeDefined() - }) - - it('should proxy RPC calls', async () => { - const mockSend = jest.fn(() => Promise.resolve({ result: '0x' })) - - jest.spyOn(web3 as any, 'useWeb3ReadOnly').mockImplementation(() => ({ - send: mockSend, - })) - - const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) - await act(() => Promise.resolve()) - - result.current?.proxy('eth_chainId', []) - expect(mockSend).toHaveBeenCalledWith('eth_chainId', []) - }) - - it('should should send transactions', async () => { - const mockSend = jest.fn(() => Promise.resolve({ result: '0x' })) - - jest.spyOn(web3 as any, 'useWeb3ReadOnly').mockImplementation(() => ({ - send: mockSend, - })) - - const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) - - const resp = result.current?.send( - { - txs: [ - { - to: '0x1234567890000000000000000000000000000000', - value: '0', - data: '0x', - }, - ], - params: { safeTxGas: 0 }, - }, - { name: 'test', description: '', url: '', iconUrl: 'test.svg' }, - ) - - expect(resp).toBeInstanceOf(Promise) - }) - - it('should get tx by safe tx hash', async () => { - jest.spyOn(gateway as any, 'getTransactionDetails').mockImplementation(() => ({ - hash: '0x123', - })) - - const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) - - await act(() => Promise.resolve()) - - const resp = await result.current?.getBySafeTxHash('0x123456789000') - - expect(gateway.getTransactionDetails).toHaveBeenCalledWith('1', '0x123456789000') - expect(resp).toEqual({ hash: '0x123' }) - }) - }) -}) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx new file mode 100644 index 0000000000..e5a84e0c40 --- /dev/null +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx @@ -0,0 +1,265 @@ +import * as gateway from '@safe-global/safe-gateway-typescript-sdk' +import * as router from 'next/router' + +import * as web3 from '@/hooks/wallets/web3' +import { renderHook } from '@/tests/test-utils' +import { TxModalContext } from '@/components/tx-flow' +import useSafeWalletProvider, { _useTxFlowApi } from './useSafeWalletProvider' +import { SafeWalletProvider } from '.' +import { StoreHydrator } from '@/store' + +const appInfo = { + name: 'test', + description: 'test', + iconUrl: 'test', + url: 'test', +} + +describe('useSafeWalletProvider', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('useSafeWalletProvider', () => { + it('should return a provider', () => { + const { result } = renderHook(() => useSafeWalletProvider(), { + initialReduxState: { + safeInfo: { + loading: false, + error: undefined, + data: { + chainId: '1', + address: { + value: '0x1234567890000000000000000000000000000000', + }, + } as gateway.SafeInfo, + }, + }, + }) + + expect(result.current instanceof SafeWalletProvider).toBe(true) + }) + }) + + describe('_useTxFlowApi', () => { + it('should return a provider', () => { + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) + + expect(result.current?.signMessage).toBeDefined() + expect(result.current?.signTypedMessage).toBeDefined() + expect(result.current?.send).toBeDefined() + expect(result.current?.getBySafeTxHash).toBeDefined() + expect(result.current?.switchChain).toBeDefined() + expect(result.current?.proxy).toBeDefined() + }) + + it('should open signing window for messages', () => { + jest.spyOn(router, 'useRouter').mockReturnValue({} as unknown as router.NextRouter) + + const mockSetTxFlow = jest.fn() + + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), { + // TODO: Improve render/renderHook to allow custom wrappers within the "defaults" + wrapper: ({ children }) => ( + + {children} + + ), + }) + + const resp = result?.current?.signMessage('message', appInfo) + + expect(mockSetTxFlow.mock.calls[0][0].props).toStrictEqual({ + logoUri: appInfo.iconUrl, + name: appInfo.name, + message: 'message', + requestId: expect.any(String), + }) + + expect(resp).toBeInstanceOf(Promise) + }) + + it('should open signing window for typed messages', () => { + jest.spyOn(router, 'useRouter').mockReturnValue({} as unknown as router.NextRouter) + + const mockSetTxFlow = jest.fn() + + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), { + // TODO: Improve render/renderHook to allow custom wrappers within the "defaults" + wrapper: ({ children }) => ( + + {children} + + ), + }) + + const typedMessage = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'account', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'EIP-1271 Example', + version: '1.0', + chainId: 5, + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + message: { + from: { + name: 'Alice', + account: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, + to: { + name: 'Bob', + account: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }, + contents: 'Hello EIP-1271!', + }, + } + + const resp = result?.current?.signTypedMessage(typedMessage, appInfo) + + expect(mockSetTxFlow.mock.calls[0][0].props).toStrictEqual({ + logoUri: appInfo.iconUrl, + name: appInfo.name, + message: typedMessage, + requestId: expect.any(String), + }) + + expect(resp).toBeInstanceOf(Promise) + }) + + it('should should send (batched) transactions', () => { + jest.spyOn(router, 'useRouter').mockReturnValue({} as unknown as router.NextRouter) + + const mockSetTxFlow = jest.fn() + + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), { + // TODO: Improve render/renderHook to allow custom wrappers within the "defaults" + wrapper: ({ children }) => ( + + {children} + + ), + }) + + const resp = result.current?.send( + { + txs: [ + { + to: '0x1234567890000000000000000000000000000000', + value: '0', + data: '0x', + }, + // Batch + { + to: '0x1234567890000000000000000000000000000000', + value: '0', + data: '0x', + }, + ], + params: { safeTxGas: 0 }, + }, + appInfo, + ) + + expect(mockSetTxFlow.mock.calls[0][0].props).toStrictEqual({ + data: { + appId: undefined, + app: { + name: appInfo.name, + url: appInfo.url, + iconUrl: appInfo.iconUrl, + }, + requestId: expect.any(String), + txs: [ + { + to: '0x1234567890000000000000000000000000000000', + value: '0', + data: '0x', + }, + // Batch + { + to: '0x1234567890000000000000000000000000000000', + value: '0', + data: '0x', + }, + ], + params: { safeTxGas: 0 }, + }, + }) + + expect(resp).toBeInstanceOf(Promise) + }) + + it('should get tx by safe tx hash', async () => { + jest.spyOn(gateway as any, 'getTransactionDetails').mockImplementation(() => ({ + hash: '0x123', + })) + + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) + + const resp = await result.current?.getBySafeTxHash('0x123456789000') + + expect(gateway.getTransactionDetails).toHaveBeenCalledWith('1', '0x123456789000') + expect(resp).toEqual({ hash: '0x123' }) + }) + + it('should switch chain', () => { + const mockPush = jest.fn() + jest.spyOn(router, 'useRouter').mockReturnValue({ + push: mockPush, + } as unknown as router.NextRouter) + + // @ts-expect-error - auto accept prompt + jest.spyOn(window, 'prompt').mockReturnValue(true) + + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), { + initialReduxState: { + chains: { + loading: false, + error: undefined, + data: [{ chainId: '1', shortName: 'eth' } as gateway.ChainInfo], + }, + }, + }) + + result.current?.switchChain('0x5', appInfo) + + expect(mockPush).toHaveBeenCalledWith({ + pathname: '/', + query: { + chain: 'eth', + }, + }) + }) + + it('should proxy RPC calls', async () => { + const mockSend = jest.fn(() => Promise.resolve({ result: '0x' })) + + jest.spyOn(web3 as any, 'useWeb3ReadOnly').mockImplementation(() => ({ + send: mockSend, + })) + + const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000')) + + result.current?.proxy('eth_chainId', []) + + expect(mockSend).toHaveBeenCalledWith('eth_chainId', []) + }) + }) +})