Repository: shazow/whatsabi
Pull request #150 (2024-11-07 06:33:26)

autoload: Add `abiFillEmptyNames` helper
This PR addresses #148 

Changes overview:
- Added the `abiFillEmptyNames` helper function and necessary types
- Added the `abiHelpers` option to the `AutoloadConfig`


Patch:
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 };


Test patch:
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<ABI[number] | unknown>;
+    want: Array<ABI[number] | unknown>;
+  };
+
+  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);
+    });
+  });
+});