diff --git a/package.json b/package.json index 855de022..b52f9d46 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "size-limit": "^11.1.4", "ts-morph": "^24.0.0", "tsx": "^4.17.0", - "typescript": "^5.5.4", + "typescript": "5.7.3", "typescript-eslint": "^8.1.0", "viem": "^2.21.9", "vite-tsconfig-paths": "^5.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0294973..f64dbd4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: devDependencies: '@ark/attest': specifier: ^0.16.0 - version: 0.16.0(typescript@5.5.4) + version: 0.16.0(typescript@5.7.3) '@biomejs/biome': specifier: ^1.8.3 version: 1.8.3 @@ -91,7 +91,7 @@ importers: version: 11.2.0 knip: specifier: ^5.27.2 - version: 5.27.2(@types/node@22.5.4)(typescript@5.5.4) + version: 5.27.2(@types/node@22.5.4)(typescript@5.7.3) prool: specifier: ^0.0.17 version: 0.0.17 @@ -114,26 +114,26 @@ importers: specifier: ^4.17.0 version: 4.17.0 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: 5.7.3 + version: 5.7.3 typescript-eslint: specifier: ^8.1.0 - version: 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) + version: 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) viem: specifier: ^2.21.9 - version: 2.21.9(typescript@5.5.4)(zod@3.23.8) + version: 2.21.9(typescript@5.7.3)(zod@3.23.8) vite-tsconfig-paths: specifier: ^5.0.1 - version: 5.0.1(typescript@5.5.4)(vite@6.1.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.5)(tsx@4.17.0)(yaml@2.5.0)) + version: 5.0.1(typescript@5.7.3)(vite@6.1.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.5)(tsx@4.17.0)(yaml@2.5.0)) vitest: specifier: 1.0.4 version: 1.0.4(@types/node@22.5.4)(lightningcss@1.29.1)(terser@5.31.5) vocs: specifier: 1.0.0-alpha.62 - version: 1.0.0-alpha.62(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(typescript@5.5.4) + version: 1.0.0-alpha.62(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(typescript@5.7.3) web3: specifier: ^4.12.1 - version: 4.12.1(typescript@5.5.4)(zod@3.23.8) + version: 4.12.1(typescript@5.7.3)(zod@3.23.8) examples/webauthn-p256: dependencies: @@ -185,7 +185,7 @@ importers: version: 19.0.0(react@19.0.0) vocs: specifier: 1.0.0-next.20250217T012932 - version: 1.0.0-next.20250217T012932(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(jiti@2.4.2)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.5.0) + version: 1.0.0-next.20250217T012932(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(jiti@2.4.2)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(tsx@4.17.0)(typescript@5.7.3)(yaml@2.5.0) src: dependencies: @@ -206,7 +206,7 @@ importers: version: 1.4.0 abitype: specifier: ^1.0.6 - version: 1.0.6(typescript@5.5.4)(zod@3.23.8) + version: 1.0.6(typescript@5.7.3)(zod@3.23.8) eventemitter3: specifier: 5.0.1 version: 5.0.1 @@ -5850,6 +5850,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + ua-parser-js@1.0.39: resolution: {integrity: sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==} hasBin: true @@ -6408,16 +6413,16 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@ark/attest@0.16.0(typescript@5.5.4)': + '@ark/attest@0.16.0(typescript@5.7.3)': dependencies: '@ark/fs': 0.7.0 '@ark/util': 0.7.0 '@prettier/sync': 0.5.2(prettier@3.3.3) '@typescript/analyze-trace': 0.10.1 - '@typescript/vfs': 1.6.0(typescript@5.5.4) + '@typescript/vfs': 1.6.0(typescript@5.7.3) arktype: 2.0.0-rc.5 prettier: 3.3.3 - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -6477,7 +6482,7 @@ snapshots: '@babel/traverse': 7.26.9 '@babel/types': 7.26.9 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6656,7 +6661,7 @@ snapshots: '@babel/parser': 7.26.9 '@babel/template': 7.26.9 '@babel/types': 7.26.9 - debug: 4.3.6 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7412,7 +7417,7 @@ snapshots: resolve: 1.22.8 semver: 7.5.4 source-map: 0.6.1 - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - '@types/node' @@ -8382,20 +8387,20 @@ snapshots: '@shikijs/core': 2.4.1 '@shikijs/types': 2.4.1 - '@shikijs/twoslash@1.22.0(typescript@5.5.4)': + '@shikijs/twoslash@1.22.0(typescript@5.7.3)': dependencies: '@shikijs/core': 1.22.0 '@shikijs/types': 1.22.0 - twoslash: 0.2.12(typescript@5.5.4) + twoslash: 0.2.12(typescript@5.7.3) transitivePeerDependencies: - supports-color - typescript - '@shikijs/twoslash@2.4.1(typescript@5.5.4)': + '@shikijs/twoslash@2.4.1(typescript@5.7.3)': dependencies: '@shikijs/core': 2.4.1 '@shikijs/types': 2.4.1 - twoslash: 0.2.12(typescript@5.5.4) + twoslash: 0.2.12(typescript@5.7.3) transitivePeerDependencies: - supports-color - typescript @@ -8664,34 +8669,34 @@ snapshots: '@types/node': 22.5.4 optional: true - '@typescript-eslint/eslint-plugin@8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/parser': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.1.0 - '@typescript-eslint/type-utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/type-utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.1.0 eslint: 9.9.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.7.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4)': + '@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.1.0 '@typescript-eslint/types': 8.1.0 - '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.1.0 debug: 4.3.6 eslint: 9.9.0(jiti@2.4.2) optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -8700,41 +8705,41 @@ snapshots: '@typescript-eslint/types': 8.1.0 '@typescript-eslint/visitor-keys': 8.1.0 - '@typescript-eslint/type-utils@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.7.3) + '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.3.6 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.7.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - eslint - supports-color '@typescript-eslint/types@8.1.0': {} - '@typescript-eslint/typescript-estree@8.1.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.1.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 8.1.0 '@typescript-eslint/visitor-keys': 8.1.0 - debug: 4.3.6 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) + ts-api-utils: 1.3.0(typescript@5.7.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4)': + '@typescript-eslint/utils@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.1.0 '@typescript-eslint/types': 8.1.0 - '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.1.0(typescript@5.7.3) eslint: 9.9.0(jiti@2.4.2) transitivePeerDependencies: - supports-color @@ -8756,10 +8761,10 @@ snapshots: treeify: 1.1.0 yargs: 16.2.0 - '@typescript/vfs@1.6.0(typescript@5.5.4)': + '@typescript/vfs@1.6.0(typescript@5.7.3)': dependencies: debug: 4.3.6 - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -9075,20 +9080,20 @@ snapshots: '@xtuc/long@4.2.2': {} - abitype@0.7.1(typescript@5.5.4)(zod@3.23.8): + abitype@0.7.1(typescript@5.7.3)(zod@3.23.8): dependencies: - typescript: 5.5.4 + typescript: 5.7.3 optionalDependencies: zod: 3.23.8 - abitype@1.0.5(typescript@5.5.4)(zod@3.23.8): + abitype@1.0.5(typescript@5.7.3)(zod@3.23.8): optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 zod: 3.23.8 - abitype@1.0.6(typescript@5.5.4)(zod@3.23.8): + abitype@1.0.6(typescript@5.7.3)(zod@3.23.8): optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 zod: 3.23.8 accepts@1.3.8: @@ -10746,7 +10751,7 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@5.27.2(@types/node@22.5.4)(typescript@5.5.4): + knip@5.27.2(@types/node@22.5.4)(typescript@5.7.3): dependencies: '@nodelib/fs.walk': 1.2.8 '@snyk/github-codeowners': 1.1.0 @@ -10763,7 +10768,7 @@ snapshots: smol-toml: 1.3.0 strip-json-comments: 5.0.1 summary: 2.1.0 - typescript: 5.5.4 + typescript: 5.7.3 zod: 3.23.8 zod-validation-error: 3.3.1(zod@3.23.8) @@ -11405,7 +11410,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.6 + debug: 4.4.0 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.1 @@ -12719,9 +12724,9 @@ snapshots: trough@2.2.0: {} - ts-api-utils@1.3.0(typescript@5.5.4): + ts-api-utils@1.3.0(typescript@5.7.3): dependencies: - typescript: 5.5.4 + typescript: 5.7.3 ts-interface-checker@0.1.13: {} @@ -12730,9 +12735,9 @@ snapshots: '@ts-morph/common': 0.25.0 code-block-writer: 13.0.3 - tsconfck@3.1.1(typescript@5.5.4): + tsconfck@3.1.1(typescript@5.7.3): optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 tslib@2.4.0: {} @@ -12749,11 +12754,11 @@ snapshots: twoslash-protocol@0.2.12: {} - twoslash@0.2.12(typescript@5.5.4): + twoslash@0.2.12(typescript@5.7.3): dependencies: - '@typescript/vfs': 1.6.0(typescript@5.5.4) + '@typescript/vfs': 1.6.0(typescript@5.7.3) twoslash-protocol: 0.2.12 - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -12763,19 +12768,21 @@ snapshots: type-detect@4.1.0: {} - typescript-eslint@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4): + typescript-eslint@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/parser': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) - '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 8.1.0(@typescript-eslint/parser@8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.1.0(eslint@9.9.0(jiti@2.4.2))(typescript@5.7.3) optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - eslint - supports-color typescript@5.5.4: {} + typescript@5.7.3: {} + ua-parser-js@1.0.39: {} ua-parser-js@1.0.40: {} @@ -12944,19 +12951,19 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - viem@2.21.9(typescript@5.5.4)(zod@3.23.8): + viem@2.21.9(typescript@5.7.3)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.10.0 '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 '@scure/bip32': 1.4.0 '@scure/bip39': 1.4.0 - abitype: 1.0.5(typescript@5.5.4)(zod@3.23.8) + abitype: 1.0.5(typescript@5.7.3)(zod@3.23.8) isows: 1.0.4(ws@8.17.1) webauthn-p256: 0.0.5 ws: 8.17.1 optionalDependencies: - typescript: 5.5.4 + typescript: 5.7.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -12983,7 +12990,7 @@ snapshots: vite-node@1.6.0(@types/node@22.5.4)(lightningcss@1.29.1)(terser@5.31.5): dependencies: cac: 6.7.14 - debug: 4.3.6 + debug: 4.4.0 pathe: 1.1.2 picocolors: 1.1.0 vite: 5.4.10(@types/node@22.5.4)(lightningcss@1.29.1)(terser@5.31.5) @@ -13016,11 +13023,11 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.0.1(typescript@5.5.4)(vite@6.1.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.5)(tsx@4.17.0)(yaml@2.5.0)): + vite-tsconfig-paths@5.0.1(typescript@5.7.3)(vite@6.1.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.5)(tsx@4.17.0)(yaml@2.5.0)): dependencies: debug: 4.3.6 globrex: 0.1.2 - tsconfck: 3.1.1(typescript@5.5.4) + tsconfck: 3.1.1(typescript@5.7.3) optionalDependencies: vite: 6.1.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.5)(tsx@4.17.0)(yaml@2.5.0) transitivePeerDependencies: @@ -13098,7 +13105,7 @@ snapshots: - supports-color - terser - vocs@1.0.0-alpha.62(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(typescript@5.5.4): + vocs@1.0.0-alpha.62(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(typescript@5.7.3): dependencies: '@floating-ui/react': 0.26.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@hono/node-server': 1.13.2(hono@4.6.10) @@ -13115,7 +13122,7 @@ snapshots: '@radix-ui/react-tabs': 1.1.1(@types/react-dom@18.3.0)(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@shikijs/rehype': 1.22.0 '@shikijs/transformers': 1.22.0 - '@shikijs/twoslash': 1.22.0(typescript@5.5.4) + '@shikijs/twoslash': 1.22.0(typescript@5.7.3) '@vanilla-extract/css': 1.16.0 '@vanilla-extract/dynamic': 2.1.2 '@vanilla-extract/vite-plugin': 3.9.5(@types/node@22.5.4)(lightningcss@1.29.1)(terser@5.31.5)(vite@5.4.10(@types/node@22.5.4)(lightningcss@1.29.1)(terser@5.31.5)) @@ -13158,7 +13165,7 @@ snapshots: shiki: 1.22.0 tailwindcss: 3.4.14 toml: 3.0.0 - twoslash: 0.2.12(typescript@5.5.4) + twoslash: 0.2.12(typescript@5.7.3) ua-parser-js: 1.0.39 unified: 11.0.5 unist-util-visit: 5.0.0 @@ -13180,7 +13187,7 @@ snapshots: - ts-node - typescript - vocs@1.0.0-next.20250217T012932(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(jiti@2.4.2)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.5.0): + vocs@1.0.0-next.20250217T012932(@types/node@22.5.4)(@types/react-dom@18.3.0)(@types/react@19.0.9)(jiti@2.4.2)(lightningcss@1.29.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.34.7)(terser@5.31.5)(tsx@4.17.0)(typescript@5.7.3)(yaml@2.5.0): dependencies: '@floating-ui/react': 0.27.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@hono/node-server': 1.13.8(hono@4.7.1) @@ -13197,7 +13204,7 @@ snapshots: '@radix-ui/react-tabs': 1.1.3(@types/react-dom@18.3.0)(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@shikijs/rehype': 2.4.1 '@shikijs/transformers': 2.4.1 - '@shikijs/twoslash': 2.4.1(typescript@5.5.4) + '@shikijs/twoslash': 2.4.1(typescript@5.7.3) '@tailwindcss/vite': 4.0.6(vite@6.1.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.5)(tsx@4.17.0)(yaml@2.5.0)) '@vanilla-extract/css': 1.17.1 '@vanilla-extract/dynamic': 2.1.2 @@ -13240,7 +13247,7 @@ snapshots: serve-static: 1.16.2 shiki: 2.4.1 toml: 3.0.0 - twoslash: 0.2.12(typescript@5.5.4) + twoslash: 0.2.12(typescript@5.7.3) ua-parser-js: 1.0.40 unified: 11.0.5 unist-util-visit: 5.0.0 @@ -13295,9 +13302,9 @@ snapshots: dependencies: web3-types: 1.7.0 - web3-eth-abi@4.2.3(typescript@5.5.4)(zod@3.23.8): + web3-eth-abi@4.2.3(typescript@5.7.3)(zod@3.23.8): dependencies: - abitype: 0.7.1(typescript@5.5.4)(zod@3.23.8) + abitype: 0.7.1(typescript@5.7.3)(zod@3.23.8) web3-errors: 1.3.0 web3-types: 1.7.0 web3-utils: 4.3.1 @@ -13316,13 +13323,13 @@ snapshots: web3-utils: 4.3.1 web3-validator: 2.0.6 - web3-eth-contract@4.7.0(typescript@5.5.4)(zod@3.23.8): + web3-eth-contract@4.7.0(typescript@5.7.3)(zod@3.23.8): dependencies: '@ethereumjs/rlp': 5.0.2 web3-core: 4.5.1 web3-errors: 1.3.0 - web3-eth: 4.8.2(typescript@5.5.4)(zod@3.23.8) - web3-eth-abi: 4.2.3(typescript@5.5.4)(zod@3.23.8) + web3-eth: 4.8.2(typescript@5.7.3)(zod@3.23.8) + web3-eth-abi: 4.2.3(typescript@5.7.3)(zod@3.23.8) web3-types: 1.7.0 web3-utils: 4.3.1 web3-validator: 2.0.6 @@ -13333,13 +13340,13 @@ snapshots: - utf-8-validate - zod - web3-eth-ens@4.4.0(typescript@5.5.4)(zod@3.23.8): + web3-eth-ens@4.4.0(typescript@5.7.3)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.10.1 web3-core: 4.5.1 web3-errors: 1.3.0 - web3-eth: 4.8.2(typescript@5.5.4)(zod@3.23.8) - web3-eth-contract: 4.7.0(typescript@5.5.4)(zod@3.23.8) + web3-eth: 4.8.2(typescript@5.7.3)(zod@3.23.8) + web3-eth-contract: 4.7.0(typescript@5.7.3)(zod@3.23.8) web3-net: 4.1.0 web3-types: 1.7.0 web3-utils: 4.3.1 @@ -13358,10 +13365,10 @@ snapshots: web3-utils: 4.3.1 web3-validator: 2.0.6 - web3-eth-personal@4.0.8(typescript@5.5.4)(zod@3.23.8): + web3-eth-personal@4.0.8(typescript@5.7.3)(zod@3.23.8): dependencies: web3-core: 4.5.1 - web3-eth: 4.8.2(typescript@5.5.4)(zod@3.23.8) + web3-eth: 4.8.2(typescript@5.7.3)(zod@3.23.8) web3-rpc-methods: 1.3.0 web3-types: 1.7.0 web3-utils: 4.3.1 @@ -13373,12 +13380,12 @@ snapshots: - utf-8-validate - zod - web3-eth@4.8.2(typescript@5.5.4)(zod@3.23.8): + web3-eth@4.8.2(typescript@5.7.3)(zod@3.23.8): dependencies: setimmediate: 1.0.5 web3-core: 4.5.1 web3-errors: 1.3.0 - web3-eth-abi: 4.2.3(typescript@5.5.4)(zod@3.23.8) + web3-eth-abi: 4.2.3(typescript@5.7.3)(zod@3.23.8) web3-eth-accounts: 4.2.1 web3-net: 4.1.0 web3-providers-ws: 4.0.8 @@ -13473,17 +13480,17 @@ snapshots: web3-types: 1.7.0 zod: 3.23.8 - web3@4.12.1(typescript@5.5.4)(zod@3.23.8): + web3@4.12.1(typescript@5.7.3)(zod@3.23.8): dependencies: web3-core: 4.5.1 web3-errors: 1.3.0 - web3-eth: 4.8.2(typescript@5.5.4)(zod@3.23.8) - web3-eth-abi: 4.2.3(typescript@5.5.4)(zod@3.23.8) + web3-eth: 4.8.2(typescript@5.7.3)(zod@3.23.8) + web3-eth-abi: 4.2.3(typescript@5.7.3)(zod@3.23.8) web3-eth-accounts: 4.2.1 - web3-eth-contract: 4.7.0(typescript@5.5.4)(zod@3.23.8) - web3-eth-ens: 4.4.0(typescript@5.5.4)(zod@3.23.8) + web3-eth-contract: 4.7.0(typescript@5.7.3)(zod@3.23.8) + web3-eth-ens: 4.4.0(typescript@5.7.3)(zod@3.23.8) web3-eth-iban: 4.0.7 - web3-eth-personal: 4.0.8(typescript@5.5.4)(zod@3.23.8) + web3-eth-personal: 4.0.8(typescript@5.7.3)(zod@3.23.8) web3-net: 4.1.0 web3-providers-http: 4.2.0 web3-providers-ws: 4.0.8 diff --git a/src/core/Abi.ts b/src/core/Abi.ts index 6b76063c..5540c04b 100644 --- a/src/core/Abi.ts +++ b/src/core/Abi.ts @@ -1,7 +1,7 @@ import * as abitype from 'abitype' import type * as Errors from './Errors.js' import * as internal from './internal/abi.js' -import type * as AbiItem_internal from './internal/abiItem.js' +import type * as internal_signatures from './internal/humanReadable/signatures.js' /** Root type for an ABI. */ export type Abi = abitype.Abi @@ -59,7 +59,7 @@ export declare namespace format { export function from( abi: abi & (abi extends readonly string[] - ? AbiItem_internal.Signatures + ? internal_signatures.Signatures : unknown), ): from.ReturnType /** diff --git a/src/core/AbiFunction.ts b/src/core/AbiFunction.ts index 40315c2b..62c74203 100644 --- a/src/core/AbiFunction.ts +++ b/src/core/AbiFunction.ts @@ -1,6 +1,7 @@ import * as abitype from 'abitype' import type * as Abi from './Abi.js' import * as AbiItem from './AbiItem.js' +import type * as AbiParameter from './AbiParameter.js' import * as AbiParameters from './AbiParameters.js' import type * as Errors from './Errors.js' import * as Hex from './Hex.js' @@ -264,7 +265,7 @@ export declare namespace decodeResult { ? abiFunction['outputs'] extends readonly [] ? undefined : abiFunction['outputs'] extends readonly [ - infer type extends abitype.AbiParameter, + infer type extends AbiParameter.AbiParameter, ] ? abitype.AbiParameterToPrimitiveType : AbiParameters.decode.ReturnType< diff --git a/src/core/AbiItem.ts b/src/core/AbiItem.ts index da5e785f..4960b3be 100644 --- a/src/core/AbiItem.ts +++ b/src/core/AbiItem.ts @@ -1,70 +1,17 @@ import * as abitype from 'abitype' import type * as Abi from './Abi.js' +import type * as AbiParameter from './AbiParameter.js' +import * as AbiParameters from './AbiParameters.js' import * as Errors from './Errors.js' import * as Hash from './Hash.js' import * as Hex from './Hex.js' import * as internal from './internal/abiItem.js' +import type * as internal_signatures from './internal/humanReadable/signatures.js' import type { UnionCompute } from './internal/types.js' /** Root type for an item on an {@link ox#Abi.Abi}. */ export type AbiItem = Abi.Abi[number] -/** - * Extracts an {@link ox#AbiItem.AbiItem} item from an {@link ox#Abi.Abi}, given a name. - * - * @example - * ```ts twoslash - * import { Abi, AbiItem } from 'ox' - * - * const abi = Abi.from([ - * 'error Foo(string)', - * 'function foo(string)', - * 'event Bar(uint256)', - * ]) - * - * type Foo = AbiItem.FromAbi - * // ^? - * - * - * - * - * - * - * - * - * ``` - */ -export type FromAbi< - abi extends Abi.Abi, - name extends ExtractNames, -> = Extract - -/** - * Extracts the names of all {@link ox#AbiItem.AbiItem} items in an {@link ox#Abi.Abi}. - * - * @example - * ```ts twoslash - * import { Abi, AbiItem } from 'ox' - * - * const abi = Abi.from([ - * 'error Foo(string)', - * 'function foo(string)', - * 'event Bar(uint256)', - * ]) - * - * type names = AbiItem.Name - * // ^? - * - * ``` - */ -export type Name = - abi extends Abi.Abi ? ExtractNames : string - -export type ExtractNames = Extract< - abi[number], - { name: string } ->['name'] - /** * Formats an {@link ox#AbiItem.AbiItem} into a **Human Readable ABI Item**. * @@ -100,11 +47,100 @@ export type ExtractNames = Extract< */ export function format( abiItem: abiItem | AbiItem, -): abitype.FormatAbiItem { - return abitype.formatAbiItem(abiItem) as never +): format.ReturnType + +// eslint-disable-next-line jsdoc/require-jsdoc +export function format(abiItem: AbiItem): string { + type Params = readonly [ + AbiParameter.AbiParameter | abitype.AbiEventParameter, + ...(readonly (AbiParameter.AbiParameter | abitype.AbiEventParameter)[]), + ] + + if (abiItem.type === 'function') + return `function ${abiItem.name}(${AbiParameters.format( + abiItem.inputs as Params, + )})${ + abiItem.stateMutability && abiItem.stateMutability !== 'nonpayable' + ? ` ${abiItem.stateMutability}` + : '' + }${ + abiItem.outputs?.length + ? ` returns (${AbiParameters.format(abiItem.outputs as Params)})` + : '' + }` + if (abiItem.type === 'event') + return `event ${abiItem.name}(${AbiParameters.format( + abiItem.inputs as Params, + )})` + if (abiItem.type === 'error') + return `error ${abiItem.name}(${AbiParameters.format( + abiItem.inputs as Params, + )})` + if (abiItem.type === 'constructor') + return `constructor(${AbiParameters.format(abiItem.inputs as Params)})${ + abiItem.stateMutability === 'payable' ? ' payable' : '' + }` + if (abiItem.type === 'fallback') + return `fallback() external${ + abiItem.stateMutability === 'payable' ? ' payable' : '' + }` + return 'receive() external payable' } export declare namespace format { + type ReturnType = AbiItem extends abiItem + ? string + : + | (abiItem extends abitype.AbiFunction + ? abitype.AbiFunction extends abiItem + ? string + : `function ${internal_signatures.AssertName}(${AbiParameters.format.ReturnType< + abiItem['inputs'] + >})${abiItem['stateMutability'] extends Exclude< + abitype.AbiStateMutability, + 'nonpayable' + > + ? ` ${abiItem['stateMutability']}` + : ''}${abiItem['outputs']['length'] extends 0 + ? '' + : ` returns (${AbiParameters.format.ReturnType})`}` + : never) + | (abiItem extends abitype.AbiEvent + ? abitype.AbiEvent extends abiItem + ? string + : `event ${internal_signatures.AssertName}(${AbiParameters.format.ReturnType< + abiItem['inputs'] + >})` + : never) + | (abiItem extends abitype.AbiError + ? abitype.AbiError extends abiItem + ? string + : `error ${internal_signatures.AssertName}(${AbiParameters.format.ReturnType< + abiItem['inputs'] + >})` + : never) + | (abiItem extends abitype.AbiConstructor + ? abitype.AbiConstructor extends abiItem + ? string + : `constructor(${AbiParameters.format.ReturnType< + abiItem['inputs'] + >})${abiItem['stateMutability'] extends 'payable' + ? ' payable' + : ''}` + : never) + | (abiItem extends abitype.AbiFallback + ? abitype.AbiFallback extends abiItem + ? string + : `fallback() external${abiItem['stateMutability'] extends 'payable' + ? ' payable' + : ''}` + : never) + | (abiItem extends abitype.AbiReceive + ? abitype.AbiReceive extends abiItem + ? string + : 'receive() external payable' + : never) + type ErrorType = Errors.GlobalErrorType } @@ -216,9 +252,11 @@ export function from< >( abiItem: (abiItem | AbiItem | string | readonly string[]) & ( - | (abiItem extends string ? internal.Signature : never) + | (abiItem extends string + ? internal_signatures.Signature + : never) | (abiItem extends readonly string[] - ? internal.Signatures + ? internal_signatures.Signatures : never) | AbiItem ), @@ -550,7 +588,7 @@ export declare namespace getSelector { export function getSignature(abiItem: string | AbiItem): string { const signature = (() => { if (typeof abiItem === 'string') return abiItem - return abitype.formatAbiItem(abiItem) + return format(abiItem) })() return internal.normalizeSignature(signature) } @@ -607,6 +645,62 @@ export declare namespace getSignatureHash { | Errors.GlobalErrorType } +/** + * Extracts an {@link ox#AbiItem.AbiItem} item from an {@link ox#Abi.Abi}, given a name. + * + * @example + * ```ts twoslash + * import { Abi, AbiItem } from 'ox' + * + * const abi = Abi.from([ + * 'error Foo(string)', + * 'function foo(string)', + * 'event Bar(uint256)', + * ]) + * + * type Foo = AbiItem.FromAbi + * // ^? + * + * + * + * + * + * + * + * + * ``` + */ +export type FromAbi< + abi extends Abi.Abi, + name extends ExtractNames, +> = Extract + +/** + * Extracts the names of all {@link ox#AbiItem.AbiItem} items in an {@link ox#Abi.Abi}. + * + * @example + * ```ts twoslash + * import { Abi, AbiItem } from 'ox' + * + * const abi = Abi.from([ + * 'error Foo(string)', + * 'function foo(string)', + * 'event Bar(uint256)', + * ]) + * + * type names = AbiItem.Name + * // ^? + * + * ``` + */ +export type Name = + abi extends Abi.Abi ? ExtractNames : string + +export type ExtractNames = Extract< + abi[number], + { name: string } +>['name'] + /** * Throws when ambiguous types are found on overloaded ABI items. * @@ -655,8 +749,8 @@ export class AmbiguityError extends Errors.BaseError { super('Found ambiguous types in overloaded ABI Items.', { metaMessages: [ // TODO: abitype to add support for signature-formatted ABI items. - `\`${x.type}\` in \`${internal.normalizeSignature(abitype.formatAbiItem(x.abiItem))}\`, and`, - `\`${y.type}\` in \`${internal.normalizeSignature(abitype.formatAbiItem(y.abiItem))}\``, + `\`${x.type}\` in \`${internal.normalizeSignature(format(x.abiItem))}\`, and`, + `\`${y.type}\` in \`${internal.normalizeSignature(format(y.abiItem))}\``, '', 'These types encode differently and cannot be distinguished at runtime.', 'Remove one of the ambiguous items in the ABI.', diff --git a/src/core/AbiParameter.ts b/src/core/AbiParameter.ts new file mode 100644 index 00000000..12422c17 --- /dev/null +++ b/src/core/AbiParameter.ts @@ -0,0 +1,214 @@ +import type * as abitype from 'abitype' + +import * as Errors from './Errors.js' +import * as internal from './internal/abiParameter.js' +import * as internal_signatures from './internal/humanReadable/signatures.js' +import * as internal_structs from './internal/humanReadable/structs.js' +import * as internal_regex from './internal/regex.js' +import type { + FilterReverse, + IsNarrowableIncludingNever, + Join, + TypeErrorMessage, +} from './internal/types.js' + +/** Root type for ABI parameters. */ +export type AbiParameter = abitype.AbiParameter + +/** + * Formats {@link ox#AbiParameter.AbiParameter} into **Human Readable ABI Parameter**. + * + * TODO + */ +export function format< + const abiParameter extends AbiParameter | abitype.AbiEventParameter, +>(abiParameter: abiParameter): format.ReturnType + +// eslint-disable-next-line jsdoc/require-jsdoc +export function format(abiParameter: AbiParameter): string { + let type = abiParameter.type + if ( + internal_regex.tupleAbiParameterType.test(abiParameter.type) && + 'components' in abiParameter + ) { + type = '(' + const length = abiParameter.components.length as number + for (let i = 0; i < length; i++) { + const component = abiParameter.components[i]! + type += format(component) + if (i < length - 1) type += ', ' + } + const result = internal_regex.execTyped<{ array?: string }>( + internal_regex.tupleAbiParameterType, + abiParameter.type, + ) + type += `)${result?.array ?? ''}` + return format({ + ...abiParameter, + type, + }) + } + // Add `indexed` to type if in `abiParameter` + if ('indexed' in abiParameter && abiParameter.indexed) + type = `${type} indexed` + // Return human-readable ABI parameter + if (abiParameter.name) return `${type} ${abiParameter.name}` + return type +} + +export declare namespace format { + type ReturnType< + abiParameter extends AbiParameter | abitype.AbiEventParameter, + > = abiParameter extends { + name?: infer name extends string + type: `tuple${infer array}` + components: infer components extends readonly AbiParameter[] + indexed?: infer indexed extends boolean + } + ? format.ReturnType< + { + type: `(${Join< + { + [key in keyof components]: format.ReturnType< + { + type: components[key]['type'] + } & (IsNarrowableIncludingNever< + components[key]['name'], + string + > extends true + ? { name: components[key]['name'] } + : unknown) & + (components[key] extends { + components: readonly AbiParameter[] + } + ? { components: components[key]['components'] } + : unknown) + > + }, + ', ' + >})${array}` + } & (IsNarrowableIncludingNever extends true + ? { name: name } + : unknown) & + (IsNarrowableIncludingNever extends true + ? { indexed: indexed } + : unknown) + > + : `${abiParameter['type']}${abiParameter extends { indexed: true } + ? ' indexed' + : ''}${abiParameter['name'] extends infer name extends string + ? name extends '' + ? '' + : ` ${internal_signatures.AssertName}` + : ''}` + + type ErrorType = Errors.GlobalErrorType +} + +/** + * Parses **JSON ABI Parameter** or **Human Readable ABI Parameter** into typed {@link ox#AbiParameter.AbiParameter}. + * + * TODO + */ +export function from< + const param extends string | readonly string[] | readonly unknown[], +>( + param: abitype.Narrow & + ( + | (param extends string + ? param extends '' + ? TypeErrorMessage<'Empty string is not allowed.'> + : unknown + : never) + | (param extends readonly string[] + ? param extends readonly [] // empty array + ? TypeErrorMessage<'At least one parameter required.'> + : string[] extends param + ? unknown + : unknown // TODO: Validate param string + : never) + ), +): from.ReturnType + +// eslint-disable-next-line jsdoc/require-jsdoc +export function from( + param: string | readonly string[] | readonly unknown[], +): AbiParameter { + if (typeof param === 'string') + return internal.parseAbiParameter(param, { + modifiers: internal_signatures.modifiers, + }) + + const abiParameter = (() => { + const structs = internal_structs.parseStructs(param as readonly string[]) + const length = param.length + for (let i = 0; i < length; i++) { + const signature = (param as readonly string[])[i]! + if (internal_signatures.isStructSignature(signature)) continue + return internal.parseAbiParameter(signature, { + modifiers: internal_signatures.modifiers, + structs, + }) + } + return undefined + })() + + if (!abiParameter) throw new InvalidAbiParameterError({ param }) + + return abiParameter +} + +export declare namespace from { + type ReturnType< + param extends string | readonly string[] | readonly unknown[], + > = + | (param extends string + ? param extends '' + ? never + : string extends param + ? AbiParameter + : internal.ParseAbiParameter< + param, + { modifier: internal_signatures.Modifier } + > + : never) + | (param extends readonly string[] + ? string[] extends param + ? AbiParameter // Return generic AbiParameter item since type was no inferrable + : internal_structs.ParseStructs extends infer structs + ? { + [key in keyof param]: param[key] extends string + ? internal_signatures.IsStructSignature< + param[key] + > extends true + ? never + : internal.ParseAbiParameter< + param[key], + { + modifier: internal_signatures.Modifier + structs: structs + } + > + : never + } extends infer mapped extends readonly unknown[] + ? FilterReverse[0] extends infer result + ? result extends undefined + ? never + : result + : never + : never + : never + : never) + + type ErrorType = Errors.GlobalErrorType +} + +export class InvalidAbiParameterError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidAbiParameterError' + + constructor({ param }: { param: string | object }) { + super('Failed to parse ABI parameter.', { + details: `parseAbiParameter(${JSON.stringify(param, null, 2)})`, + }) + } +} diff --git a/src/core/AbiParameters.ts b/src/core/AbiParameters.ts index d47d87e2..7effe052 100644 --- a/src/core/AbiParameters.ts +++ b/src/core/AbiParameters.ts @@ -1,4 +1,5 @@ import * as abitype from 'abitype' +import * as AbiParameter from './AbiParameter.js' import * as Address from './Address.js' import * as Bytes from './Bytes.js' import * as Errors from './Errors.js' @@ -6,6 +7,7 @@ import * as Hex from './Hex.js' import * as Solidity from './Solidity.js' import * as internal from './internal/abiParameters.js' import * as Cursor from './internal/cursor.js' +import type { Join } from './internal/types.js' /** Root type for ABI parameters. */ export type AbiParameters = readonly abitype.AbiParameter[] @@ -367,10 +369,31 @@ export function format< ...(readonly (Parameter | abitype.AbiEventParameter)[]), ], ): abitype.FormatAbiParameters { - return abitype.formatAbiParameters(parameters) + let params = '' + const length = parameters.length + for (let i = 0; i < length; i++) { + const abiParameter = parameters[i]! + params += AbiParameter.format(abiParameter) + if (i !== length - 1) params += ', ' + } + return params as abitype.FormatAbiParameters } export declare namespace format { + type ReturnType< + abiParameters extends readonly [ + AbiParameter.AbiParameter | abitype.AbiEventParameter, + ...(readonly (AbiParameter.AbiParameter | abitype.AbiEventParameter)[]), + ], + > = Join< + { + [key in keyof abiParameters]: AbiParameter.format.ReturnType< + abiParameters[key] + > + }, + ', ' + > + type ErrorType = Errors.GlobalErrorType } diff --git a/src/core/Address.ts b/src/core/Address.ts index f86a191e..bf103c4c 100644 --- a/src/core/Address.ts +++ b/src/core/Address.ts @@ -4,8 +4,7 @@ import * as Caches from './Caches.js' import * as Errors from './Errors.js' import * as Hash from './Hash.js' import * as PublicKey from './PublicKey.js' - -const addressRegex = /*#__PURE__*/ /^0x[a-fA-F0-9]{40}$/ +import * as internal_regex from './internal/regex.js' /** Root type for Address. */ export type Address = abitype_Address @@ -37,7 +36,7 @@ export function assert( ): asserts value is Address { const { strict = true } = options - if (!addressRegex.test(value)) + if (!internal_regex.address.test(value)) throw new InvalidAddressError({ address: value, cause: new InvalidInputError(), diff --git a/src/core/Ens.ts b/src/core/Ens.ts index a08cd581..46a66435 100644 --- a/src/core/Ens.ts +++ b/src/core/Ens.ts @@ -63,7 +63,9 @@ export function namehash(name: string) { const hashed = hashFromEncodedLabel ? Bytes.fromHex(hashFromEncodedLabel) : Hash.keccak256(Bytes.fromString(labels[i]!), { as: 'Bytes' }) - result = Hash.keccak256(Bytes.concat(result, hashed), { as: 'Bytes' }) + result = Hash.keccak256(Bytes.concat(result, hashed), { + as: 'Bytes', + }) as typeof result } return Hex.fromBytes(result) diff --git a/src/core/_test/AbiItem.test-d.ts b/src/core/_test/AbiItem.test-d.ts new file mode 100644 index 00000000..68b004d1 --- /dev/null +++ b/src/core/_test/AbiItem.test-d.ts @@ -0,0 +1,119 @@ +import type { + Abi, + AbiConstructor, + AbiError, + AbiEvent, + AbiFallback, + AbiFunction, + AbiReceive, +} from 'abitype' +import { AbiItem } from 'ox' +import { expectTypeOf, test } from 'vitest' + +test('AbiItem.format', () => { + const result = AbiItem.format({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [], + outputs: [], + }) + expectTypeOf(result).toEqualTypeOf<'function foo()'>() + expectTypeOf( + AbiItem.format({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + components: [ + { + type: 'string', + }, + ], + }, + { + type: 'address', + }, + ], + outputs: [], + }), + ).toEqualTypeOf<'function foo((string), address)'>() + + const abiItem: Abi[number] = { + type: 'function', + name: 'foo', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + } + expectTypeOf(AbiItem.format(abiItem)).toEqualTypeOf() + + expectTypeOf( + AbiItem.format({ + type: 'fallback', + stateMutability: 'nonpayable', + }), + ).toEqualTypeOf<'fallback() external'>() + expectTypeOf( + AbiItem.format({ + type: 'fallback', + stateMutability: 'payable', + }), + ).toEqualTypeOf<'fallback() external payable'>() +}) + +test('AbiItem.format.ReturnType', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + AbiItem.format.ReturnType + >().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + + type Result = AbiItem.format.ReturnType<{ + readonly name: 'foo' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }> + expectTypeOf().toEqualTypeOf<'function foo()'>() + + expectTypeOf< + AbiItem.format.ReturnType<{ + readonly name: 'address' + readonly type: 'function' + readonly stateMutability: 'nonpayable' + readonly inputs: readonly [] + readonly outputs: readonly [] + }> + >().toEqualTypeOf<'function [Error: "address" is a protected Solidity keyword.]()'>() + + expectTypeOf< + AbiItem.format.ReturnType<{ + readonly name: 'Transfer' + readonly type: 'event' + readonly inputs: readonly [ + { + readonly name: 'from' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'to' + readonly type: 'address' + readonly indexed: true + }, + { + readonly name: 'amount' + readonly type: 'uint256' + }, + ] + }> + >().toEqualTypeOf<'event Transfer(address indexed from, address indexed to, uint256 amount)'>() +}) diff --git a/src/core/_test/AbiItem.test.ts b/src/core/_test/AbiItem.test.ts index e9f89407..5be18c26 100644 --- a/src/core/_test/AbiItem.test.ts +++ b/src/core/_test/AbiItem.test.ts @@ -8,6 +8,7 @@ import { type Hex, } from 'ox' import { describe, expect, test } from 'vitest' +import { seaportAbi } from '../../../test/abis/json.js' import { seaportContractConfig, wagmiContractConfig, @@ -20,27 +21,102 @@ import { describe('format', () => { test('default', () => { - const abiItem = AbiItem.from({ - type: 'function', - name: 'approve', - stateMutability: 'nonpayable', - inputs: [ - { - name: 'spender', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - outputs: [{ type: 'bool' }], - }) - const formatted = AbiItem.format(abiItem) - expect(formatted).toMatchInlineSnapshot( - `"function approve(address spender, uint256 amount) returns (bool)"`, + const result = AbiItem.format(seaportAbi[1]) + expect(result).toMatchInlineSnapshot( + '"function cancel((address offerer, address zone, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount)[] offer, (uint8 itemType, address token, uint256 identifierOrCriteria, uint256 startAmount, uint256 endAmount, address recipient)[] consideration, uint8 orderType, uint256 startTime, uint256 endTime, bytes32 zoneHash, uint256 salt, bytes32 conduitKey, uint256 counter)[] orders) returns (bool cancelled)"', ) }) + + test.each([ + { + abiItem: { + type: 'function', + name: 'foo', + inputs: [{ type: 'string' }], + outputs: [], + stateMutability: 'nonpayable', + } as const, + expected: 'function foo(string)', + }, + { + abiItem: { + type: 'event', + name: 'Foo', + inputs: [ + { type: 'address', name: 'from', indexed: true }, + { type: 'address', name: 'to', indexed: true }, + { type: 'uint256', name: 'amount' }, + ], + } as const, + expected: + 'event Foo(address indexed from, address indexed to, uint256 amount)', + }, + { + abiItem: { + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + } as const, + expected: 'constructor(string)', + }, + { + abiItem: { + type: 'constructor', + stateMutability: 'payable', + inputs: [{ type: 'string' }], + } as const, + expected: 'constructor(string) payable', + }, + { + abiItem: { + type: 'fallback', + stateMutability: 'nonpayable', + } as const, + expected: 'fallback() external', + }, + { + abiItem: { + type: 'fallback', + stateMutability: 'payable', + } as const, + expected: 'fallback() external payable', + }, + { + abiItem: { + type: 'receive', + stateMutability: 'payable', + } as const, + expected: 'receive() external payable', + }, + { + abiItem: { + type: 'function', + name: 'initWormhole', + inputs: [ + { + type: 'tuple[]', + name: 'configs', + components: [ + { + type: 'uint256', + name: 'chainId', + }, + { + type: 'uint16', + name: 'wormholeChainId', + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + } as const, + expected: + 'function initWormhole((uint256 chainId, uint16 wormholeChainId)[] configs)', + }, + ])('AbiItem.format($expected)', ({ abiItem, expected }) => { + expect(AbiItem.format(abiItem)).toEqual(expected) + }) }) describe('from', () => { diff --git a/src/core/_test/AbiParameter.bench.ts b/src/core/_test/AbiParameter.bench.ts new file mode 100644 index 00000000..cf799004 --- /dev/null +++ b/src/core/_test/AbiParameter.bench.ts @@ -0,0 +1,75 @@ +import { formatAbiParameter, parseAbiParameter } from 'abitype' +import { ParamType } from 'ethers' +import { AbiParameter } from 'ox' +import { bench, describe } from 'vitest' + +describe('Format basic ABI Parameter', () => { + const basic = { type: 'address', name: 'foo' } + + bench('ox', () => { + AbiParameter.format(basic) + }) + + bench('abitype', () => { + formatAbiParameter(basic) + }) + + bench('ethers@6', () => { + ParamType.from(basic).format('minimal') + }) +}) + +describe('Format inline tuple ABI Parameter', () => { + const inlineTuple = { + type: 'tuple', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', + } + + bench('ox', () => { + AbiParameter.format(inlineTuple) + }) + + bench('abitype', () => { + formatAbiParameter(inlineTuple) + }) + + bench('ethers@6', () => { + ParamType.from(inlineTuple).format('minimal') + }) +}) + +describe('Parse basic ABI Parameter', () => { + const basic = 'string foo' + + bench('ox', () => { + AbiParameter.from(basic) + }) + + bench('abitype', () => { + parseAbiParameter(basic) + }) + + bench('ethers@6', () => { + ParamType.from(basic) + }) +}) + +describe('Parse inline tuple ABI Parameter', () => { + const inlineTuple = '(string bar, string baz) foo' + + bench('ox', () => { + AbiParameter.from(inlineTuple) + }) + + bench('abitype', () => { + parseAbiParameter(inlineTuple) + }) + + bench('ethers@6', () => { + ParamType.from(inlineTuple) + }) +}) diff --git a/src/core/_test/AbiParameter.test-d.ts b/src/core/_test/AbiParameter.test-d.ts new file mode 100644 index 00000000..1dc3c817 --- /dev/null +++ b/src/core/_test/AbiParameter.test-d.ts @@ -0,0 +1,192 @@ +import { AbiParameter } from 'ox' +import { expectTypeOf, test } from 'vitest' + +test('AbiParameter.from', () => { + // @ts-expect-error empty array not allowed + expectTypeOf(AbiParameter.from([])).toEqualTypeOf() + expectTypeOf( + AbiParameter.from(['struct Foo { string name; }']), + ).toEqualTypeOf() + + expectTypeOf(AbiParameter.from('(string)')).toEqualTypeOf<{ + readonly type: 'tuple' + readonly components: readonly [{ readonly type: 'string' }] + }>() + + const param: string = 'address' + expectTypeOf( + AbiParameter.from(param), + ).toEqualTypeOf() +}) + +test('AbiParameter.from.ReturnType', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + AbiParameter.from.ReturnType<['struct Foo { string name; }']> + >().toEqualTypeOf() + + // string + expectTypeOf>().toEqualTypeOf<{ + readonly type: 'address' + readonly name: 'from' + }>() + expectTypeOf< + AbiParameter.from.ReturnType<'address indexed from'> + >().toEqualTypeOf<{ + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }>() + expectTypeOf< + AbiParameter.from.ReturnType<'address calldata foo'> + >().toEqualTypeOf<{ + readonly type: 'address' + readonly name: 'foo' + }>() + + // Array + type Result = AbiParameter.from.ReturnType< + ['Foo', 'struct Foo { string name; }'] + > + expectTypeOf().toEqualTypeOf<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }>() + + expectTypeOf< + AbiParameter.from.ReturnType<'(string bar) foo'> + >().toEqualTypeOf<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + readonly name: 'foo' + }>() +}) + +test('AbiParameter.format', () => { + expectTypeOf( + AbiParameter.format({ + type: 'tuple', + components: [{ type: 'string' }], + }), + ).toEqualTypeOf<'(string)'>() + + const param = { type: 'address' } + const param2: AbiParameter.AbiParameter = param + expectTypeOf(AbiParameter.format(param)).toEqualTypeOf() + expectTypeOf(AbiParameter.format(param2)).toEqualTypeOf() +}) + +test('AbiParameter.format.ReturnType', () => { + // string + expectTypeOf< + AbiParameter.format.ReturnType<{ + readonly type: 'address' + readonly name: 'from' + }> + >().toEqualTypeOf<'address from'>() + expectTypeOf< + AbiParameter.format.ReturnType<{ + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }> + >().toEqualTypeOf<'address indexed from'>() + expectTypeOf< + AbiParameter.format.ReturnType<{ + readonly type: 'address' + readonly name: '' + }> + >().toEqualTypeOf<'address'>() + + expectTypeOf< + AbiParameter.format.ReturnType<{ + type: 'address' + name: 'address' + }> + >().toEqualTypeOf<'address [Error: "address" is a protected Solidity keyword.]'>() + + expectTypeOf< + AbiParameter.format.ReturnType<{ + type: 'address' + name: '123' + }> + >().toEqualTypeOf<'address [Error: Identifier "123" cannot be a number string.]'>() + + // Array + expectTypeOf< + AbiParameter.format.ReturnType<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }> + >().toEqualTypeOf<'(string name)'>() + + expectTypeOf< + AbiParameter.format.ReturnType<{ + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + readonly name: 'foo' + }> + >().toEqualTypeOf<'(string bar) foo'>() + + type Result = AbiParameter.format.ReturnType<{ + readonly components: [ + { + readonly components: [ + { + readonly type: 'string' + readonly name: 'foo' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }> + expectTypeOf().toEqualTypeOf<'((string foo))'>() + + type Result2 = AbiParameter.format.ReturnType<{ + readonly components: [ + { + readonly components: [ + { + readonly components: [ + { + readonly components: [ + { + readonly type: 'string' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }, + ] + readonly type: 'tuple' + }> + expectTypeOf().toEqualTypeOf<'((((string))))'>() +}) diff --git a/src/core/_test/AbiParameter.test.ts b/src/core/_test/AbiParameter.test.ts new file mode 100644 index 00000000..96c235cf --- /dev/null +++ b/src/core/_test/AbiParameter.test.ts @@ -0,0 +1,326 @@ +import { AbiParameter } from 'ox' +import { assertType, describe, expect, expectTypeOf, test } from 'vitest' + +describe('format', () => { + test('default', () => { + const result = AbiParameter.format({ type: 'address', name: 'foo' }) + expect(result).toEqual('address foo') + expectTypeOf(result).toEqualTypeOf<'address foo'>() + }) + + test('tuple', () => { + const result = AbiParameter.format({ + type: 'tuple', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', + }) + expect(result).toMatchInlineSnapshot('"(string bar, string baz) foo"') + expectTypeOf(result).toEqualTypeOf<'(string bar, string baz) foo'>() + }) + + test('tuple[][]', () => { + const result = AbiParameter.format({ + type: 'tuple[123][]', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', + }) + expect(result).toMatchInlineSnapshot( + '"(string bar, string baz)[123][] foo"', + ) + expectTypeOf(result).toEqualTypeOf<'(string bar, string baz)[123][] foo'>() + }) + + test.each([ + { + abiParameter: { type: 'string' }, + expected: 'string', + }, + { + abiParameter: { name: 'foo', type: 'string' }, + expected: 'string foo', + }, + { + abiParameter: { name: 'foo', type: 'string', indexed: true }, + expected: 'string indexed foo', + }, + { + abiParameter: { type: 'tuple', components: [{ type: 'string' }] }, + expected: '(string)', + }, + { + abiParameter: { + type: 'tuple', + components: [{ name: 'foo', type: 'string' }], + }, + expected: '(string foo)', + }, + { + abiParameter: { + type: 'tuple', + name: 'foo', + components: [{ name: 'bar', type: 'string' }], + }, + expected: '(string bar) foo', + }, + { + abiParameter: { + type: 'tuple', + name: 'foo', + components: [ + { name: 'bar', type: 'string' }, + { name: 'baz', type: 'string' }, + ], + }, + expected: '(string bar, string baz) foo', + }, + { + abiParameter: { type: 'string', indexed: false }, + expected: 'string', + }, + { + abiParameter: { type: 'string', indexed: true }, + expected: 'string indexed', + }, + { + abiParameter: { type: 'string', indexed: true, name: 'foo' }, + expected: 'string indexed foo', + }, + ])('AbiParameter.format($abiParameter)', ({ abiParameter, expected }) => { + expect(AbiParameter.format(abiParameter)).toEqual(expected) + }) + + test('nested tuple', () => { + const result = AbiParameter.format({ + components: [ + { + components: [ + { + components: [ + { + components: [ + { + name: 'baz', + type: 'string', + }, + ], + name: 'bar', + type: 'tuple', + }, + ], + name: 'foo', + type: 'tuple[1]', + }, + ], + name: 'boo', + type: 'tuple', + }, + ], + type: 'tuple', + }) + expect(result).toMatchInlineSnapshot('"((((string baz) bar)[1] foo) boo)"') + assertType<'((((string baz) bar)[1] foo) boo)'>(result) + }) +}) + +describe('from', () => { + test('AbiParameter.from', () => { + // @ts-expect-error invalid signature type + expect(() => AbiParameter.from('')).toThrowErrorMatchingInlineSnapshot( + '[AbiParameter.InvalidParameterError: Invalid ABI parameter.]', + ) + // @ts-expect-error invalid signature type + expect(() => AbiParameter.from([])).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameter.InvalidAbiParameterError: Failed to parse ABI parameter. + + Details: parseAbiParameter([])] + `, + ) + expect(() => + AbiParameter.from(['struct Foo { string name; }']), + ).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameter.InvalidAbiParameterError: Failed to parse ABI parameter. + + Details: parseAbiParameter([ + "struct Foo { string name; }" + ])] + `, + ) + + expect(() => + AbiParameter.from([ + 'struct Foo { string memory bar; }', + 'Foo indexed foo', + ]), + ).toThrowErrorMatchingInlineSnapshot( + ` + [AbiParameter.InvalidModifierError: Invalid ABI parameter. + + Modifier "memory" not allowed in "struct" type. + + Details: string memory bar] + `, + ) + + expect([AbiParameter.from('address from')]).toMatchInlineSnapshot(` + [ + { + "name": "from", + "type": "address", + }, + ] +`) + }) + + test.each([ + { signature: 'string', expected: { type: 'string' } }, + { signature: 'string foo', expected: { name: 'foo', type: 'string' } }, + { + signature: 'string indexed foo', + expected: { name: 'foo', type: 'string', indexed: true }, + }, + { + signature: 'string calldata foo', + expected: { name: 'foo', type: 'string' }, + }, + { + signature: '(string)', + expected: { type: 'tuple', components: [{ type: 'string' }] }, + }, + { + signature: '(string foo)', + expected: { + type: 'tuple', + components: [{ name: 'foo', type: 'string' }], + }, + }, + { + signature: '(string bar) foo', + expected: { + type: 'tuple', + name: 'foo', + components: [{ name: 'bar', type: 'string' }], + }, + }, + { + signature: '(string bar, string baz) foo', + expected: { + type: 'tuple', + name: 'foo', + components: [ + { name: 'bar', type: 'string' }, + { name: 'baz', type: 'string' }, + ], + }, + }, + { signature: 'string[]', expected: { type: 'string[]' } }, + ])('AbiParameter.from($signature)', ({ signature, expected }) => { + expect(AbiParameter.from(signature)).toEqual(expected) + }) + + test.each([ + { + signatures: ['struct Foo { string bar; }', 'Foo'], + expected: { + type: 'tuple', + components: [{ name: 'bar', type: 'string' }], + }, + }, + { + signatures: ['struct Foo { string bar; }', 'Foo foo'], + expected: { + type: 'tuple', + name: 'foo', + components: [{ name: 'bar', type: 'string' }], + }, + }, + { + signatures: ['struct Foo { string bar; }', 'Foo indexed foo'], + expected: { + type: 'tuple', + name: 'foo', + indexed: true, + components: [{ name: 'bar', type: 'string' }], + }, + }, + ])('AbiParameter.from($signatures)', ({ signatures, expected }) => { + expect(AbiParameter.from(signatures)).toEqual(expected) + }) + + test('nested tuple', () => { + const result = AbiParameter.from('((((string baz) bar)[1] foo) boo)') + expect(result).toMatchInlineSnapshot(` + { + "components": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "name": "baz", + "type": "string", + }, + ], + "name": "bar", + "type": "tuple", + }, + ], + "name": "foo", + "type": "tuple[1]", + }, + ], + "name": "boo", + "type": "tuple", + }, + ], + "type": "tuple", + } + `) + assertType<{ + type: 'tuple' + components: readonly [ + { + type: 'tuple' + components: readonly [ + { + type: 'tuple[1]' + components: readonly [ + { + type: 'tuple' + components: readonly [ + { + type: 'string' + name: 'baz' + }, + ] + name: 'bar' + }, + ] + name: 'foo' + }, + ] + name: 'boo' + }, + ] + }>(result) + }) +}) + +test('exports', () => { + expect(Object.keys(AbiParameter)).toMatchInlineSnapshot(` + [ + "format", + "from", + "InvalidAbiParameterError", + ] + `) +}) diff --git a/src/core/_test/AbiParameters.test-d.ts b/src/core/_test/AbiParameters.test-d.ts new file mode 100644 index 00000000..8b6f1d45 --- /dev/null +++ b/src/core/_test/AbiParameters.test-d.ts @@ -0,0 +1,88 @@ +import { type AbiParameter, AbiParameters } from 'ox' +import { expectTypeOf, test } from 'vitest' + +test('AbiParameters.format', () => { + expectTypeOf( + AbiParameters.format([ + { + type: 'tuple', + components: [{ type: 'string' }], + }, + ]), + ).toEqualTypeOf<'(string)'>() + + const param = { type: 'address' } + const param2: AbiParameter.AbiParameter = param + + expectTypeOf(AbiParameters.format([param])).toEqualTypeOf() + + expectTypeOf( + AbiParameters.format([param, param]), + ).toEqualTypeOf<`${string}, ${string}`>() + + expectTypeOf( + AbiParameters.format([param2, param2]), + ).toEqualTypeOf<`${string}, ${string}`>() +}) + +test('AbiParameters.format.ReturnType', () => { + // @ts-expect-error must have at least one parameter + expectTypeOf>().toEqualTypeOf() + + // string + expectTypeOf< + AbiParameters.format.ReturnType< + [ + { + readonly type: 'address' + readonly name: 'from' + }, + ] + > + >().toEqualTypeOf<'address from'>() + expectTypeOf< + AbiParameters.format.ReturnType< + [ + { + readonly type: 'address' + readonly name: 'from' + readonly indexed: true + }, + ] + > + >().toEqualTypeOf<'address indexed from'>() + + // Array + expectTypeOf< + AbiParameters.format.ReturnType< + [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'name' + readonly type: 'string' + }, + ] + }, + ] + > + >().toEqualTypeOf<'(string name)'>() + + expectTypeOf< + AbiParameters.format.ReturnType< + [ + { + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + readonly name: 'foo' + }, + ] + > + >().toEqualTypeOf<'(string bar) foo'>() +}) diff --git a/src/core/_test/AbiParameters.test.ts b/src/core/_test/AbiParameters.test.ts index 97ba9792..b10654d0 100644 --- a/src/core/_test/AbiParameters.test.ts +++ b/src/core/_test/AbiParameters.test.ts @@ -1,5 +1,5 @@ import { AbiParameters } from 'ox' -import { describe, expect, test } from 'vitest' +import { describe, expect, expectTypeOf, test } from 'vitest' import { address } from '../../../test/constants/addresses.js' describe('encodePacked', () => { @@ -334,18 +334,32 @@ describe('from', () => { describe('format', () => { test('default', () => { - const parameters = AbiParameters.from([ - { - name: 'spender', - type: 'address', - }, + const result = AbiParameters.format([ + { type: 'address', name: 'foo' }, + { type: 'uint256', name: 'bar' }, + ]) + expect(result).toEqual('address foo, uint256 bar') + expectTypeOf(result).toEqualTypeOf<'address foo, uint256 bar'>() + }) + + test('tuple', () => { + const result = AbiParameters.format([ { - name: 'amount', - type: 'uint256', + type: 'tuple', + components: [ + { type: 'string', name: 'bar' }, + { type: 'string', name: 'baz' }, + ], + name: 'foo', }, + { type: 'uint256', name: 'bar' }, ]) - const formatted = AbiParameters.format(parameters) - expect(formatted).toMatchInlineSnapshot(`"address spender, uint256 amount"`) + expect(result).toMatchInlineSnapshot( + '"(string bar, string baz) foo, uint256 bar"', + ) + expectTypeOf( + result, + ).toEqualTypeOf<'(string bar, string baz) foo, uint256 bar'>() }) }) diff --git a/src/core/_test/internal/abiParameter.test-d.ts b/src/core/_test/internal/abiParameter.test-d.ts new file mode 100644 index 00000000..94063e1b --- /dev/null +++ b/src/core/_test/internal/abiParameter.test-d.ts @@ -0,0 +1,270 @@ +import { assertType, expectTypeOf, test } from 'vitest' + +import type { + ParseAbiParameter, + SplitParameters, + _ValidateAbiParameter, +} from '../../internal/abiParameter.js' + +type OptionsWithModifier = { modifier: 'calldata'; structs: unknown } +type OptionsWithIndexed = { modifier: 'indexed'; structs: unknown } +type OptionsWithStructs = { + structs: { + Foo: [{ type: 'address'; name: 'bar' }] + } +} + +test('ParseAbiParameter', () => { + // `${Type} ${Modifier} ${Name}` format + assertType>({ + type: 'string', + name: 'foo', + }) + assertType>({ + type: 'string', + indexed: true, + name: 'foo', + }) + assertType< + ParseAbiParameter< + 'Foo calldata foo', + OptionsWithModifier & OptionsWithStructs + > + >({ + type: 'tuple', + name: 'foo', + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter< + 'Foo indexed foo', + OptionsWithIndexed & OptionsWithStructs + > + >({ + type: 'tuple', + indexed: true, + name: 'foo', + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter< + 'Foo[][1] indexed foo', + OptionsWithIndexed & OptionsWithStructs + > + >({ + name: 'foo', + type: 'tuple[][1]', + indexed: true, + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter< + 'Foo[][1] calldata foo', + OptionsWithModifier & OptionsWithStructs + > + >({ + name: 'foo', + type: 'tuple[][1]', + components: [{ type: 'address', name: 'bar' }], + }) + + // `${Type} ${NameOrModifier}` format + assertType>({ + type: 'string', + name: 'foo', + }) + assertType>({ + type: 'string', + name: ['Error: "indexed" is a protected Solidity keyword.'], + }) + assertType>({ + type: 'string', + }) + assertType>({ + type: 'string', + indexed: true, + }) + assertType< + ParseAbiParameter<'Foo calldata', OptionsWithModifier & OptionsWithStructs> + >({ + type: 'tuple', + components: [{ type: 'address', name: 'bar' }], + }) + assertType< + ParseAbiParameter<'Foo indexed', OptionsWithIndexed & OptionsWithStructs> + >({ + type: 'tuple', + indexed: true, + components: [{ type: 'address', name: 'bar' }], + }) + assertType>({ + name: 'foo', + type: 'tuple[][1]', + components: [{ type: 'address', name: 'bar' }], + }) + assertType>({ + name: 'foo', + type: 'tuple[1]', + components: [{ type: 'address', name: 'bar' }], + }) + + // `${Type}` format + assertType>({ + type: 'string', + }) + assertType>({ + type: 'tuple', + components: [{ type: 'address', name: 'bar' }], + }) + assertType>({ + type: 'tuple[][1]', + components: [{ type: 'address', name: 'bar' }], + }) + + // tuple format + assertType>({ + type: 'tuple', + components: [{ type: 'string' }], + }) + assertType>({ + type: 'tuple', + components: [{ type: 'string' }, { type: 'string' }], + }) + assertType>({ + type: 'tuple', + components: [ + { type: 'string' }, + { type: 'tuple', components: [{ type: 'string' }] }, + ], + }) + + assertType>({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }, + ], + }) + + assertType>({ + type: 'address', + name: ['Error: "alias" is a protected Solidity keyword.'], + }) + // assertType>({ + // type: ['Error: Type "Foo" is not a valid ABI type.'], + // name: 'foo', + // }) + + assertType>({ type: 'int256' }) + assertType>({ type: 'uint256' }) + assertType>({ type: 'uint256[]' }) + assertType>({ type: 'uint256[10][]' }) +}) + +test('SplitParameters', () => { + expectTypeOf>().toEqualTypeOf<[]>() + expectTypeOf>().toEqualTypeOf<['string']>() + expectTypeOf>().toEqualTypeOf< + ['string', 'string'] + >() + expectTypeOf>().toEqualTypeOf< + ['string indexed foo'] + >() + expectTypeOf>().toEqualTypeOf< + ['string foo', 'string bar'] + >() + expectTypeOf< + SplitParameters<'address owner, (bool loading, (string[][] names) cats)[] dog, uint tokenId'> + >().toEqualTypeOf< + [ + 'address owner', + '(bool loading, (string[][] names) cats)[] dog', + 'uint tokenId', + ] + >() + + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "((string)" has too many opening parentheses.', + ] + >() + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "((((string))" has too many opening parentheses.', + ] + >() + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "(string)" has too many closing parentheses.', + ] + >() + expectTypeOf>().toEqualTypeOf< + [ + 'Error: Unbalanced parentheses. "(string)" has too many closing parentheses.', + ] + >() +}) + +test('_ValidateAbiParameter', () => { + expectTypeOf<_ValidateAbiParameter<{ type: 'string' }>>().toEqualTypeOf<{ + type: 'string' + }>() + expectTypeOf< + _ValidateAbiParameter<{ type: 'string'; name: 'foo' }> + >().toEqualTypeOf<{ + type: 'string' + name: 'foo' + }>() + + expectTypeOf<_ValidateAbiParameter<{ type: 'int' }>>().toEqualTypeOf<{ + readonly type: 'int256' + }>() + expectTypeOf<_ValidateAbiParameter<{ type: 'uint' }>>().toEqualTypeOf<{ + readonly type: 'uint256' + }>() + expectTypeOf<_ValidateAbiParameter<{ type: 'uint[]' }>>().toEqualTypeOf<{ + readonly type: 'uint256[]' + }>() + expectTypeOf<_ValidateAbiParameter<{ type: 'uint[10][]' }>>().toEqualTypeOf<{ + readonly type: 'uint256[10][]' + }>() + + // expectTypeOf< + // _ValidateAbiParameter<{ type: 'string'; name: 'f0!' }> + // >().toEqualTypeOf<{ + // type: 'string' + // readonly name: ['Error: "f0!" contains invalid character.'] + // }>() + // expectTypeOf< + // _ValidateAbiParameter<{ type: 'string'; name: 'alias' }> + // >().toEqualTypeOf<{ + // type: 'string' + // readonly name: ['Error: "alias" is a protected Solidity keyword.'] + // }>() + // expectTypeOf< + // _ValidateAbiParameter<{ type: 'Bar'; name: 'foo' }> + // >().toEqualTypeOf<{ + // readonly type: ['Error: Type "Bar" is not a valid ABI type.'] + // name: 'foo' + // }>() +}) diff --git a/src/core/_test/internal/humanReadable/types/signatures.test-d.ts b/src/core/_test/internal/humanReadable/types/signatures.test-d.ts new file mode 100644 index 00000000..99401c24 --- /dev/null +++ b/src/core/_test/internal/humanReadable/types/signatures.test-d.ts @@ -0,0 +1,255 @@ +import { assertType, expectTypeOf, test } from 'vitest' + +import type { + IsConstructorSignature, + IsErrorSignature, + IsEventSignature, + IsFallbackSignature, + IsFunctionSignature, + IsName, + IsSignature, + IsSolidityKeyword, + IsStructSignature, + IsValidCharacter, + Signature, + Signatures, + SolidityKeywords, + ValidateName, +} from '../../../../internal/humanReadable/signatures.js' + +test('IsErrorSignature', () => { + // basic + assertType>(true) + + // params + assertType>(true) + assertType>(true) + assertType>(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsEventSignature', () => { + // basic + assertType>(true) + + // params + assertType>(true) + assertType>(true) + assertType>(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsFunctionSignature', () => { + // basic + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + // combinations + assertType>(true) + assertType>(true) + assertType>(true) + assertType< + IsFunctionSignature<'function foo() public view returns (uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo() public view returns(uint256)'> + >(true) + // params + assertType>(true) + assertType>( + true, + ) + assertType>( + true, + ) + assertType>(true) + assertType>(true) + assertType< + IsFunctionSignature<'function foo(uint256) view returns (uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) view returns(uint256)'> + >(true) + assertType>(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns (uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns(uint256)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns (uint256 tokenId)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns(uint256 tokenId)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns (uint256 tokenId, uint256 balance)'> + >(true) + assertType< + IsFunctionSignature<'function foo(uint256) public view returns(uint256 tokenId, uint256 balance)'> + >(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsStructSignature', () => { + // basic + assertType>(true) + + // properties + assertType>(true) + assertType>(true) + assertType>( + true, + ) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsConstructorSignature', () => { + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>( + true, + ) + assertType>(true) + assertType>( + true, + ) + + assertType>(false) + assertType>(false) +}) + +test('IsFallbackSignature', () => { + assertType>(true) + assertType>(true) + + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('IsSignature', () => { + // basic + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + assertType>(true) + + // invalid + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) + assertType>(false) +}) + +test('Signature', () => { + assertType>('function foo()') + assertType>([ + 'Error: Signature "function foo ()" is invalid.', + ]) + // assertType>([ + // 'Error: Signature "function foo??()" is invalid.', + // ]) +}) + +test('Signatures', () => { + assertType>(['function foo()']) + assertType>([ + ['Error: Signature "function foo ()" is invalid at position 0.'], + ]) +}) + +test('IsName', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no whitespace + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no solidity keywords + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no number strings + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + // no invalid characters + // expectTypeOf>().toEqualTypeOf() + // expectTypeOf>().toEqualTypeOf() +}) + +test('ValidateName', () => { + expectTypeOf>().toEqualTypeOf<'foo'>() + expectTypeOf>().toEqualTypeOf<'foo$'>() + expectTypeOf>().toEqualTypeOf< + ['Error: Identifier "foo bar" cannot contain whitespace.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: "alias" is a protected Solidity keyword.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: Identifier "123" cannot be a number string.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: Identifier "12foo" cannot start with a number.'] + >() + expectTypeOf>().toEqualTypeOf< + ['Error: "foo?" contains invalid character.'] + >() +}) + +test('IsSolidityKeyword', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) + +test('IsValidCharacter', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) diff --git a/src/core/_test/internal/humanReadable/types/structs.test-d.ts b/src/core/_test/internal/humanReadable/types/structs.test-d.ts new file mode 100644 index 00000000..2ecf2114 --- /dev/null +++ b/src/core/_test/internal/humanReadable/types/structs.test-d.ts @@ -0,0 +1,221 @@ +import { expectTypeOf, test } from 'vitest' + +import type { + ParseStruct, + ParseStructProperties, + ParseStructs, + ResolveStructs, + StructLookup, +} from '../../../../internal/humanReadable/structs.js' + +test('ParseStructs', () => { + type Result = ParseStructs< + [ + 'struct Person { Name name; }', + 'struct Name { Foo foo; }', + 'struct Foo { string bar; }', + 'function addPerson(Person person)', + ] + > + expectTypeOf().toEqualTypeOf<{ + Person: readonly [ + { + readonly name: 'name' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly type: 'string' + readonly name: 'bar' + }, + ] + }, + ] + }, + ] + Foo: readonly [{ readonly type: 'string'; readonly name: 'bar' }] + Name: readonly [ + { + readonly type: 'tuple' + readonly name: 'foo' + readonly components: readonly [ + { readonly type: 'string'; readonly name: 'bar' }, + ] + }, + ] + }>() + + expectTypeOf< + ParseStructs<['struct Foo { Bar bar; }', 'struct Bar { Foo foo; }']> + >().toEqualTypeOf<{ + Foo: readonly [ + { + readonly name: 'bar' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + [ + 'Error: Circular reference detected. Struct "Bar" is a circular reference.', + ], + ] + }, + ] + }, + ] + Bar: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'bar' + readonly type: 'tuple' + readonly components: readonly [ + [ + 'Error: Circular reference detected. Struct "Foo" is a circular reference.', + ], + ] + }, + ] + }, + ] + }>() + + expectTypeOf>().toEqualTypeOf<{ + Foo: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + [ + 'Error: Circular reference detected. Struct "Foo" is a circular reference.', + ], + ] + }, + ] + }>() + + expectTypeOf< + ParseStructs<['struct Person { Name name;']> + >().toEqualTypeOf<{}>() + + expectTypeOf>().toEqualTypeOf<{}>() + expectTypeOf< + ParseStructs<['function addPerson(Person person)']> + >().toEqualTypeOf<{}>() +}) + +test('ParseStruct', () => { + expectTypeOf< + ParseStruct<'struct Foo { string foo; string bar; }'> + >().toEqualTypeOf<{ + readonly name: 'Foo' + readonly components: [ + { readonly type: 'string'; readonly name: 'foo' }, + { readonly type: 'string'; readonly name: 'bar' }, + ] + }>() + expectTypeOf>().toEqualTypeOf<{ + readonly name: 'Foo' + readonly components: [] + }>() + expectTypeOf>().toEqualTypeOf<{ + readonly name: 'Foo' + readonly components: [ + { + readonly type: 'Bar' + readonly name: 'bar' + }, + ] + }>() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) + +test('ResolveStructs', () => { + type Result = ResolveStructs< + [{ type: 'Name'; name: 'name' }], + { + Person: [{ type: 'Name'; name: 'name' }] + Name: [{ type: 'Foo'; name: 'foo' }] + Foo: [{ type: 'string'; name: 'bar' }, { type: 'uint16'; name: 'baz' }] + } + > + expectTypeOf().toEqualTypeOf< + readonly [ + { + readonly name: 'name' + readonly type: 'tuple' + readonly components: readonly [ + { + readonly name: 'foo' + readonly type: 'tuple' + readonly components: readonly [ + { type: 'string'; name: 'bar' }, + { type: 'uint16'; name: 'baz' }, + ] + }, + ] + }, + ] + >() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf< + ResolveStructs< + [{ type: 'Foo[]'; name: 'foo' }], + { Foo: [{ type: 'string'; name: 'bar' }] } + > + >().toEqualTypeOf< + readonly [ + { + readonly name: 'foo' + readonly type: 'tuple[]' + readonly components: readonly [ + { + type: 'string' + name: 'bar' + }, + ] + }, + ] + >() +}) + +test('ParseStructProperties', () => { + expectTypeOf>().toEqualTypeOf< + [{ readonly type: 'string' }] + >() + expectTypeOf>().toEqualTypeOf< + [{ readonly type: 'string'; readonly name: 'foo' }] + >() + expectTypeOf< + ParseStructProperties<'string; string;'> extends [ + { readonly type: 'string' }, + { readonly type: 'string' }, + ] + ? true + : false + >().toEqualTypeOf() + expectTypeOf< + ParseStructProperties<'string foo; string bar;'> + >().toEqualTypeOf< + [ + { readonly type: 'string'; readonly name: 'foo' }, + { readonly type: 'string'; readonly name: 'bar' }, + ] + >() + + expectTypeOf>().toEqualTypeOf<[]>() + expectTypeOf>().toEqualTypeOf<[]>() + expectTypeOf>().toEqualTypeOf< + [{ readonly type: 'string' }] + >() +}) diff --git a/src/core/_test/internal/humanReadable/types/utils.test-d.ts b/src/core/_test/internal/humanReadable/types/utils.test-d.ts new file mode 100644 index 00000000..61677c94 --- /dev/null +++ b/src/core/_test/internal/humanReadable/types/utils.test-d.ts @@ -0,0 +1,764 @@ +import { assertType, expectTypeOf, test } from 'vitest' + +import type { + ParseAbiParameters, + ParseSignature, + _ParseFunctionParametersAndStateMutability, + _ParseTuple, + _SplitNameOrModifier, + _UnwrapNameOrModifier, +} from '../../../../internal/humanReadable/types/utils.js' + +type OptionsWithModifier = { modifier: 'calldata'; structs: unknown } +type OptionsWithIndexed = { modifier: 'indexed'; structs: unknown } +type OptionsWithStructs = { + structs: { + Foo: [{ type: 'address'; name: 'bar' }] + } +} + +test('ParseSignature', () => { + type Structs = { + Baz: [{ type: 'address'; name: 'baz' }] + } + + // Error + assertType>({ + type: 'error', + name: 'Foo', + inputs: [], + }) + assertType>({ + type: 'error', + name: 'Foo', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'error', + name: 'Foo', + inputs: [{ type: 'string', name: 'bar' }], + }) + assertType>({ + type: 'error', + name: 'Foo', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + + // Events + assertType>({ + type: 'event', + name: 'Foo', + inputs: [], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [{ type: 'string', name: 'bar' }], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [{ type: 'string', indexed: true, name: 'bar' }], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + assertType>({ + type: 'event', + name: 'Foo', + inputs: [ + { + type: 'tuple', + indexed: true, + name: 'bar', + components: [{ type: 'string' }], + }, + ], + }) + + // Constructor + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [], + }) + assertType>({ + type: 'constructor', + stateMutability: 'payable', + inputs: [], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'payable', + inputs: [{ type: 'string' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'foo' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'foo' }], + }) + assertType>({ + type: 'constructor', + stateMutability: 'nonpayable', + inputs: [{ type: 'tuple', name: 'foo', components: [{ type: 'string' }] }], + }) + + // Fallback + assertType>({ + type: 'fallback', + stateMutability: 'nonpayable', + }) + assertType>({ + type: 'fallback', + stateMutability: 'payable', + }) + + // Receive + assertType>({ + type: 'receive', + stateMutability: 'payable', + }) + + // Functions + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [], + }) + + // inputs + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'view', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + outputs: [], + }) + + assertType>({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'string', + name: ['Error: "indexed" is a protected Solidity keyword.'], + }, + ], + outputs: [], + }) + assertType>({ + name: 'foo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'string', + name: ['Error: Identifier "indexed bar" cannot contain whitespace.'], + }, + ], + outputs: [], + }) + + // outputs + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [{ type: 'string' }], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [], + outputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'view', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo() public payable returns (string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo() public payable returns(string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [], + outputs: [{ type: 'string', name: 'bar' }], + }) + + // inputs and outputs + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string' }], + outputs: [{ type: 'string' }], + }) + assertType>({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo(string foo) public payable returns(string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [{ type: 'string', name: 'foo' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo(Baz bar) returns (Baz bar)', Structs> + >({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + outputs: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'address', name: 'baz' }], + }, + ], + }) + assertType< + ParseSignature<'function foo(string bar) view returns (string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'view', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + assertType< + ParseSignature<'function foo(string bar) public payable returns (string bar)'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'payable', + inputs: [{ type: 'string', name: 'bar' }], + outputs: [{ type: 'string', name: 'bar' }], + }) + + assertType< + ParseSignature<'function foo(((string)) calldata) returns (string, (string))'> + >({ + type: 'function', + name: 'foo', + stateMutability: 'nonpayable', + inputs: [ + { + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }, + ], + outputs: [ + { + type: 'string', + }, + { + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }) +}) + +test('ParseAbiParameters', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf< + readonly [{ readonly type: 'string' }] + >() + expectTypeOf< + ParseAbiParameters<['string', 'string']> extends readonly [ + { readonly type: 'string' }, + { readonly type: 'string' }, + ] + ? true + : false + >().toEqualTypeOf() +}) + +test('_ParseFunctionParametersAndStateMutability', () => { + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo()'> + >().toEqualTypeOf<{ + Inputs: '' + StateMutability: 'nonpayable' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo(string bar)'> + >().toEqualTypeOf<{ + Inputs: 'string bar' + StateMutability: 'nonpayable' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo() view'> + >().toEqualTypeOf<{ + Inputs: '' + StateMutability: 'view' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo(string bar) view'> + >().toEqualTypeOf<{ + Inputs: 'string bar' + StateMutability: 'view' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function foo(string bar, uint256) external view'> + >().toEqualTypeOf<{ + Inputs: 'string bar, uint256' + StateMutability: 'view' + }>() + + expectTypeOf< + _ParseFunctionParametersAndStateMutability<'function stepChanges((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle) stateChanges, uint256 action, bool revetOnInvalidMoves) pure returns ((uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle))'> + >().toEqualTypeOf<{ + Inputs: '(uint256 characterID, uint64 newPosition, uint24 xp, uint24 epoch, uint8 hp, (int32 x, int32 y, uint8 hp, uint8 kind)[5] monsters, (uint8 monsterIndexPlus1, uint8 attackCardsUsed1, uint8 attackCardsUsed2, uint8 defenseCardsUsed1, uint8 defenseCardsUsed2) battle) stateChanges, uint256 action, bool revetOnInvalidMoves' + StateMutability: 'pure' + }>() +}) + +test('_ParseTuple', () => { + // basic tuples + assertType<_ParseTuple<'(string)'>>({ + type: 'tuple', + components: [{ type: 'string' }], + }) + assertType<_ParseTuple<'(string, string)'>>({ + type: 'tuple', + components: [{ type: 'string' }, { type: 'string' }], + }) + assertType<_ParseTuple<'((string, string), string)'>>({ + type: 'tuple', + components: [ + { type: 'tuple', components: [{ type: 'string' }, { type: 'string' }] }, + { type: 'string' }, + ], + }) + assertType<_ParseTuple<'((string))'>>({ + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }) + assertType<_ParseTuple<'(((string)))'>>({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }, + ], + }) + assertType<_ParseTuple<'(string calldata)'>>({ + type: 'tuple', + components: [ + { + type: 'string', + name: ['Error: "calldata" is a protected Solidity keyword.'], + }, + ], + }) + assertType<_ParseTuple<'(Foo)', OptionsWithStructs>>({ + type: 'tuple', + components: [ + { type: 'tuple', components: [{ type: 'address', name: 'bar' }] }, + ], + }) + + // named tuple params + assertType<_ParseTuple<'(string foo)'>>({ + type: 'tuple', + components: [{ type: 'string', name: 'foo' }], + }) + assertType<_ParseTuple<'(string bar) foo'>>({ + name: 'foo', + type: 'tuple', + components: [{ type: 'string', name: 'bar' }], + }) + assertType<_ParseTuple<'((string bar) foo)'>>({ + type: 'tuple', + components: [ + { + type: 'tuple', + name: 'foo', + components: [{ type: 'string', name: 'bar' }], + }, + ], + }) + assertType<_ParseTuple<'(Foo) foo', OptionsWithStructs>>({ + type: 'tuple', + name: 'foo', + components: [ + { type: 'tuple', components: [{ type: 'address', name: 'bar' }] }, + ], + }) + assertType<_ParseTuple<'((string)) calldata', OptionsWithModifier>>({ + type: 'tuple', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }) + + // mixed basic and named tuple params + assertType<_ParseTuple<'(string, string foo)'>>({ + type: 'tuple', + components: [{ type: 'string' }, { type: 'string', name: 'foo' }], + }) + assertType<_ParseTuple<'(string, string bar) foo'>>({ + name: 'foo', + type: 'tuple', + components: [{ type: 'string' }, { type: 'string', name: 'bar' }], + }) + assertType< + _ParseTuple<'(string baz, string bar) indexed foo', OptionsWithIndexed> + >({ + name: 'foo', + type: 'tuple', + components: [ + { type: 'string', name: 'baz' }, + { type: 'string', name: 'bar' }, + ], + indexed: true, + }) + + // inline tuples of tuples + assertType<_ParseTuple<'(string)[]'>>({ + type: 'tuple[]', + components: [{ type: 'string' }], + }) + assertType<_ParseTuple<'(string, string)[]'>>({ + type: 'tuple[]', + components: [{ type: 'string' }, { type: 'string' }], + }) + assertType<_ParseTuple<'((string))[]'>>({ + type: 'tuple[]', + components: [{ type: 'tuple', components: [{ type: 'string' }] }], + }) + assertType<_ParseTuple<'((string)[])[]'>>({ + type: 'tuple[]', + components: [ + { + type: 'tuple[]', + components: [{ type: 'string' }], + }, + ], + }) + + // inline tuples of tuples with name and/or modifier attached + assertType<_ParseTuple<'(string)[] foo'>>({ + type: 'tuple[]', + name: 'foo', + components: [{ type: 'string' }], + }) + assertType<_ParseTuple<'(string, string bar)[] foo'>>({ + type: 'tuple[]', + name: 'foo', + components: [{ type: 'string' }, { type: 'string', name: 'bar' }], + }) + assertType<_ParseTuple<'((string baz) bar)[] foo'>>({ + type: 'tuple[]', + name: 'foo', + components: [ + { + type: 'tuple', + name: 'bar', + components: [{ type: 'string', name: 'baz' }], + }, + ], + }) + assertType< + _ParseTuple< + '((string)[])[] indexed foo', + OptionsWithIndexed & OptionsWithStructs + > + >({ + type: 'tuple[]', + name: 'foo', + indexed: true, + components: [ + { + type: 'tuple[]', + components: [{ type: 'string' }], + }, + ], + }) + assertType<_ParseTuple<'((string) foo)[]'>>({ + type: 'tuple[]', + components: [ + { type: 'tuple', name: 'foo', components: [{ type: 'string' }] }, + ], + }) + assertType<_ParseTuple<'(string) indexed bar', OptionsWithIndexed>>({ + type: 'tuple', + name: 'bar', + indexed: true, + components: [{ type: 'string' }], + }) + + assertType<_ParseTuple<'((((string))) bar)'>>({ + type: 'tuple', + components: [ + { + name: 'bar', + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }, + ], + }, + ], + }) + assertType<_ParseTuple<'(((string) baz) bar) foo'>>({ + type: 'tuple', + name: 'foo', + components: [ + { + name: 'bar', + type: 'tuple', + components: [ + { + name: 'baz', + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }, + ], + }) + assertType<_ParseTuple<'((((string) baz)) bar) foo'>>({ + type: 'tuple', + name: 'foo', + components: [ + { + name: 'bar', + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + name: 'baz', + type: 'tuple', + components: [{ type: 'string' }], + }, + ], + }, + ], + }, + ], + }) + + // Modifiers not converted inside tuples + assertType<_ParseTuple<'(string indexed)[] foo', OptionsWithIndexed>>({ + type: 'tuple[]', + name: 'foo', + components: [ + { + type: 'string', + name: ['Error: "indexed" is a protected Solidity keyword.'], + }, + ], + }) + + assertType<_ParseTuple<'((((string baz) bar)[1] foo) boo)'>>({ + type: 'tuple', + components: [ + { + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }, + ], + }) + assertType<_ParseTuple<'(((string baz) bar)[1] foo) boo'>>({ + type: 'tuple', + components: [ + { + type: 'tuple[1]', + components: [ + { + type: 'tuple', + components: [ + { + type: 'string', + name: 'baz', + }, + ], + name: 'bar', + }, + ], + name: 'foo', + }, + ], + name: 'boo', + }) +}) + +test('_SplitNameOrModifier', () => { + expectTypeOf<_SplitNameOrModifier<'foo'>>().toEqualTypeOf<{ + readonly name: 'foo' + }>() + expectTypeOf< + _SplitNameOrModifier<'indexed foo', { modifier: 'indexed' }> + >().toEqualTypeOf<{ + readonly name: 'foo' + readonly indexed: true + }>() + expectTypeOf< + _SplitNameOrModifier<'calldata foo', { modifier: 'calldata' }> + >().toEqualTypeOf<{ + readonly name: 'foo' + }>() +}) + +test('_UnwrapNameOrModifier', () => { + expectTypeOf<_UnwrapNameOrModifier<'bar) foo'>>().toEqualTypeOf<{ + End: 'bar' + nameOrModifier: 'foo' + }>() + expectTypeOf<_UnwrapNameOrModifier<'baz) bar) foo'>>().toEqualTypeOf<{ + End: 'baz) bar' + nameOrModifier: 'foo' + }>() + expectTypeOf<_UnwrapNameOrModifier<'string) calldata foo'>>().toEqualTypeOf<{ + End: 'string' + nameOrModifier: 'calldata foo' + }>() +}) diff --git a/src/core/internal/abiConstructor.ts b/src/core/internal/abiConstructor.ts index 081360f9..184c9abd 100644 --- a/src/core/internal/abiConstructor.ts +++ b/src/core/internal/abiConstructor.ts @@ -1,12 +1,12 @@ -import type * as AbiItem_internal from './abiItem.js' +import type * as Signatures_internal from './humanReadable/signatures.js' import type { TypeErrorMessage } from './types.js' /** @internal */ export type IsSignature = - | (AbiItem_internal.IsConstructorSignature extends true + | (Signatures_internal.IsConstructorSignature extends true ? true : never) - | (AbiItem_internal.IsStructSignature extends true + | (Signatures_internal.IsStructSignature extends true ? true : never) extends infer condition ? [condition] extends [never] diff --git a/src/core/internal/abiError.ts b/src/core/internal/abiError.ts index c6897303..d4602868 100644 --- a/src/core/internal/abiError.ts +++ b/src/core/internal/abiError.ts @@ -1,10 +1,12 @@ -import type * as AbiItem_internal from './abiItem.js' +import type * as Signatures_internal from './humanReadable/signatures.js' import type { TypeErrorMessage } from './types.js' /** @internal */ export type IsSignature = - | (AbiItem_internal.IsErrorSignature extends true ? true : never) - | (AbiItem_internal.IsStructSignature extends true + | (Signatures_internal.IsErrorSignature extends true + ? true + : never) + | (Signatures_internal.IsStructSignature extends true ? true : never) extends infer condition ? [condition] extends [never] diff --git a/src/core/internal/abiEvent.ts b/src/core/internal/abiEvent.ts index 319be24a..fcbfc267 100644 --- a/src/core/internal/abiEvent.ts +++ b/src/core/internal/abiEvent.ts @@ -1,7 +1,7 @@ import type * as abitype from 'abitype' import type * as Filter from '../Filter.js' import type * as Hex from '../Hex.js' -import type * as AbiItem_internal from './abiItem.js' +import type * as internal_signatures from './humanReadable/signatures.js' import type { Compute, Filter as Filter_internal, @@ -26,8 +26,10 @@ export type DefaultEventParameterOptions = { /** @internal */ export type IsSignature = - | (AbiItem_internal.IsEventSignature extends true ? true : never) - | (AbiItem_internal.IsStructSignature extends true + | (internal_signatures.IsEventSignature extends true + ? true + : never) + | (internal_signatures.IsStructSignature extends true ? true : never) extends infer condition ? [condition] extends [never] diff --git a/src/core/internal/abiFunction.ts b/src/core/internal/abiFunction.ts index 9569518f..638fb8af 100644 --- a/src/core/internal/abiFunction.ts +++ b/src/core/internal/abiFunction.ts @@ -1,12 +1,12 @@ -import type * as AbiItem_internal from './abiItem.js' +import type * as internal_signatures from './humanReadable/signatures.js' import type { TypeErrorMessage } from './types.js' /** @internal */ export type IsSignature = - | (AbiItem_internal.IsFunctionSignature extends true + | (internal_signatures.IsFunctionSignature extends true ? true : never) - | (AbiItem_internal.IsStructSignature extends true + | (internal_signatures.IsStructSignature extends true ? true : never) extends infer condition ? [condition] extends [never] diff --git a/src/core/internal/abiItem.ts b/src/core/internal/abiItem.ts index 23043ee4..e259bbe9 100644 --- a/src/core/internal/abiItem.ts +++ b/src/core/internal/abiItem.ts @@ -4,13 +4,7 @@ import type * as AbiItem from '../AbiItem.js' import type * as AbiParameters from '../AbiParameters.js' import * as Address from '../Address.js' import * as Errors from '../Errors.js' -import type { - Compute, - IsNever, - IsUnion, - TypeErrorMessage, - UnionToTuple, -} from './types.js' +import type { Compute, IsNever, IsUnion, UnionToTuple } from './types.js' /** @internal */ export type ExtractArgs< @@ -76,330 +70,6 @@ export type TupleToUnion< : never }[number] -/** @internal */ -export type ErrorSignature< - name extends string = string, - parameters extends string = string, -> = `error ${name}(${parameters})` - -/** @internal */ -export type IsErrorSignature = - signature extends ErrorSignature ? IsName : false - -/** @internal */ -export type EventSignature< - name extends string = string, - parameters extends string = string, -> = `event ${name}(${parameters})` - -/** @internal */ -export type IsEventSignature = - signature extends EventSignature ? IsName : false - -/** @internal */ -export type FunctionSignature< - name extends string = string, - tail extends string = string, -> = `function ${name}(${tail}` -export type IsFunctionSignature = - signature extends FunctionSignature - ? IsName extends true - ? signature extends ValidFunctionSignatures - ? true - : // Check that `Parameters` is not absorbing other types (e.g. `returns`) - signature extends `function ${string}(${infer parameters})` - ? parameters extends InvalidFunctionParameters - ? false - : true - : false - : false - : false -/** @internal */ -export type Scope = 'public' | 'external' // `internal` or `private` functions wouldn't make it to ABI so can ignore - -/** @internal */ -export type Returns = `returns (${string})` | `returns(${string})` - -// Almost all valid function signatures, except `function ${string}(${infer parameters})` since `parameters` can absorb returns -/** @internal */ -export type ValidFunctionSignatures = - | `function ${string}()` - // basic - | `function ${string}() ${Returns}` - | `function ${string}() ${abitype.AbiStateMutability}` - | `function ${string}() ${Scope}` - // combinations - | `function ${string}() ${abitype.AbiStateMutability} ${Returns}` - | `function ${string}() ${Scope} ${Returns}` - | `function ${string}() ${Scope} ${abitype.AbiStateMutability}` - | `function ${string}() ${Scope} ${abitype.AbiStateMutability} ${Returns}` - // Parameters - | `function ${string}(${string}) ${Returns}` - | `function ${string}(${string}) ${abitype.AbiStateMutability}` - | `function ${string}(${string}) ${Scope}` - | `function ${string}(${string}) ${abitype.AbiStateMutability} ${Returns}` - | `function ${string}(${string}) ${Scope} ${Returns}` - | `function ${string}(${string}) ${Scope} ${abitype.AbiStateMutability}` - | `function ${string}(${string}) ${Scope} ${abitype.AbiStateMutability} ${Returns}` - -/** @internal */ -export type StructSignature< - name extends string = string, - properties extends string = string, -> = `struct ${name} {${properties}}` - -/** @internal */ -export type IsStructSignature = - signature extends StructSignature ? IsName : false - -/** @internal */ -export type ConstructorSignature = - `constructor(${tail}` - -/** @internal */ -export type IsConstructorSignature = - signature extends ConstructorSignature - ? signature extends ValidConstructorSignatures - ? true - : false - : false - -/** @internal */ -export type ValidConstructorSignatures = - | `constructor(${string})` - | `constructor(${string}) payable` - -/** @internal */ -export type FallbackSignature = - `fallback() external${abiStateMutability}` - -/** @internal */ -export type ReceiveSignature = 'receive() external payable' - -// TODO: Maybe use this for signature validation one day -// https://twitter.com/devanshj__/status/1610423724708343808 -/** @internal */ -export type IsSignature = - | (IsErrorSignature extends true ? true : never) - | (IsEventSignature extends true ? true : never) - | (IsFunctionSignature extends true ? true : never) - | (IsStructSignature extends true ? true : never) - | (IsConstructorSignature extends true ? true : never) - | (type extends FallbackSignature ? true : never) - | (type extends ReceiveSignature ? true : never) extends infer condition - ? [condition] extends [never] - ? false - : true - : false - -/** @internal */ -export type Signature< - string1 extends string, - string2 extends string | unknown = unknown, -> = IsSignature extends true - ? string1 - : string extends string1 // if exactly `string` (not narrowed), then pass through as valid - ? string1 - : TypeErrorMessage<`Signature "${string1}" is invalid${string2 extends string - ? ` at position ${string2}` - : ''}.`> - -/** @internal */ -export type Signatures = { - [key in keyof signatures]: Signature -} - -/** @internal */ -export type IsName = name extends '' - ? false - : ValidateName extends name - ? true - : false - -/** @internal */ -export type ValidateName< - name extends string, - checkCharacters extends boolean = false, -> = name extends `${string}${' '}${string}` - ? TypeErrorMessage<`Identifier "${name}" cannot contain whitespace.`> - : IsSolidityKeyword extends true - ? TypeErrorMessage<`"${name}" is a protected Solidity keyword.`> - : name extends `${number}` - ? TypeErrorMessage<`Identifier "${name}" cannot be a number string.`> - : name extends `${number}${string}` - ? TypeErrorMessage<`Identifier "${name}" cannot start with a number.`> - : checkCharacters extends true - ? IsValidCharacter extends true - ? name - : TypeErrorMessage<`"${name}" contains invalid character.`> - : name - -/** @internal */ -export type IsSolidityKeyword = - type extends SolidityKeywords ? true : false - -/** @internal */ -export type SolidityKeywords = - | 'after' - | 'alias' - | 'anonymous' - | 'apply' - | 'auto' - | 'byte' - | 'calldata' - | 'case' - | 'catch' - | 'constant' - | 'copyof' - | 'default' - | 'defined' - | 'error' - | 'event' - | 'external' - | 'false' - | 'final' - | 'function' - | 'immutable' - | 'implements' - | 'in' - | 'indexed' - | 'inline' - | 'internal' - | 'let' - | 'mapping' - | 'match' - | 'memory' - | 'mutable' - | 'null' - | 'of' - | 'override' - | 'partial' - | 'private' - | 'promise' - | 'public' - | 'pure' - | 'reference' - | 'relocatable' - | 'return' - | 'returns' - | 'sizeof' - | 'static' - | 'storage' - | 'struct' - | 'super' - | 'supports' - | 'switch' - | 'this' - | 'true' - | 'try' - | 'typedef' - | 'typeof' - | 'var' - | 'view' - | 'virtual' - | `address${`[${string}]` | ''}` - | `bool${`[${string}]` | ''}` - | `string${`[${string}]` | ''}` - | `tuple${`[${string}]` | ''}` - | `bytes${number | ''}${`[${string}]` | ''}` - | `${'u' | ''}int${number | ''}${`[${string}]` | ''}` - -/** @internal */ -export type IsValidCharacter = - character extends `${ValidCharacters}${infer tail}` - ? tail extends '' - ? true - : IsValidCharacter - : false - -// biome-ignore format: no formatting -/** @internal */ -export type ValidCharacters = - // uppercase letters - | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' - // lowercase letters - | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' - // numbers - | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' - // special characters - | '_' | '$' - -// Template string inference can absorb `returns`: -// type Result = `function foo(string) return s (uint256)` extends `function ${string}(${infer Parameters})` ? Parameters : never -// // ^? type Result = "string ) return s (uint256" -// So we need to validate against `returns` keyword with all combinations of whitespace -/** @internal */ -export type InvalidFunctionParameters = - | `${string}${MangledReturns} (${string}` - | `${string}) ${MangledReturns}${string}` - | `${string})${string}${MangledReturns}${string}(${string}` - -// r_e_t_u_r_n_s -/** @internal */ -export type MangledReturns = - // Single - | `r${string}eturns` - | `re${string}turns` - | `ret${string}urns` - | `retu${string}rns` - | `retur${string}ns` - | `return${string}s` - // Double - // `r_e*` - | `r${string}e${string}turns` - | `r${string}et${string}urns` - | `r${string}etu${string}rns` - | `r${string}etur${string}ns` - | `r${string}eturn${string}s` - // `re_t*` - | `re${string}t${string}urns` - | `re${string}tu${string}rns` - | `re${string}tur${string}ns` - | `re${string}turn${string}s` - // `ret_u*` - | `ret${string}u${string}rns` - | `ret${string}ur${string}ns` - | `ret${string}urn${string}s` - // `retu_r*` - | `retu${string}r${string}ns` - | `retu${string}rn${string}s` - // `retur_n*` - | `retur${string}n${string}s` - // Triple - // `r_e_t*` - | `r${string}e${string}t${string}urns` - | `r${string}e${string}tu${string}rns` - | `r${string}e${string}tur${string}ns` - | `r${string}e${string}turn${string}s` - // `re_t_u*` - | `re${string}t${string}u${string}rns` - | `re${string}t${string}ur${string}ns` - | `re${string}t${string}urn${string}s` - // `ret_u_r*` - | `ret${string}u${string}r${string}ns` - | `ret${string}u${string}rn${string}s` - // `retu_r_n*` - | `retu${string}r${string}n${string}s` - // Quadruple - // `r_e_t_u*` - | `r${string}e${string}t${string}u${string}rns` - | `r${string}e${string}t${string}ur${string}ns` - | `r${string}e${string}t${string}urn${string}s` - // `re_t_u_r*` - | `re${string}t${string}u${string}r${string}ns` - | `re${string}t${string}u${string}rn${string}s` - // `ret_u_r_n*` - | `ret${string}u${string}r${string}n${string}s` - // Quintuple - // `r_e_t_u_r*` - | `r${string}e${string}t${string}u${string}r${string}ns` - | `r${string}e${string}t${string}u${string}rn${string}s` - // `re_t_u_r_n*` - | `re${string}t${string}u${string}r${string}n${string}s` - // Sextuple - // `r_e_t_u_r_n_s` - | `r${string}e${string}t${string}u${string}r${string}n${string}s` - /** @internal */ export type Widen = | ([unknown] extends [type] ? unknown : never) diff --git a/src/core/internal/abiParameter.ts b/src/core/internal/abiParameter.ts new file mode 100644 index 00000000..59e44698 --- /dev/null +++ b/src/core/internal/abiParameter.ts @@ -0,0 +1,430 @@ +import type * as abitype from 'abitype' + +import type * as AbiParameter from '../AbiParameter.js' +import { + InvalidFunctionModifierError, + InvalidModifierError, + InvalidParameterError, + InvalidParenthesisError, + SolidityProtectedKeywordError, + UnknownSolidityTypeError, +} from './humanReadable/errors.js' +import { + type FunctionModifier, + type Modifier, + type ValidateName, + functionModifiers, +} from './humanReadable/signatures.js' +import type * as structs from './humanReadable/structs.js' +import type { + _ParseTuple, + _SplitNameOrModifier, +} from './humanReadable/types/utils.js' +import { bytesRegex, execTyped, integerRegex, isTupleRegex } from './regex.js' +import type { + Evaluate, + IsUnknown, + Merge, + Trim, + TypeErrorMessage, +} from './types.js' + +export function parseAbiParameter( + param: string, + options?: + | { + modifiers?: Set + structs?: structs.StructLookup + type?: abitype.AbiItemType | 'struct' + } + | undefined, +) { + // optional namespace cache by `type` + const parameterCacheKey = getParameterCacheKey( + param, + options?.type, + options?.structs, + ) + if (parameterCache.has(parameterCacheKey)) + return parameterCache.get(parameterCacheKey)! + + const isTuple = isTupleRegex.test(param) + const match = execTyped<{ + array?: string + modifier?: Modifier + name?: string + type: string + }>( + isTuple ? abiParameterWithTupleRegex : abiParameterWithoutTupleRegex, + param, + ) + if (!match) throw new InvalidParameterError({ param }) + + if (match.name && isSolidityKeyword(match.name)) + throw new SolidityProtectedKeywordError({ + param, + name: match.name, + }) + + const name = match.name ? { name: match.name } : {} + const indexed = match.modifier === 'indexed' ? { indexed: true } : {} + const structs = options?.structs ?? {} + let type: string + let components = {} + if (isTuple) { + type = 'tuple' + const params = splitParameters(match.type) + const components_ = [] + const length = params.length + for (let i = 0; i < length; i++) { + // remove `modifiers` from `options` to prevent from being added to tuple components + components_.push(parseAbiParameter(params[i]!, { structs })) + } + components = { components: components_ } + } else if (match.type in structs) { + type = 'tuple' + components = { components: structs[match.type] } + } else if (dynamicIntegerRegex.test(match.type)) { + type = `${match.type}256` + } else { + type = match.type + if (!(options?.type === 'struct') && !isSolidityType(type)) + throw new UnknownSolidityTypeError({ type }) + } + + if (match.modifier) { + // Check if modifier exists, but is not allowed (e.g. `indexed` in `functionModifiers`) + if (!options?.modifiers?.has?.(match.modifier)) + throw new InvalidModifierError({ + param, + type: options?.type, + modifier: match.modifier, + }) + + // Check if resolved `type` is valid if there is a function modifier + if ( + functionModifiers.has(match.modifier as FunctionModifier) && + !isValidDataLocation(type, !!match.array) + ) + throw new InvalidFunctionModifierError({ + param, + type: options?.type, + modifier: match.modifier, + }) + } + + const abiParameter = { + type: `${type}${match.array ?? ''}`, + ...name, + ...indexed, + ...components, + } + parameterCache.set(parameterCacheKey, abiParameter) + return abiParameter +} + +const abiParameterWithoutTupleRegex = + /^(?[a-zA-Z$_][a-zA-Z0-9$_]*)(?(?:\[\d*?\])+?)?(?:\s(?calldata|indexed|memory|storage{1}))?(?:\s(?[a-zA-Z$_][a-zA-Z0-9$_]*))?$/ +const abiParameterWithTupleRegex = + /^\((?.+?)\)(?(?:\[\d*?\])+?)?(?:\s(?calldata|indexed|memory|storage{1}))?(?:\s(?[a-zA-Z$_][a-zA-Z0-9$_]*))?$/ +const dynamicIntegerRegex = /^u?int$/ + +/** @internal */ +export function isSolidityKeyword(name: string) { + return ( + name === 'address' || + name === 'bool' || + name === 'function' || + name === 'string' || + name === 'tuple' || + bytesRegex.test(name) || + integerRegex.test(name) || + protectedKeywordsRegex.test(name) + ) +} +const protectedKeywordsRegex = + /^(?:after|alias|anonymous|apply|auto|byte|calldata|case|catch|constant|copyof|default|defined|error|event|external|false|final|function|immutable|implements|in|indexed|inline|internal|let|mapping|match|memory|mutable|null|of|override|partial|private|promise|public|pure|reference|relocatable|return|returns|sizeof|static|storage|struct|super|supports|switch|this|true|try|typedef|typeof|var|view|virtual)$/ + +/** @internal */ +export function isValidDataLocation( + type: string, + isArray: boolean, +): type is Exclude< + abitype.AbiType, + | abitype.SolidityString + | Extract + | abitype.SolidityArray +> { + return isArray || type === 'bytes' || type === 'string' || type === 'tuple' +} + +export function isSolidityType( + type: string, +): type is Exclude< + abitype.AbiType, + abitype.SolidityTuple | abitype.SolidityArray +> { + return ( + type === 'address' || + type === 'bool' || + type === 'function' || + type === 'string' || + bytesRegex.test(type) || + integerRegex.test(type) + ) +} + +// s/o latika for this +export function splitParameters( + params: string, + result: string[] = [], + current = '', + depth = 0, +): readonly string[] { + const length = params.trim().length + // biome-ignore lint/correctness/noUnreachable: recursive + for (let i = 0; i < length; i++) { + const char = params[i] + const tail = params.slice(i + 1) + switch (char) { + case ',': + return depth === 0 + ? splitParameters(tail, [...result, current.trim()]) + : splitParameters(tail, result, `${current}${char}`, depth) + case '(': + return splitParameters(tail, result, `${current}${char}`, depth + 1) + case ')': + return splitParameters(tail, result, `${current}${char}`, depth - 1) + default: + return splitParameters(tail, result, `${current}${char}`, depth) + } + } + + if (current === '') return result + if (depth !== 0) throw new InvalidParenthesisError({ current, depth }) + + result.push(current.trim()) + return result +} + +/** + * Gets {@link parameterCache} cache key namespaced by {@link type}. This prevents parameters from being accessible to types that don't allow them (e.g. `string indexed foo` not allowed outside of `type: 'event'`). + * @param param ABI parameter string + * @param type ABI parameter type + * @returns Cache key for {@link parameterCache} + */ +export function getParameterCacheKey( + param: string, + type?: abitype.AbiItemType | 'struct', + structs?: structs.StructLookup, +) { + let structKey = '' + if (structs) + for (const struct of Object.entries(structs)) { + if (!struct) continue + let propertyKey = '' + for (const property of struct[1]) { + propertyKey += `[${property.type}${property.name ? `:${property.name}` : ''}]` + } + structKey += `(${struct[0]}{${propertyKey}})` + } + if (type) return `${type}:${param}${structKey}` + return param +} + +/** + * Basic cache seeded with common ABI parameter strings. + * + * **Note: When seeding more parameters, make sure you benchmark performance. The current number is the ideal balance between performance and having an already existing cache.** + */ +export const parameterCache = new Map< + string, + AbiParameter.AbiParameter & { indexed?: boolean } +>([ + // Unnamed + ['address', { type: 'address' }], + ['bool', { type: 'bool' }], + ['bytes', { type: 'bytes' }], + ['bytes32', { type: 'bytes32' }], + ['int', { type: 'int256' }], + ['int256', { type: 'int256' }], + ['string', { type: 'string' }], + ['uint', { type: 'uint256' }], + ['uint8', { type: 'uint8' }], + ['uint16', { type: 'uint16' }], + ['uint24', { type: 'uint24' }], + ['uint32', { type: 'uint32' }], + ['uint64', { type: 'uint64' }], + ['uint96', { type: 'uint96' }], + ['uint112', { type: 'uint112' }], + ['uint160', { type: 'uint160' }], + ['uint192', { type: 'uint192' }], + ['uint256', { type: 'uint256' }], + + // Named + ['address owner', { type: 'address', name: 'owner' }], + ['address to', { type: 'address', name: 'to' }], + ['bool approved', { type: 'bool', name: 'approved' }], + ['bytes _data', { type: 'bytes', name: '_data' }], + ['bytes data', { type: 'bytes', name: 'data' }], + ['bytes signature', { type: 'bytes', name: 'signature' }], + ['bytes32 hash', { type: 'bytes32', name: 'hash' }], + ['bytes32 r', { type: 'bytes32', name: 'r' }], + ['bytes32 root', { type: 'bytes32', name: 'root' }], + ['bytes32 s', { type: 'bytes32', name: 's' }], + ['string name', { type: 'string', name: 'name' }], + ['string symbol', { type: 'string', name: 'symbol' }], + ['string tokenURI', { type: 'string', name: 'tokenURI' }], + ['uint tokenId', { type: 'uint256', name: 'tokenId' }], + ['uint8 v', { type: 'uint8', name: 'v' }], + ['uint256 balance', { type: 'uint256', name: 'balance' }], + ['uint256 tokenId', { type: 'uint256', name: 'tokenId' }], + ['uint256 value', { type: 'uint256', name: 'value' }], + + // Indexed + [ + 'event:address indexed from', + { type: 'address', name: 'from', indexed: true }, + ], + ['event:address indexed to', { type: 'address', name: 'to', indexed: true }], + [ + 'event:uint indexed tokenId', + { type: 'uint256', name: 'tokenId', indexed: true }, + ], + [ + 'event:uint256 indexed tokenId', + { type: 'uint256', name: 'tokenId', indexed: true }, + ], +]) + +/// Types + +export type ParseAbiParameter< + signature extends string, + options extends ParseOptions = DefaultParseOptions, +> = ( + signature extends `(${string})${string}` + ? _ParseTuple + : // Convert string to shallow AbiParameter (structs resolved yet) + // Check for `${Type} ${nameOrModifier}` format (e.g. `uint256 foo`, `uint256 indexed`, `uint256 indexed foo`) + signature extends `${infer type} ${infer tail}` + ? Trim extends infer trimmed extends string + ? // TODO: data location modifiers only allowed for struct/array types + { readonly type: Trim } & _SplitNameOrModifier + : never + : // Must be `${Type}` format (e.g. `uint256`) + { readonly type: signature } +) extends infer shallowParameter extends AbiParameter.AbiParameter & { + type: string + indexed?: boolean +} + ? // Resolve struct types + // Starting with plain struct types (e.g. `Foo`) + ( + shallowParameter['type'] extends keyof options['structs'] + ? { + readonly type: 'tuple' + readonly components: options['structs'][shallowParameter['type']] + } & (IsUnknown extends false + ? { readonly name: shallowParameter['name'] } + : object) & + (shallowParameter['indexed'] extends true + ? { readonly indexed: true } + : object) + : // Resolve tuple structs (e.g. `Foo[]`, `Foo[2]`, `Foo[][2]`, etc.) + shallowParameter['type'] extends `${infer type extends string & + keyof options['structs']}[${infer tail}]` + ? { + readonly type: `tuple[${tail}]` + readonly components: options['structs'][type] + } & (IsUnknown extends false + ? { readonly name: shallowParameter['name'] } + : object) & + (shallowParameter['indexed'] extends true + ? { readonly indexed: true } + : object) + : // Not a struct, just return + shallowParameter + ) extends infer Parameter extends AbiParameter.AbiParameter & { + type: string + indexed?: boolean + } + ? Evaluate<_ValidateAbiParameter> + : never + : never + +export type ParseOptions = { + modifier?: Modifier + structs?: structs.StructLookup | unknown +} +export type DefaultParseOptions = object + +export type SplitParameters< + signature extends string, + result extends unknown[] = [], + current extends string = '', + depth extends readonly number[] = [], +> = signature extends '' + ? current extends '' + ? [...result] // empty string was passed in to `SplitParameters` + : depth['length'] extends 0 + ? [...result, Trim] + : TypeErrorMessage<`Unbalanced parentheses. "${current}" has too many opening parentheses.`> + : signature extends `${infer char}${infer tail}` + ? char extends ',' + ? depth['length'] extends 0 + ? SplitParameters], ''> + : SplitParameters + : char extends '(' + ? SplitParameters + : char extends ')' + ? depth['length'] extends 0 + ? TypeErrorMessage<`Unbalanced parentheses. "${current}" has too many closing parentheses.`> + : SplitParameters> + : SplitParameters + : [] +type Pop = type extends [...infer head, any] + ? head + : [] + +export type _ValidateAbiParameter< + abiParameter extends AbiParameter.AbiParameter, +> = + // Validate `name` + ( + abiParameter extends { name: string } + ? ValidateName extends infer name + ? name extends abiParameter['name'] + ? abiParameter + : // Add `Error` as `name` + Merge + : never + : abiParameter + ) extends infer parameter + ? // Validate `type` against `AbiType` + ( + abitype.ResolvedRegister['strictAbiType'] extends true + ? parameter extends { type: abitype.AbiType } + ? parameter + : Merge< + parameter, + { + readonly type: TypeErrorMessage<`Type "${parameter extends { + type: string + } + ? parameter['type'] + : string}" is not a valid ABI type.`> + } + > + : parameter + ) extends infer parameter2 extends { type: unknown } + ? // Convert `(u)int` to `(u)int256` + parameter2['type'] extends `${infer prefix extends + | 'u' + | ''}int${infer suffix extends `[${string}]` | ''}` + ? Evaluate< + Merge + > + : parameter2 + : never + : never diff --git a/src/core/internal/humanReadable/errors.ts b/src/core/internal/humanReadable/errors.ts new file mode 100644 index 00000000..bb1a4c3b --- /dev/null +++ b/src/core/internal/humanReadable/errors.ts @@ -0,0 +1,184 @@ +import type * as abitype from 'abitype' + +import type * as AbiParameter from '../../AbiParameter.js' +import * as Errors from '../../Errors.js' +import type { Modifier } from './signatures.js' + +export class InvalidAbiItemError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidAbiItemError' + + constructor({ signature }: { signature: string | object }) { + super('Failed to parse ABI item.', { + details: `parseAbiItem(${JSON.stringify(signature, null, 2)})`, + }) + } +} + +export class UnknownTypeError extends Errors.BaseError { + override readonly name = 'AbiParameter.UnknownTypeError' + + constructor({ type }: { type: string }) { + super('Unknown type.', { + metaMessages: [ + `Type "${type}" is not a valid ABI type. Perhaps you forgot to include a struct signature?`, + ], + }) + } +} + +export class UnknownSolidityTypeError extends Errors.BaseError { + override readonly name = 'AbiParameter.UnknownSolidityTypeError' + + constructor({ type }: { type: string }) { + super('Unknown type.', { + metaMessages: [`Type "${type}" is not a valid ABI type.`], + }) + } +} + +export class InvalidParameterError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidParameterError' + + constructor({ param }: { param: string }) { + super('Invalid ABI parameter.', { + details: param, + }) + } +} + +export class SolidityProtectedKeywordError extends Errors.BaseError { + override readonly name = 'AbiParameter.SolidityProtectedKeywordError' + + constructor({ param, name }: { param: string; name: string }) { + super('Invalid ABI parameter.', { + details: param, + metaMessages: [ + `"${name}" is a protected Solidity keyword. More info: https://docs.soliditylang.org/en/latest/cheatsheet.html`, + ], + }) + } +} + +export class InvalidModifierError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidModifierError' + + constructor({ + param, + type, + modifier, + }: { + param: string + type?: abitype.AbiItemType | 'struct' | undefined + modifier: Modifier + }) { + super('Invalid ABI parameter.', { + details: param, + metaMessages: [ + `Modifier "${modifier}" not allowed${ + type ? ` in "${type}" type` : '' + }.`, + ], + }) + } +} + +export class InvalidFunctionModifierError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidFunctionModifierError' + + constructor({ + param, + type, + modifier, + }: { + param: string + type?: abitype.AbiItemType | 'struct' | undefined + modifier: Modifier + }) { + super('Invalid ABI parameter.', { + details: param, + metaMessages: [ + `Modifier "${modifier}" not allowed${ + type ? ` in "${type}" type` : '' + }.`, + `Data location can only be specified for array, struct, or mapping types, but "${modifier}" was given.`, + ], + }) + } +} + +export class InvalidAbiTypeParameterError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidAbiTypeParameterError' + + constructor({ + abiParameter, + }: { + abiParameter: AbiParameter.AbiParameter & { indexed?: boolean | undefined } + }) { + super('Invalid ABI parameter.', { + details: JSON.stringify(abiParameter, null, 2), + metaMessages: ['ABI parameter type is invalid.'], + }) + } +} + +export class InvalidSignatureError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidSignatureError' + + constructor({ + signature, + type, + }: { + signature: string + type: abitype.AbiItemType | 'struct' + }) { + super(`Invalid ${type} signature.`, { + details: signature, + }) + } +} + +export class UnknownSignatureError extends Errors.BaseError { + override readonly name = 'AbiParameter.UnknownSignatureError' + + constructor({ signature }: { signature: string }) { + super('Unknown signature.', { + details: signature, + }) + } +} + +export class InvalidStructSignatureError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidStructSignatureError' + + constructor({ signature }: { signature: string }) { + super('Invalid struct signature.', { + details: signature, + metaMessages: ['No properties exist.'], + }) + } +} + +export class InvalidParenthesisError extends Errors.BaseError { + override readonly name = 'AbiParameter.InvalidParenthesisError' + + constructor({ current, depth }: { current: string; depth: number }) { + super('Unbalanced parentheses.', { + metaMessages: [ + `"${current.trim()}" has too many ${ + depth > 0 ? 'opening' : 'closing' + } parentheses.`, + ], + details: `Depth "${depth}"`, + }) + } +} + +export class CircularReferenceError extends Errors.BaseError { + override readonly name = 'AbiParameter.CircularReferenceError' + + constructor({ type }: { type: string }) { + super('Circular reference detected.', { + metaMessages: [`Struct "${type}" is a circular reference.`], + }) + } +} diff --git a/src/core/internal/humanReadable/runtime/utils.ts b/src/core/internal/humanReadable/runtime/utils.ts new file mode 100644 index 00000000..9337438e --- /dev/null +++ b/src/core/internal/humanReadable/runtime/utils.ts @@ -0,0 +1,167 @@ +import { parseAbiParameter, splitParameters } from '../../abiParameter.js' +import { InvalidSignatureError, UnknownSignatureError } from '../errors.js' +import { + eventModifiers, + execConstructorSignature, + execErrorSignature, + execEventSignature, + execFallbackSignature, + execFunctionSignature, + functionModifiers, + isConstructorSignature, + isErrorSignature, + isEventSignature, + isFallbackSignature, + isFunctionSignature, + isReceiveSignature, +} from '../signatures.js' +import type { StructLookup } from '../structs.js' + +export function parseSignature(signature: string, structs: StructLookup = {}) { + if (isFunctionSignature(signature)) + return parseFunctionSignature(signature, structs) + + if (isEventSignature(signature)) + return parseEventSignature(signature, structs) + + if (isErrorSignature(signature)) + return parseErrorSignature(signature, structs) + + if (isConstructorSignature(signature)) + return parseConstructorSignature(signature, structs) + + if (isFallbackSignature(signature)) return parseFallbackSignature(signature) + + if (isReceiveSignature(signature)) + return { + type: 'receive', + stateMutability: 'payable', + } + + throw new UnknownSignatureError({ signature }) +} + +export function parseFunctionSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execFunctionSignature(signature) + if (!match) + throw new InvalidSignatureError({ + signature, + type: 'function', + }) + + const inputParams = splitParameters(match.parameters) + const inputs = [] + const inputLength = inputParams.length + for (let i = 0; i < inputLength; i++) { + inputs.push( + parseAbiParameter(inputParams[i]!, { + modifiers: functionModifiers, + structs, + type: 'function', + }), + ) + } + + const outputs = [] + if (match.returns) { + const outputParams = splitParameters(match.returns) + const outputLength = outputParams.length + for (let i = 0; i < outputLength; i++) { + outputs.push( + parseAbiParameter(outputParams[i]!, { + modifiers: functionModifiers, + structs, + type: 'function', + }), + ) + } + } + + return { + name: match.name, + type: 'function', + stateMutability: match.stateMutability ?? 'nonpayable', + inputs, + outputs, + } +} + +export function parseEventSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execEventSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'event' }) + + const params = splitParameters(match.parameters) + const abiParameters = [] + const length = params.length + for (let i = 0; i < length; i++) + abiParameters.push( + parseAbiParameter(params[i]!, { + modifiers: eventModifiers, + structs, + type: 'event', + }), + ) + return { name: match.name, type: 'event', inputs: abiParameters } +} + +export function parseErrorSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execErrorSignature(signature) + if (!match) throw new InvalidSignatureError({ signature, type: 'error' }) + + const params = splitParameters(match.parameters) + const abiParameters = [] + const length = params.length + for (let i = 0; i < length; i++) + abiParameters.push( + parseAbiParameter(params[i]!, { structs, type: 'error' }), + ) + return { name: match.name, type: 'error', inputs: abiParameters } +} + +export function parseConstructorSignature( + signature: string, + structs: StructLookup = {}, +) { + const match = execConstructorSignature(signature) + if (!match) + throw new InvalidSignatureError({ + signature, + type: 'constructor', + }) + + const params = splitParameters(match.parameters) + const abiParameters = [] + const length = params.length + for (let i = 0; i < length; i++) + abiParameters.push( + parseAbiParameter(params[i]!, { structs, type: 'constructor' }), + ) + return { + type: 'constructor', + stateMutability: match.stateMutability ?? 'nonpayable', + inputs: abiParameters, + } +} + +export function parseFallbackSignature(signature: string) { + const match = execFallbackSignature(signature) + if (!match) + throw new InvalidSignatureError({ + signature, + type: 'fallback', + }) + + return { + type: 'fallback', + stateMutability: match.stateMutability ?? 'nonpayable', + } +} diff --git a/src/core/internal/humanReadable/signatures.ts b/src/core/internal/humanReadable/signatures.ts new file mode 100644 index 00000000..624f4fa5 --- /dev/null +++ b/src/core/internal/humanReadable/signatures.ts @@ -0,0 +1,437 @@ +import type { AbiStateMutability } from 'abitype' + +import { execTyped } from '../regex.js' +import type { TypeErrorMessage } from '../types.js' + +// https://regexr.com/7gmok +const errorSignatureRegex = + /^error (?[a-zA-Z$_][a-zA-Z0-9$_]*)\((?.*?)\)$/ +export function isErrorSignature(signature: string) { + return errorSignatureRegex.test(signature) +} +export function execErrorSignature(signature: string) { + return execTyped<{ name: string; parameters: string }>( + errorSignatureRegex, + signature, + ) +} + +// https://regexr.com/7gmoq +const eventSignatureRegex = + /^event (?[a-zA-Z$_][a-zA-Z0-9$_]*)\((?.*?)\)$/ +export function isEventSignature(signature: string) { + return eventSignatureRegex.test(signature) +} +export function execEventSignature(signature: string) { + return execTyped<{ name: string; parameters: string }>( + eventSignatureRegex, + signature, + ) +} + +// https://regexr.com/7gmot +const functionSignatureRegex = + /^function (?[a-zA-Z$_][a-zA-Z0-9$_]*)\((?.*?)\)(?: (?external|public{1}))?(?: (?pure|view|nonpayable|payable{1}))?(?: returns\s?\((?.*?)\))?$/ +export function isFunctionSignature(signature: string) { + return functionSignatureRegex.test(signature) +} +export function execFunctionSignature(signature: string) { + return execTyped<{ + name: string + parameters: string + stateMutability?: AbiStateMutability + returns?: string + }>(functionSignatureRegex, signature) +} + +// https://regexr.com/7gmp3 +const structSignatureRegex = + /^struct (?[a-zA-Z$_][a-zA-Z0-9$_]*) \{(?.*?)\}$/ +export function isStructSignature(signature: string) { + return structSignatureRegex.test(signature) +} +export function execStructSignature(signature: string) { + return execTyped<{ name: string; properties: string }>( + structSignatureRegex, + signature, + ) +} + +// https://regexr.com/78u01 +const constructorSignatureRegex = + /^constructor\((?.*?)\)(?:\s(?payable{1}))?$/ +export function isConstructorSignature(signature: string) { + return constructorSignatureRegex.test(signature) +} +export function execConstructorSignature(signature: string) { + return execTyped<{ + parameters: string + stateMutability?: Extract + }>(constructorSignatureRegex, signature) +} + +// https://regexr.com/7srtn +const fallbackSignatureRegex = + /^fallback\(\) external(?:\s(?payable{1}))?$/ +export function isFallbackSignature(signature: string) { + return fallbackSignatureRegex.test(signature) +} +export function execFallbackSignature(signature: string) { + return execTyped<{ + parameters: string + stateMutability?: Extract + }>(fallbackSignatureRegex, signature) +} + +// https://regexr.com/78u1k +const receiveSignatureRegex = /^receive\(\) external payable$/ +export function isReceiveSignature(signature: string) { + return receiveSignatureRegex.test(signature) +} + +export const modifiers = new Set([ + 'memory', + 'indexed', + 'storage', + 'calldata', +]) +export const eventModifiers = new Set(['indexed']) +export const functionModifiers = new Set([ + 'calldata', + 'memory', + 'storage', +]) + +/// Types + +/** @internal */ +export type ErrorSignature< + name extends string = string, + parameters extends string = string, +> = `error ${name}(${parameters})` +/** @internal */ +export type IsErrorSignature = + signature extends ErrorSignature ? IsName : false + +/** @internal */ +export type EventSignature< + name extends string = string, + parameters extends string = string, +> = `event ${name}(${parameters})` +/** @internal */ +export type IsEventSignature = + signature extends EventSignature ? IsName : false + +/** @internal */ +export type FunctionSignature< + name extends string = string, + tail extends string = string, +> = `function ${name}(${tail}` +/** @internal */ +export type IsFunctionSignature = + signature extends FunctionSignature + ? IsName extends true + ? signature extends ValidFunctionSignatures + ? true + : // Check that `Parameters` is not absorbing other types (e.g. `returns`) + signature extends `function ${string}(${infer parameters})` + ? parameters extends InvalidFunctionParameters + ? false + : true + : false + : false + : false +/** @internal */ +export type Scope = 'public' | 'external' // `internal` or `private` functions wouldn't make it to ABI so can ignore +type Returns = `returns (${string})` | `returns(${string})` +// Almost all valid function signatures, except `function ${string}(${infer parameters})` since `parameters` can absorb returns +type ValidFunctionSignatures = + | `function ${string}()` + // basic + | `function ${string}() ${Returns}` + | `function ${string}() ${AbiStateMutability}` + | `function ${string}() ${Scope}` + // combinations + | `function ${string}() ${AbiStateMutability} ${Returns}` + | `function ${string}() ${Scope} ${Returns}` + | `function ${string}() ${Scope} ${AbiStateMutability}` + | `function ${string}() ${Scope} ${AbiStateMutability} ${Returns}` + // Parameters + | `function ${string}(${string}) ${Returns}` + | `function ${string}(${string}) ${AbiStateMutability}` + | `function ${string}(${string}) ${Scope}` + | `function ${string}(${string}) ${AbiStateMutability} ${Returns}` + | `function ${string}(${string}) ${Scope} ${Returns}` + | `function ${string}(${string}) ${Scope} ${AbiStateMutability}` + | `function ${string}(${string}) ${Scope} ${AbiStateMutability} ${Returns}` + +/** @internal */ +export type StructSignature< + name extends string = string, + properties extends string = string, +> = `struct ${name} {${properties}}` +/** @internal */ +export type IsStructSignature = + signature extends StructSignature ? IsName : false + +type ConstructorSignature = `constructor(${tail}` +/** @internal */ +export type IsConstructorSignature = + signature extends ConstructorSignature + ? signature extends ValidConstructorSignatures + ? true + : false + : false +type ValidConstructorSignatures = + | `constructor(${string})` + | `constructor(${string}) payable` + +/** @internal */ +export type FallbackSignature< + abiStateMutability extends '' | ' payable' = '' | ' payable', +> = `fallback() external${abiStateMutability}` +/** @internal */ +export type IsFallbackSignature = signature extends + | FallbackSignature<''> + | FallbackSignature<' payable'> + ? true + : false + +/** @internal */ +export type ReceiveSignature = 'receive() external payable' + +// TODO: Maybe use this for signature validation one day +// https://twitter.com/devanshj__/status/1610423724708343808 +/** @internal */ +export type IsSignature = + | (IsErrorSignature extends true ? true : never) + | (IsEventSignature extends true ? true : never) + | (IsFunctionSignature extends true ? true : never) + | (IsStructSignature extends true ? true : never) + | (IsConstructorSignature extends true ? true : never) + | (IsFallbackSignature extends true ? true : never) + | (type extends ReceiveSignature ? true : never) extends infer condition + ? [condition] extends [never] + ? false + : true + : false + +/** @internal */ +export type Signature< + string1 extends string, + string2 extends string | unknown = unknown, +> = IsSignature extends true + ? string1 + : string extends string1 // if exactly `string` (not narrowed), then pass through as valid + ? string1 + : TypeErrorMessage<`Signature "${string1}" is invalid${string2 extends string + ? ` at position ${string2}` + : ''}.`> + +/** @internal */ +export type Signatures = { + [key in keyof signatures]: Signature +} + +/** @internal */ +export type Modifier = 'calldata' | 'indexed' | 'memory' | 'storage' +/** @internal */ +export type FunctionModifier = Extract< + Modifier, + 'calldata' | 'memory' | 'storage' +> +/** @internal */ +export type EventModifier = Extract + +/** @internal */ +export type IsName = name extends '' + ? false + : ValidateName extends name + ? true + : false + +/** @internal */ +export type AssertName = + ValidateName extends infer invalidName extends string[] + ? `[${invalidName[number]}]` + : name + +/** @internal */ +export type ValidateName< + name extends string, + checkCharacters extends boolean = false, +> = name extends `${string}${' '}${string}` + ? TypeErrorMessage<`Identifier "${name}" cannot contain whitespace.`> + : IsSolidityKeyword extends true + ? TypeErrorMessage<`"${name}" is a protected Solidity keyword.`> + : name extends `${number}` + ? TypeErrorMessage<`Identifier "${name}" cannot be a number string.`> + : name extends `${number}${string}` + ? TypeErrorMessage<`Identifier "${name}" cannot start with a number.`> + : checkCharacters extends true + ? IsValidCharacter extends true + ? name + : TypeErrorMessage<`"${name}" contains invalid character.`> + : name + +/** @internal */ +export type IsSolidityKeyword = + type extends SolidityKeywords ? true : false + +/** @internal */ +export type SolidityKeywords = + | 'after' + | 'alias' + | 'anonymous' + | 'apply' + | 'auto' + | 'byte' + | 'calldata' + | 'case' + | 'catch' + | 'constant' + | 'copyof' + | 'default' + | 'defined' + | 'error' + | 'event' + | 'external' + | 'false' + | 'final' + | 'function' + | 'immutable' + | 'implements' + | 'in' + | 'indexed' + | 'inline' + | 'internal' + | 'let' + | 'mapping' + | 'match' + | 'memory' + | 'mutable' + | 'null' + | 'of' + | 'override' + | 'partial' + | 'private' + | 'promise' + | 'public' + | 'pure' + | 'reference' + | 'relocatable' + | 'return' + | 'returns' + | 'sizeof' + | 'static' + | 'storage' + | 'struct' + | 'super' + | 'supports' + | 'switch' + | 'this' + | 'true' + | 'try' + | 'typedef' + | 'typeof' + | 'var' + | 'view' + | 'virtual' + | `address${`[${string}]` | ''}` + | `bool${`[${string}]` | ''}` + | `string${`[${string}]` | ''}` + | `tuple${`[${string}]` | ''}` + | `bytes${number | ''}${`[${string}]` | ''}` + | `${'u' | ''}int${number | ''}${`[${string}]` | ''}` + +/** @internal */ +export type IsValidCharacter = + character extends `${ValidCharacters}${infer tail}` + ? tail extends '' + ? true + : IsValidCharacter + : false + +// biome-ignore format: no formatting +type ValidCharacters = + // uppercase letters + | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' + // lowercase letters + | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' + // numbers + | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + // special characters + | '_' | '$' + +// Template string inference can absorb `returns`: +// type Result = `function foo(string) return s (uint256)` extends `function ${string}(${infer Parameters})` ? Parameters : never +// // ^? type Result = "string ) return s (uint256" +// So we need to validate against `returns` keyword with all combinations of whitespace +type InvalidFunctionParameters = + | `${string}${MangledReturns} (${string}` + | `${string}) ${MangledReturns}${string}` + | `${string})${string}${MangledReturns}${string}(${string}` +// r_e_t_u_r_n_s +type MangledReturns = + // Single + | `r${string}eturns` + | `re${string}turns` + | `ret${string}urns` + | `retu${string}rns` + | `retur${string}ns` + | `return${string}s` + // Double + // `r_e*` + | `r${string}e${string}turns` + | `r${string}et${string}urns` + | `r${string}etu${string}rns` + | `r${string}etur${string}ns` + | `r${string}eturn${string}s` + // `re_t*` + | `re${string}t${string}urns` + | `re${string}tu${string}rns` + | `re${string}tur${string}ns` + | `re${string}turn${string}s` + // `ret_u*` + | `ret${string}u${string}rns` + | `ret${string}ur${string}ns` + | `ret${string}urn${string}s` + // `retu_r*` + | `retu${string}r${string}ns` + | `retu${string}rn${string}s` + // `retur_n*` + | `retur${string}n${string}s` + // Triple + // `r_e_t*` + | `r${string}e${string}t${string}urns` + | `r${string}e${string}tu${string}rns` + | `r${string}e${string}tur${string}ns` + | `r${string}e${string}turn${string}s` + // `re_t_u*` + | `re${string}t${string}u${string}rns` + | `re${string}t${string}ur${string}ns` + | `re${string}t${string}urn${string}s` + // `ret_u_r*` + | `ret${string}u${string}r${string}ns` + | `ret${string}u${string}rn${string}s` + // `retu_r_n*` + | `retu${string}r${string}n${string}s` + // Quadruple + // `r_e_t_u*` + | `r${string}e${string}t${string}u${string}rns` + | `r${string}e${string}t${string}ur${string}ns` + | `r${string}e${string}t${string}urn${string}s` + // `re_t_u_r*` + | `re${string}t${string}u${string}r${string}ns` + | `re${string}t${string}u${string}rn${string}s` + // `ret_u_r_n*` + | `ret${string}u${string}r${string}n${string}s` + // Quintuple + // `r_e_t_u_r*` + | `r${string}e${string}t${string}u${string}r${string}ns` + | `r${string}e${string}t${string}u${string}rn${string}s` + // `re_t_u_r_n*` + | `re${string}t${string}u${string}r${string}n${string}s` + // Sextuple + // `r_e_t_u_r_n_s` + | `r${string}e${string}t${string}u${string}r${string}n${string}s` diff --git a/src/core/internal/humanReadable/structs.ts b/src/core/internal/humanReadable/structs.ts new file mode 100644 index 00000000..c08a5382 --- /dev/null +++ b/src/core/internal/humanReadable/structs.ts @@ -0,0 +1,198 @@ +import type * as AbiParameter from '../../AbiParameter.js' +import { + type ParseAbiParameter, + isSolidityType, + parseAbiParameter, +} from '../abiParameter.js' +import { execTyped, isTupleRegex } from '../regex.js' +import type { Trim, TypeErrorMessage } from '../types.js' +import { + CircularReferenceError, + InvalidAbiTypeParameterError, + InvalidSignatureError, + InvalidStructSignatureError, + UnknownTypeError, +} from './errors.js' +import { + type StructSignature, + execStructSignature, + isStructSignature, +} from './signatures.js' + +export function parseStructs(signatures: readonly string[]) { + // Create "shallow" version of each struct (and filter out non-structs or invalid structs) + const shallowStructs: StructLookup = {} + const signaturesLength = signatures.length + for (let i = 0; i < signaturesLength; i++) { + const signature = signatures[i]! + if (!isStructSignature(signature)) continue + + const match = execStructSignature(signature) + if (!match) + throw new InvalidSignatureError({ + signature, + type: 'struct', + }) + + const properties = match.properties.split(';') + + const components: AbiParameter.AbiParameter[] = [] + const propertiesLength = properties.length + for (let k = 0; k < propertiesLength; k++) { + const property = properties[k]! + const trimmed = property.trim() + if (!trimmed) continue + const abiParameter = parseAbiParameter(trimmed, { + type: 'struct', + }) + components.push(abiParameter) + } + + if (!components.length) throw new InvalidStructSignatureError({ signature }) + shallowStructs[match.name] = components + } + + // Resolve nested structs inside each parameter + const resolvedStructs: StructLookup = {} + const entries = Object.entries(shallowStructs) + const entriesLength = entries.length + for (let i = 0; i < entriesLength; i++) { + const [name, parameters] = entries[i]! + resolvedStructs[name] = resolveStructs(parameters, shallowStructs) + } + + return resolvedStructs +} + +const typeWithoutTupleRegex = + /^(?[a-zA-Z$_][a-zA-Z0-9$_]*)(?(?:\[\d*?\])+?)?$/ + +function resolveStructs( + abiParameters: readonly (AbiParameter.AbiParameter & { indexed?: true })[], + structs: StructLookup, + ancestors = new Set(), +) { + const components: AbiParameter.AbiParameter[] = [] + const length = abiParameters.length + for (let i = 0; i < length; i++) { + const abiParameter = abiParameters[i]! + const isTuple = isTupleRegex.test(abiParameter.type) + if (isTuple) components.push(abiParameter) + else { + const match = execTyped<{ array?: string; type: string }>( + typeWithoutTupleRegex, + abiParameter.type, + ) + if (!match?.type) throw new InvalidAbiTypeParameterError({ abiParameter }) + + const { array, type } = match + if (type in structs) { + if (ancestors.has(type)) throw new CircularReferenceError({ type }) + + components.push({ + ...abiParameter, + type: `tuple${array ?? ''}`, + components: resolveStructs( + structs[type] ?? [], + structs, + new Set([...ancestors, type]), + ), + }) + } else { + if (isSolidityType(type)) components.push(abiParameter) + else throw new UnknownTypeError({ type }) + } + } + } + + return components +} + +/// Types + +export type StructLookup = Record + +export type ParseStructs = + // Create "shallow" version of each struct (and filter out non-structs or invalid structs) + { + [signature in signatures[number] as ParseStruct extends infer struct extends + { + name: string + } + ? struct['name'] + : never]: ParseStruct['components'] + } extends infer structs extends Record< + string, + readonly (AbiParameter.AbiParameter & { type: string })[] + > + ? // Resolve nested structs inside each struct + { + [structName in keyof structs]: ResolveStructs< + structs[structName], + structs + > + } + : never + +export type ParseStruct< + signature extends string, + structs extends StructLookup | unknown = unknown, +> = signature extends StructSignature + ? { + readonly name: Trim + readonly components: ParseStructProperties + } + : never + +export type ResolveStructs< + abiParameters extends readonly (AbiParameter.AbiParameter & { + type: string + })[], + structs extends Record< + string, + readonly (AbiParameter.AbiParameter & { type: string })[] + >, + keyReferences extends { [_: string]: unknown } | unknown = unknown, +> = readonly [ + ...{ + [key in keyof abiParameters]: abiParameters[key]['type'] extends `${infer head extends + string & keyof structs}[${infer tail}]` // Struct arrays (e.g. `type: 'Struct[]'`, `type: 'Struct[10]'`, `type: 'Struct[][]'`) + ? head extends keyof keyReferences + ? TypeErrorMessage<`Circular reference detected. Struct "${abiParameters[key]['type']}" is a circular reference.`> + : { + readonly name: abiParameters[key]['name'] + readonly type: `tuple[${tail}]` + readonly components: ResolveStructs< + structs[head], + structs, + keyReferences & { [_ in head]: true } + > + } + : // Basic struct (e.g. `type: 'Struct'`) + abiParameters[key]['type'] extends keyof structs + ? abiParameters[key]['type'] extends keyof keyReferences + ? TypeErrorMessage<`Circular reference detected. Struct "${abiParameters[key]['type']}" is a circular reference.`> + : { + readonly name: abiParameters[key]['name'] + readonly type: 'tuple' + readonly components: ResolveStructs< + structs[abiParameters[key]['type']], + structs, + keyReferences & { [_ in abiParameters[key]['type']]: true } + > + } + : abiParameters[key] + }, +] + +export type ParseStructProperties< + signature extends string, + structs extends StructLookup | unknown = unknown, + result extends any[] = [], +> = Trim extends `${infer head};${infer tail}` + ? ParseStructProperties< + tail, + structs, + [...result, ParseAbiParameter] + > + : result diff --git a/src/core/internal/humanReadable/types/utils.ts b/src/core/internal/humanReadable/types/utils.ts new file mode 100644 index 00000000..b5232b12 --- /dev/null +++ b/src/core/internal/humanReadable/types/utils.ts @@ -0,0 +1,268 @@ +import type * as abitype from 'abitype' + +import type { + DefaultParseOptions, + ParseAbiParameter, + ParseOptions, + SplitParameters, +} from '../../abiParameter.js' +import type { Evaluate, Trim } from '../../types.js' +import type { + ErrorSignature, + EventModifier, + EventSignature, + FallbackSignature, + FunctionModifier, + FunctionSignature, + IsConstructorSignature, + IsErrorSignature, + IsEventSignature, + IsFunctionSignature, + Modifier, + ReceiveSignature, + Scope, +} from '../signatures.js' +import type { StructLookup } from '../structs.js' + +export type ParseSignature< + signature extends string, + structs extends StructLookup | unknown = unknown, +> = + | (IsErrorSignature extends true + ? signature extends ErrorSignature + ? { + readonly name: name + readonly type: 'error' + readonly inputs: ParseAbiParameters< + SplitParameters, + { structs: structs } + > + } + : never + : never) + | (IsEventSignature extends true + ? signature extends EventSignature + ? { + readonly name: name + readonly type: 'event' + readonly inputs: ParseAbiParameters< + SplitParameters, + { modifier: EventModifier; structs: structs } + > + } + : never + : never) + | (IsFunctionSignature extends true + ? signature extends FunctionSignature + ? { + readonly name: name + readonly type: 'function' + readonly stateMutability: _ParseFunctionParametersAndStateMutability['StateMutability'] + readonly inputs: ParseAbiParameters< + SplitParameters< + _ParseFunctionParametersAndStateMutability['Inputs'] + >, + { modifier: FunctionModifier; structs: structs } + > + readonly outputs: tail extends + | `${string}returns (${infer returns})` + | `${string}returns(${ + // biome-ignore lint/suspicious/noRedeclare: + infer returns + })` + ? ParseAbiParameters< + SplitParameters, + { modifier: FunctionModifier; structs: structs } + > + : readonly [] + } + : never + : never) + | (IsConstructorSignature extends true + ? { + readonly type: 'constructor' + readonly stateMutability: _ParseConstructorParametersAndStateMutability['StateMutability'] + readonly inputs: ParseAbiParameters< + SplitParameters< + _ParseConstructorParametersAndStateMutability['Inputs'] + >, + { structs: structs } + > + } + : never) + | (signature extends FallbackSignature + ? { + readonly type: 'fallback' + readonly stateMutability: stateMutability extends `${string}payable` + ? 'payable' + : 'nonpayable' + } + : never) + | (signature extends ReceiveSignature + ? { + readonly type: 'receive' + readonly stateMutability: 'payable' + } + : never) + +export type ParseAbiParameters< + signatures extends readonly string[], + options extends ParseOptions = DefaultParseOptions, +> = signatures extends [''] + ? readonly [] + : readonly [ + ...{ + [key in keyof signatures]: ParseAbiParameter + }, + ] + +export type _ParseFunctionParametersAndStateMutability< + signature extends string, +> = signature extends + | `${infer head}returns (${string})` + | `${ + // biome-ignore lint/suspicious/noRedeclare: + infer head + }returns(${string})` + ? _ParseFunctionParametersAndStateMutability> + : signature extends `function ${string}(${infer parameters})` + ? { Inputs: parameters; StateMutability: 'nonpayable' } + : signature extends `function ${string}(${infer parameters}) ${infer scopeOrStateMutability extends + | Scope + | abitype.AbiStateMutability + | `${Scope} ${abitype.AbiStateMutability}`}` + ? { + Inputs: parameters + StateMutability: _ParseStateMutability + } + : signature extends `function ${string}(${infer tail}` + ? _UnwrapNameOrModifier extends { + nameOrModifier: infer scopeOrStateMutability extends string + End: infer parameters + } + ? { + Inputs: parameters + StateMutability: _ParseStateMutability + } + : never + : never + +type _ParseStateMutability = + signature extends `${Scope} ${infer stateMutability extends abitype.AbiStateMutability}` + ? stateMutability + : signature extends abitype.AbiStateMutability + ? signature + : 'nonpayable' + +type _ParseConstructorParametersAndStateMutability = + signature extends `constructor(${infer parameters}) payable` + ? { Inputs: parameters; StateMutability: 'payable' } + : signature extends `constructor(${infer parameters})` + ? { Inputs: parameters; StateMutability: 'nonpayable' } + : never + +export type _ParseTuple< + signature extends `(${string})${string}`, + options extends ParseOptions = DefaultParseOptions, +> = /** Tuples without name or modifier (e.g. `(string)`, `(string foo)`) */ +signature extends `(${infer parameters})` + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } + : // Array or fixed-length array tuples (e.g. `(string)[]`, `(string)[5]`) + signature extends `(${infer head})[${'' | `${abitype.SolidityFixedArrayRange}`}]` + ? signature extends `(${head})[${infer size}]` + ? { + readonly type: `tuple[${size}]` + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } + : never + : // Array or fixed-length array tuples with name and/or modifier attached (e.g. `(string)[] foo`, `(string)[5] foo`) + signature extends `(${infer parameters})[${ + | '' + | `${abitype.SolidityFixedArrayRange}`}] ${infer nameOrModifier}` + ? signature extends `(${parameters})[${infer size}] ${nameOrModifier}` + ? nameOrModifier extends `${string}) ${string}` + ? _UnwrapNameOrModifier extends infer parts extends { + nameOrModifier: string + End: string + } + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters<`${parameters})[${size}] ${parts['End']}`>, + Omit + > + } & _SplitNameOrModifier + : never + : { + readonly type: `tuple[${size}]` + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } & _SplitNameOrModifier + : never + : // Tuples with name and/or modifier attached (e.g. `(string) foo`, `(string bar) foo`) + signature extends `(${infer parameters}) ${infer nameOrModifier}` + ? // Check that `nameOrModifier` didn't get matched to `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) + nameOrModifier extends `${string}) ${string}` + ? _UnwrapNameOrModifier extends infer parts extends { + nameOrModifier: string + End: string + } + ? { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters<`${parameters}) ${parts['End']}`>, + Omit + > + } & _SplitNameOrModifier + : never + : { + readonly type: 'tuple' + readonly components: ParseAbiParameters< + SplitParameters, + Omit + > + } & _SplitNameOrModifier + : never + +// Split name and modifier (e.g. `indexed foo` => `{ name: 'foo', indexed: true }`) +export type _SplitNameOrModifier< + signature extends string, + options extends ParseOptions = DefaultParseOptions, +> = Trim extends infer trimmed + ? options extends { modifier: Modifier } + ? // TODO: Check that modifier is allowed + trimmed extends `${infer mod extends options['modifier']} ${infer name}` + ? Evaluate< + { readonly name: Trim } & (mod extends 'indexed' + ? { readonly indexed: true } + : object) + > + : trimmed extends options['modifier'] + ? trimmed extends 'indexed' + ? { readonly indexed: true } + : object + : { readonly name: trimmed } + : { readonly name: trimmed } + : never + +// `baz) bar) foo` (e.g. `(((string) baz) bar) foo`) +export type _UnwrapNameOrModifier< + signature extends string, + current extends string = '', +> = signature extends `${infer head}) ${infer tail}` + ? _UnwrapNameOrModifier< + tail, + `${current}${current extends '' ? '' : ') '}${head}` + > + : { End: Trim; nameOrModifier: Trim } diff --git a/src/core/internal/regex.ts b/src/core/internal/regex.ts new file mode 100644 index 00000000..d2d721ab --- /dev/null +++ b/src/core/internal/regex.ts @@ -0,0 +1,22 @@ +export const address = /*#__PURE__*/ /^0x[a-fA-F0-9]{40}$/ + +// TODO: This looks cool. Need to check the performance of `new RegExp` versus defined inline though. +// https://twitter.com/GabrielVergnaud/status/1622906834343366657 +export function execTyped(regex: RegExp, string: string) { + const match = regex.exec(string) + return match?.groups as type | undefined +} + +// https://regexr.com/7f7rv +export const tupleAbiParameterType = /^tuple(?(\[(\d*)\])*)$/ + +// `bytes`: binary type of `M` bytes, `0 < M <= 32` +// https://regexr.com/6va55 +export const bytesRegex = /^bytes([1-9]|1[0-9]|2[0-9]|3[0-2])?$/ + +// `(u)int`: (un)signed integer type of `M` bits, `0 < M <= 256`, `M % 8 == 0` +// https://regexr.com/6v8hp +export const integerRegex = + /^u?int(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)?$/ + +export const isTupleRegex = /^\(.+?\).*?$/ diff --git a/src/core/internal/types.ts b/src/core/internal/types.ts index 66400cdf..e0f6830f 100644 --- a/src/core/internal/types.ts +++ b/src/core/internal/types.ts @@ -27,14 +27,31 @@ export type Branded = T & { [symbol]: U } * @internal */ export type Filter< - T extends readonly unknown[], - P, - Acc extends readonly unknown[] = [], -> = T extends readonly [infer F, ...infer Rest extends readonly unknown[]] - ? [F] extends [P] - ? Filter - : Filter - : readonly [...Acc] + items extends readonly unknown[], + item, + acc extends readonly unknown[] = [], +> = items extends readonly [ + infer head, + ...infer tail extends readonly unknown[], +] + ? [head] extends [item] + ? Filter + : Filter + : readonly [...acc] + +export type FilterReverse< + items extends readonly unknown[], + item, + /// + acc extends readonly unknown[] = [], +> = items extends readonly [ + infer head, + ...infer tail extends readonly unknown[], +] + ? [head] extends [item] + ? Filter + : Filter + : readonly [...acc] /** * Checks if `T` can be narrowed further than `U` @@ -50,6 +67,15 @@ export type IsNarrowable = IsNever< > extends true ? false : true +export type IsNarrowableIncludingNever = + IsUnknown extends true + ? false + : IsNever< + (type extends type2 ? true : false) & + (type2 extends type ? false : true) + > extends true + ? false + : true /** * Checks if `T` is `never` @@ -62,6 +88,43 @@ export type IsNarrowable = IsNever< */ export type IsNever = [T] extends [never] ? true : false +/** + * Joins array into string + * + * @param array - Array to join + * @param separator - Separator + * @returns string + * + * @example + * type Result = Join<['a', 'b', 'c'], '-'> + * // ^? type Result = 'a-b-c' + * + * @internal + */ +export type Join< + array extends readonly unknown[], + separator extends string | number, +> = array extends readonly [infer head, ...infer tail] + ? tail['length'] extends 0 + ? `${head & string}` + : `${head & string}${separator}${Join}` + : never + +/** + * Merges two object types into new type + * + * @param object1 - Object to merge into + * @param object2 - Object to merge and override keys from {@link object1} + * @returns New object type with keys from {@link object1} and {@link object2}. If a key exists in both {@link object1} and {@link object2}, the key from {@link object2} will be used. + * + * @example + * type Result = Merge<{ foo: string }, { foo: number; bar: string }> + * // ^? type Result = { foo: number; bar: string } + * + * @internal + */ +export type Merge = Omit & object2 + /** * Removes `readonly` from all properties of an object. * @@ -97,6 +160,18 @@ export type Or = T extends readonly [ : Or : false +/** + * Combines members of an intersection into a readable type. + * + * @link https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg + * @example + * type Result = Evaluate<{ a: string } | { b: string } | { c: number, d: bigint }> + * // ^? type Result = { a: string; b: string; c: number; d: bigint } + * + * @internal + */ +export type Evaluate = { [key in keyof type]: type[key] } & unknown + /** * Checks if `T` is `undefined` * @@ -182,7 +257,7 @@ export type NoUndefined = T extends undefined ? never : T * * @internal */ -export type Omit = Pick< +export type StrictOmit = Pick< type, Exclude > @@ -198,7 +273,7 @@ export type Omit = Pick< * * @internal */ -export type PartialBy = Omit & +export type PartialBy = StrictOmit & ExactPartial> export type RecursiveArray = T | readonly RecursiveArray[] @@ -214,7 +289,7 @@ export type RecursiveArray = T | readonly RecursiveArray[] * * @internal */ -export type RequiredBy = Omit & +export type RequiredBy = StrictOmit & ExactRequired> /** @@ -237,6 +312,29 @@ export type Some< ? Some : false +/** + * Trims empty space from type {@link t}. + * + * @param t - Type to trim + * @param chars - Characters to trim + * @returns Trimmed type + * + * @example + * type Result = Trim<' foo '> + * // ^? type Result = "foo" + */ +export type Trim = TrimLeft< + TrimRight, + chars +> +type TrimLeft = t extends `${chars}${infer tail}` + ? TrimLeft + : t +type TrimRight< + t, + chars extends string = ' ', +> = t extends `${infer head}${chars}` ? TrimRight : t + /** * Prints custom error message * @@ -336,7 +434,7 @@ export type Undefined = { // Loose types /** - * Loose version of {@link Omit} + * Loose version of {@link StrictOmit} * @internal */ export type LooseOmit = Pick< @@ -366,7 +464,7 @@ export type UnionLooseOmit = type extends any * @internal */ export type UnionOmit = type extends any - ? Omit + ? StrictOmit : never /** diff --git a/src/index.ts b/src/index.ts index 78ba3282..a5601759 100644 --- a/src/index.ts +++ b/src/index.ts @@ -476,6 +476,13 @@ export * as AbiEvent from './core/AbiEvent.js' */ export * as AbiFunction from './core/AbiFunction.js' +/** + * @category ABI + * + * TODO + */ +export * as AbiParameter from './core/AbiParameter.js' + /** * Utilities & types for working with [ABI Items](https://docs.soliditylang.org/en/latest/abi-spec.html#json) *