From b7fa5d80ee532f60abf7c6adaaeabeae4482f4ba Mon Sep 17 00:00:00 2001 From: Ivan Rukhavets Date: Fri, 3 May 2024 11:07:40 +0200 Subject: [PATCH] Migrate EVM typegen to new codec (#261) --- .../new-evm-typegen_2024-05-03-08-59.json | 10 + .../new-evm-typegen_2024-05-03-08-59.json | 10 + common/config/rush/pnpm-lock.yaml | 148 +++++--- evm/evm-abi/package.json | 3 +- evm/evm-abi/src/abi-components/event.ts | 2 + evm/evm-abi/src/abi-components/function.ts | 4 +- evm/evm-abi/src/index.ts | 6 +- evm/evm-typegen/package.json | 6 +- evm/evm-typegen/src/abi.support.ts | 135 ------- evm/evm-typegen/src/main.ts | 283 ++++++++------- evm/evm-typegen/src/multicall.ts | 3 + evm/evm-typegen/src/typegen.ts | 339 ++++++++++++------ evm/evm-typegen/src/util/fetch.ts | 14 +- evm/evm-typegen/src/util/types.ts | 106 +++--- test/erc20-transfers/package.json | 2 + test/erc20-transfers/src/abi/abi.support.ts | 120 ------- test/erc20-transfers/src/abi/erc20.abi.ts | 210 ----------- test/erc20-transfers/src/abi/erc20.ts | 108 +++--- 18 files changed, 627 insertions(+), 882 deletions(-) create mode 100644 common/changes/@subsquid/evm-abi/new-evm-typegen_2024-05-03-08-59.json create mode 100644 common/changes/@subsquid/evm-typegen/new-evm-typegen_2024-05-03-08-59.json delete mode 100644 evm/evm-typegen/src/abi.support.ts delete mode 100644 test/erc20-transfers/src/abi/abi.support.ts delete mode 100644 test/erc20-transfers/src/abi/erc20.abi.ts diff --git a/common/changes/@subsquid/evm-abi/new-evm-typegen_2024-05-03-08-59.json b/common/changes/@subsquid/evm-abi/new-evm-typegen_2024-05-03-08-59.json new file mode 100644 index 000000000..f8c926e22 --- /dev/null +++ b/common/changes/@subsquid/evm-abi/new-evm-typegen_2024-05-03-08-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@subsquid/evm-abi", + "comment": "Added types for event arguments, function parameters and return values", + "type": "minor" + } + ], + "packageName": "@subsquid/evm-abi" +} \ No newline at end of file diff --git a/common/changes/@subsquid/evm-typegen/new-evm-typegen_2024-05-03-08-59.json b/common/changes/@subsquid/evm-typegen/new-evm-typegen_2024-05-03-08-59.json new file mode 100644 index 000000000..905d7b959 --- /dev/null +++ b/common/changes/@subsquid/evm-typegen/new-evm-typegen_2024-05-03-08-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@subsquid/evm-typegen", + "comment": "Migrate typegen to new codec; Drop dependency to ethers.js", + "type": "major" + } + ], + "packageName": "@subsquid/evm-typegen" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 531079263..8564520c7 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -311,6 +311,9 @@ dependencies: '@types/xxhashjs': specifier: ^0.2.4 version: 0.2.4 + abitype: + specifier: ^1.0.0 + version: 1.0.0(typescript@5.3.3) ajv: specifier: ^8.12.0 version: 8.12.0 @@ -371,6 +374,9 @@ dependencies: jsonc-parser: specifier: ^3.2.0 version: 3.2.1 + keccak256: + specifier: ^1.0.6 + version: 1.0.6 keyv: specifier: ~4.5.4 version: 4.5.4 @@ -386,6 +392,9 @@ dependencies: pg: specifier: ^8.11.3 version: 8.11.5 + prettier: + specifier: ^3.2.5 + version: 3.2.5 prom-client: specifier: ^14.2.0 version: 14.2.0 @@ -4943,6 +4952,24 @@ packages: engines: {'0': node >= 0.2.0} dev: false + /keccak256@1.0.6: + resolution: {integrity: sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw==} + dependencies: + bn.js: 5.2.1 + buffer: 6.0.3 + keccak: 3.0.4 + dev: false + + /keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.0 + readable-stream: 3.6.2 + dev: false + /keyv@3.1.0: resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} dependencies: @@ -5247,6 +5274,10 @@ packages: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: false + /node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + dev: false + /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -5614,6 +5645,12 @@ packages: engines: {node: '>=4'} dev: false + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: false + /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5696,6 +5733,15 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -6006,6 +6052,12 @@ packages: strip-ansi: 7.1.0 dev: false + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -6355,6 +6407,10 @@ packages: node-gyp-build: 4.8.0 dev: false + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -6872,7 +6928,7 @@ packages: dev: false file:projects/batch-processor.tgz: - resolution: {integrity: sha512-4NsCr/63brjR3o4mZ64uwgA49Z8uXyBnhduMCAiJWH78ena0EKFvWxrWgUOeVD5O1KLEKKPybELTB52S2h81Qw==, tarball: file:projects/batch-processor.tgz} + resolution: {integrity: sha512-TAxeOSaoBNBY0QO5/WLaX2dkZkJ2svupQO0Sz/f1H0nsvJKbfD5hXloeFHCRPIO7mk9iHKVC1SPHJXOLiuwYjg==, tarball: file:projects/batch-processor.tgz} name: '@rush-temp/batch-processor' version: 0.0.0 dependencies: @@ -6890,7 +6946,7 @@ packages: dev: false file:projects/borsh-bench.tgz: - resolution: {integrity: sha512-1MkeC9bbWRLkREmkZzqIM0g4VKcPSYLfRRjZGGouuqkYTlabFHkiH/PuZgY4c4YSkVLnp9oxmi2R4VMi6XETVQ==, tarball: file:projects/borsh-bench.tgz} + resolution: {integrity: sha512-+bizIFJ+snTWTONsmwfb2g4Nh/iZkNUc+0+uqgHGhdyFaPHjeT0+CFdgkMc/VOjYgC0r8xCkabL7toGpHCO/yA==, tarball: file:projects/borsh-bench.tgz} name: '@rush-temp/borsh-bench' version: 0.0.0 dependencies: @@ -6916,7 +6972,7 @@ packages: dev: false file:projects/commands.tgz: - resolution: {integrity: sha512-jLVHiPtQVNrYmDfRSWpNwgZI9c4uSykaK9yngvHa2eCCvI676ZyQLDSyGaiI826OWFZLuRji/1DeiP/xBEASjQ==, tarball: file:projects/commands.tgz} + resolution: {integrity: sha512-BoqXGU5cFyVCOlUwjSGo/IWpruiJfoI+S+EaVPUFq2Grnr3EGSsL1ycJ7k7fxEIWfiks44eucGXGRNm/CoCMOg==, tarball: file:projects/commands.tgz} name: '@rush-temp/commands' version: 0.0.0 dependencies: @@ -6928,7 +6984,7 @@ packages: dev: false file:projects/data-test.tgz: - resolution: {integrity: sha512-FfWZBe8Q89tLUr0ADe9ie2Q7xrjm2yEDslD6eYYjLbkxhGJDKkMZgaZuA/tZGEOrt+Eg6y8j1BPurpgBW3DICQ==, tarball: file:projects/data-test.tgz} + resolution: {integrity: sha512-+nsYJMCw7TENFjjX3Kozj2E0evaaoIX+foZjH9COwqW59f60tuQq7FPTMMLy1ZxYPbbgEoFidnqrY6vnE8hctQ==, tarball: file:projects/data-test.tgz} name: '@rush-temp/data-test' version: 0.0.0 dependencies: @@ -6976,13 +7032,14 @@ packages: dev: false file:projects/evm-abi.tgz(supports-color@8.1.1): - resolution: {integrity: sha512-iGwaVpQ1b2kAr7NplbftX+bTwhll7eiEyT8T3DM/ElBFzUTm599Ku1qQn5tf27v+Zx0AI/o2H7m/GYpZHdDJ+w==, tarball: file:projects/evm-abi.tgz} + resolution: {integrity: sha512-n0iKZh2IavFOXaK342RnYVMbyzX6HD3k6xqzXaBuDBYxvxUUUEuHfntX5NwaEwesG2OHG2RVvTr4MulPAvHH9w==, tarball: file:projects/evm-abi.tgz} id: file:projects/evm-abi.tgz name: '@rush-temp/evm-abi' version: 0.0.0 dependencies: '@types/node': 18.19.31 ethers: 5.7.2 + keccak256: 1.0.6 typescript: 5.3.3 viem: 2.9.16(typescript@5.3.3) vitest: 1.5.0(@types/node@18.19.31)(supports-color@8.1.1) @@ -7034,7 +7091,7 @@ packages: dev: false file:projects/evm-processor.tgz: - resolution: {integrity: sha512-4YWmT1p6YH6c61cCkZ7Mj6VOHfXIZfHZJyrn7v+Z9Hu19iOle2j3vthnBQjqosOU0lBeh4ENdlI1hd0STtB7DA==, tarball: file:projects/evm-processor.tgz} + resolution: {integrity: sha512-HyFG/X1g9MYOcWr4/GerPTfAdLYqDTZhK1m0CAZIY43YSKkK4HbflqBNw712WVnhD0ok/K+N6VaBuIl4roW0OQ==, tarball: file:projects/evm-processor.tgz} name: '@rush-temp/evm-processor' version: 0.0.0 dependencies: @@ -7043,21 +7100,22 @@ packages: dev: false file:projects/evm-typegen.tgz: - resolution: {integrity: sha512-s4XjoGSphdj7Y0QPhtw1+mKnn5jt5H5JVQqT6EWOQrqmdkh84VvhCx8q81T6OOU6+S8+zEwdopP4nTDK/h2UsA==, tarball: file:projects/evm-typegen.tgz} + resolution: {integrity: sha512-GKRfQx5JOkidKsJJ4uXBJ93THW0qb5vjWMj1SfQ+X9S1w/QOnRlnqlgfAG1EeW0Y4VGnmpMbLmIXgWCGiR0Q3w==, tarball: file:projects/evm-typegen.tgz} name: '@rush-temp/evm-typegen' version: 0.0.0 dependencies: '@types/node': 18.19.31 + abitype: 1.0.0(typescript@5.3.3) commander: 11.1.0 ethers: 6.11.1 + prettier: 3.2.5 typescript: 5.3.3 transitivePeerDependencies: - - bufferutil - - utf-8-validate + - zod dev: false file:projects/frontier.tgz: - resolution: {integrity: sha512-QrlIIJjS8hg5iOfBd7M3lgGBGCO2z4pXFaQWAINeniBTrVGan8qO7VvCw0X58rpla4HpPve22goM2EoeegBiWQ==, tarball: file:projects/frontier.tgz} + resolution: {integrity: sha512-S7+MzM0HFmHht6hNoO4gHZeUpr8sNnjqhND8Rix+wAfX2j4x/9pizzGD8Z6bwkFdOc5aHGEsUohXbaF45B8kmQ==, tarball: file:projects/frontier.tgz} name: '@rush-temp/frontier' version: 0.0.0 dependencies: @@ -7088,7 +7146,7 @@ packages: dev: false file:projects/graphql-server.tgz(supports-color@8.1.1)(ts-node@10.9.2): - resolution: {integrity: sha512-pP4moNVOtmm9Wpn3sx1ZyqiCW3IK/aBdDvAvUG7VmLSRuV6NcqGDp2uSWC8DlhGnu2osuDzGBwi9lCC1O3RHVA==, tarball: file:projects/graphql-server.tgz} + resolution: {integrity: sha512-H+MgJVyNdVQZwkViPK51qkKYrUNfv6wuCbc/KlCd/hK7+/qHCD6UeXkqCX+pbA4Jn2KZ65cKQe4rrxxdBpGgEA==, tarball: file:projects/graphql-server.tgz} id: file:projects/graphql-server.tgz name: '@rush-temp/graphql-server' version: 0.0.0 @@ -7145,7 +7203,7 @@ packages: dev: false file:projects/http-client.tgz: - resolution: {integrity: sha512-A9DvfuHNKl2ACuoV7LkBOltou5GwgvuNtWoYEfB4R98VO9RXT1OsRIsOPYE+dQAqf8kPsydKyoFhJq09NIM81A==, tarball: file:projects/http-client.tgz} + resolution: {integrity: sha512-uyQzIRNd0y6x8RLmlSfuN/m8dXckfyLKQb3e6iki37DBBp+R2eAKo+CCLQg+2abinMWgnUxye2nZY45hGNDvSw==, tarball: file:projects/http-client.tgz} name: '@rush-temp/http-client' version: 0.0.0 dependencies: @@ -7155,7 +7213,7 @@ packages: dev: false file:projects/ink-abi.tgz: - resolution: {integrity: sha512-t5YoFb5SFemS0Q2UyDGYGW79y7/VNYJKOQ5ZlKO3Xns/0jXLqWuKEXwaaJugGnElsZrS5cTJy1i2XP4qwd8WuA==, tarball: file:projects/ink-abi.tgz} + resolution: {integrity: sha512-1fECV1QdzLFGOMHWUqugB7CdtjbvVdqNoZik4R2N1dqBEmuQmEhBLvqRwPz7FlUBISavzw5XOqjHUaN3uJVvxw==, tarball: file:projects/ink-abi.tgz} name: '@rush-temp/ink-abi' version: 0.0.0 dependencies: @@ -7168,7 +7226,7 @@ packages: dev: false file:projects/ink-typegen.tgz: - resolution: {integrity: sha512-/AhFMQbupC3fA8dq4Uv8Z/+YKqjCrmQwWTh1FLVuW1EBqOLwj43vvsYxwHPFh0OeROcjyXN70tgtQDFeVw+V9w==, tarball: file:projects/ink-typegen.tgz} + resolution: {integrity: sha512-O/kSkDq2aQEoNbYH5P0pRjFgXJc7n/luYiGc9YzO7hVEVL/E7j5LiynrlFhmfuvAt7iTwTn/lGEj68SN6Xvcbw==, tarball: file:projects/ink-typegen.tgz} name: '@rush-temp/ink-typegen' version: 0.0.0 dependencies: @@ -7192,7 +7250,7 @@ packages: dev: false file:projects/openreader.tgz(supports-color@8.1.1): - resolution: {integrity: sha512-d5pe+rrIHUzm4KOG+sebZODpgnrls3naveU0Ej+wClc6SB9SgMBPXYfv6863dlTOZi0ktFdK0Ns2+6CKsQ7yAw==, tarball: file:projects/openreader.tgz} + resolution: {integrity: sha512-w7Sr0FL3Uw7Y6gUfwwXow7i9q9aEJKqa1ppm3gr7ehvBzHbPWSrWA8fGTH9KedUMoSzLBuJRI6fDz3hNpbS0DQ==, tarball: file:projects/openreader.tgz} id: file:projects/openreader.tgz name: '@rush-temp/openreader' version: 0.0.0 @@ -7228,7 +7286,7 @@ packages: dev: false file:projects/ops-xcm-typegen.tgz: - resolution: {integrity: sha512-wtj/Wq5Y3+nW6r1oG/mcn55TVyHFL2sEtCmrkVQUA41RGZ31oqr8v/oBySqT0im3YZVKiw3tN+ihSKSW0e2pRw==, tarball: file:projects/ops-xcm-typegen.tgz} + resolution: {integrity: sha512-dnluj1Rz6Rhrs3hVjE31EcleGddobOtdghiTtD0TshBGtMRcRCm87cHlXJ3dPXIkrsl8rvU7HWAnQZber2Y+Tg==, tarball: file:projects/ops-xcm-typegen.tgz} name: '@rush-temp/ops-xcm-typegen' version: 0.0.0 dependencies: @@ -7237,7 +7295,7 @@ packages: dev: false file:projects/raw-archive-validator.tgz: - resolution: {integrity: sha512-GIaNsZeRZbKPRVV9sWY4mPI7pT5eL+l8yjlBTOt6zCgw/vm6sGkMX/DP7qPsDKcwkP1sPPfJZgoy+E3tL/yMqA==, tarball: file:projects/raw-archive-validator.tgz} + resolution: {integrity: sha512-benVVBgrVaz2bJe+k23ex1z6Zsz3nYcW8uj1+m1gmJ7JRag+ZfSAo2ODIMXbCvLzecce/wn2UYI8jlOtVS5XjQ==, tarball: file:projects/raw-archive-validator.tgz} name: '@rush-temp/raw-archive-validator' version: 0.0.0 dependencies: @@ -7247,7 +7305,7 @@ packages: dev: false file:projects/rpc-client.tgz: - resolution: {integrity: sha512-3dtSuQyuOHepc5y8Clzx21FhwiaVawIKuxawuDm6R4Ni7FqIX9LG8sA+c2C0p4fDu9ds8Qgg+EALSPeLp7RHAQ==, tarball: file:projects/rpc-client.tgz} + resolution: {integrity: sha512-GEWqVRas3lM/oh9aZDA8TzmOf/GFXkWzbFP1V5VJjJAcWAM2JhYpnyJA1Cz0pyeC/mYm3B+kmVCMtqfrMiMrmA==, tarball: file:projects/rpc-client.tgz} name: '@rush-temp/rpc-client' version: 0.0.0 dependencies: @@ -7267,7 +7325,7 @@ packages: dev: false file:projects/scale-type-system.tgz: - resolution: {integrity: sha512-N9BDKzu4PMscJAEkPrxf3Zm/ebTqycIAqqD+mvl+XFoDII5Dwtu20TKXXSJenS/pXcG+NKviM1B8UbPTuGFE7A==, tarball: file:projects/scale-type-system.tgz} + resolution: {integrity: sha512-DQbXk34bE+eFfqNDLtuR9AQ6/IA2aj5iPrgNJIvhx4VRTc2DPSzoOLHsEUnxJVkU/TLkddqYNDXGc9wZNAXlvQ==, tarball: file:projects/scale-type-system.tgz} name: '@rush-temp/scale-type-system' version: 0.0.0 dependencies: @@ -7307,7 +7365,7 @@ packages: dev: false file:projects/solana-dump.tgz: - resolution: {integrity: sha512-R5E7joVFpVriCIrCS6pzJ1SgvGi+7Yq94MEoJlIk+9glKpjyLew8RFht2FDnnJ4iwLB3LjIyoUSCS2WwPi1+lQ==, tarball: file:projects/solana-dump.tgz} + resolution: {integrity: sha512-ojMrf7R50cyZNAV2HQPc56hKKdv6ANfFxe4PBWIKNKwZgH+ivJkaIjTyrO1SPOy5LgIiSAv2cCsoL2voQImaRQ==, tarball: file:projects/solana-dump.tgz} name: '@rush-temp/solana-dump' version: 0.0.0 dependencies: @@ -7347,7 +7405,7 @@ packages: dev: false file:projects/solana-ingest.tgz: - resolution: {integrity: sha512-CgxvO58UEBDHMGgUXKb/Vka/+zljUvt9eHTcImNC4ZlFs2sxg7OSfHrua1CDQLYw8TeNyS7dGUNpf2yzPpgP+A==, tarball: file:projects/solana-ingest.tgz} + resolution: {integrity: sha512-3byjt3nZUW4Ddzn3Og5zjv0qQv4r1y1OScsETSgjzv5ksQaf4+FSz409sfwr1+nC0gWhIJCRKLMXd7jEhSIVow==, tarball: file:projects/solana-ingest.tgz} name: '@rush-temp/solana-ingest' version: 0.0.0 dependencies: @@ -7356,7 +7414,7 @@ packages: dev: false file:projects/solana-normalization.tgz: - resolution: {integrity: sha512-GDF4JKo4MqjpFxOE2Tj3QrFkk+UWzRFkpnPZRWVaWuupALB0x1uH2BOZ4FD7zEH0AEdsiaBoXCGHV+YhXRiuOA==, tarball: file:projects/solana-normalization.tgz} + resolution: {integrity: sha512-0+jq0Mlxyaehu/EGgER3YXLYrn1thicMdmTh8OpMCge3SpxIL9Ykgg6TErLnD/ze1ZtzdzOrloye7ZZGJJzSUA==, tarball: file:projects/solana-normalization.tgz} name: '@rush-temp/solana-normalization' version: 0.0.0 dependencies: @@ -7365,7 +7423,7 @@ packages: dev: false file:projects/solana-objects.tgz: - resolution: {integrity: sha512-D917JH+I/EQ8yyuN9U4VQZIUr5DQFMD1YuOSCy9dDk+VII3augB24OTPICj/QWNO4798Lmk49fgMVrzHxTseqw==, tarball: file:projects/solana-objects.tgz} + resolution: {integrity: sha512-j5fsmlgZc4gIRrUWnMwCoxe8BnQpoEin0Bh42PTs6CX36sF0W0xz8rw6gA+Gb2BuECbIqKVFWsNrmC3k2ieMGA==, tarball: file:projects/solana-objects.tgz} name: '@rush-temp/solana-objects' version: 0.0.0 dependencies: @@ -7383,7 +7441,7 @@ packages: dev: false file:projects/solana-rpc.tgz: - resolution: {integrity: sha512-/0GlUcsXranIoKMRo15WBs5UxP5RRQ76z1G0QOrPexGYordMjfHxp4r7MgILljY3b3up/6YKaLeTErTUqxwTEw==, tarball: file:projects/solana-rpc.tgz} + resolution: {integrity: sha512-64wPdSYNRNWOiuDOaBsr5rYEJp/amuEH6SUG5CC0/LAXC/PS428xLAEmv4obqDgZdzmThPhSOq/Gh2iludzbFA==, tarball: file:projects/solana-rpc.tgz} name: '@rush-temp/solana-rpc' version: 0.0.0 dependencies: @@ -7392,7 +7450,7 @@ packages: dev: false file:projects/solana-stream.tgz: - resolution: {integrity: sha512-wJCwXZ++4Rred7vibwvkeNqzEOTi7L4M4L35uf2xHbxVJoY7lEUndc9vlKoF/zGKW4GwO3P6gL+X/CMkYNsZCA==, tarball: file:projects/solana-stream.tgz} + resolution: {integrity: sha512-rInYl2E0JSXW1H/95/Egm4h0PoJocJv8FsPHFE0WirzMqsvtEj0VtubKd+/DJII+Wu6sSpFBIDft7UYVmjWuWQ==, tarball: file:projects/solana-stream.tgz} name: '@rush-temp/solana-stream' version: 0.0.0 dependencies: @@ -7422,7 +7480,7 @@ packages: dev: false file:projects/substrate-data-raw.tgz: - resolution: {integrity: sha512-2tOQ+Z44B0ZNxiyxfC4UzUZvOS8wD9Xwk/h3BeJVBhmGTGI+DzhCt/ReL54c1KBNpD4DKgDoXmRWIXMCuXjT9Q==, tarball: file:projects/substrate-data-raw.tgz} + resolution: {integrity: sha512-sP85VuD1iPnY2MYEoRmilq1ix7+NPQFk7iOMfFeXrNnQb/SKQ6wi23DEy8385kYch5yALFpX9KzAzoVV3eR5DQ==, tarball: file:projects/substrate-data-raw.tgz} name: '@rush-temp/substrate-data-raw' version: 0.0.0 dependencies: @@ -7431,7 +7489,7 @@ packages: dev: false file:projects/substrate-data.tgz: - resolution: {integrity: sha512-aIQONSEwOJM+3639CdlNqCP9+i0Nh3dngfIG2cQiYIVFH+oracjbK3o83OXosgUf+/CPk835A8Fjw//z4wnNtw==, tarball: file:projects/substrate-data.tgz} + resolution: {integrity: sha512-JrKRHErM5d/2dylTVCN7AKy1JoHgq8FpkytRKXTjwIWHdTH+eSu5L2x8byz11zOzCnMcMoygabOOIyfVRHXZNQ==, tarball: file:projects/substrate-data.tgz} name: '@rush-temp/substrate-data' version: 0.0.0 dependencies: @@ -7442,7 +7500,7 @@ packages: dev: false file:projects/substrate-dump.tgz: - resolution: {integrity: sha512-esw7+TZG5YmOL6ovLEPj+Ntle3hpzGLWIDmQsI84GRPyBpSxfctdLJTVnZ6uzBSBnt/K8OnaWQ1jbt+IsuBgEA==, tarball: file:projects/substrate-dump.tgz} + resolution: {integrity: sha512-c0d4DXnU6nChfZqUStNYQQaPMwSA6+YoKVHTQc1YZHUd68TvwNMQCRybXc0H+XYsOe1c3b536KVX1jZRGwQ75A==, tarball: file:projects/substrate-dump.tgz} name: '@rush-temp/substrate-dump' version: 0.0.0 dependencies: @@ -7451,7 +7509,7 @@ packages: dev: false file:projects/substrate-ingest.tgz: - resolution: {integrity: sha512-1TzM2USl07UUJBaQpKHJeq30i0a/KmVbe1yDvp9lDVdFNtWdC9Me28TqB8fWX1c2Czf6EFPkKe2bKZklSVw3Eg==, tarball: file:projects/substrate-ingest.tgz} + resolution: {integrity: sha512-mWw1pAceuqsKir548EaZQgl4iDhPb4igrXizMpdiTT2bIoK+Bm0Ma5wIpDQf5eqInt5nK+x5REJ51CQu9MqiOg==, tarball: file:projects/substrate-ingest.tgz} name: '@rush-temp/substrate-ingest' version: 0.0.0 dependencies: @@ -7459,7 +7517,7 @@ packages: dev: false file:projects/substrate-metadata-explorer.tgz: - resolution: {integrity: sha512-fyUp4QJcZsyWxHq2VIp4PXXIY2QY7OFQhEGPAfaYSLL6msGAXlQ9ynMurYxObr2kBTD9Vlk7zaA8NpObCl9x0Q==, tarball: file:projects/substrate-metadata-explorer.tgz} + resolution: {integrity: sha512-6Qn7o/sNxyVxutsXg2Ox37YbQXYUnIeTjC+D8OCvwk1ezIasScMt41G0xjDXc3Ny1ja53nWmNkwmqFNfPUY7rw==, tarball: file:projects/substrate-metadata-explorer.tgz} name: '@rush-temp/substrate-metadata-explorer' version: 0.0.0 dependencies: @@ -7469,7 +7527,7 @@ packages: dev: false file:projects/substrate-metadata-service.tgz: - resolution: {integrity: sha512-cVOAJUIPnSxSJSOQQAue7zCZJ8gtuYcK7D0o5jxKgdt14aA0cqpuHqieclOgjYnYjtmTfNxY3SDKptp6YR6Mfw==, tarball: file:projects/substrate-metadata-service.tgz} + resolution: {integrity: sha512-8ks9HATfGZ8+yejLTnNp80m/5m9m3E/5E/Ozd8xQE4hCpdhvHznZCB/+9XGklgRhg1c3k1LRipZYVOCYIWkZlQ==, tarball: file:projects/substrate-metadata-service.tgz} name: '@rush-temp/substrate-metadata-service' version: 0.0.0 dependencies: @@ -7479,7 +7537,7 @@ packages: dev: false file:projects/substrate-processor.tgz: - resolution: {integrity: sha512-cLrLar337vbeWcUnPjP/EAo7cXUea9bI2BDIwOTe4+OuLIo97/P7DP2h9gDivVZMKRRx+2CsR79etRIlxYC/8g==, tarball: file:projects/substrate-processor.tgz} + resolution: {integrity: sha512-0RNMvz1MNK1W/b9a39v1cRz3CvbA//tK1b596KKp0vdj42TQApzfPnJmKtSYI7qGccO5zfThYPTmBQ8urcWDkg==, tarball: file:projects/substrate-processor.tgz} name: '@rush-temp/substrate-processor' version: 0.0.0 dependencies: @@ -7488,7 +7546,7 @@ packages: dev: false file:projects/substrate-runtime.tgz: - resolution: {integrity: sha512-IW/h+WQpJ6FTz6b66yAM8hxNHfDHJ1giojeqGje56sXK6TOkeDzCI8RdYO9jziQ26CA3wdGitnwbw3ggEjZ7Hg==, tarball: file:projects/substrate-runtime.tgz} + resolution: {integrity: sha512-ALp1WTZkz30QCJLANQgDGYhYjqGQ/JMEMa2E6XnFllmO0PyfTCkjQhgqaPNtdWUeOzSSYMjewhRfI+kBkb966A==, tarball: file:projects/substrate-runtime.tgz} name: '@rush-temp/substrate-runtime' version: 0.0.0 dependencies: @@ -7501,7 +7559,7 @@ packages: dev: false file:projects/substrate-typegen.tgz: - resolution: {integrity: sha512-QlLPLLnqzArBzdflHB8XIZK0xKpulQY2/DhIkY9ndcO7mE8051o21VFvpLbT8nxYVLsAryMkNiBJ6mcZxF1vZg==, tarball: file:projects/substrate-typegen.tgz} + resolution: {integrity: sha512-0WeFoRlQfMS9YC7Q4zIKJrpIDmZhg8bAU6aMKQohdYPR67QdiQT7TPkPSJMgZH4N0J29wy964Pk29VQuyT/M1A==, tarball: file:projects/substrate-typegen.tgz} name: '@rush-temp/substrate-typegen' version: 0.0.0 dependencies: @@ -7511,7 +7569,7 @@ packages: dev: false file:projects/typeorm-codegen.tgz: - resolution: {integrity: sha512-tPH1Qqos0xlWTZJ+gMMshamWpRfToTeZwQL4z9xjbKfOTl6zauj2ofT6794ukwY7541bwtx5qKs5sVwDsu+2yw==, tarball: file:projects/typeorm-codegen.tgz} + resolution: {integrity: sha512-x9IWnLozuQzoyyLaDbhnz9TnI8KNdzcli8q/99fVU9+Fx6kaQD/lq6bzaN3xa4cPkKUW5342e67T/64/eh/M0g==, tarball: file:projects/typeorm-codegen.tgz} name: '@rush-temp/typeorm-codegen' version: 0.0.0 dependencies: @@ -7551,7 +7609,7 @@ packages: dev: false file:projects/typeorm-migration.tgz(pg@8.11.5)(supports-color@8.1.1)(ts-node@10.9.2): - resolution: {integrity: sha512-Ocxm1MoKWb/KUgGR8SOzGhVw4dXcumdCSKZnTAYH/qcJU+Gk5coa41mMItxOxtYKEuYrIn971eb/i22sUwQLdw==, tarball: file:projects/typeorm-migration.tgz} + resolution: {integrity: sha512-qCfUZSOhNYS3i1mzx0QD/u6CVUCC88S2nTvKOaYFU8mhsL0JdsC77H2QEfFCQAG+tmDlYIJ2RuC2ODL/JCO3dQ==, tarball: file:projects/typeorm-migration.tgz} id: file:projects/typeorm-migration.tgz name: '@rush-temp/typeorm-migration' version: 0.0.0 @@ -7583,7 +7641,7 @@ packages: dev: false file:projects/typeorm-store.tgz(supports-color@8.1.1)(ts-node@10.9.2): - resolution: {integrity: sha512-2LxupKIRUWc0p/24HcW+F5jQ/MTkzLLOfOwj1ThyeoSC2RhpRi3A4Xx+VMTQhnDhJUfWQ3jOWdFLKgwsTpiTow==, tarball: file:projects/typeorm-store.tgz} + resolution: {integrity: sha512-gbZJ/WhrsOhojEVJ8ci+laWAg9ri8N9qISrB6FGXtpXkaNKFNuYfzCVJkujiUWvLEWooumCxjm8+Ksa2T+TeOw==, tarball: file:projects/typeorm-store.tgz} id: file:projects/typeorm-store.tgz name: '@rush-temp/typeorm-store' version: 0.0.0 @@ -7626,7 +7684,7 @@ packages: dev: false file:projects/util-internal-archive-client.tgz: - resolution: {integrity: sha512-ZZu9CxUELY8Rg514KQ2B/fsw5stAga4hBzgW9+BX/xrXI1dFGGAM44KqFSSfsSfoDKfux50kijFYUY7gZgEtCg==, tarball: file:projects/util-internal-archive-client.tgz} + resolution: {integrity: sha512-kMWWCiAZvetlvlVlRIuZIMQfwkdMbm1/B0Mn7rswBsvviNZdens+q8qborhvNBez02e1cQ4L94ct1NWyyaheNQ==, tarball: file:projects/util-internal-archive-client.tgz} name: '@rush-temp/util-internal-archive-client' version: 0.0.0 dependencies: @@ -7635,7 +7693,7 @@ packages: dev: false file:projects/util-internal-archive-layout.tgz: - resolution: {integrity: sha512-e5rf7hleh+Q08AThubH39/QgyvByakRr/MrTu7IfxJ+6msaUxxNz/icuuyHy4y0NP4jYWNeCPwWm5Q2zBib6GA==, tarball: file:projects/util-internal-archive-layout.tgz} + resolution: {integrity: sha512-6suG/zPD4XBsO+sfjjOzUrQvdpk4WIg9PYrdLrXqKRS9d1eviurqWXXy3hoaGpyU41eWtal2FVD6uUsqazzT3g==, tarball: file:projects/util-internal-archive-layout.tgz} name: '@rush-temp/util-internal-archive-layout' version: 0.0.0 dependencies: @@ -7689,7 +7747,7 @@ packages: dev: false file:projects/util-internal-dump-cli.tgz: - resolution: {integrity: sha512-F/1BYRJyscunPQaYfSikpBHejMrSrFay75txcc4/zAv8O6Oee1RmydwF0CLERAYhBJZdsB52Lq25pYqaC3qf1A==, tarball: file:projects/util-internal-dump-cli.tgz} + resolution: {integrity: sha512-qx+nvgta1MjNR8BXHLm5ZTOhe5Md5xxfod7hf7eK7ajT8YBIsgmryLqvh9AO637Lnu0Rx65qzTDP5Hq6b7dCRg==, tarball: file:projects/util-internal-dump-cli.tgz} name: '@rush-temp/util-internal-dump-cli' version: 0.0.0 dependencies: @@ -7733,7 +7791,7 @@ packages: dev: false file:projects/util-internal-ingest-cli.tgz: - resolution: {integrity: sha512-4810yPiGIhGnOnFVhomoxmEP61Q0MxwJhoRKBw922T9q2iT5aZ7tkPI/kxdW4sH6omvt9XLomdsrkIjMTRb90A==, tarball: file:projects/util-internal-ingest-cli.tgz} + resolution: {integrity: sha512-XGc+tfdopIliI3nVoE6DQOgP6RA/aQHhmNesY2uqA7WZOWboFnmoRnoa1TCInBNrvLG9ASMtlNtWBsP8Qt1GUA==, tarball: file:projects/util-internal-ingest-cli.tgz} name: '@rush-temp/util-internal-ingest-cli' version: 0.0.0 dependencies: @@ -7743,7 +7801,7 @@ packages: dev: false file:projects/util-internal-ingest-tools.tgz: - resolution: {integrity: sha512-0yQXs9T77XqUUdVOEookxGvltF0qNvg14C+d4zd+RW0C2i4LCpSbor46KmVceqnIFGUGMTft0l0ibnZDoDu9Zw==, tarball: file:projects/util-internal-ingest-tools.tgz} + resolution: {integrity: sha512-+5tvYpsVWWgL/gTbs0rLaVDUJRHy6ieQ8RVa+sxmx/ZPM0+SZaKBuXnY1RBShNIJjewjmNv/f572BlXP7FWzMw==, tarball: file:projects/util-internal-ingest-tools.tgz} name: '@rush-temp/util-internal-ingest-tools' version: 0.0.0 dependencies: @@ -7770,7 +7828,7 @@ packages: dev: false file:projects/util-internal-processor-tools.tgz: - resolution: {integrity: sha512-CKlg4ObB+8Re2WSDp8jfEyq/hX0SjTUE2li6cOsSSPK/NalbVkUFVDmzdBEacS/sEBn1VsERl4RP61CnrFbzaA==, tarball: file:projects/util-internal-processor-tools.tgz} + resolution: {integrity: sha512-HQmrng6wmFx+QAyKBTYSOuVQsWW9W/8bHJE5X+OPj+qz/CK/FktXIHjjmWi0exXfQl5sUeL0fOzt2HyxfPb6Eg==, tarball: file:projects/util-internal-processor-tools.tgz} name: '@rush-temp/util-internal-processor-tools' version: 0.0.0 dependencies: @@ -7790,7 +7848,7 @@ packages: dev: false file:projects/util-internal-range.tgz: - resolution: {integrity: sha512-FJm7+rweOz9YYmHzm8MLD2wMl1i+QdwCGGXr3AFYaq11AUouPuw41eA5F4ehGxbwB4xaYu4LCN9L2J8sErylFQ==, tarball: file:projects/util-internal-range.tgz} + resolution: {integrity: sha512-6IbHhimGih0w+dV2hsXXl3ssQbbG5Rbv5xXIDHfTqs6XmMyFMzl5rl/CT2NCK+MNvh8UZ206+ziHRBvi5ljt5w==, tarball: file:projects/util-internal-range.tgz} name: '@rush-temp/util-internal-range' version: 0.0.0 dependencies: @@ -7878,7 +7936,7 @@ packages: dev: false file:projects/workspace.tgz: - resolution: {integrity: sha512-YykxITUdkFGhHylZXA8P6bq+TrIwJ0J+1VkrxxTGcxoiyGiqJKNbnZB4hVy8yo5/UZ0LuFAuF6DWCMacqKLRSg==, tarball: file:projects/workspace.tgz} + resolution: {integrity: sha512-axbEjyyRaGKnMSm9UhB9moVhBOIA8uBDl4eOt0XH2bRqZ9kQz47ciQN0UHH+QHG6+P/xrO1xTYbWra+f9Rdjfw==, tarball: file:projects/workspace.tgz} name: '@rush-temp/workspace' version: 0.0.0 dependencies: diff --git a/evm/evm-abi/package.json b/evm/evm-abi/package.json index d744f2b34..819b68842 100644 --- a/evm/evm-abi/package.json +++ b/evm/evm-abi/package.json @@ -16,7 +16,8 @@ "build": "rm -rf lib && tsc -p tsconfig.build.json" }, "dependencies": { - "@subsquid/evm-codec": "^0.0.0" + "@subsquid/evm-codec": "^0.0.0", + "keccak256": "^1.0.6" }, "devDependencies": { "@types/node": "^18.18.14", diff --git a/evm/evm-abi/src/abi-components/event.ts b/evm/evm-abi/src/abi-components/event.ts index 9cbef763e..a49655fef 100644 --- a/evm/evm-abi/src/abi-components/event.ts +++ b/evm/evm-abi/src/abi-components/event.ts @@ -14,6 +14,8 @@ export type IndexedCodecs = Pretty<{ [K in keyof T]: T[K] extends { indexed: true; isDynamic: true } ? typeof bytes32 & { indexed: true } : T[K] }> +export type EventParams> = T extends AbiEvent ? StructTypes : never + export class AbiEvent { public readonly params: any constructor(public readonly topic: string, args: T) { diff --git a/evm/evm-abi/src/abi-components/function.ts b/evm/evm-abi/src/abi-components/function.ts index 459781961..cd85b656c 100644 --- a/evm/evm-abi/src/abi-components/function.ts +++ b/evm/evm-abi/src/abi-components/function.ts @@ -9,7 +9,9 @@ function slotsCount(codecs: readonly Codec[]) { return count } -type FunctionReturn = T extends Codec ? U : T extends Struct ? StructTypes : undefined +export type FunctionReturn = T extends Codec ? U : T extends Struct ? StructTypes : void + +export type FunctionArguments> = T extends AbiFunction ? StructTypes : never export class AbiFunction | Struct | undefined> { readonly #selector: Buffer diff --git a/evm/evm-abi/src/index.ts b/evm/evm-abi/src/index.ts index 3c1456d70..f2473abf4 100644 --- a/evm/evm-abi/src/index.ts +++ b/evm/evm-abi/src/index.ts @@ -1,5 +1,7 @@ export { ContractBase } from './contract-base' export { indexed } from './indexed' -export { fun, AbiFunction } from './abi-components/function' -export { event, AbiEvent, type EventRecord } from './abi-components/event' +export { fun, AbiFunction, type FunctionReturn, type FunctionArguments } from './abi-components/function' +export { event, AbiEvent, type EventRecord, type EventParams } from './abi-components/event' +import keccak256 from 'keccak256' +export { keccak256 } diff --git a/evm/evm-typegen/package.json b/evm/evm-typegen/package.json index 633ca75f8..2e4f464f7 100644 --- a/evm/evm-typegen/package.json +++ b/evm/evm-typegen/package.json @@ -24,12 +24,12 @@ "@subsquid/util-internal": "^3.2.0", "@subsquid/util-internal-code-printer": "^1.2.2", "@subsquid/util-internal-commander": "^1.4.0", + "@subsquid/evm-codec": "0.0.0", + "@subsquid/evm-abi": "0.0.0", "commander": "^11.1.0" }, - "peerDependencies": { - "ethers": "^6.9.0" - }, "devDependencies": { + "abitype": "^1.0.0", "@types/node": "^18.18.14", "typescript": "~5.3.2" } diff --git a/evm/evm-typegen/src/abi.support.ts b/evm/evm-typegen/src/abi.support.ts deleted file mode 100644 index deac8281b..000000000 --- a/evm/evm-typegen/src/abi.support.ts +++ /dev/null @@ -1,135 +0,0 @@ -import assert from 'assert' -import * as ethers from 'ethers' - -export interface EventRecord { - topics: string[] - data: string -} -export type LogRecord = EventRecord - -export class LogEvent { - private fragment: ethers.EventFragment - - constructor(private abi: ethers.Interface, public readonly topic: string) { - let fragment = abi.getEvent(topic) - assert(fragment != null, 'Missing fragment') - this.fragment = fragment - } - - is(rec: EventRecord): boolean { - return rec.topics[0] === this.topic - } - - decode(rec: EventRecord): Args { - return this.abi.decodeEventLog(this.fragment, rec.data, rec.topics) as any as Args - } -} - -export interface FuncRecord { - sighash?: string - input: string -} - -export class Func { - private fragment: ethers.FunctionFragment - - constructor(private abi: ethers.Interface, public readonly sighash: string) { - let fragment = abi.getFunction(sighash) - assert(fragment != null, 'Missing fragment') - this.fragment = fragment - } - - is(rec: FuncRecord): boolean { - let sighash = rec.sighash ? rec.sighash : rec.input.slice(0, 10) - return sighash === this.sighash - } - - decode(input: ethers.BytesLike): Args & FieldArgs - decode(rec: FuncRecord): Args & FieldArgs - decode(inputOrRec: ethers.BytesLike | FuncRecord): Args & FieldArgs { - const input = ethers.isBytesLike(inputOrRec) ? inputOrRec : inputOrRec.input - return this.abi.decodeFunctionData(this.fragment, input) as any as Args & FieldArgs - } - - encode(args: Args): string { - return this.abi.encodeFunctionData(this.fragment, args) - } - - decodeResult(output: ethers.BytesLike): Result { - const decoded = this.abi.decodeFunctionResult(this.fragment, output) - return decoded.length > 1 ? decoded : decoded[0] - } - - tryDecodeResult(output: ethers.BytesLike): Result | undefined { - try { - return this.decodeResult(output) - } catch(err: any) { - return undefined - } - } -} - - -export function isFunctionResultDecodingError(val: unknown): val is Error & {data: string} { - if (!(val instanceof Error)) return false - let err = val as any - return err.code == 'CALL_EXCEPTION' - && typeof err.data == 'string' - && !err.errorArgs - && !err.errorName -} - - -export interface ChainContext { - _chain: Chain -} - - -export interface BlockContext { - _chain: Chain - block: Block -} - - -export interface Block { - height: number -} - - -export interface Chain { - client: { - call: (method: string, params?: unknown[]) => Promise - } -} - - -export class ContractBase { - private readonly _chain: Chain - private readonly blockHeight: number - readonly address: string - - constructor(ctx: BlockContext, address: string) - constructor(ctx: ChainContext, block: Block, address: string) - constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string) { - this._chain = ctx._chain - if (typeof blockOrAddress === 'string') { - this.blockHeight = ctx.block.height - this.address = ethers.getAddress(blockOrAddress) - } else { - if (address == null) { - throw new Error('missing contract address') - } - this.blockHeight = blockOrAddress.height - this.address = ethers.getAddress(address) - } - } - - async eth_call(func: Func, args: Args): Promise { - let data = func.encode(args) - let result = await this._chain.client.call('eth_call', [ - {to: this.address, data}, - '0x'+this.blockHeight.toString(16) - ]) - return func.decodeResult(result) - } -} diff --git a/evm/evm-typegen/src/main.ts b/evm/evm-typegen/src/main.ts index e8cdb78f5..5dc404694 100644 --- a/evm/evm-typegen/src/main.ts +++ b/evm/evm-typegen/src/main.ts @@ -1,40 +1,39 @@ import * as fs from 'fs' -import * as ethers from 'ethers' import path from 'path' -import {InvalidArgumentError, program} from 'commander' -import {createLogger} from '@subsquid/logger' -import {runProgram, wait} from '@subsquid/util-internal' -import {OutDir} from '@subsquid/util-internal-code-printer' +import { InvalidArgumentError, program } from 'commander' +import { createLogger } from '@subsquid/logger' +import { runProgram, wait } from '@subsquid/util-internal' import * as validator from '@subsquid/util-internal-commander' -import {Typegen} from './typegen' -import {GET} from './util/fetch' - +import { Typegen } from './typegen' +import { GET } from './util/fetch' +import { OutDir } from '@subsquid/util-internal-code-printer' const LOG = createLogger('sqd:evm-typegen') - -runProgram(async function() { +runProgram( + async function () { program - .description(` + .description( + ` Generates TypeScript facades for EVM transactions, logs and eth_call queries. The generated facades are assumed to be used by "squids" indexing EVM data. - `.trim()) - .name('squid-evm-typegen') - .argument('', 'output directory for generated definitions') - .argument('[abi...]', 'ABI file', specArgument) - .option('--multicall', 'generate facade for MakerDAO multicall contract') - .option( - '--etherscan-api ', - 'etherscan API to fetch contract ABI by a known address', - validator.Url(['http:', 'https:']) - ) - .option( - '--etherscan-api-key ', - 'etherscan API key' - ) - .option('--clean', 'delete output directory before run') - .addHelpText('afterAll', ` + `.trim(), + ) + .name('squid-evm-typegen') + .argument('', 'output directory for generated definitions') + .argument('[abi...]', 'ABI file', specArgument) + .option('--multicall', 'generate facade for MakerDAO multicall contract') + .option( + '--etherscan-api ', + 'etherscan API to fetch contract ABI by a known address', + validator.Url(['http:', 'https:']), + ) + .option('--etherscan-api-key ', 'etherscan API key') + .option('--clean', 'delete output directory before run') + .addHelpText( + 'afterAll', + ` ABI file can be specified in three ways: 1. as a plain JSON file: @@ -53,152 +52,166 @@ In all cases typegen will use ABI's basename as a basename of generated files. You can overwrite basename of generated files using fragment (#) suffix. squid-evm-typegen src/abi 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413#contract - `) + `, + ) program.parse() let opts = program.opts() as { - clean?: boolean, - multicall?: boolean, - etherscanApi?: string - etherscanApiKey?: string + clean?: boolean + multicall?: boolean + etherscanApi?: string + etherscanApiKey?: string } let dest = new OutDir(program.processedArgs[0]) let specs = program.processedArgs[1] as Spec[] if (opts.clean && dest.exists()) { - LOG.info(`deleting ${dest.path()}`) - dest.del() + LOG.info(`deleting ${dest.path()}`) + dest.del() } if (specs.length == 0 && !opts.multicall) { - LOG.warn('no ABI files given, nothing to generate') - return + LOG.warn('no ABI files given, nothing to generate') + return } - dest.add('abi.support.ts', [__dirname, '../src/abi.support.ts']) - LOG.info(`saved ${dest.path('abi.support.ts')}`) - if (opts.multicall) { - dest.add('multicall.ts', [__dirname, '../src/multicall.ts']) - LOG.info(`saved ${dest.path('multicall.ts')}`) + dest.add('multicall.ts', [__dirname, '../src/multicall.ts']) + LOG.info(`saved ${dest.path('multicall.ts')}`) } for (let spec of specs) { - LOG.info(`processing ${spec.src}`) - let abi_json = await read(spec, opts) - let abi = new ethers.Interface(abi_json) - new Typegen(dest, abi, spec.name, LOG).generate() - } -}, err => LOG.fatal(err)) - - -async function read(spec: Spec, options?: {etherscanApi?: string, etherscanApiKey?: string}): Promise { - if (spec.kind == 'address') { - return fetchFromEtherscan(spec.src, options?.etherscanApi, options?.etherscanApiKey) - } - let abi: any - if (spec.kind == 'url') { - abi = await GET(spec.src) - } else { - abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8')) - } - if (Array.isArray(abi)) { - return abi - } else if (Array.isArray(abi?.abi)) { - return abi.abi - } else { - throw new Error('Unrecognized ABI format') + LOG.info(`processing ${spec.src}`) + let abi_json = await read(spec, opts) + await new Typegen(dest, abi_json, spec.name, LOG).generate() } + }, + (err) => LOG.fatal(err), +) + +async function read( + spec: Spec, + options?: { etherscanApi?: string; etherscanApiKey?: string }, +): Promise { + if (spec.kind == 'address') { + return fetchFromEtherscan( + spec.src, + options?.etherscanApi, + options?.etherscanApiKey, + ) + } + let abi: any + if (spec.kind == 'url') { + abi = await GET(spec.src) + } else { + abi = JSON.parse(fs.readFileSync(spec.src, 'utf-8')) + } + if (Array.isArray(abi)) { + return abi + } else if (Array.isArray(abi?.abi)) { + return abi.abi + } else { + throw new Error('Unrecognized ABI format') + } } - -async function fetchFromEtherscan(address: string, api?: string, apiKey?: string): Promise { - api = api || 'https://api.etherscan.io/' - let url = new URL('api?module=contract&action=getabi', api) - url.searchParams.set('address', address) - if (apiKey) { - url.searchParams.set('apiKey', apiKey) - } - let response: {status: string, result: string} - let attempts = 0 - while (true) { - response = await GET(url.toString()) - if (response.status == '0' && response.result.includes('rate limit') && attempts < 4) { - attempts += 1 - let timeout = attempts * 2 - LOG.warn(`faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`) - await wait(timeout * 1000) - } else { - break - } - } - if (response.status == '1') { - return JSON.parse(response.result) +async function fetchFromEtherscan( + address: string, + api?: string, + apiKey?: string, +): Promise { + api = api || 'https://api.etherscan.io/' + let url = new URL('api?module=contract&action=getabi', api) + url.searchParams.set('address', address) + if (apiKey) { + url.searchParams.set('apiKey', apiKey) + } + let response: { status: string; result: string } + let attempts = 0 + while (true) { + response = await GET(url.toString()) + if ( + response.status == '0' && + response.result.includes('rate limit') && + attempts < 4 + ) { + attempts += 1 + let timeout = attempts * 2 + LOG.warn( + `faced rate limit error while trying to fetch contract ABI. Trying again in ${timeout} seconds.`, + ) + await wait(timeout * 1000) } else { - throw new Error(`Failed to fetch contract ABI from ${api}: ${response.result}`) + break } + } + if (response.status == '1') { + return JSON.parse(response.result) + } else { + throw new Error( + `Failed to fetch contract ABI from ${api}: ${response.result}`, + ) + } } - interface Spec { - kind: 'address' | 'url' | 'file' - src: string - name: string + kind: 'address' | 'url' | 'file' + src: string + name: string } - function specArgument(value: string, prev?: Spec[]): Spec[] { - let spec = parseSpec(value) - prev = prev || [] - prev.push(spec) - return prev + let spec = parseSpec(value) + prev = prev || [] + prev.push(spec) + return prev } +function isAddress(spec: string): boolean { + return spec.match(/^0x[0-9a-fA-F]{40}$/) !== null +} function parseSpec(spec: string): Spec { - let [src, fragment] = splitFragment(spec) - if (src.startsWith('0x')) { - if (!ethers.isAddress(src)) throw new InvalidArgumentError('Invalid contract address') - return { - kind: 'address', - src, - name: fragment || src - } - } else if (src.includes('://')) { - let u = new URL( - validator.Url(['http:', 'https:'])(src) - ) - return { - kind: 'url', - src, - name: fragment || basename(u.pathname) - } - } else { - return { - kind: 'file', - src, - name: fragment || basename(src) - } + let [src, fragment] = splitFragment(spec) + if (src.startsWith('0x')) { + if (!isAddress(src)) + throw new InvalidArgumentError('Invalid contract address') + return { + kind: 'address', + src, + name: fragment || src, + } + } else if (src.includes('://')) { + let u = new URL(validator.Url(['http:', 'https:'])(src)) + return { + kind: 'url', + src, + name: fragment || basename(u.pathname), } + } else { + return { + kind: 'file', + src, + name: fragment || basename(src), + } + } } - function splitFragment(spec: string): [string, string] { - let parts = spec.split('#') - if (parts.length > 1) { - let fragment = parts.pop()! - return [parts.join('#'), fragment] - } else { - return [spec, ''] - } + let parts = spec.split('#') + if (parts.length > 1) { + let fragment = parts.pop()! + return [parts.join('#'), fragment] + } else { + return [spec, ''] + } } - function basename(file: string): string { - let name = path.parse(file).name - if (name) return name - throw new InvalidArgumentError( - `Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20` - ) + let name = path.parse(file).name + if (name) return name + throw new InvalidArgumentError( + `Can't derive target basename for output files. Use url fragment to specify it, e.g. #erc20`, + ) } diff --git a/evm/evm-typegen/src/multicall.ts b/evm/evm-typegen/src/multicall.ts index 4b20dca4f..66efcd1e2 100644 --- a/evm/evm-typegen/src/multicall.ts +++ b/evm/evm-typegen/src/multicall.ts @@ -1,3 +1,5 @@ +/** + * TODO migrate multicall import * as ethers from 'ethers' import {ContractBase, Func} from './abi.support' @@ -210,3 +212,4 @@ function* splitIntoPages(size: number, page: number): Iterable<[from: number, to from = to } } +*/ diff --git a/evm/evm-typegen/src/typegen.ts b/evm/evm-typegen/src/typegen.ts index ec0a4ed54..db52a7f18 100644 --- a/evm/evm-typegen/src/typegen.ts +++ b/evm/evm-typegen/src/typegen.ts @@ -1,143 +1,260 @@ -import * as ethers from 'ethers' -import {Logger} from '@subsquid/logger' -import {def} from '@subsquid/util-internal' -import {FileOutput, OutDir} from '@subsquid/util-internal-code-printer' -import {getFullTupleType, getReturnType, getStructType, getTupleType, getType} from './util/types' +import { Logger } from '@subsquid/logger' +import { def } from '@subsquid/util-internal' +import { keccak256 } from '@subsquid/evm-abi' +import { getType } from './util/types' +import type { Abi, AbiEvent, AbiFunction, AbiParameter } from 'abitype' +import { FileOutput, OutDir } from '@subsquid/util-internal-code-printer' +function areItemEqual(item1: AbiEvent | AbiFunction, item2: AbiEvent | AbiFunction): boolean { + if (item1.name !== item2.name || item1.inputs.length !== item2.inputs.length || item1.type !== item2.type) { + return false + } + return item1.inputs.every((input, idx) => { + const input2 = item2.inputs[idx] + return input.name === input2.name && input.type === input2.type + }) +} export class Typegen { - private out: FileOutput + private out: FileOutput - constructor(private dest: OutDir, private abi: ethers.Interface, private basename: string, private log: Logger) { - this.out = dest.file(basename + '.ts') - } + constructor( + dest: OutDir, + private abi: Abi, + basename: string, + private log: Logger, + ) { + this.out = dest.file(basename + '.ts') + } + + async generate() { + this.out.line(`import * as p from '@subsquid/evm-codec'`) + this.out.line( + `import { event, fun, indexed, ContractBase } from '@subsquid/evm-abi'`, + ) + this.out.line( + `import type { EventParams as EParams, FunctionArguments, FunctionReturn } from '@subsquid/evm-abi'`, + ) - generate(): void { - this.out.line("import * as ethers from 'ethers'") - this.out.line("import {LogEvent, Func, ContractBase} from './abi.support'") - this.out.line(`import {ABI_JSON} from './${this.basename}.abi'`) - this.out.line() - this.out.line("export const abi = new ethers.Interface(ABI_JSON);") + this.generateEvents() + this.generateFunctions() + this.generateContract() + this.generateEventTypes() + this.generateFunctionTypes() - this.generateEvents() - this.generateFunctions() - this.generateContract() + await this.out.write() + this.log.info(`saved ${this.out.file}`) + } - this.writeAbi() - this.out.write() - this.log.info(`saved ${this.out.file}`) + private generateEvents() { + let events = this.getEvents() + if (events.length == 0) { + return } + this.out.line() + this.out.block(`export const events =`, () => { + for (let e of events) { + this.out.line( + `${this.getPropName(e)}: event("${this.topic0(e)}", {${this.toTypes( + e.inputs, + )}}),`, + ) + } + }) + } - private writeAbi() { - let out = this.dest.file(this.basename + '.abi.ts') - let json = this.abi.formatJson() - json = JSON.stringify(JSON.parse(json), null, 4) - out.line(`export const ABI_JSON = ${json}`) - out.write() - this.log.info(`saved ${out.file}`) + private topic0(e: AbiEvent): string { + return `0x${keccak256(this.sighash(e)).toString('hex')}` + } + + private toTypes(inputs: readonly AbiParameter[]): string { + return inputs.map((input, idx) => getType(input, idx)).join(', ') + } + + private generateFunctions() { + let functions = this.getFunctions() + if (functions.length == 0) { + return } + this.out.line() + this.out.block(`export const functions =`, () => { + for (let f of functions) { + let returnType = '' + if (f.outputs?.length === 1) { + returnType = getType({ ...f.outputs[0], name: undefined }) + } + if (f.outputs?.length > 1) { + returnType = `{${this.toTypes(f.outputs)}}` + } + + this.out.line( + `${this.getPropName(f)}: fun("${this.functionSelector( + f, + )}", {${this.toTypes(f.inputs)}}, ${returnType}),`, + ) + } + }) + } + + private functionSelector(f: AbiFunction): string { + const sighash = this.sighash(f) + return `0x${keccak256(sighash).slice(0, 4).toString('hex')}` + } - private generateEvents() { - let events = this.getEvents() - if (events.length == 0) { - return + private generateContract() { + this.out.line() + this.out.block(`export class Contract extends ContractBase`, () => { + let functions = this.getFunctions() + for (let f of functions) { + if ((f.stateMutability === 'pure' || f.stateMutability === 'view') && + f.outputs?.length + ) { + this.out.line() + let argNames = f.inputs.map((a, idx) => a.name || `_${idx}`) + const ref = this.getPropNameGetter(f) + const [argsType] = this.toFunctionTypes(f) + let args = f.inputs + .map( + (a, idx) => + `${argNames[idx]}: ${argsType}["${argNames[idx]}"]`, + ) + .join(', ') + this.out.block(`${this.getPropName(f)}(${args})`, () => { + this.out.line( + `return this.eth_call(functions${ref}, {${argNames.join(', ')}})`, + ) + }) } - this.out.line() - this.out.block(`export const events =`, () => { - for (let e of events) { - this.out.line(`${this.getPropName(e)}: new LogEvent<${getFullTupleType(e.inputs)}>(`) - this.out.indentation(() => this.out.line(`abi, '${e.topicHash}'`)) - this.out.line('),') - } - }) + } + }) + } + + private cannonicalType(param: AbiParameter): string { + if (!param.type.startsWith('tuple')) { + return param.type } + const arrayBrackets = param.type.slice(5) + return `(${(param as any).components.map((param: AbiParameter) => + this.cannonicalType(param), + )})${arrayBrackets}` + } - private generateFunctions() { - let functions = this.getFunctions() - if (functions.length == 0) { - return - } - this.out.line() - this.out.block(`export const functions =`, () => { - for (let f of functions) { - let sighash = f.selector - let pArgs = getTupleType(f.inputs) - let pArgStruct = getStructType(f.inputs) - let pResult = getReturnType(f.outputs) - this.out.line(`${this.getPropName(f)}: new Func<${pArgs}, ${pArgStruct}, ${pResult}>(`) - this.out.indentation(() => this.out.line(`abi, '${sighash}'`)) - this.out.line('),') - } - }) + private sighash(item: AbiEvent | AbiFunction): string { + return `${item.name}(${item.inputs + .map((param) => this.cannonicalType(param)) + .join(',')})` + } + + private getPropName(item: AbiEvent | AbiFunction): string { + if (this.getOverloads(item) == 1) { + return item.name + } else { + return `"${this.sighash(item)}"` } + } - private generateContract() { - this.out.line() - this.out.block(`export class Contract extends ContractBase`, () => { - let functions = this.getFunctions() - for (let f of functions) { - if (f.constant && f.outputs?.length) { - this.out.line() - let argNames = f.inputs.map((a, idx) => a.name || `arg${idx}`) - let args = f.inputs.map((a, idx) => `${argNames[idx]}: ${getType(a)}`).join(', ') - this.out.block(`${this.getPropName(f)}(${args}): Promise<${getReturnType(f.outputs)}>`, () => { - this.out.line(`return this.eth_call(functions${this.getRef(f)}, [${argNames.join(', ')}])`) - }) - } - } - }) + private getPropNameGetter(item: AbiEvent | AbiFunction): string { + if (this.getOverloads(item) == 1) { + return '.' + item.name + } else { + return `["${this.sighash(item)}"]` } + } - private getRef(item: ethers.EventFragment | ethers.FunctionFragment): string { - let key = this.getPropName(item) - if (key[0] == "'") { - return `[${key}]` - } else { - return '.' + key - } + private getOverloads(item: AbiEvent | AbiFunction): number { + if (item.type === 'event') { + return this.eventOverloads()[item.name] + } else { + return this.functionOverloads()[item.name] } + } - private getPropName(item: ethers.EventFragment | ethers.FunctionFragment): string { - if (this.getOverloads(item) == 1) { - return item.name - } else { - return `'${item.format('sighash')}'` - } + private capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1) + } + + private getOverloadIndex(item: AbiEvent | AbiFunction): number { + const abi = [...this.getEvents(), ...this.getFunctions()] + const overloads = abi.filter((x) => x.name === item.name) + return overloads.findIndex((x) => areItemEqual(x, item)) + } + + private toEventType(e: AbiEvent): string { + if (this.getOverloads(e) === 1) { + return `${this.capitalize(e.name)}EventArgs` } + const index = this.getOverloadIndex(e) + return `${this.capitalize(e.name)}EventArgs_${index}` + } - private getOverloads(item: ethers.EventFragment | ethers.FunctionFragment): number { - if (item instanceof ethers.EventFragment) { - return this.eventOverloads()[item.name] - } else { - return this.functionOverloads()[item.name] - } + private generateEventTypes() { + const events = this.getEvents() + if (events.length == 0) { + return + } + this.out.line() + this.out.line(`/// Event types`) + for (let e of events) { + const propName = this.getPropNameGetter(e) + this.out.line( + `export type ${this.toEventType(e)} = EParams`, + ) } + } - @def - private functionOverloads(): Record { - let overloads: Record = {} - for (let item of this.getFunctions()) { - overloads[item.name] = (overloads[item.name] || 0) + 1 - } - return overloads + private toFunctionTypes(f: AbiFunction): [string, string] { + if (this.getOverloads(f) === 1) { + return [`${this.capitalize(f.name)}Params`, `${this.capitalize(f.name)}Return`] } + const index = this.getOverloadIndex(f) + return [`${this.capitalize(f.name)}Params_${index}`, `${this.capitalize(f.name)}Return_${index}`] + } - @def - private eventOverloads(): Record { - let overloads: Record = {} - for (let item of this.getEvents()) { - overloads[item.name] = (overloads[item.name] || 0) + 1 - } - return overloads + private generateFunctionTypes() { + let functions = this.getFunctions() + if (functions.length == 0) { + return + } + this.out.line() + this.out.line(`/// Function types`) + for (let f of functions) { + const propName = this.getPropNameGetter(f) + const [args, ret] = this.toFunctionTypes(f) + this.out.line( + `export type ${args} = FunctionArguments`, + ) + this.out.line( + `export type ${ret} = FunctionReturn`, + ) + this.out.line() } + } - @def - private getFunctions(): ethers.FunctionFragment[] { - return this.abi.fragments.filter(f => f.type === 'function') as ethers.FunctionFragment[] + @def + private functionOverloads(): Record { + let overloads: Record = {} + for (let item of this.getFunctions()) { + overloads[item.name] = (overloads[item.name] || 0) + 1 } + return overloads + } - @def - private getEvents(): ethers.EventFragment[] { - return this.abi.fragments.filter(f => f.type === 'event') as ethers.EventFragment[] + @def + private eventOverloads(): Record { + let overloads: Record = {} + for (let item of this.getEvents()) { + overloads[item.name] = (overloads[item.name] || 0) + 1 } + return overloads + } + + @def + private getFunctions(): AbiFunction[] { + return this.abi.filter((f) => f.type === 'function') as AbiFunction[] + } + + @def + private getEvents(): AbiEvent[] { + return this.abi.filter((f) => f.type === 'event') as AbiEvent[] + } } diff --git a/evm/evm-typegen/src/util/fetch.ts b/evm/evm-typegen/src/util/fetch.ts index 2b8d9b0ba..03c937944 100644 --- a/evm/evm-typegen/src/util/fetch.ts +++ b/evm/evm-typegen/src/util/fetch.ts @@ -1,13 +1,11 @@ -import {createLogger} from '@subsquid/logger' -import {HttpClient} from '@subsquid/http-client' - +import { createLogger } from '@subsquid/logger' +import { HttpClient } from '@subsquid/http-client' const http = new HttpClient({ - log: createLogger('sqd:evm-typegen:fetch'), - retryAttempts: 3 + log: createLogger('sqd:evm-typegen:fetch'), + retryAttempts: 3, }) - -export function GET(url: string): Promise { - return http.get(url) +export function GET(url: string): Promise { + return http.get(url) } diff --git a/evm/evm-typegen/src/util/types.ts b/evm/evm-typegen/src/util/types.ts index 0c0d82212..d396e898a 100644 --- a/evm/evm-typegen/src/util/types.ts +++ b/evm/evm-typegen/src/util/types.ts @@ -1,72 +1,54 @@ -import assert from 'assert' -import type {ParamType} from 'ethers' +import type { AbiEventParameter } from 'abitype' - -// taken from: https://github.com/ethers-io/ethers.js/blob/948f77050dae884fe88932fd88af75560aac9d78/packages/cli/src.ts/typescript.ts#L10 -export function getType(param: ParamType): string { - if (param.baseType === 'array') { - assert(param.arrayChildren != null, 'Missing children for array type') - return 'Array<' + getType(param.arrayChildren) + '>' - } - - if (param.baseType === 'tuple') { - assert(param.components != null, 'Missing components for tuple type') - return getFullTupleType(param.components) - } - - if (param.type === 'address' || param.type === 'string') { - return 'string' - } - - if (param.type === 'bool') { - return 'boolean' - } - - let match = param.type.match(/^(u?int)([0-9]+)$/) - if (match) { - return parseInt(match[2]) < 53 ? 'number' : 'bigint' - } - - if (param.type.substring(0, 5) === 'bytes') { - return 'string' - } - - throw new Error('unknown type') +function isStaticArray(param: AbiEventParameter) { + return param.type.match(/\[\d+]$/) } - -export function getFullTupleType(params: ReadonlyArray): string { - let tuple = getTupleType(params) - let struct = getStructType(params) - if (struct == '{}') { - return tuple - } else { - return `(${tuple} & ${struct})` - } +function isDynamicArray(param: AbiEventParameter) { + return param.type.endsWith('[]') } - -export function getTupleType(params: ReadonlyArray): string { - return '[' + params.map(p => { - return p.name ? `${p.name}: ${getType(p)}` : `_: ${getType(p)}` - }).join(', ') + ']' +function elementsCount(param: AbiEventParameter) { + return Number(param.type.match(/\[(\d+)]$/)?.[1] ?? 0) } - -// https://github.com/ethers-io/ethers.js/blob/278f84174409b470fa7992e1f8b5693e6e5d2dac/src.ts/abi/coders/tuple.ts#L36 -export function getStructType(params: ReadonlyArray): string { - let array: any = [] - let counts: Record = {} - for (let p of params) { - if (p.name && array[p.name] == null) { - counts[p.name] = (counts[p.name] || 0) + 1 - } - } - let fields = params.filter(p => counts[p.name] == 1) - return '{' + fields.map(f => `${f.name}: ${getType(f)}`).join(', ') + '}' +function arrayChildType(param: AbiEventParameter) { + return param.type.replace(/\[\d*]$/, '') } - -export function getReturnType(outputs: ReadonlyArray) { - return outputs.length == 1 ? getType(outputs[0]) : getFullTupleType(outputs) +export function getType(param: AbiEventParameter, index?: number): string { + const { name, ...namelessParam } = param + + if (name) { + return `"${name}": ${getType(namelessParam as any)}` + } + + if (index !== undefined) { + return `"_${index}": ${getType(namelessParam)}` + } + + const { indexed, ...indexlessParam } = param + if (indexed) { + return `indexed(${getType(indexlessParam as any)})` + } + if (isStaticArray(param)) { + const elements = elementsCount(param) + return `p.fixedSizeArray(${getType({ + ...param, + type: arrayChildType(param), + })}, ${elements})` + } + + if (isDynamicArray(param)) { + return `p.array(${getType({ + ...param, + type: arrayChildType(param), + })})` + } + + if (param.type === 'tuple') { + return `p.struct({${(param as any).components.map((type: AbiEventParameter, idx: number) => getType(type, idx)).join(', ')}})` + } + + return `p.${param.type}` } diff --git a/test/erc20-transfers/package.json b/test/erc20-transfers/package.json index c65be48c3..2ad913b4c 100644 --- a/test/erc20-transfers/package.json +++ b/test/erc20-transfers/package.json @@ -17,6 +17,8 @@ }, "devDependencies": { "@subsquid/evm-typegen": "^3.3.0", + "@subsquid/evm-codec": "0.0.0", + "@subsquid/evm-abi": "0.0.0", "@subsquid/typeorm-codegen": "^2.0.0", "@types/node": "^18.18.14", "typescript": "~5.3.2" diff --git a/test/erc20-transfers/src/abi/abi.support.ts b/test/erc20-transfers/src/abi/abi.support.ts deleted file mode 100644 index c9cb14fa8..000000000 --- a/test/erc20-transfers/src/abi/abi.support.ts +++ /dev/null @@ -1,120 +0,0 @@ -import assert from 'assert' -import * as ethers from 'ethers' - - -export interface LogRecord { - topics: string[] - data: string -} - - -export class LogEvent { - private fragment: ethers.EventFragment - - constructor(private abi: ethers.Interface, public readonly topic: string) { - let fragment = abi.getEvent(topic) - assert(fragment != null, 'Missing fragment') - this.fragment = fragment - } - - decode(rec: LogRecord): Args { - return this.abi.decodeEventLog(this.fragment, rec.data, rec.topics) as any as Args - } -} - - -export class Func { - private fragment: ethers.FunctionFragment - - constructor(private abi: ethers.Interface, public readonly sighash: string) { - let fragment = abi.getFunction(sighash) - assert(fragment != null, 'Missing fragment') - this.fragment = fragment - } - - decode(input: ethers.BytesLike): Args & FieldArgs { - return this.abi.decodeFunctionData(this.fragment, input) as any as Args & FieldArgs - } - - encode(args: Args): string { - return this.abi.encodeFunctionData(this.fragment, args) - } - - decodeResult(output: ethers.BytesLike): Result { - const decoded = this.abi.decodeFunctionResult(this.fragment, output) - return decoded.length > 1 ? decoded : decoded[0] - } - - tryDecodeResult(output: ethers.BytesLike): Result | undefined { - try { - return this.decodeResult(output) - } catch(err: any) { - return undefined - } - } -} - - -export function isFunctionResultDecodingError(val: unknown): val is Error & {data: string} { - if (!(val instanceof Error)) return false - let err = val as any - return err.code == 'CALL_EXCEPTION' - && typeof err.data == 'string' - && !err.errorArgs - && !err.errorName -} - - -export interface ChainContext { - _chain: Chain -} - - -export interface BlockContext { - _chain: Chain - block: Block -} - - -export interface Block { - height: number -} - - -export interface Chain { - client: { - call: (method: string, params?: unknown[]) => Promise - } -} - - -export class ContractBase { - private readonly _chain: Chain - private readonly blockHeight: number - readonly address: string - - constructor(ctx: BlockContext, address: string) - constructor(ctx: ChainContext, block: Block, address: string) - constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string) { - this._chain = ctx._chain - if (typeof blockOrAddress === 'string') { - this.blockHeight = ctx.block.height - this.address = ethers.getAddress(blockOrAddress) - } else { - if (address == null) { - throw new Error('missing contract address') - } - this.blockHeight = blockOrAddress.height - this.address = ethers.getAddress(address) - } - } - - async eth_call(func: Func, args: Args): Promise { - let data = func.encode(args) - let result = await this._chain.client.call('eth_call', [ - {to: this.address, data}, - '0x'+this.blockHeight.toString(16) - ]) - return func.decodeResult(result) - } -} diff --git a/test/erc20-transfers/src/abi/erc20.abi.ts b/test/erc20-transfers/src/abi/erc20.abi.ts deleted file mode 100644 index c813cc1f3..000000000 --- a/test/erc20-transfers/src/abi/erc20.abi.ts +++ /dev/null @@ -1,210 +0,0 @@ -export const ABI_JSON = [ - { - "type": "function", - "name": "name", - "constant": true, - "stateMutability": "view", - "payable": false, - "inputs": [], - "outputs": [ - { - "type": "string" - } - ] - }, - { - "type": "function", - "name": "approve", - "constant": false, - "payable": false, - "inputs": [ - { - "type": "address", - "name": "_spender" - }, - { - "type": "uint256", - "name": "_value" - } - ], - "outputs": [ - { - "type": "bool" - } - ] - }, - { - "type": "function", - "name": "totalSupply", - "constant": true, - "stateMutability": "view", - "payable": false, - "inputs": [], - "outputs": [ - { - "type": "uint256" - } - ] - }, - { - "type": "function", - "name": "transferFrom", - "constant": false, - "payable": false, - "inputs": [ - { - "type": "address", - "name": "_from" - }, - { - "type": "address", - "name": "_to" - }, - { - "type": "uint256", - "name": "_value" - } - ], - "outputs": [ - { - "type": "bool" - } - ] - }, - { - "type": "function", - "name": "decimals", - "constant": true, - "stateMutability": "view", - "payable": false, - "inputs": [], - "outputs": [ - { - "type": "uint8" - } - ] - }, - { - "type": "function", - "name": "balanceOf", - "constant": true, - "stateMutability": "view", - "payable": false, - "inputs": [ - { - "type": "address", - "name": "_owner" - } - ], - "outputs": [ - { - "type": "uint256", - "name": "balance" - } - ] - }, - { - "type": "function", - "name": "symbol", - "constant": true, - "stateMutability": "view", - "payable": false, - "inputs": [], - "outputs": [ - { - "type": "string" - } - ] - }, - { - "type": "function", - "name": "transfer", - "constant": false, - "payable": false, - "inputs": [ - { - "type": "address", - "name": "_to" - }, - { - "type": "uint256", - "name": "_value" - } - ], - "outputs": [ - { - "type": "bool" - } - ] - }, - { - "type": "function", - "name": "allowance", - "constant": true, - "stateMutability": "view", - "payable": false, - "inputs": [ - { - "type": "address", - "name": "_owner" - }, - { - "type": "address", - "name": "_spender" - } - ], - "outputs": [ - { - "type": "uint256" - } - ] - }, - { - "type": "fallback", - "stateMutability": "payable" - }, - { - "type": "event", - "anonymous": false, - "name": "Approval", - "inputs": [ - { - "type": "address", - "name": "owner", - "indexed": true - }, - { - "type": "address", - "name": "spender", - "indexed": true - }, - { - "type": "uint256", - "name": "value", - "indexed": false - } - ] - }, - { - "type": "event", - "anonymous": false, - "name": "Transfer", - "inputs": [ - { - "type": "address", - "name": "from", - "indexed": true - }, - { - "type": "address", - "name": "to", - "indexed": true - }, - { - "type": "uint256", - "name": "value", - "indexed": false - } - ] - } -] diff --git a/test/erc20-transfers/src/abi/erc20.ts b/test/erc20-transfers/src/abi/erc20.ts index 674cfb880..775cbb31c 100644 --- a/test/erc20-transfers/src/abi/erc20.ts +++ b/test/erc20-transfers/src/abi/erc20.ts @@ -1,71 +1,81 @@ -import * as ethers from 'ethers' -import {LogEvent, Func, ContractBase} from './abi.support' -import {ABI_JSON} from './erc20.abi' +import * as p from '@subsquid/evm-codec' +import { event, fun, indexed, ContractBase } from '@subsquid/evm-abi' +import type { EventParams as EParams, FunctionArguments, FunctionReturn } from '@subsquid/evm-abi' -export const abi = new ethers.Interface(ABI_JSON); export const events = { - Approval: new LogEvent<([owner: string, spender: string, value: bigint] & {owner: string, spender: string, value: bigint})>( - abi, '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925' - ), - Transfer: new LogEvent<([from: string, to: string, value: bigint] & {from: string, to: string, value: bigint})>( - abi, '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' - ), + Approval: event("0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", {"owner": indexed(p.address), "spender": indexed(p.address), "value": p.uint256}), + Transfer: event("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", {"from": indexed(p.address), "to": indexed(p.address), "value": p.uint256}), } export const functions = { - name: new Func<[], {}, string>( - abi, '0x06fdde03' - ), - approve: new Func<[_spender: string, _value: bigint], {_spender: string, _value: bigint}, boolean>( - abi, '0x095ea7b3' - ), - totalSupply: new Func<[], {}, bigint>( - abi, '0x18160ddd' - ), - transferFrom: new Func<[_from: string, _to: string, _value: bigint], {_from: string, _to: string, _value: bigint}, boolean>( - abi, '0x23b872dd' - ), - decimals: new Func<[], {}, number>( - abi, '0x313ce567' - ), - balanceOf: new Func<[_owner: string], {_owner: string}, bigint>( - abi, '0x70a08231' - ), - symbol: new Func<[], {}, string>( - abi, '0x95d89b41' - ), - transfer: new Func<[_to: string, _value: bigint], {_to: string, _value: bigint}, boolean>( - abi, '0xa9059cbb' - ), - allowance: new Func<[_owner: string, _spender: string], {_owner: string, _spender: string}, bigint>( - abi, '0xdd62ed3e' - ), + name: fun("0x06fdde03", {}, p.string), + approve: fun("0x095ea7b3", {"_spender": p.address, "_value": p.uint256}, p.bool), + totalSupply: fun("0x18160ddd", {}, p.uint256), + transferFrom: fun("0x23b872dd", {"_from": p.address, "_to": p.address, "_value": p.uint256}, p.bool), + decimals: fun("0x313ce567", {}, p.uint8), + balanceOf: fun("0x70a08231", {"_owner": p.address}, p.uint256), + symbol: fun("0x95d89b41", {}, p.string), + transfer: fun("0xa9059cbb", {"_to": p.address, "_value": p.uint256}, p.bool), + allowance: fun("0xdd62ed3e", {"_owner": p.address, "_spender": p.address}, p.uint256), } export class Contract extends ContractBase { - name(): Promise { - return this.eth_call(functions.name, []) + name() { + return this.eth_call(functions.name, {}) } - totalSupply(): Promise { - return this.eth_call(functions.totalSupply, []) + totalSupply() { + return this.eth_call(functions.totalSupply, {}) } - decimals(): Promise { - return this.eth_call(functions.decimals, []) + decimals() { + return this.eth_call(functions.decimals, {}) } - balanceOf(_owner: string): Promise { - return this.eth_call(functions.balanceOf, [_owner]) + balanceOf(_owner: BalanceOfParams["_owner"]) { + return this.eth_call(functions.balanceOf, {_owner}) } - symbol(): Promise { - return this.eth_call(functions.symbol, []) + symbol() { + return this.eth_call(functions.symbol, {}) } - allowance(_owner: string, _spender: string): Promise { - return this.eth_call(functions.allowance, [_owner, _spender]) + allowance(_owner: AllowanceParams["_owner"], _spender: AllowanceParams["_spender"]) { + return this.eth_call(functions.allowance, {_owner, _spender}) } } + +/// Event types +export type ApprovalEventArgs = EParams +export type TransferEventArgs = EParams + +/// Function types +export type NameParams = FunctionArguments +export type NameReturn = FunctionReturn + +export type ApproveParams = FunctionArguments +export type ApproveReturn = FunctionReturn + +export type TotalSupplyParams = FunctionArguments +export type TotalSupplyReturn = FunctionReturn + +export type TransferFromParams = FunctionArguments +export type TransferFromReturn = FunctionReturn + +export type DecimalsParams = FunctionArguments +export type DecimalsReturn = FunctionReturn + +export type BalanceOfParams = FunctionArguments +export type BalanceOfReturn = FunctionReturn + +export type SymbolParams = FunctionArguments +export type SymbolReturn = FunctionReturn + +export type TransferParams = FunctionArguments +export type TransferReturn = FunctionReturn + +export type AllowanceParams = FunctionArguments +export type AllowanceReturn = FunctionReturn +