diff --git a/packages/app/src/composables/useTransactionData.ts b/packages/app/src/composables/useTransactionData.ts index 8a110b283..e0de9d5c6 100644 --- a/packages/app/src/composables/useTransactionData.ts +++ b/packages/app/src/composables/useTransactionData.ts @@ -9,23 +9,35 @@ import useContractABI from "./useContractABI"; import type { AbiFragment } from "./useAddress"; import type { InputType } from "./useEventLog"; import type { Address } from "@/types"; +import type { ParamType, Result } from "ethers"; const defaultAbiCoder: AbiCoder = AbiCoder.defaultAbiCoder(); +export enum InputComponentType { + TUPLE = "tuple", + ARRAY = "array", + BASE = "base", +} + +export type MethodData = { + name: string; + inputs: InputData[]; +}; + +export type InputData = { + name: string; + type: InputType | string; + value: string; + inputs: InputData[]; + inputComponentType: InputComponentType; + encodedValue: string; +}; export type TransactionData = { calldata: string; contractAddress: Address; value: string; sighash: string; - method?: { - name: string; - inputs: { - name: string; - type: InputType; - value: string; - encodedValue: string; - }[]; - }; + method?: MethodData; }; export function decodeDataWithABI( @@ -33,7 +45,6 @@ export function decodeDataWithABI( abi: AbiFragment[] ): TransactionData["method"] | undefined { const contractInterface = new Interface(abi); - try { const decodedData = contractInterface.parseTransaction({ data: transactionData.calldata, @@ -41,18 +52,94 @@ export function decodeDataWithABI( })!; return { name: decodedData.name, - inputs: decodedData.fragment.inputs.map((input) => ({ - name: input.name, - type: input.type as InputType, - value: decodedData.args[input.name]?.toString(), - encodedValue: defaultAbiCoder.encode([input.type], [decodedData.args[input.name]]).split("0x")[1], - })), + inputs: decodedData.fragment.inputs.flatMap((input) => decodeInputData(input, decodedData.args[input.name])), }; } catch { return undefined; } } +export function decodeInputData(input: ParamType, args: Result): InputData[] { + if (!input) { + throw new Error("input_is_null"); + } + + if (input.isArray()) { + return decodeArrayInputData(input, args); + } + + if (input.isTuple()) { + return decodeTupleInputData(input, args); + } + + return [ + { + name: input.name, + type: input.type as InputType, + value: args.toString(), + encodedValue: defaultAbiCoder.encode([input.type], [args]).split("0x")[1], + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + ]; +} + +function decodeArrayInputData(input: ParamType, args: Result): InputData[] { + const inputs = args.flatMap((arg) => decodeInputData(input.arrayChildren!, arg)); + + return [ + { + name: input.name, + type: `${inputs[0]?.type ? `${inputs[0]?.type}[]` : input.type}`, + value: joinArrayValues( + inputs.map((input) => input.value), + "[", + "]" + ), + inputs: inputs, + encodedValue: joinArrayValues( + inputs.map((input) => input.encodedValue), + "[", + "]" + ), + inputComponentType: InputComponentType.ARRAY, + }, + ]; +} + +function decodeTupleInputData(input: ParamType, args: Result): InputData[] { + const inputs = input.components!.flatMap((component: ParamType, index: number) => + decodeInputData(component, args[index]) + ); + + return [ + { + name: input.name, + type: joinArrayValues( + inputs.map((input) => input.type), + "tuple(", + ")" + ), + value: joinArrayValues( + inputs.map((input) => input.value), + "(", + ")" + ), + inputs, + encodedValue: joinArrayValues( + inputs.map((input) => input.encodedValue), + "(", + ")" + ), + inputComponentType: InputComponentType.TUPLE, + }, + ]; +} + +function joinArrayValues(inputs: string[], prefix = "", suffix = ""): string { + return `${prefix}${inputs.join(",")}${suffix}`; +} + export default (context = useContext()) => { const { collection: ABICollection, diff --git a/packages/app/tests/composables/useTransactionData.spec.ts b/packages/app/tests/composables/useTransactionData.spec.ts index 0d6b921bb..f988cf9bb 100644 --- a/packages/app/tests/composables/useTransactionData.spec.ts +++ b/packages/app/tests/composables/useTransactionData.spec.ts @@ -1,14 +1,21 @@ import { describe, expect, it, type SpyInstance, vi } from "vitest"; +import { ParamType } from "ethers"; import { $fetch, FetchError } from "ohmyfetch"; -import useTransactionData, { decodeDataWithABI, type TransactionData } from "@/composables/useTransactionData"; +import useTransactionData, { + decodeDataWithABI, + decodeInputData, + InputComponentType, + type TransactionData, +} from "@/composables/useTransactionData"; import ERC20ProxyVerificationInfo from "@/../mock/contracts/ERC20ProxyVerificationInfo.json"; import ERC20VerificationInfo from "@/../mock/contracts/ERC20VerificationInfo.json"; import type { AbiFragment } from "@/composables/useAddress"; import type { Address } from "@/types"; +import type { Result } from "ethers"; vi.mock("ohmyfetch", () => { return { @@ -53,12 +60,16 @@ const transactionDataDecodedMethod = { inputs: [ { name: "recipient", + inputs: [], + inputComponentType: InputComponentType.BASE, type: "address", value: "0xa1cf087DB965Ab02Fb3CFaCe1f5c63935815f044", encodedValue: "000000000000000000000000a1cf087db965ab02fb3cface1f5c63935815f044", }, { name: "amount", + inputs: [], + inputComponentType: InputComponentType.BASE, type: "uint256", value: "1", encodedValue: "0000000000000000000000000000000000000000000000000000000000000001", @@ -175,12 +186,16 @@ describe("useTransactionData:", () => { inputs: [ { encodedValue: "000000000000000000000000a1cf087db965ab02fb3cface1f5c63935815f044", + inputs: [], + inputComponentType: InputComponentType.BASE, name: "recipient", type: "address", value: "0xa1cf087DB965Ab02Fb3CFaCe1f5c63935815f044", }, { encodedValue: "0000000000000000000000000000000000000000000000000000000000000001", + inputs: [], + inputComponentType: InputComponentType.BASE, name: "amount", type: "uint256", value: "1", @@ -199,4 +214,177 @@ describe("useTransactionData:", () => { expect(result).toBe(undefined); }); }); + + describe("decodeInputData", () => { + it("should decode a simple input type", () => { + const input = ParamType.from({ + name: "value", + type: "uint256", + baseType: "scalar", + isArray: () => true, + isTuple: () => false, + }); + const args = 42; + + const result = decodeInputData(input, args as unknown as Result); + + expect(result).toEqual([ + { + name: "value", + type: "uint256", + value: "42", + encodedValue: expect.any(String), + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + ]); + }); + + it("should decode an array input type", () => { + const input = ParamType.from({ + name: "values", + baseType: "array", + type: "uint256[]", + arrayChildren: { name: "value", type: "uint256" }, + }); + const args = [42, 43]; + + const result = decodeInputData(input, args as unknown as Result); + + expect(result).toEqual([ + { + name: "values", + type: "uint256[]", + value: "[42,43]", + inputs: [ + { + name: "", + type: "uint256", + value: "42", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002a", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + { + name: "", + type: "uint256", + value: "43", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002b", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + ], + encodedValue: + "[000000000000000000000000000000000000000000000000000000000000002a,000000000000000000000000000000000000000000000000000000000000002b]", + inputComponentType: InputComponentType.ARRAY, + }, + ]); + }); + + it("should decode a tuple input type", () => { + const input = ParamType.from({ + name: "tupleValue", + type: "tuple", + baseType: "tuple", + components: [ + { name: "value1", type: "uint256", baseType: "scalar" }, + { name: "value2", type: "string", baseType: "scalar" }, + ], + }); + const args = ["42", "test"]; + + const result = decodeInputData(input, args as unknown as Result); + expect(result).toEqual([ + { + name: "tupleValue", + type: "tuple(uint256,string)", + value: "(42,test)", + inputs: [ + { + name: "value1", + type: "uint256", + value: "42", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002a", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + { + name: "value2", + type: "string", + value: "test", + encodedValue: + "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + ], + encodedValue: + "(000000000000000000000000000000000000000000000000000000000000002a,000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000)", + inputComponentType: InputComponentType.TUPLE, + }, + ]); + }); + + it("should decode a tuple with an array", () => { + const input = ParamType.from({ + name: "tupleValue", + type: "tuple", + baseType: "tuple", + components: [ + { name: "value1", type: "uint256", baseType: "scalar" }, + { name: "value2", type: "uint256[]", baseType: "array", arrayChildren: { name: "value", type: "uint256" } }, + ], + }); + const args = [42, [43, 44]]; + + const result = decodeInputData(input, args as unknown as Result); + + expect(result).toEqual([ + { + name: "tupleValue", + type: "tuple(uint256,uint256[])", + value: "(42,[43,44])", + inputs: [ + { + name: "value1", + type: "uint256", + value: "42", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002a", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + { + name: "value2", + type: "uint256[]", + value: "[43,44]", + inputs: [ + { + name: "", + type: "uint256", + value: "43", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002b", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + { + name: "", + type: "uint256", + value: "44", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002c", + inputs: [], + inputComponentType: InputComponentType.BASE, + }, + ], + encodedValue: + "[000000000000000000000000000000000000000000000000000000000000002b,000000000000000000000000000000000000000000000000000000000000002c]", + inputComponentType: InputComponentType.ARRAY, + }, + ], + encodedValue: + "(000000000000000000000000000000000000000000000000000000000000002a,[000000000000000000000000000000000000000000000000000000000000002b,000000000000000000000000000000000000000000000000000000000000002c])", + inputComponentType: InputComponentType.TUPLE, + }, + ]); + }); + }); });