diff --git a/src/services/safe-wallet-provider/index.test.ts b/src/services/safe-wallet-provider/index.test.ts new file mode 100644 index 0000000000..a4c54a2f4a --- /dev/null +++ b/src/services/safe-wallet-provider/index.test.ts @@ -0,0 +1,353 @@ +// Unit tests for the SafeWalletProvider class +import { SafeWalletProvider } from '.' + +const safe = { + safeAddress: '0x123', + chainId: 1, +} + +const appInfo = { + name: 'test', + description: 'test', + iconUrl: 'test', + url: 'test', +} + +describe('SafeWalletProvider', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + describe('wallet_switchEthereumChain', () => { + it('should call the switchChain method when the method is wallet_switchEthereumChain', async () => { + const switchChain = jest.fn() + const sdk = { + switchChain, + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + await safeWalletProvider.request( + 1, + { method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }] } as any, + {} as any, + ) + + expect(switchChain).toHaveBeenCalledWith('0x1', {}) + }) + + it('should throw an error when the chain is not supported', async () => { + const sdk = { + switchChain: jest.fn().mockRejectedValue(new Error('Unsupported chain')), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + await expect( + safeWalletProvider.request( + 1, + { method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }] } as any, + {} as any, + ), + ).resolves.toEqual({ + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Unsupported chain', + }, + }) + }) + }) + + describe('eth_accounts', () => { + it('should return the safe address when the method is eth_accounts', async () => { + const sdk = {} + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request(1, { method: 'eth_accounts' } as any, {} as any) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: ['0x123'], + }) + }) + }) + + 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', + }) + }) + }) + + 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', + }) + }) + + it('should throw an error when the address 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: 'personal_sign', params: ['message', '0x456'] } as any, {} as any), + ).resolves.toEqual({ + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + 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: 'personal_sign', params: ['message', '123'] } 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: ['message', '0x123'] } as any, + {} as any, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: '0x', + }) + }) + }) + + describe('eth_signTypedData', () => { + it('should return the signature when the method is eth_signTypedData', async () => { + const sdk = { + signTypedMessage: 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, + {} 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: 'eth_signTypedData', params: ['0x456', {}] } as any, {} as any), + ).resolves.toEqual({ + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'The address is invalid', + }, + }) + }) + }) + + describe('eth_sendTransaction', () => { + it('should return the transaction hash when the method is eth_sendTransaction', 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: 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', + }) + }) + + it('should throw an error when the transaction is not signed by the safe', async () => { + const sdk = { + send: jest.fn().mockRejectedValue(new Error('User rejected the transaction')), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + await expect( + safeWalletProvider.request( + 1, + { method: 'eth_sendTransaction', params: [{ from: '0x123', to: '0x123', value: '0x123' }] } as any, + appInfo, + ), + ).resolves.toEqual({ + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'User rejected the transaction', + }, + }) + }) + }) + + describe('eth_getTransactionByHash', () => { + it('should return the transaction when the method is eth_getTransactionByHash', async () => { + const sdk = { + getBySafeTxHash: jest.fn().mockResolvedValue({ txHash: '0x777' }), + proxy: jest.fn().mockResolvedValue({ hash: '0x999' }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request( + 1, + { method: 'eth_getTransactionByHash', params: ['0x123'] } as any, + appInfo, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: { hash: '0x999' }, + }) + }) + + it('should send a transaction and return the transaction when it is in the submitted transactions', async () => { + const sdk = { + send: jest.fn().mockResolvedValue({ safeTxHash: '0x777' }), + getBySafeTxHash: jest.fn().mockResolvedValue({ txHash: '0x777' }), + proxy: jest.fn().mockResolvedValue({ hash: '0x999' }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + // Send the transaction + await safeWalletProvider.request( + 1, + { method: 'eth_sendTransaction', params: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000 }] } as any, + appInfo, + ) + + const result = await safeWalletProvider.request( + 1, + { method: 'eth_getTransactionByHash', params: ['0x777'] } as any, + appInfo, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: { + blockHash: null, + blockNumber: null, + from: '0x123', + gas: 0, + gasPrice: '0x00', + hash: '0x777', + input: '0x', + nonce: 0, + to: '0x123', + transactionIndex: null, + value: '0x123', + }, + }) + }) + }) + + describe('eth_getTransactionReceipt', () => { + it('should return the transaction receipt when the method is eth_getTransactionReceipt', async () => { + const sdk = { + getBySafeTxHash: jest.fn().mockResolvedValue({ txHash: '0x777' }), + proxy: jest.fn().mockResolvedValue({ hash: '0x999' }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const result = await safeWalletProvider.request( + 1, + { method: 'eth_getTransactionReceipt', params: ['0x123'] } as any, + appInfo, + ) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: { hash: '0x999' }, + }) + }) + }) +}) diff --git a/src/services/safe-wallet-provider/index.ts b/src/services/safe-wallet-provider/index.ts index 0c7ff2e323..9bf098533c 100644 --- a/src/services/safe-wallet-provider/index.ts +++ b/src/services/safe-wallet-provider/index.ts @@ -124,7 +124,7 @@ export class SafeWalletProvider { tx.gas = parseInt(tx.gas, 16) } - const resp = await this.sdk.send( + const { safeTxHash } = await this.sdk.send( { txs: [tx], params: { safeTxGas: Number(tx.gas) }, @@ -133,9 +133,9 @@ export class SafeWalletProvider { ) // Store fake transaction - this.submittedTxs.set(resp.safeTxHash, { + this.submittedTxs.set(safeTxHash, { from: this.safe.safeAddress, - hash: resp.safeTxHash, + hash: safeTxHash, gas: 0, gasPrice: '0x00', nonce: 0, @@ -147,7 +147,7 @@ export class SafeWalletProvider { transactionIndex: null, }) - return resp.safeTxHash + return safeTxHash case 'eth_getTransactionByHash': let txHash = params[0] as string @@ -155,6 +155,7 @@ export class SafeWalletProvider { const resp = await this.sdk.getBySafeTxHash(txHash) txHash = resp.txHash || txHash } catch (e) {} + // Use fake transaction if we don't have a real tx hash if (this.submittedTxs.has(txHash)) { return this.submittedTxs.get(txHash)