diff --git a/src/__tests__/abi.test.ts b/src/__tests__/abi.test.ts new file mode 100644 index 0000000..b9c8b79 --- /dev/null +++ b/src/__tests__/abi.test.ts @@ -0,0 +1,258 @@ +import { expect, describe, it } from "vitest"; + +import { ABI, fillEmptyNames } from "../abi"; + +describe("fillEmptyNames", () => { + type testCase = { + name: string; + abi: Array; + want: Array; + }; + + const testCases: testCase[] = [ + { + name: "Basic test", + abi: [ + { + type: "function", + selector: "0x95d376d7", + payable: false, + stateMutability: "payable", + inputs: [ + { + type: "tuple", + name: "", + components: [ + { type: "uint32", name: "" }, + { type: "bytes", name: "" }, + { type: "bytes32", name: "" }, + { type: "uint64", name: "" }, + { type: "address", name: "" }, + ], + }, + { type: "bytes", name: "" }, + ], + outputs: [ + { + type: "tuple", + name: "", + components: [ + { type: "uint32", name: "" }, + { type: "bytes", name: "" }, + { type: "bytes32", name: "" }, + { type: "uint64", name: "" }, + { type: "address", name: "" }, + ], + }, + { type: "bytes", name: "" }, + ], + sig: "assignJob((uint32,bytes,bytes32,uint64,address),bytes)", + name: "assignJob", + constant: false, + }, + ], + want: [ + { + type: "function", + selector: "0x95d376d7", + payable: false, + stateMutability: "payable", + inputs: [ + { + type: "tuple", + name: "", + components: [ + { type: "uint32", name: "_param0" }, + { type: "bytes", name: "_param1" }, + { type: "bytes32", name: "_param2" }, + { type: "uint64", name: "_param3" }, + { type: "address", name: "_param4" }, + ], + }, + { type: "bytes", name: "" }, + ], + outputs: [ + { + type: "tuple", + name: "", + components: [ + { type: "uint32", name: "_param0" }, + { type: "bytes", name: "_param1" }, + { type: "bytes32", name: "_param2" }, + { type: "uint64", name: "_param3" }, + { type: "address", name: "_param4" }, + ], + }, + { type: "bytes", name: "" }, + ], + sig: "assignJob((uint32,bytes,bytes32,uint64,address),bytes)", + name: "assignJob", + constant: false, + }, + ], + }, + { + name: "Nested tuple test", + abi: [ + { + name: "test", + selector: "0x12345679", + inputs: [ + { + name: "", + type: "tuple", + components: [ + { + type: "tuple", + name: "", + components: [ + { + type: "tuple", + name: "", + components: [ + { type: "uint256", name: "" }, + { type: "address", name: "x" }, + ], + }, + { + type: "tuple", + name: "n1", + components: [ + { type: "uint256", name: "y" }, + { type: "address", name: "" }, + ], + }, + ], + }, + ], + }, + ], + stateMutability: "view", + type: "function", + }, + ], + want: [ + { + name: "test", + selector: "0x12345679", + inputs: [ + { + name: "", + type: "tuple", + components: [ + { + type: "tuple", + name: "_param0", + components: [ + { + type: "tuple", + name: "_param0", + components: [ + { + type: "uint256", + name: "_param0", + }, + { type: "address", name: "x" }, + ], + }, + { + type: "tuple", + name: "n1", + components: [ + { type: "uint256", name: "y" }, + { + type: "address", + name: "_param1", + }, + ], + }, + ], + }, + ], + }, + ], + stateMutability: "view", + type: "function", + }, + ], + }, + { + name: "Other Tuple types: Tuple[] and Tuple[k]", + abi: [ + { + name: "test", + selector: "0x12345679", + inputs: [ + { + name: "", + type: "tuple", + components: [ + { + name: "", + type: "tuple[]", + components: [ + { name: "", type: "uint256" }, + { name: "", type: "uint256" }, + ], + }, + { + name: "", + type: "tuple[2]", + components: [ + { name: "", type: "uint256" }, + { name: "", type: "uint256" }, + ], + }, + ], + }, + ], + stateMutability: "view", + type: "function", + }, + ], + want: [ + { + name: "test", + selector: "0x12345679", + inputs: [ + { + name: "", + type: "tuple", + components: [ + { + name: "_param0", + type: "tuple[]", + components: [ + { name: "_param0", type: "uint256" }, + { name: "_param1", type: "uint256" }, + ], + }, + { + name: "_param1", + type: "tuple[2]", + components: [ + { name: "_param0", type: "uint256" }, + { name: "_param1", type: "uint256" }, + ], + }, + ], + }, + ], + stateMutability: "view", + type: "function", + }, + ], + }, + { + name: "Empty ABI", + abi: [], + want: [], + }, + ]; + + testCases.forEach((tc) => { + it(tc.name, () => { + expect(fillEmptyNames(tc.abi as ABI)).toStrictEqual(tc.want); + }); + }); +}); diff --git a/src/abi.ts b/src/abi.ts index 995e177..d3dcd1d 100644 --- a/src/abi.ts +++ b/src/abi.ts @@ -4,8 +4,8 @@ export type ABIFunction = { type: "function"; // TODO: constructor, receive, fallback selector: string; name?: string; - outputs?: {type: string, length?: number, name: string}[]; - inputs?: {type: string, name: string}[]; + outputs?: ABIOutput[]; + inputs?: ABIInput[]; sig?: string; sigAlts?: string[]; payable?: boolean; @@ -21,4 +21,114 @@ export type ABIEvent = { // TODO: ... }; +export type ABIInput = { + type: string; + name: string; + length?: number; + components?: ABIInOut[]; +} + +export type ABIOutput = { + type: string; + name: string; + components?: ABIInOut[]; +} + +export type ABIInOut = ABIInput|ABIOutput; + export type ABI = (ABIFunction|ABIEvent)[]; + +/** + * Fills tuple component's empty names in an ABI with generated names + * + * @example + * Input: { + * "type": "function", + * "selector": "0x95d376d7", + * "payable": false, + * "stateMutability": "payable", + * "inputs": [ + * { + * "type": "tuple", + * "name": "", + * "components": [ + * { "type": "uint32", "name": "" }, + * { "type": "bytes", "name": "" }, + * { "type": "bytes32", "name": "" }, + * { "type": "uint64", "name": "" }, + * { "type": "address", "name": "" } + * ] + * }, + * { "type": "bytes", "name": "" } + * ], + * "sig": "assignJob((uint32,bytes,bytes32,uint64,address),bytes)", + * "name": "assignJob", + * "constant": false + * } + * + * Output: { + * "type": "function", + * "selector": "0x95d376d7", + * "payable": false, + * "stateMutability": "payable", + * "inputs": [ + * { + * "type": "tuple", + * "name": "", + * "components": [ + * { "type": "uint32", "name": "_param0" }, + * { "type": "bytes", "name": "_param1" }, + * { "type": "bytes32", "name": "_param2" }, + * { "type": "uint64", "name": "_param3" }, + * { "type": "address", "name": "_param4" } + * ] + * }, + * { "type": "bytes", "name": "" } + * ], + * "sig": "assignJob((uint32,bytes,bytes32,uint64,address),bytes)", + * "name": "assignJob", + * "constant": false + * } + * + * @param abi The ABI to process + * @returns A new ABI with tuple component names filled + */ +export function fillEmptyNames(abi: ABI): ABI { + function processComponents(components: ABIInOut[]): void { + components.forEach((component, index) => { + component.name ||= `_param${index}`; + if (isTupleType(component.type) && component.components) { + processComponents(component.components); + } + }); + } + + const result: ABI = abi.map((item) => { + if (item.type === "function") { + const func: ABIFunction = { ...item }; + func.inputs?.forEach((input) => { + if (isTupleType(input.type) && input.components) { + processComponents(input.components); + } + }); + func.outputs?.forEach((output) => { + if (isTupleType(output.type) && output.components) { + processComponents(output.components); + } + }); + return func; + } + return item; + }); + + return result; +} + +/** + * Checks if a type is a tuple type (e.g. "tuple", "tuple[]", "tuple[2]") + * @param type type to check + * @returns true if the type is a tuple type + */ +function isTupleType(type: string): boolean { + return type.startsWith("tuple"); +} \ No newline at end of file diff --git a/src/whatsabi.ts b/src/whatsabi.ts index 3747ad0..04ec134 100644 --- a/src/whatsabi.ts +++ b/src/whatsabi.ts @@ -26,5 +26,8 @@ export { proxies }; import * as providers from "./providers.js"; export { providers }; +import * as abi from "./abi.js"; +export { abi }; + import * as errors from "./errors.js"; export { errors };