From 79938db47b1786760541a8c0efd880947b76287d Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Mon, 19 Feb 2024 12:14:52 +0100 Subject: [PATCH] Reapply "Reapply ABI generation outside of `/dist` (#1157)" (#1158) (#1159) This reverts commit 73f5941a340d7e9c8032f26f0a1e30ff50ca32b5. This generates type safe ABIs are generated from the `@safe-global/safe-deployment` package. --- .gitignore | 3 + .prettierignore | 3 + Dockerfile | 4 +- package.json | 7 +- scripts/generate-abis.js | 83 +++++++++++++++++++ .../__tests__/multi-send-encoder.builder.ts | 9 +- .../__tests__/safe-decoder.helper.spec.ts | 8 -- .../__tests__/safe-encoder.builder.ts | 72 +++------------- .../contracts/multi-send-decoder.helper.ts | 19 +---- .../contracts/safe-decoder.helper.ts | 16 +--- .../proxy-factory-encoder.builder.ts | 11 +-- .../contracts/proxy-factory-decoder.helper.ts | 10 +-- test/jest-all.json | 1 + test/jest-e2e.json | 1 + tsconfig.json | 1 + 15 files changed, 127 insertions(+), 121 deletions(-) create mode 100644 scripts/generate-abis.js diff --git a/.gitignore b/.gitignore index 424a394b9c..38a65080da 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ lerna-debug.log* # Database mounted volume data + +# ABIs +/abis diff --git a/.prettierignore b/.prettierignore index b0fc18a512..57c7875899 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,4 @@ /.yarn + +# ABIs +/abis diff --git a/Dockerfile b/Dockerfile index 7e112609dd..d365c7b49e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ WORKDIR /app COPY --chown=node:node .yarn/releases ./.yarn/releases COPY --chown=node:node .yarn/patches ./.yarn/patches COPY --chown=node:node package.json yarn.lock .yarnrc.yml tsconfig*.json ./ +COPY --chown=node:node scripts/generate-abis.js ./scripts/generate-abis.js RUN --mount=type=cache,target=/root/.yarn yarn COPY --chown=node:node assets ./assets COPY --chown=node:node migrations ./migrations @@ -28,8 +29,9 @@ ARG BUILD_NUMBER ENV APPLICATION_VERSION=${VERSION} \ APPLICATION_BUILD_NUMBER=${BUILD_NUMBER} +COPY --chown=node:node --from=base /app/abis ./abis COPY --chown=node:node --from=base /app/node_modules ./node_modules COPY --chown=node:node --from=base /app/dist ./dist COPY --chown=node:node --from=base /app/assets ./assets COPY --chown=node:node --from=base /app/migrations ./migrations -CMD [ "node", "dist/main.js" ] +CMD [ "node", "dist/src/main.js" ] diff --git a/package.json b/package.json index eece0d8ede..d9be967872 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "private": true, "license": "MIT", "scripts": { - "build": "nest build", + "build": "yarn generate-abis && nest build", "format": "prettier --write .", "format-check": "prettier --check .", + "generate-abis": "node ./scripts/generate-abis.js", + "postinstall": "yarn generate-abis", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint-check": "eslint \"{src,apps,libs,test}/**/*.ts\"", "test": "jest", @@ -91,6 +93,7 @@ ], "testEnvironment": "node", "moduleNameMapper": { + "^@/abis/(.*)$": "/../abis/$1", "^@/(.*)$": "/../src/$1" }, "globalSetup": "/../test/global-setup.ts" diff --git a/scripts/generate-abis.js b/scripts/generate-abis.js new file mode 100644 index 0000000000..0a32fc7f6f --- /dev/null +++ b/scripts/generate-abis.js @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require('path'); +const fs = require('fs'); + +/** + * This generates const TypeScript ABIs for each asset in + * `@safe-global/safe-deployments` package for `viem` to infer. + * + * Although it is possible to get a singleton programmatically and + * import the JSON directly, neither is strictly typed. + * + * Once it is possible to import JSON "as const", the deployments + * package should be updated to return the singletons as such. + * + * @see https://github.com/microsoft/TypeScript/issues/32063 + */ + +// Path to directory containing JSON assets +const assetsDir = path.join( + process.cwd(), + 'node_modules', + '@safe-global', + 'safe-deployments', + 'dist', + 'assets', +); + +// Path to directory where ABIs will be written +const outputDir = path.join(process.cwd(), 'abis', 'safe'); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function main() { + // Remove any existing ABIs + try { + fs.rmSync(outputDir, { recursive: true }); + } catch { + // Swallow error if directory does not exist (first run) + } + + // For each version... + for (const version of fs.readdirSync(assetsDir)) { + const versionOutputDir = path.join(outputDir, version); + + fs.mkdirSync(versionOutputDir, { recursive: true }); + + const versionDir = path.join(assetsDir, version); + + // ...parse the ABI for each asset + for (const assetFile of fs.readdirSync(versionDir)) { + // Read the asset JSON + const assetPath = path.join(assetsDir, version, assetFile); + const assetJson = fs.readFileSync(assetPath, 'utf8'); + + // Parse the asset JSON + const { contractName, abi } = JSON.parse(assetJson); + + // Write the ABI to a file + const fileName = `${contractName}.abi.ts`; + const filePath = path.join(versionOutputDir, fileName); + + // It is generally better to use the Stream API for larger files + // but as we are storing the JSON in memory, this is likely of + // minimal benefit. As this script runs on build, we need not + // worry too much about performance though. + const stream = fs.createWriteStream(filePath); + + // Write formatted ABI to file + stream.write( + '// This file is auto-generated by scripts/generate-abis.js\nexport default ', + ); + stream.write(JSON.stringify(abi, null, 2)); + + // Most important step: assert ABI as readonly + stream.write(' as const;'); + + stream.end(); + } + } + + console.log('ABIs generated successfully!'); +} + +main(); diff --git a/src/domain/contracts/contracts/__tests__/multi-send-encoder.builder.ts b/src/domain/contracts/contracts/__tests__/multi-send-encoder.builder.ts index 13c5999a4d..79e1ce7105 100644 --- a/src/domain/contracts/contracts/__tests__/multi-send-encoder.builder.ts +++ b/src/domain/contracts/contracts/__tests__/multi-send-encoder.builder.ts @@ -6,9 +6,9 @@ import { encodePacked, getAddress, Hex, - parseAbi, size, } from 'viem'; +import MultiSendCallOnly130 from '@/abis/safe/v1.3.0/MultiSendCallOnly.abi'; import { Builder } from '@/__tests__/builder'; // multiSend @@ -21,16 +21,11 @@ class MultiSendEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function multiSend(bytes memory transactions)' as const; - encode(): Hex { - const abi = parseAbi([MultiSendEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: MultiSendCallOnly130, functionName: 'multiSend', args: [args.transactions], }); diff --git a/src/domain/contracts/contracts/__tests__/safe-decoder.helper.spec.ts b/src/domain/contracts/contracts/__tests__/safe-decoder.helper.spec.ts index f2413eaa20..4f8207c400 100644 --- a/src/domain/contracts/contracts/__tests__/safe-decoder.helper.spec.ts +++ b/src/domain/contracts/contracts/__tests__/safe-decoder.helper.spec.ts @@ -1,5 +1,3 @@ -import { Hex } from 'viem'; -import { faker } from '@faker-js/faker'; import { SafeDecoder } from '@/domain/contracts/contracts/safe-decoder.helper'; import { addOwnerWithThresholdEncoder, @@ -103,10 +101,4 @@ describe('SafeDecoder', () => { ], }); }); - - it('throws if the function call cannot be decoded', () => { - const data = faker.string.hexadecimal({ length: 138 }) as Hex; - - expect(() => target.decodeFunctionData({ data })).toThrow(); - }); }); diff --git a/src/domain/contracts/contracts/__tests__/safe-encoder.builder.ts b/src/domain/contracts/contracts/__tests__/safe-encoder.builder.ts index 9a81bf4213..021250dafd 100644 --- a/src/domain/contracts/contracts/__tests__/safe-encoder.builder.ts +++ b/src/domain/contracts/contracts/__tests__/safe-encoder.builder.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; -import { encodeFunctionData, getAddress, Hex, pad, parseAbi } from 'viem'; - +import { encodeFunctionData, getAddress, Hex, pad } from 'viem'; +import Safe130 from '@/abis/safe/v1.3.0/GnosisSafe.abi'; import { Safe } from '@/domain/safe/entities/safe.entity'; import { IEncoder } from '@/__tests__/encoder-builder'; import { Builder } from '@/__tests__/builder'; @@ -38,16 +38,11 @@ type SetupArgs = { }; class SetupEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver)'; - encode(): Hex { - const abi = parseAbi([SetupEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'setup', args: [ args.owners, @@ -94,16 +89,11 @@ class ExecTransactionEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures)' as const; - encode(): Hex { - const abi = parseAbi([ExecTransactionEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'execTransaction', args: [ args.to, @@ -146,16 +136,11 @@ class AddOwnerWithThresholdEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function addOwnerWithThreshold(address owner, uint256 _threshold)' as const; - encode(): Hex { - const abi = parseAbi([AddOwnerWithThresholdEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'addOwnerWithThreshold', args: [args.owner, args.threshold], }); @@ -180,16 +165,11 @@ class RemoveOwnerEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function removeOwner(address prevOwner, address owner, uint256 _threshold)'; - encode(): Hex { - const abi = parseAbi([RemoveOwnerEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'removeOwner', args: [args.prevOwner, args.owner, args.threshold], }); @@ -220,16 +200,11 @@ class SwapOwnerEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function swapOwner(address prevOwner, address oldOwner, address newOwner)'; - encode(): Hex { - const abi = parseAbi([SwapOwnerEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'swapOwner', args: [args.prevOwner, args.oldOwner, args.newOwner], }); @@ -258,16 +233,11 @@ class ChangeThresholdEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function changeThreshold(uint256 _threshold)'; - encode(): Hex { - const abi = parseAbi([ChangeThresholdEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'changeThreshold', args: [args.threshold], }); @@ -291,15 +261,11 @@ class EnableModuleEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = 'function enableModule(address module)'; - encode(): Hex { - const abi = parseAbi([EnableModuleEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'enableModule', args: [args.module], }); @@ -324,16 +290,11 @@ class DisableModuleEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function disableModule(address prevModule, address module)'; - encode(): Hex { - const abi = parseAbi([DisableModuleEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'disableModule', args: [args.prevModule, args.module], }); @@ -356,16 +317,11 @@ class SetFallbackHandlerEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function setFallbackHandler(address handler)'; - encode(): Hex { - const abi = parseAbi([SetFallbackHandlerEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'setFallbackHandler', args: [args.handler], }); @@ -389,15 +345,11 @@ class SetGuardEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = 'function setGuard(address guard)'; - encode(): Hex { - const abi = parseAbi([SetGuardEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: Safe130, functionName: 'setGuard', args: [args.guard], }); diff --git a/src/domain/contracts/contracts/multi-send-decoder.helper.ts b/src/domain/contracts/contracts/multi-send-decoder.helper.ts index 50d6eabe42..796ee808a4 100644 --- a/src/domain/contracts/contracts/multi-send-decoder.helper.ts +++ b/src/domain/contracts/contracts/multi-send-decoder.helper.ts @@ -1,21 +1,10 @@ import { AbiDecoder } from '@/domain/contracts/contracts/abi-decoder.helper'; import { Injectable } from '@nestjs/common'; -import { - getAddress, - Hex, - hexToBigInt, - hexToNumber, - parseAbi, - size, - slice, -} from 'viem'; - -const MULTISEND_ABI = parseAbi([ - 'function multiSend(bytes memory transactions)', -]); +import { getAddress, Hex, hexToBigInt, hexToNumber, size, slice } from 'viem'; +import MultiSendCallOnly130 from '@/abis/safe/v1.3.0/MultiSendCallOnly.abi'; @Injectable() -export class MultiSendDecoder extends AbiDecoder { +export class MultiSendDecoder extends AbiDecoder { // uint8 operation, address to, value uint256, dataLength uint256, bytes data private static readonly OPERATION_SIZE = 1; private static readonly TO_SIZE = 20; @@ -23,7 +12,7 @@ export class MultiSendDecoder extends AbiDecoder { private static readonly DATA_LENGTH_SIZE = 32; constructor() { - super(MULTISEND_ABI); + super(MultiSendCallOnly130); } mapMultiSendTransactions(multiSendData: Hex): Array<{ diff --git a/src/domain/contracts/contracts/safe-decoder.helper.ts b/src/domain/contracts/contracts/safe-decoder.helper.ts index 5ba13b2db1..dfa8906f93 100644 --- a/src/domain/contracts/contracts/safe-decoder.helper.ts +++ b/src/domain/contracts/contracts/safe-decoder.helper.ts @@ -1,20 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { parseAbi } from 'viem'; +import Safe130 from '@/abis/safe/v1.3.0/GnosisSafe.abi'; import { AbiDecoder } from '@/domain/contracts/contracts/abi-decoder.helper'; -const SAFE_ABI = parseAbi([ - 'function setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver)', - // Owner management - 'function addOwnerWithThreshold(address owner, uint256 _threshold)', - 'function removeOwner(address prevOwner, address owner, uint256 _threshold)', - 'function swapOwner(address prevOwner, address oldOwner, address newOwner)', - 'function changeThreshold(uint256 _threshold)', - 'function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures)', -]); - @Injectable() -export class SafeDecoder extends AbiDecoder { +export class SafeDecoder extends AbiDecoder { constructor() { - super(SAFE_ABI); + super(Safe130); } } diff --git a/src/domain/relay/contracts/__tests__/proxy-factory-encoder.builder.ts b/src/domain/relay/contracts/__tests__/proxy-factory-encoder.builder.ts index bf1007c160..5c027e9570 100644 --- a/src/domain/relay/contracts/__tests__/proxy-factory-encoder.builder.ts +++ b/src/domain/relay/contracts/__tests__/proxy-factory-encoder.builder.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; -import { encodeFunctionData, getAddress, Hex, parseAbi } from 'viem'; - +import { encodeFunctionData, getAddress, Hex } from 'viem'; +import ProxyFactory130 from '@/abis/safe/v1.3.0/GnosisSafeProxyFactory.abi'; import { IEncoder } from '@/__tests__/encoder-builder'; import { Builder } from '@/__tests__/builder'; import { setupEncoder } from '@/domain/contracts/contracts/__tests__/safe-encoder.builder'; @@ -17,16 +17,11 @@ class SetupEncoder extends Builder implements IEncoder { - static readonly FUNCTION_SIGNATURE = - 'function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce)'; - encode(): Hex { - const abi = parseAbi([SetupEncoder.FUNCTION_SIGNATURE]); - const args = this.build(); return encodeFunctionData({ - abi, + abi: ProxyFactory130, functionName: 'createProxyWithNonce', args: [args.singleton, args.initializer, args.saltNonce], }); diff --git a/src/domain/relay/contracts/proxy-factory-decoder.helper.ts b/src/domain/relay/contracts/proxy-factory-decoder.helper.ts index 7d5d379315..297befbc4a 100644 --- a/src/domain/relay/contracts/proxy-factory-decoder.helper.ts +++ b/src/domain/relay/contracts/proxy-factory-decoder.helper.ts @@ -1,14 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { parseAbi } from 'viem'; +import ProxyFactory130 from '@/abis/safe/v1.3.0/GnosisSafeProxyFactory.abi'; import { AbiDecoder } from '@/domain/contracts/contracts/abi-decoder.helper'; -const PROXY_FACTORY_ABI = parseAbi([ - 'function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce)', -]); - @Injectable() -export class ProxyFactoryDecoder extends AbiDecoder { +export class ProxyFactoryDecoder extends AbiDecoder { constructor() { - super(PROXY_FACTORY_ABI); + super(ProxyFactory130); } } diff --git a/test/jest-all.json b/test/jest-all.json index ec45693637..7e878bbd93 100644 --- a/test/jest-all.json +++ b/test/jest-all.json @@ -11,6 +11,7 @@ }, "coverageDirectory": "./coverage", "moduleNameMapper": { + "^@/abis/(.*)$": "/abis/$1", "^@/(.*)$": "/src/$1" }, "globalSetup": "/test/global-setup.ts" diff --git a/test/jest-e2e.json b/test/jest-e2e.json index 8de2758a83..9b21fb9645 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -11,6 +11,7 @@ }, "coverageDirectory": "./coverage", "moduleNameMapper": { + "^@/abis/(.*)$": "/abis/$1", "^@/(.*)$": "/src/$1" }, "globalSetup": "/test/global-setup.ts" diff --git a/tsconfig.json b/tsconfig.json index dcc464d59e..bfc6cb9b61 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "noFallthroughCasesInSwitch": false, "resolveJsonModule": true, "paths": { + "@/abis/*": ["abis/*"], "@/*": ["src/*"], }, "strict": true,