From c7b8899e20f8c07a22c66cc11bfdef43b51cb733 Mon Sep 17 00:00:00 2001 From: Pierre Troger Date: Fri, 19 Jan 2024 03:05:14 -0400 Subject: [PATCH] end to end working tests for transactions inputs --- .../src/lib/__test__/unit/decoders.spec.ts | 181 +++++-- .../src/lib/__test__/unit/mocks.ts | 190 ++++---- .../__test__/unit/param-extractors.spec.ts | 18 - .../src/lib/decoders.ts | 461 ++++++++++++++---- .../src/lib/domain.ts | 2 +- .../src/lib/intent.types.ts | 18 +- .../src/lib/methodId.ts | 16 +- .../src/lib/param-extractors.ts | 332 +++++++++++-- .../src/lib/types.ts | 63 ++- 9 files changed, 974 insertions(+), 307 deletions(-) delete mode 100644 packages/transaction-request-intent/src/lib/__test__/unit/param-extractors.spec.ts diff --git a/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts b/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts index 39df9ab1b..ac6099c38 100644 --- a/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts +++ b/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts @@ -1,57 +1,142 @@ -// type Hex = `0x${string}` +import { InputType, Intents } from '../../domain' +import { decode } from '../../export' +import { + mockErc1155BatchSafeTransferFrom, + mockErc1155SafeTransferFrom, + mockErc20Transfer, + mockErc721SafeTransferFrom +} from './mocks' describe('decode', () => { describe('transaction request input', () => { describe('transfers', () => { - it('pass', () => { - expect(true).toBeTruthy() + it('decodes erc20 transfer', () => { + const decoded = decode(mockErc20Transfer.input) + expect(decoded).toEqual(mockErc20Transfer.intent) }) - // it('should decode erc20 transfer', () => { - // const input: TransactionInput = { - // type: InputType.TRANSACTION_REQUEST, - // txRequest: { - // to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', - // data: `${Erc20Methods.TRANSFER}000000000000000000000000fe8f4de6e39c523ced231e7a72628f58e0ffee71000000000000000000000000000000000000000000000000000000000007a120` as Hex, - // from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', - // chainId: '137', - // nonce: 10 - // } - // } - // const decoded = decode(input) - // const expected: TransferErc20 = { - // type: Intents.TRANSFER_ERC20, - // contract: 'eip155:137:0x031d8C0cA142921c459bCB28104c0FF37928F9eD' as Caip10, - // to: 'eip155:137:0xfe8f4de6e39c523ced231e7a72628f58e0ffee71' as Caip10, - // from: 'eip155:137:0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448' as Caip10, - // amount: '500000000000000000000' - // } - // expect(decoded).toEqual(expected) - // }) - // it('should decode erc20 transferFrom with contract registry', () => { - // const contractRegistry = { - // 'eip155:137:0x031d8C0cA142921c459bCB28104c0FF37928F9eD': AssetTypeEnum.ERC20 - // } as ContractRegistry - // const input: TransactionInput = { - // type: InputType.TRANSACTION_REQUEST, - // txRequest: { - // to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', - // data: `${Erc20Methods.TRANSFER_FROM}000000000000000000000000fe8f4de6e39c523ced231e7a72628f58e0ffee71000000000000000000000000000000000000000000000000000000000007a120` as Hex, - // from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', - // chainId: '137', - // nonce: 10 - // }, - // contractRegistry - // } - // const decoded = decode(input) - // const expected: TransferErc20 = { - // type: Intents.TRANSFER_ERC20, - // contract: 'eip155:137:0x031d8C0cA142921c459bCB28104c0FF37928F9eD' as Caip10, - // to: 'eip155:137:0xfe8f4de6e39c523ced231e7a72628f58e0ffee71' as Caip10, - // from: 'eip155:137:0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448' as Caip10, - // amount: '500000000000000000000' - // } + it('decodes erc721 safeTransferFrom', () => { + const decoded = decode(mockErc721SafeTransferFrom.input) + expect(decoded).toEqual(mockErc721SafeTransferFrom.intent) + }) + // it('decodes erc721 safeTransferFromWithBytes', () => { + // const expected = mockErc721SafeTransferFromWithBytes.input; + // const decoded = mockErc721SafeTransferFromWithBytes.intent // expect(decoded).toEqual(expected) - // }) + // }); + // it('decodes erc721 transferFrom with contract registry', () => { + // }); + it('decodes erc1155 safeTransferFrom', () => { + const decoded = decode(mockErc1155SafeTransferFrom.input) + expect(decoded).toEqual(mockErc1155SafeTransferFrom.intent) + }) + it('decodes erc1155 safeBatchTransferFrom', () => { + const decoded = decode(mockErc1155BatchSafeTransferFrom.input) + expect(decoded).toEqual(mockErc1155BatchSafeTransferFrom.intent) + }) + it('decodes erc20 and erc721 transferFrom with contract registry', () => { + // const erc20contractRegistry = { + // 'eip155:137:0x031d8C0cA142921c459bCB28104c0FF37928F9eD': AssetTypeEnum.ERC20 + // } as ContractRegistry + // const decoded = decode({ + // ...mockTransferFrom.input, + // contractRegistry: erc20contractRegistry + // }) + // expect(decoded).toEqual({ + // ...mockTransferFrom.intent, + // type: Intents.TRANSFER_ERC20, + // }) + // const erc721contractRegistry = { + // 'eip155:137:0x031d8C0cA142921c459bCB28104c0FF37928F9eD': AssetTypeEnum.ERC721 + // } as ContractRegistry + // const decoded2 = decode({ + // ...mockTransferFrom.input, + // contractRegistry: erc721contractRegistry + // }) + // expect(decoded2).toEqual({ + // ...mockTransferFrom.intent, + // type: Intents.TRANSFER_ERC721, + // }) + // const noMatchContractRegistry = { + // 'eip155:137:0x031d': AssetTypeEnum.ERC1155 + // } as ContractRegistry + // const decoded3 = decode({ + // ...mockTransferFrom.input, + // contractRegistry: noMatchContractRegistry + // }) + // expect(decoded3).toEqual({ + // ...mockTransferFrom.intent, + // type: Intents.CALL_CONTRACT, + // }) + }) + it('decodes a Native Transfer', () => { + const decoded = decode({ + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + value: '0x4124', + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + chainId: 137, + nonce: 10 + } + }) + expect(decoded).toEqual({ + type: Intents.TRANSFER_NATIVE, + to: 'eip155:137:0x031d8c0ca142921c459bcb28104c0ff37928f9ed', + from: 'eip155:137:0xed123cf8e3ba51c6c15da1eac74b2b5deea31448', + amount: '16676', + token: 'eip155:137/slip44/966' + }) + }) + it('defaults to contract call intent', () => { + const decoded = decode({ + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + data: '0xf2d12b1200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000ae00000000000000000000000000000000000000000000000000000000000000be000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c51910000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004c0000000000000000000000000601c01253057d267e7fb8684f608785b03dffb5a000000000000000000000000000000e7ec00e7b300774b00001314b8610022b80000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000065a7ffc40000000000000000000000000000000000000000000000000000000065a9513a0000000000000000000000000000000000000000000000000000000000000000360c6ebe00000000000000000000000000000000000000001e5b59b0367d550f0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037112afa04c0000000000000000000000000000000000000000000000000000037112afa04c0000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000073f9ea501f1d874c6afa3442c8971e1e278469a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000601c01253057d267e7fb8684f608785b03dffb5a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001606ddfd9b8000000000000000000000000000000000000000000000000000001606ddfd9b8000000000000000000000000000000a26b00c1f0df003000390027140000faa719000000000000000000000000000000000000000000000000000000000000004041bdcf7843c4d42a367340978cf0fc2f231cb3ec776981647da5661593ebcd5dca3d8b9586431c20c790d4e29f15c9c2e92f156f4ff723cd225c4857e144aac2000000000000000000000000000000000000000000000000000000000000007e0035ef74daa541eb3fc24e0f167893eed3ed2c51910000000065a8267691654e0142e721e29ee0657613ea6767b9b2a2ca61ec2a3795324b9bc370a76aba30e69c73bff110ab7443a1f3c5127547b7fdb9912b3f19bd099a3ff24dde69000000000000000000000000000000000000000000000000000000000000001732000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000048000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c519100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065a7ffc40000000000000000000000000000000000000000000000000000000065a9513a0000000000000000000000000000000000000000000000000000000000000000360c6ebe000000000000000000000000000000000000000015125a606e8248df0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000073f9ea501f1d874c6afa3442c8971e1e278469a3000000000000000000000000000000000000000000000000000000000000173200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008cf8bff0b00000000000000000000000000000000000000000000000000000008cf8bff0b000000000000000000000000000cda31ef080e99f60573c4d8c426d32b05a44ac4f00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003523c45a3a5800000000000000000000000000000000000000000000000000003523c45a3a580000000000000000000000000035ef74daa541eb3fc24e0f167893eed3ed2c51910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000173200000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000360c6ebe', + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + chainId: 137, + nonce: 10 + } + }) + expect(decoded).toEqual({ + type: Intents.CALL_CONTRACT, + from: 'eip155:137:0xed123cf8e3ba51c6c15da1eac74b2b5deea31448', + contract: 'eip155:137:0x031d8c0ca142921c459bcb28104c0ff37928f9ed', + hexSignature: '0xf2d12b12' + }) + }) }) + // describe('transaction management', () => { + // it('decodes retry transaction', () => { + // }); + // it('decodes cancel transaction', () => { + // }); + // }); + // describe('contract creation', () => { + // it('decodes safe wallet creation deployment', () => { + // }); + // it('decodes erc4337 wallet deployment', () => { + // }); + // it('defaults to contract deployment intent', () => { + // }); + // }) + // it('decodes approve token allowance', () => { + // }); + // it('defaults to contract call intent', () => { + // }); }) + // describe('message and typed data input', () => { + // it('decodes message', () => { + // }); + // it('decodes typed data', () => { + // }); + // it('decodes raw message', () => { + // }); + // it('decodes permit', () => { + // }); + // it('decodes permit2', () => { + // }); + // it('defaults to raw payload', () => { + // }); + // }); }) diff --git a/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts b/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts index 4df9d1fb9..6c90ca832 100644 --- a/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts +++ b/packages/transaction-request-intent/src/lib/__test__/unit/mocks.ts @@ -1,8 +1,9 @@ import { TransactionRequest } from '@narval/authz-shared' import { Address } from 'viem' import { Caip10, Caip19 } from '../../caip' -import { Intents } from '../../domain' -import { TransferNative } from '../../intent.types' +import { InputType, Intents } from '../../domain' +import { TransferErc1155, TransferErc20, TransferErc721, TransferNative } from '../../intent.types' +import { TransactionInput } from '../../types' export const ONE_ETH = BigInt('1000000000000000000') @@ -243,10 +244,10 @@ export const NATIVE_TRANSFER_INTENT: TransferNative = { to: 'eip155:137/eoa:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4' as Caip10, from: 'eip155:137/eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b' as Caip10, amount: '0x8000', - token: 'eip155:1/slip44:60' as Caip19 // Caip19 for ETH + token: 'eip155:1/slip44/60' as Caip19 // Caip19 for ETH } -export const ERC20_TRANSFER_TX_REQUEST: TransactionRequest = { +const ERC20_TRANSFER_TX_REQUEST: TransactionRequest = { from: TREASURY_WALLET_X.address as Address, to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD' as Address, chainId: ACCOUNT_Q_137.chainId, @@ -255,102 +256,123 @@ export const ERC20_TRANSFER_TX_REQUEST: TransactionRequest = { type: '2' } -export const NATIVE_TRANSFER_TX_REQUEST: TransactionRequest = { +const ERC20_TRANSFER_INTENT: TransferErc20 = { + type: Intents.TRANSFER_ERC20, + to: `eip155:137:0x031d8c0ca142921c459bcb28104c0ff37928f9ed` as Caip10, + from: `eip155:137:${ERC20_TRANSFER_TX_REQUEST.from.toLowerCase()}` as Caip10, + contract: `eip155:137:${ERC20_TRANSFER_TX_REQUEST.to?.toLowerCase()}` as Caip10, + amount: '428406414311469998210669' +} + +const ERC721_SAFE_TRANSFER_FROM_TX_REQUEST: TransactionRequest = { from: TREASURY_WALLET_X.address as Address, to: ACCOUNT_Q_137.address as Address, chainId: ACCOUNT_Q_137.chainId, - value: '0x8000', - data: '0x', + data: '0x42842e0e000000000000000000000000ea7278a0d8306658dd6d38274dde084f24cd8a11000000000000000000000000b253f6156e64b12ba0dec3974062dbbaee139f0c000000000000000000000000000000000000000000000000000000000000a0d5', nonce: 192, type: '2' } -export const REGO_REQUEST = { - action: Action.SIGN_TRANSACTION, - request: NATIVE_TRANSFER_TX_REQUEST, - intent: NATIVE_TRANSFER_INTENT, - resource: { - uid: TREASURY_WALLET_X.uid - }, - principal: { - uid: MATT.uid - }, - signatures: [] +const ERC721_SAFE_TRANSFER_FROM_INTENT: TransferErc721 = { + type: Intents.TRANSFER_ERC721, + to: `eip155:137:0xb253f6156e64b12ba0dec3974062dbbaee139f0c` as Caip10, + from: `eip155:137:${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.from.toLowerCase()}` as Caip10, + contract: `eip155:137:${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.to?.toLowerCase()}` as Caip10, + nftId: `eip155:137/erc721:${ERC721_SAFE_TRANSFER_FROM_TX_REQUEST.to}/41173` as Caip19 +} + +export const mockErc721SafeTransferFrom = { + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: ERC721_SAFE_TRANSFER_FROM_TX_REQUEST + } as TransactionInput, + intent: ERC721_SAFE_TRANSFER_FROM_INTENT } -// Role Permissions -// Of course we can have different permissions per resource, but for now we'll just use the same permissions for all resources. +const transferFromData = + '0x23b872dd000000000000000000000000ce5550ac05e0c6ab27418de56fc57c852de961d400000000000000000000000059895c2cdaa07cc3ac20ef0918d2597a277b276c000000000000000000000000000000000000000000000000000000000000159c' -export const ROOT_PERMISSIONS: RolePermission = { - permit: true +export const mockTransferFrom = { + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: { + from: TREASURY_WALLET_X.address as Address, + to: ACCOUNT_Q_137.address as Address, + chainId: ACCOUNT_Q_137.chainId, + data: transferFromData, + nonce: 192, + type: '2' + } + } as TransactionInput, + intent: { + to: `eip155:137:0x59895c2cdaa07cc3ac20ef0918d2597a277b276c` as Caip10, + from: `eip155:137:${TREASURY_WALLET_X.address}` as Caip10, + contract: `eip155:137:${ACCOUNT_Q_137.address}` as Caip10, + amount: '5532' + } } -export const ADMIN_PERMISSIONS: RolePermission = { - permit: true, - admin_quorum_threshold: 1 +const ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST: TransactionRequest = { + from: TREASURY_WALLET_X.address as Address, + to: ACCOUNT_Q_137.address as Address, + chainId: ACCOUNT_Q_137.chainId, + data: '0xf242432a000000000000000000000000d15b4cb5495cffa8d01970018ea3bc4942e34b7a00000000000000000000000000ca04c45da318d5b7e7b14d5381ca59f09c73f000000000000000000000000000000000000000000000000000000000000000af000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000704760f2a0b00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000ca04c45da318d5b7e7b14d5381ca59f09c73f00000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006042b8a88ec00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000005c0000000000000000000000000d15b4cb5495cffa8d01970018ea3bc4942e34b7a000000000000000000000000d15b4cb5495cffa8d01970018ea3bc4942e34b7a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000004e0000000000000000000000000cb02f88ea1b95ba4adccfc0d0ac2a6052b70f2e400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000065a9d6a20000000000000000000000000000000000000000000000000000000065adcb5d000000000000000000000000000000000000000000000000000000000000000060665ba51d4da48b00000000000000004cb15528fa439a0e4e2a583202e926d50000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f00000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf127000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024b2b50d4fec700000000000000000000000000000000000000000000000000024b2b50d4fec7000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000939821fd096b4e4f67f369af67cf9411b1a2816000000000000000000000000000000000000000000000000000000000000000af00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cb02f88ea1b95ba4adccfc0d0ac2a6052b70f2e400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf127000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d5bc40aa656c0000000000000000000000000000000000000000000000000001d5bc40aa656c00000000000000000000000000001f429aa3c402e9deabe8a8ecae7d37b0d35452c0000000000000000000000000000000000000000000000000000000000000041efa2557bc958943cb6a7df19ae9c8b1b516515b5184f21516458a44d559141d94229134f3da4243b36151bba8cc65bf4d441a277326a87e59b1b5dcb18c681021b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d4da48b60665ba5', + nonce: 192, + type: '2' } -export const MANAGER_PERMISSIONS: RolePermission = { - permit: true, - admin_quorum_threshold: 2 +const ERC1155_SAFE_TRANSFER_FROM_INTENT: TransferErc1155 = { + type: Intents.TRANSFER_ERC1155, + to: `eip155:137:0x00ca04c45da318d5b7e7b14d5381ca59f09c73f0` as Caip10, + from: `eip155:137:${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.from.toLowerCase()}` as Caip10, + contract: `eip155:137:${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.to?.toLowerCase()}` as Caip10, + transfers: [{ tokenId: `eip155:137/erc1155:${ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST.to}/175` as Caip19, amount: '1' }] +} +export const mockErc1155SafeTransferFrom = { + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: ERC1155_SAFE_TRANSFER_FROM_TX_REQUEST + } as TransactionInput, + intent: ERC1155_SAFE_TRANSFER_FROM_INTENT } -export const mockEntityData: RegoData = { - entities: { - users: { - [ROOT_USER.uid]: ROOT_USER, - [MATT.uid]: MATT, - [AAUser.uid]: AAUser, - [BBUser.uid]: BBUser - }, - user_groups: { - [DEV_USER_GROUP.uid]: DEV_USER_GROUP, - [TREASURY_USER_GROUP.uid]: TREASURY_USER_GROUP - }, - wallets: { - [SHY_ACCOUNT_WALLET.uid]: SHY_ACCOUNT_WALLET, - [PIERRE_WALLET.uid]: PIERRE_WALLET, - [WALLET_Q.uid]: WALLET_Q, - [TREASURY_WALLET_X.uid]: TREASURY_WALLET_X - }, - wallet_groups: { - [DEV_WALLET_GROUP.uid]: DEV_WALLET_GROUP, - [TREASURY_WALLET_GROUP.uid]: TREASURY_WALLET_GROUP - }, - address_book: { - [SHY_ACCOUNT_137.uid]: SHY_ACCOUNT_137, - [SHY_ACCOUNT_1.uid]: SHY_ACCOUNT_1, - [ACCOUNT_INTERNAL_WXZ_137.uid]: ACCOUNT_INTERNAL_WXZ_137, - [ACCOUNT_Q_137.uid]: ACCOUNT_Q_137 - } - }, - permissions: { - [Action.CREATE_USER]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS, - [UserRoles.ADMIN]: ADMIN_PERMISSIONS - }, - [Action.EDIT_USER]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS, - [UserRoles.ADMIN]: ADMIN_PERMISSIONS - }, - [Action.DELETE_USER]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS, - [UserRoles.ADMIN]: ADMIN_PERMISSIONS - }, - [Action.CREATE_WALLET]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS - }, - [Action.EDIT_WALLET]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS, - [UserRoles.MANAGER]: MANAGER_PERMISSIONS - }, - [Action.ASSIGN_WALLET]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS, - [UserRoles.MANAGER]: MANAGER_PERMISSIONS +const ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST = { + from: TREASURY_WALLET_X.address as Address, + to: ACCOUNT_Q_137.address as Address, + chainId: ACCOUNT_Q_137.chainId, + data: '0x2eb2c2d60000000000000000000000008b149b00ce4ad98878ec342d69eb42dcbcbd6306000000000000000000000000383370726a5bd619e0d2af8ef37a58013b823a8c00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000b9c00000000000000000000000000000000000000000000000000000000000000a200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000610000000000000000000000000000000000000000000000000000000065a9d9340feb2529261db182817eef5823d8f659495e6bdc097b95cac8011dd73a13be723d4c281943fbfff8b8ba89cfa2a44286411e94e8eb9601502914ffd41764bc781c00000000000000000000000000000000000000000000000000000000000000', + nonce: 2 +} + +const ERC1155_BATCH_SAFE_TRANSFER_FROM_INTENT = { + type: Intents.TRANSFER_ERC1155, + from: `eip155:137:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.from.toLowerCase()}` as Caip10, + to: `eip155:137:0x383370726a5bd619e0d2af8ef37a58013b823a8c` as Caip10, + contract: `eip155:137:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to?.toLowerCase()}` as Caip10, + transfers: [ + { + tokenId: `eip155:137/erc1155:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to}/2972` as Caip19, + amount: '1' }, - [Action.UNASSIGN_WALLET]: { - [UserRoles.ROOT]: ROOT_PERMISSIONS, - [UserRoles.MANAGER]: MANAGER_PERMISSIONS + { + tokenId: `eip155:137/erc1155:${ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST.to}/162` as Caip19, + amount: '1' } - } + ] +} + +export const mockErc1155BatchSafeTransferFrom = { + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: ERC1155_BATCH_SAFE_TRANSFER_FROM_REQUEST + } as TransactionInput, + intent: ERC1155_BATCH_SAFE_TRANSFER_FROM_INTENT +} + +export const mockErc20Transfer = { + input: { + type: InputType.TRANSACTION_REQUEST, + txRequest: ERC20_TRANSFER_TX_REQUEST + } as TransactionInput, + intent: ERC20_TRANSFER_INTENT } diff --git a/packages/transaction-request-intent/src/lib/__test__/unit/param-extractors.spec.ts b/packages/transaction-request-intent/src/lib/__test__/unit/param-extractors.spec.ts deleted file mode 100644 index f3aef1570..000000000 --- a/packages/transaction-request-intent/src/lib/__test__/unit/param-extractors.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { extractErc20TransferAmount } from '../../param-extractors' - -const invalidData = '0xInvalidData' -const validData = - '0xa9059cbb000000000000000000000000031d8c0ca142921c459bcb28104c0ff37928f9ed000000000000000000000000000000000000000000005ab7f55035d1e7b4fe6d' - -describe('extractErc20TransferAmount', () => { - it('throws on incorrect data', () => { - expect(() => extractErc20TransferAmount(invalidData)).toThrow('Malformed transaction request') - }) - - // TODO (@Pierre): Check if the test or implementation are correct. - it('successfully extract amount on valid data', () => { - expect(extractErc20TransferAmount(validData)).toEqual( - '54802253485514079331440257873334643289895370523631496173467651529125391250897' - ) - }) -}) diff --git a/packages/transaction-request-intent/src/lib/decoders.ts b/packages/transaction-request-intent/src/lib/decoders.ts index 6646ecaa4..cb409e6b8 100644 --- a/packages/transaction-request-intent/src/lib/decoders.ts +++ b/packages/transaction-request-intent/src/lib/decoders.ts @@ -1,24 +1,61 @@ import { TransactionRequest } from '@narval/authz-shared' import { Hex } from 'viem' -import { encodeEoaAccountId } from './caip' -import { AssetTypeEnum, Intents, NULL_METHOD_ID, TransactionCategory, TransactionStatus } from './domain' +import { Caip19, encodeEoaAccountId, encodeEoaAssetId } from './caip' +import { + AssetTypeEnum, + EipStandardEnum, + Intents, + NULL_METHOD_ID, + TransactionCategory, + TransactionStatus +} from './domain' import { TransactionRequestIntentError } from './error' -import { Intent, TransferErc20 } from './intent.types' +import { + CallContract, + ERC1155Transfer, + Intent, + TransferErc1155, + TransferErc20, + TransferErc721, + TransferNative +} from './intent.types' import { AMBIGUOUS_FUNCTION, HEX_SIG_TO_INTENT } from './methodId' -import { extractErc20Amount } from './param-extractors' -import { ContractRegistry, DecodeErc20Input, DecoderRegistry, TransactionInput, TransactionRegistry } from './types' - -export const getMethodId = (data?: string): string => (data ? data.slice(0, 10) : NULL_METHOD_ID) - -type ValidatedContractCallInput = { - data: Hex - to: Hex - chainId: number - from: Hex - nonce: number -} - -export const validateContractCallIntent = (txRequest: TransactionRequest): ValidatedContractCallInput => { +import { + assertHexString, + extractors, + isErc1155SafeTransferFromParams, + isErc721SafeTransferFromParams, + isSafeBatchTransferFromParams, + isTransferParams +} from './param-extractors' +import { + ContractCreationDecoders, + ContractCreationIntents, + ContractDeploymentDecoder, + ContractDeploymentInput, + ContractInteractionDecoder, + ContractInteractionDecoders, + ContractInteractionIntents, + ContractRegistry, + DecodeTransferInput, + NativeTransferDecoder, + NativeTransferDecoders, + NativeTransferInput, + NativeTransferIntents, + TransactionInput, + TransactionIntents, + TransactionRegistry, + ValidatedInput, + Validator, + ValidatorRegistry +} from './types' + +export const getMethodId = (data?: string): Hex => (data ? assertHexString(data.slice(0, 10)) : NULL_METHOD_ID) + +export const validateContractCallIntent: Validator = ( + txRequest: TransactionRequest, + methodId: Hex +): DecodeTransferInput => { const { data, to, chainId, from, nonce } = txRequest if (!data || !to || !chainId || !nonce) { throw new TransactionRequestIntentError({ @@ -33,12 +70,13 @@ export const validateContractCallIntent = (txRequest: TransactionRequest): Valid } }) } - return { nonce, data, to, chainId, from } + const dataWithoutMethodId = `0x${data.slice(10)}` as Hex + return { nonce, data: dataWithoutMethodId, to, chainId, from, methodId } } -export const validateNativeTransferIntent = (txRequest: TransactionRequest) => { - const { value, chainId } = txRequest - if (!value || !chainId) { +export const validateNativeTransferIntent: Validator = (txRequest: TransactionRequest, methodId: Hex) => { + const { value, chainId, to, from, nonce } = txRequest + if (!value || !chainId || !to || !from || !nonce) { throw new TransactionRequestIntentError({ message: 'Malformed native transfer transaction request: missing value or chainId', status: 400, @@ -49,38 +87,161 @@ export const validateNativeTransferIntent = (txRequest: TransactionRequest) => { } }) } - return { value, chainId } + return { to, from, value, chainId, nonce, methodId } +} + +const decodeErc721Transfer: ContractInteractionDecoder = ({ + to, + from, + data, + chainId, + methodId +}: DecodeTransferInput): TransferErc721 => { + const params = extractors[methodId](data, methodId) + if (!isErc721SafeTransferFromParams(params)) { + throw new TransactionRequestIntentError({ + message: 'Failed to retrieve params for erc721 function', + status: 400, + context: { + data, + methodId, + params + } + }) + } + const intent: TransferErc721 = { + to: encodeEoaAccountId({ + chainId, + evmAccountAddress: params.to + }), + from: encodeEoaAccountId({ + chainId, + evmAccountAddress: from + }), + type: Intents.TRANSFER_ERC721, + nftId: encodeEoaAssetId({ + eipStandard: EipStandardEnum.EIP155, + assetType: AssetTypeEnum.ERC721, + chainId, + evmAccountAddress: to, + tokenId: params.tokenId.toString() + }), + contract: encodeEoaAccountId({ + chainId, + evmAccountAddress: to + }) + } + return intent +} + +const decodeDeployContract: ContractDeploymentDecoder = ({ from }: ContractDeploymentInput): Intent => { + const intent = { from } + return intent as Intent +} + +const decodeErc20Transfer: ContractInteractionDecoder = ({ + to, + from, + data, + chainId, + methodId +}: DecodeTransferInput): TransferErc20 => { + // if (methodId === AmbiguousMethods.TRANSFER_FROM) { + // return decodeTransferFrom({ + // to, + // from, + // data, + // chainId, + // methodId, + // type: Intents.TRANSFER_ERC20 + // }) as + // } + const params = extractors[methodId](data, methodId) + if (!isTransferParams(params)) { + throw new TransactionRequestIntentError({ + message: 'Failed to retrieve params for erc20 function', + status: 400, + context: { + data, + methodId, + params + } + }) + } + const intent: TransferErc20 = { + to: encodeEoaAccountId({ + chainId, + evmAccountAddress: params.recipient + }), + from: encodeEoaAccountId({ + chainId, + evmAccountAddress: from + }), + type: Intents.TRANSFER_ERC20, + amount: params.amount, + contract: encodeEoaAccountId({ + chainId, + evmAccountAddress: to + }) + } + return intent } -// const decodeErc721 = ({ +// const decodeTransferFrom = ({ +// to, +// from, // data, -// methodId, // chainId, -// assetType, -// to -// }: { -// data: Hex -// methodId: string -// chainId: number -// assetType: AssetTypeEnum -// to: Hex -// }) => { +// methodId, +// type +// }: DecodeTransferInput & {type: Intents.TRANSFER_ERC20 | Intents.TRANSFER_ERC721}): TransferErc20 | TransferErc721 => { +// const params = extractors[methodId](data, methodId) +// if (!isTransferFromParams(params)) { +// throw new TransactionRequestIntentError({ +// message: 'Failed to retrieve params for erc20 function', +// status: 400, +// context: { +// data, +// methodId, +// params +// } +// }) +// } +// if (type === Intents.TRANSFER_ERC20) { +// const intent: TransferErc20 = { +// to: encodeEoaAccountId({ +// chainId, +// evmAccountAddress: params.recipient +// }), +// from: encodeEoaAccountId({ +// chainId, +// evmAccountAddress: from +// }), +// type: Intents.TRANSFER_ERC20, +// amount: params.amount, +// contract: encodeEoaAccountId({ +// chainId, +// evmAccountAddress: to +// }) +// } +// return intent; +// } // const intent: TransferErc721 = { // to: encodeEoaAccountId({ // chainId, -// evmAccountAddress: to +// evmAccountAddress: params.recipient // }), // from: encodeEoaAccountId({ // chainId, -// evmAccountAddress: to +// evmAccountAddress: from // }), // type: Intents.TRANSFER_ERC721, // nftId: encodeEoaAssetId({ // eipStandard: EipStandardEnum.EIP155, -// assetType, +// assetType: AssetTypeEnum.ERC721, // chainId, // evmAccountAddress: to, -// tokenId: extractErc721AssetId(data, methodId) +// tokenId: params.amount.toString() // }), // contract: encodeEoaAccountId({ // chainId, @@ -90,31 +251,126 @@ export const validateNativeTransferIntent = (txRequest: TransactionRequest) => { // return intent // } -const decodeErc20 = ({ to, from, data, chainId, methodId }: DecodeErc20Input): TransferErc20 => { - const intent: TransferErc20 = { +const decodeErc1155: ContractInteractionDecoder = ({ + to, + from, + data, + chainId, + methodId +}: DecodeTransferInput): TransferErc1155 => { + const params = extractors[methodId](data, methodId) + const transfers: ERC1155Transfer[] = [] + if (isSafeBatchTransferFromParams(params)) { + if (params.amounts.length !== params.tokenIds.length) { + throw new TransactionRequestIntentError({ + message: 'Not the same number of amounts and ids', + status: 400, + context: { + data, + methodId, + params + } + }) + } + params.tokenIds.forEach((tokenId, index) => { + transfers.push({ + tokenId: encodeEoaAssetId({ + eipStandard: EipStandardEnum.EIP155, + assetType: AssetTypeEnum.ERC1155, + chainId, + evmAccountAddress: to, + tokenId + }), + amount: params.amounts[index] + }) + }) + } else if (isErc1155SafeTransferFromParams(params)) { + transfers.push({ + tokenId: encodeEoaAssetId({ + eipStandard: EipStandardEnum.EIP155, + assetType: AssetTypeEnum.ERC1155, + chainId, + evmAccountAddress: to, + tokenId: params.tokenId + }), + amount: params.amount + }) + } else { + throw new TransactionRequestIntentError({ + message: 'Failed to retrieve params for erc1155 function', + status: 400, + context: { + data, + methodId, + params + } + }) + } + const intent: TransferErc1155 = { to: encodeEoaAccountId({ chainId, - evmAccountAddress: to + evmAccountAddress: params.to }), from: encodeEoaAccountId({ chainId, evmAccountAddress: from }), - type: Intents.TRANSFER_ERC20, - amount: extractErc20Amount(data, methodId), contract: encodeEoaAccountId({ chainId, evmAccountAddress: to - }) + }), + type: Intents.TRANSFER_ERC1155, + transfers } return intent } -// const decodeNativeTransferIntent = ({}) => {} - -// const decodeErc1155 = ({}) => {} +const nativeCaip19 = (chainId: number): Caip19 => { + if (chainId === 1) { + return 'eip155:1/slip44/60' as Caip19 + } else if (chainId === 137) { + return 'eip155:137/slip44/966' as Caip19 + } + throw new TransactionRequestIntentError({ + message: 'Invalid chainId', + status: 400, + context: { + chainId + } + }) +} +const decodeNativeTransfer: NativeTransferDecoder = ({ to, from, value, chainId }: NativeTransferInput) => { + const intent: TransferNative = { + to: encodeEoaAccountId({ + chainId, + evmAccountAddress: to + }), + from: encodeEoaAccountId({ + chainId, + evmAccountAddress: from + }), + type: Intents.TRANSFER_NATIVE, + amount: Number(value).toString(), + token: nativeCaip19(chainId) + } + return intent +} -// const decodeContractCall = ({}) => {} +const decodeContractCall = ({ to, from, chainId, methodId }: DecodeTransferInput): CallContract => { + const intent: CallContract = { + from: encodeEoaAccountId({ + chainId, + evmAccountAddress: from + }), + type: Intents.CALL_CONTRACT, + contract: encodeEoaAccountId({ + chainId, + evmAccountAddress: to + }), + hexSignature: methodId + } + return intent +} // const decodeSignMessage = ({}) => {} @@ -140,66 +396,48 @@ const decodeErc20 = ({ to, from, data, chainId, methodId }: DecodeErc20Input): T // const decodePermit2 = ({}) => {} -const validators = { +const validators: ValidatorRegistry = { [TransactionCategory.NATIVE_TRANSFER]: validateNativeTransferIntent, - // [TransactionCategory.CONTRACT_CREATION]: validateContractCreationIntent, + [TransactionCategory.CONTRACT_CREATION]: validateContractCallIntent, [TransactionCategory.CONTRACT_INTERACTION]: validateContractCallIntent } -const decoders: DecoderRegistry = { - [Intents.TRANSFER_NATIVE]: decodeErc20, - [Intents.TRANSFER_ERC20]: decodeErc20, - [Intents.TRANSFER_ERC721]: decodeErc20, - [Intents.TRANSFER_ERC1155]: decodeErc20, - [Intents.CALL_CONTRACT]: decodeErc20, - [Intents.SIGN_MESSAGE]: decodeErc20, - [Intents.SIGN_RAW_MESSAGE]: decodeErc20, - [Intents.SIGN_RAW_PAYLOAD]: decodeErc20, - [Intents.SIGN_TYPED_DATA]: decodeErc20, - [Intents.RETRY_TRANSACTION]: decodeErc20, - [Intents.CANCEL_TRANSACTION]: decodeErc20, - [Intents.DEPLOY_CONTRACT]: decodeErc20, - [Intents.DEPLOY_ERC_4337_WALLET]: decodeErc20, - [Intents.DEPLOY_SAFE_WALLET]: decodeErc20, - [Intents.APPROVE_TOKEN_ALLOWANCE]: decodeErc20, - [Intents.PERMIT]: decodeErc20, - [Intents.PERMIT2]: decodeErc20 +const contractCreationDecoders: ContractCreationDecoders = { + [Intents.DEPLOY_CONTRACT]: decodeDeployContract, + [Intents.DEPLOY_ERC_4337_WALLET]: decodeDeployContract, + [Intents.DEPLOY_SAFE_WALLET]: decodeDeployContract } -// const decoders: DecoderRegistry = { -// [Intents.TRANSFER_NATIVE]: decodeNativeTransferIntent, -// [Intents.TRANSFER_ERC20]: decodeErc20, -// [Intents.TRANSFER_ERC721]: decodeErc721, -// [Intents.TRANSFER_ERC1155]: decodeErc1155, -// [Intents.CALL_CONTRACT]: decodeContractCall, -// [Intents.SIGN_MESSAGE]: decodeSignMessage, -// [Intents.SIGN_RAW_MESSAGE]: decodeSignRawMessage, -// [Intents.SIGN_RAW_PAYLOAD]: decodeSignRawPayload, -// [Intents.SIGN_TYPED_DATA]: decodeSignTypedData, -// [Intents.RETRY_TRANSACTION]: decodeRetryTransaction, -// [Intents.CANCEL_TRANSACTION]: decodeCancelTransaction, -// [Intents.DEPLOY_CONTRACT]: decodeDeployContract, -// [Intents.DEPLOY_ERC_4337_WALLET]: decodeDeployErc4337Wallet, -// [Intents.DEPLOY_SAFE_WALLET]: decodeDeploySafeWallet, -// [Intents.APPROVE_TOKEN_ALLOWANCE]: decodeApproveTokenAllowance, -// [Intents.PERMIT]: decodePermit, -// [Intents.PERMIT2]: decodePermit2, -// } +const nativeTransferDecoders: NativeTransferDecoders = { + [Intents.TRANSFER_NATIVE]: decodeNativeTransfer +} + +const contractInteractionDecoders: ContractInteractionDecoders = { + [Intents.TRANSFER_ERC20]: decodeErc20Transfer, + [Intents.TRANSFER_ERC721]: decodeErc721Transfer, + [Intents.TRANSFER_ERC1155]: decodeErc1155, + [Intents.CALL_CONTRACT]: decodeContractCall, + [Intents.APPROVE_TOKEN_ALLOWANCE]: decodeErc20Transfer, + [Intents.RETRY_TRANSACTION]: decodeDeployContract, + [Intents.CANCEL_TRANSACTION]: decodeDeployContract +} const contractTypeLookup = ( - txRequest: ValidatedContractCallInput, + txRequest: ValidatedInput, contractRegistry: ContractRegistry ): AssetTypeEnum | undefined => { - const key = encodeEoaAccountId({ - chainId: txRequest.chainId, - evmAccountAddress: txRequest.to - }) - const assetType = contractRegistry[key] - return assetType + if ('to' in txRequest && txRequest.to) { + const key = encodeEoaAccountId({ + chainId: txRequest.chainId, + evmAccountAddress: txRequest.to + }) + return contractRegistry[key] + } + return undefined } const transactionLookup = ( - txRequest: ValidatedContractCallInput, + txRequest: ValidatedInput, transactionRegistry?: TransactionRegistry ): TransactionStatus | undefined => { const account = encodeEoaAccountId({ @@ -213,12 +451,12 @@ const transactionLookup = ( return undefined } -const getIntentType = ( +const getTransactionIntentType = ( methodId: string, - txRequest: ValidatedContractCallInput, + txRequest: ValidatedInput, contractRegistry?: ContractRegistry, transactionRegistry?: TransactionRegistry -): Intents => { +): TransactionIntents => { const trxStatus = transactionLookup(txRequest, transactionRegistry) if (trxStatus === TransactionStatus.PENDING) { return Intents.RETRY_TRANSACTION @@ -226,6 +464,9 @@ const getIntentType = ( if (trxStatus === TransactionStatus.FAILED) { return Intents.CANCEL_TRANSACTION } + if (methodId === NULL_METHOD_ID) { + return Intents.TRANSFER_NATIVE + } if (AMBIGUOUS_FUNCTION[methodId] && contractRegistry) { const assetType = contractTypeLookup(txRequest, contractRegistry) if (assetType === AssetTypeEnum.ERC721) { @@ -238,13 +479,31 @@ const getIntentType = ( return HEX_SIG_TO_INTENT[methodId] || Intents.CALL_CONTRACT } +export const getCategory = (methodId: string, to?: Hex | null): TransactionCategory => { + if (methodId === NULL_METHOD_ID) { + return TransactionCategory.NATIVE_TRANSFER + } + if (to === null) { + return TransactionCategory.CONTRACT_CREATION + } + return TransactionCategory.CONTRACT_INTERACTION +} + export const decodeTransaction = ({ txRequest, transactionRegistry, contractRegistry }: TransactionInput): Intent => { const { data } = txRequest const methodId = getMethodId(data) - const validatedTxRequest = validators[TransactionCategory.CONTRACT_INTERACTION](txRequest) - const type = getIntentType(methodId, validatedTxRequest, contractRegistry, transactionRegistry) - - return decoders[type]({ ...validatedTxRequest, methodId }) + const category = getCategory(methodId, txRequest.to) + const validatedTxRequest = validators[category](txRequest, methodId) + const type = getTransactionIntentType(methodId, validatedTxRequest, contractRegistry, transactionRegistry) + + switch (category) { + case TransactionCategory.NATIVE_TRANSFER: + return nativeTransferDecoders[type as NativeTransferIntents](validatedTxRequest as NativeTransferInput) + case TransactionCategory.CONTRACT_INTERACTION: + return contractInteractionDecoders[type as ContractInteractionIntents](validatedTxRequest as DecodeTransferInput) + case TransactionCategory.CONTRACT_CREATION: + return contractCreationDecoders[type as ContractCreationIntents](validatedTxRequest as ContractDeploymentInput) + } } // export const decodeMessage = ({ diff --git a/packages/transaction-request-intent/src/lib/domain.ts b/packages/transaction-request-intent/src/lib/domain.ts index 9ac31469a..49da1f3a0 100644 --- a/packages/transaction-request-intent/src/lib/domain.ts +++ b/packages/transaction-request-intent/src/lib/domain.ts @@ -6,7 +6,6 @@ export enum InputType { } export enum TransactionCategory { - TRANSACTION_MANAGEMENT = 'transactionManagement', NATIVE_TRANSFER = 'nativeTransfer', CONTRACT_CREATION = 'ContractCreation', CONTRACT_INTERACTION = 'ContractCall' @@ -18,6 +17,7 @@ export enum TransactionStatus { } export enum Intents { + TRANSFER_WRAPPED_NATIVE = 'transferWrappedNative', TRANSFER_NATIVE = 'transferNative', TRANSFER_ERC20 = 'transferErc20', TRANSFER_ERC721 = 'transferErc721', diff --git a/packages/transaction-request-intent/src/lib/intent.types.ts b/packages/transaction-request-intent/src/lib/intent.types.ts index 45518de83..2a09caec6 100644 --- a/packages/transaction-request-intent/src/lib/intent.types.ts +++ b/packages/transaction-request-intent/src/lib/intent.types.ts @@ -11,6 +11,15 @@ export type TransferNative = { amount: string } +export type WrappedNativeTransfer = { + type: Intents.TRANSFER_WRAPPED_NATIVE + to: Caip10 + from: Caip10 + token: Caip19 + wrapper: Caip10 + amount: string +} + export type TransferErc20 = { type: Intents.TRANSFER_ERC20 to: Caip10 @@ -27,13 +36,16 @@ export type TransferErc721 = { nftId: Caip19 } +export type ERC1155Transfer = { + tokenId: Caip19 + amount: string +} export type TransferErc1155 = { type: Intents.TRANSFER_ERC1155 to: Caip10 from: Caip10 contract: Caip10 - assetId: Caip19 - amount: string + transfers: ERC1155Transfer[] } export type CallContract = { @@ -123,4 +135,4 @@ export type Permit2 = { deadline: string } -export type Intent = TransferNative | TransferErc20 | TransferErc721 | CallContract +export type Intent = TransferNative | TransferErc20 | TransferErc721 | TransferErc1155 | CallContract diff --git a/packages/transaction-request-intent/src/lib/methodId.ts b/packages/transaction-request-intent/src/lib/methodId.ts index cd3dc6cef..0128d063e 100644 --- a/packages/transaction-request-intent/src/lib/methodId.ts +++ b/packages/transaction-request-intent/src/lib/methodId.ts @@ -1,30 +1,35 @@ import { AbiParameter } from 'viem' import { Intents } from './domain' +import { TransactionIntents } from './types' export type Erc20MethodId = keyof typeof Erc20TransferAbi export type Erc721MethodId = keyof typeof Erc721TransferAbi export const Erc20Methods = { - TRANSFER: '0xa9059cbb', + TRANSFER: '0xa9059cbb' +} + +export const AmbiguousMethods = { TRANSFER_FROM: '0x23b872dd' } export const Erc721Methods = { - TRANSFER_FROM: '0x23b872dd', SAFE_TRANSFER_FROM: '0x42842e0e', SAFE_TRANSFER_FROM_WITH_BYTES: '0xb88d4fde' } export const Erc1155Methods = { SAFE_TRANSFER_FROM: '0xa22cb465', - SAFE_BATCH_TRANSFER_FROM: '0xf242432a' + SAFE_TRANSFER_FROM_WITH_BYTES: '0xf242432a', + SAFE_BATCH_TRANSFER_FROM: '0x2eb2c2d6' } -export const HEX_SIG_TO_INTENT: { [methodId: string]: Intents } = { +export const HEX_SIG_TO_INTENT: { [methodId: string]: TransactionIntents } = { [Erc20Methods.TRANSFER]: Intents.TRANSFER_ERC20, [Erc721Methods.SAFE_TRANSFER_FROM]: Intents.TRANSFER_ERC721, [Erc721Methods.SAFE_TRANSFER_FROM_WITH_BYTES]: Intents.TRANSFER_ERC721, [Erc1155Methods.SAFE_TRANSFER_FROM]: Intents.TRANSFER_ERC1155, + [Erc1155Methods.SAFE_TRANSFER_FROM_WITH_BYTES]: Intents.TRANSFER_ERC1155, [Erc1155Methods.SAFE_BATCH_TRANSFER_FROM]: Intents.TRANSFER_ERC1155 } @@ -113,5 +118,6 @@ export const Erc1155SafeBatchTransferFromAbiParameters: AbiParameter[] = [ export const Erc1155TransferAbi = { '0xa22cb465': Erc1155SafeTransferFromAbiParameters, - '0xf242432a': Erc1155SafeBatchTransferFromAbiParameters + '0xf242432a': Erc1155SafeTransferFromAbiParameters, + '0x2eb2c2d6': Erc1155SafeBatchTransferFromAbiParameters } diff --git a/packages/transaction-request-intent/src/lib/param-extractors.ts b/packages/transaction-request-intent/src/lib/param-extractors.ts index ef07ea2aa..25ab10690 100644 --- a/packages/transaction-request-intent/src/lib/param-extractors.ts +++ b/packages/transaction-request-intent/src/lib/param-extractors.ts @@ -1,66 +1,312 @@ -import { AbiParameter, Hex, decodeAbiParameters } from 'viem' +import { Hex, decodeAbiParameters } from 'viem' +import { TransactionRequestIntentError } from './error' import { + AmbiguousMethods, + Erc1155Methods, + Erc1155SafeBatchTransferFromAbiParameters, + Erc1155SafeTransferFromAbiParameters, Erc20Methods, - Erc20TransferAbi, Erc20TransferAbiParameters, - Erc721SafeTransferFromAbiParameters, - Erc721TransferAbi, - TransferFromAbiParameters + Erc721Methods, + Erc721SafeTransferFromAbiParameters } from './methodId' -export function decodeAbiParametersWrapper( - params: TParams, - data: Hex -): TReturnType { - return decodeAbiParameters(params, data) as unknown as TReturnType +export const isString = (value: unknown): value is string => { + return typeof value === 'string' } -// Todo: How can we typesafe the return value of decodeAbiParameters, without -// re-doing a lot of the work that is already done in viem -export const extractErc20TransferAmount = (data: Hex): string => { - try { - const paramValues = decodeAbiParameters(Erc20TransferAbiParameters, data) - - const amount = paramValues[1] - if (!amount) throw new Error('Malformed transaction request') - return amount.toString() - } catch (error) { - // TODO (@Pierre, 18/01/24): Revisit the error handling. - throw new Error('Malformed transaction request') +export const isBigInt = (value: unknown): value is bigint => { + return typeof value === 'bigint' +} + +export const assertBigInt = (value: unknown): bigint => { + if (isBigInt(value)) { + return value + } + throw new Error('Value is not a bigint') +} + +function isHex(value: unknown): value is Hex { + return isString(value) && value.startsWith('0x') +} + +export const assertHexString = (value: unknown): Hex => { + if (isHex(value)) { + return value + } + throw new Error('Value is not a hex string') +} + +function assertString(value: unknown): string { + if (isString(value)) { + return value + } + throw new Error('Value is not a string') +} + +// Checks if a value is a number +export function isNumber(value: unknown): value is number { + return typeof value === 'number' && !isNaN(value) +} + +export const assertNumber = (value: unknown): number => { + if (isNumber(value)) { + return value } + throw new Error('Value is not a number') } -export const extractErc20TransferFromAmount = (data: Hex): string => { - const paramValues = decodeAbiParameters(TransferFromAbiParameters, data) +// Checks if a value is a boolean +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean' +} + +export const assertBoolean = (value: unknown): boolean => { + if (isBoolean(value)) { + return value + } + throw new Error('Value is not a boolean') +} - const amount = paramValues[2] - if (!amount) throw new Error('Malformed transaction request') - return amount.toString() +// Checks if a value is an array +export function isArray(value: unknown): value is Array { + return Array.isArray(value) } -export const extractErc20Amount = (data: Hex, methodId: string): string => { - if (!(methodId in Erc20TransferAbi)) { - throw new Error('Invalid methodId') +type AssertType = 'string' | 'bigint' | 'number' | 'boolean' | 'hex' + +export const assertArray = (value: unknown, type: AssertType): T[] => { + if (!Array.isArray(value)) { + throw new Error('Value is not an array') + } + switch (type) { + case 'string': { + return value.map(assertString) as T[] + } + case 'bigint': { + return value.map(assertBigInt) as T[] + } + case 'number': { + return value.map(assertNumber) as T[] + } + case 'boolean': { + return value.map(assertBoolean) as T[] + } + case 'hex': { + return value.map(assertHexString) as T[] + } + default: { + return value + } } +} - switch (methodId) { - case Erc20Methods.TRANSFER: - return extractErc20TransferAmount(data) - case Erc20Methods.TRANSFER_FROM: - return extractErc20TransferFromAmount(data) - default: - throw new Error('Invalid methodId') +export const handleError = (context: object, message: string, e?: unknown): never => { + throw new TransactionRequestIntentError({ + message, + status: 400, + context: { ...context, error: e } + }) +} + +type TransferParams = { + recipient: Hex + amount: string +} +export const isTransferParams = (params: unknown): params is TransferParams => { + return ( + typeof params === 'object' && + params !== null && + 'recipient' in params && + 'amount' in params && + isString(params.recipient) && + isString(params.amount) + ) +} +export const extractTransferParamValues = (data: Hex, methodId: Hex): TransferParams => { + const paramValues = decodeAbiParameters(Erc20TransferAbiParameters, data) + try { + const recipient = assertHexString(paramValues[0]) + const amount = assertBigInt(paramValues[1]) + return { recipient, amount: amount.toString().toLowerCase() } + } catch (e) { + return handleError({ data, methodId }, 'Invalid transfer params', e) } } -export const extractErc721AssetId = (data: Hex, methodId: string): string => { - if (!(methodId in Erc721TransferAbi)) { - throw new Error('Invalid methodId') +type TransferFromParams = { + sender: Hex + recipient: Hex + amount: string +} +export const isTransferFromParams = (params: unknown): params is TransferFromParams => { + return ( + typeof params === 'object' && + params !== null && + 'sender' in params && + 'recipient' in params && + 'amount' in params && + isHex(params.sender) && + isHex(params.recipient) && + isString(params.amount) + ) +} +export const extractTransferFromParamValues = (data: Hex, methodId: Hex): TransferFromParams => { + const paramValues = decodeAbiParameters(Erc721SafeTransferFromAbiParameters, data) + try { + const sender = assertHexString(paramValues[0]) + const recipient = assertHexString(paramValues[1]) + const amount = assertBigInt(paramValues[2]) + return { sender, recipient, amount: amount.toString().toLowerCase() } + } catch (e) { + return handleError({ data, methodId }, 'Invalid transfer from params', e) } +} + +type Erc721SafeTransferFromParams = { + from: Hex + to: Hex + tokenId: bigint +} +export const isErc721SafeTransferFromParams = (params: unknown): params is Erc721SafeTransferFromParams => { + return ( + typeof params === 'object' && + params !== null && + 'from' in params && + 'to' in params && + 'tokenId' in params && + isHex(params.from) && + isHex(params.to) && + isBigInt(params.tokenId) + ) +} - // No need for specific mapping here, tokenId is always the third parameter +export const extractErc721SafeTransferFromParamValues = (data: Hex, methodId: Hex): Erc721SafeTransferFromParams => { const paramValues = decodeAbiParameters(Erc721SafeTransferFromAbiParameters, data) + try { + const from = assertHexString(paramValues[0]) + const to = assertHexString(paramValues[1]) + const tokenId = assertBigInt(paramValues[2]) + return { from, to, tokenId } + } catch (e) { + return handleError({ data, methodId }, 'Invalid erc721SafeTransferFrom params', e) + } +} + +// type SafeTransferFromWithBytesParams = { +// from: string +// to: string +// tokenId: string +// data: string +// }; +// export const isSafeTransferFromWithBytesParams = (params: unknown): params is SafeTransferFromWithBytesParams => { +// return typeof params === 'object' && +// params !== null && +// 'from' in params && +// 'to' in params && +// 'tokenId' in params && +// 'data' in params && +// isString(params.from) && +// isString(params.to) && +// isString(params.tokenId) && +// isString(params.data); +// } +// export const extractSafeTransferFromBytesParamValues = (data: Hex, methodId: Hex): SafeTransferFromWithBytesParams => { +// const paramValues = decodeAbiParameters(Erc721SafeTransferFromAbiParameters, data) +// try { +// const from = assertHexString(paramValues[0]); +// const to = assertHexString(paramValues[1]); +// const tokenId = assertBigInt(paramValues[2]); +// const data = assertString(paramValues[3]); +// return { from, to, tokenId: tokenId.toString().toLowerCase(), data }; +// } catch (e) { +// return handleError({ data, methodId }, 'Invalid sender, recipient or tokenId', e); +// } +// } + +type Erc1155SafeTransferFromParams = { + from: Hex + to: Hex + tokenId: string + amount: string + data: Hex +} +export const isErc1155SafeTransferFromParams = (params: unknown): params is Erc1155SafeTransferFromParams => { + return ( + typeof params === 'object' && + params !== null && + 'from' in params && + 'to' in params && + 'tokenId' in params && + 'amount' in params && + 'data' in params && + isHex(params.from) && + isHex(params.to) && + isString(params.tokenId) && + isString(params.amount) && + isString(params.data) + ) +} + +export const extractErc1155SafeTransferFromParamValues = (data: Hex, methodId: Hex): Erc1155SafeTransferFromParams => { + const paramValues = decodeAbiParameters(Erc1155SafeTransferFromAbiParameters, data) + try { + const from = assertHexString(paramValues[0]) + const to = assertHexString(paramValues[1]) + const tokenId = assertBigInt(paramValues[2]) + const amount = assertBigInt(paramValues[3]) + const data = assertHexString(paramValues[4]) + return { from, to, tokenId: tokenId.toString().toLowerCase(), amount: amount.toString().toLowerCase(), data } + } catch (e) { + return handleError({ data, methodId }, 'Invalid erc1155safeTransferParams', e) + } +} + +type SafeBatchTransferFromParams = { + from: Hex + to: Hex + tokenIds: string[] + amounts: string[] + data: Hex +} +export const isSafeBatchTransferFromParams = (params: unknown): params is SafeBatchTransferFromParams => { + return ( + typeof params === 'object' && + params !== null && + 'from' in params && + 'to' in params && + 'tokenIds' in params && + 'amounts' in params && + 'data' in params && + isHex(params.from) && + isHex(params.to) && + isArray(params.tokenIds) && + isArray(params.amounts) && + isHex(params.data) + ) +} +export const extractSafeBatchTransferFromParamValues = (data: Hex, methodId: Hex): SafeBatchTransferFromParams => { + const paramValues = decodeAbiParameters(Erc1155SafeBatchTransferFromAbiParameters, data) + try { + const from = assertHexString(paramValues[0]) + const to = assertHexString(paramValues[1]) + const tokenIds = assertArray(paramValues[2], 'bigint') + const amounts = assertArray(paramValues[3], 'bigint') + const data = assertHexString(paramValues[4]) + const stringAmounts = amounts.map((amount) => amount.toString().toLowerCase()) + const stringTokenIds = tokenIds.map((tokenId) => tokenId.toString().toLowerCase()) + return { from, to, tokenIds: stringTokenIds, amounts: stringAmounts, data } + } catch (e) { + return handleError({ data, methodId }, 'Invalid safeBatchtransferParams', e) + } +} - if (!paramValues[2]) throw new Error('Malformed transaction request') - return paramValues[2].toString() +export const extractors = { + [Erc20Methods.TRANSFER]: extractTransferParamValues, + [AmbiguousMethods.TRANSFER_FROM]: extractTransferFromParamValues, + [Erc721Methods.SAFE_TRANSFER_FROM]: extractErc721SafeTransferFromParamValues, + [Erc721Methods.SAFE_TRANSFER_FROM_WITH_BYTES]: extractErc721SafeTransferFromParamValues, + [Erc1155Methods.SAFE_TRANSFER_FROM]: extractErc1155SafeTransferFromParamValues, + [Erc1155Methods.SAFE_TRANSFER_FROM_WITH_BYTES]: extractErc1155SafeTransferFromParamValues, + [Erc1155Methods.SAFE_BATCH_TRANSFER_FROM]: extractSafeBatchTransferFromParamValues } diff --git a/packages/transaction-request-intent/src/lib/types.ts b/packages/transaction-request-intent/src/lib/types.ts index 475dc0e66..64d1ee867 100644 --- a/packages/transaction-request-intent/src/lib/types.ts +++ b/packages/transaction-request-intent/src/lib/types.ts @@ -1,7 +1,7 @@ import { TransactionRequest } from '@narval/authz-shared' import { Address, Hex, TypedDataDomain, TypedData as TypedDataParams } from 'viem' import { Caip10 } from './caip' -import { AssetTypeEnum, InputType, Intents, TransactionStatus } from './domain' +import { AssetTypeEnum, InputType, Intents, TransactionCategory, TransactionStatus } from './domain' import { Intent } from './intent.types' export type Message = { @@ -61,15 +61,70 @@ export type TransactionInput = { transactionRegistry?: TransactionRegistry } -export type DecodeErc20Input = { +export type DecodeTransferInput = { to: Hex from: Hex data: Hex chainId: number - methodId: string + methodId: Hex + nonce: number } -type ValidatedInput = DecodeErc20Input +export type NativeTransferInput = { + to: Hex + from: Hex + value: Hex + chainId: number + nonce: number +} + +export type ContractDeploymentInput = { + from: Hex + data: Hex + chainId: number + nonce: number +} + +export type ValidatedInput = DecodeTransferInput | NativeTransferInput | ContractDeploymentInput +export type Validator = (txRequest: TransactionRequest, methodId: Hex) => ValidatedInput +export type ValidatorRegistry = { + [key in TransactionCategory]: Validator +} + +export type ContractInteractionDecoder = (input: DecodeTransferInput) => Intent +export type NativeTransferDecoder = (input: NativeTransferInput) => Intent +export type ContractDeploymentDecoder = (input: ContractDeploymentInput) => Intent + +export type NativeTransferIntents = Intents.TRANSFER_NATIVE +export type ContractInteractionIntents = + | Intents.RETRY_TRANSACTION + | Intents.CANCEL_TRANSACTION + | Intents.TRANSFER_ERC20 + | Intents.TRANSFER_ERC721 + | Intents.TRANSFER_ERC1155 + | Intents.CALL_CONTRACT + | Intents.APPROVE_TOKEN_ALLOWANCE +export type ContractCreationIntents = + | Intents.DEPLOY_CONTRACT + | Intents.DEPLOY_ERC_4337_WALLET + | Intents.DEPLOY_SAFE_WALLET +export type TransactionIntents = NativeTransferIntents | ContractInteractionIntents | ContractCreationIntents + +export type ContractInteractionDecoders = { + [key in ContractInteractionIntents]: ContractInteractionDecoder +} +export type ContractCreationDecoders = { + [key in ContractCreationIntents]: ContractDeploymentDecoder +} +export type NativeTransferDecoders = { + [key in NativeTransferIntents]: NativeTransferDecoder +} + +export type TransactionDecodersRegistry = { + [TransactionCategory.NATIVE_TRANSFER]: NativeTransferDecoders + [TransactionCategory.CONTRACT_INTERACTION]: ContractInteractionDecoders + [TransactionCategory.CONTRACT_CREATION]: ContractCreationDecoders +} export type Decoder = (input: ValidatedInput) => Intent export type DecoderRegistry = {