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', [])
+ })
+ })
+})