From b5bad3da7b5c4424509fadae9b134026f847453b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= Date: Thu, 22 Feb 2024 09:43:21 +0100 Subject: [PATCH] refactor: improve typing on mana repo (#44) Our types have plenty of holes in it, due to disabled `noImplicitAny` and not using `noUncheckedIndexedAccess`. This PR creates base config which all repos should eventually use and migrates mana to start using it. Changes on mana side: - use zod for validating request shape - use Map instead of objects when it makes more sense - remove implicit any in params --- apps/api/package.json | 2 +- apps/api/tsconfig.build.json | 4 --- apps/mana/package.json | 6 ++-- apps/mana/src/eth/dependencies.ts | 12 +++---- apps/mana/src/eth/index.ts | 22 +++++++++--- apps/mana/src/eth/rpc.ts | 30 ++++++++-------- apps/mana/src/knex.ts | 4 +-- apps/mana/src/stark/dependencies.ts | 21 ++++++----- apps/mana/src/stark/herodotus.ts | 54 +++++++++++++++++++---------- apps/mana/src/stark/index.ts | 19 ++++++++-- apps/mana/src/stark/networks.ts | 14 +++++--- apps/mana/src/stark/rpc.ts | 7 ++-- apps/mana/src/utils.ts | 6 ++-- apps/mana/tsconfig.json | 8 ++--- tsconfig.json | 10 ++++++ yarn.lock | 11 ++++-- 16 files changed, 146 insertions(+), 84 deletions(-) delete mode 100644 apps/api/tsconfig.build.json create mode 100644 tsconfig.json diff --git a/apps/api/package.json b/apps/api/package.json index f28807854..82b04b07a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -7,7 +7,7 @@ "codegen": "checkpoint generate", "lint": "eslint src/ test/ --ext .ts --fix", "prebuild": "yarn codegen", - "build": "tsc -p tsconfig.build.json", + "build": "tsc", "dev": "nodemon src/index.ts", "start": "node dist/src/index.js", "test": "vitest run" diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json deleted file mode 100644 index 92411eb40..000000000 --- a/apps/api/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["test/**/*.spec.ts", "test/**/*.test.ts"] -} diff --git a/apps/mana/package.json b/apps/mana/package.json index 4e49b41bb..0fb719849 100644 --- a/apps/mana/package.json +++ b/apps/mana/package.json @@ -23,6 +23,7 @@ "@scure/bip32": "^1.3.3", "@scure/bip39": "^1.2.2", "@snapshot-labs/sx": "^0.1.0", + "@types/cors": "^2.8.17", "abi-wan-kanabi": "^2.0.0", "async-mutex": "^0.4.0", "connection-string": "^4.4.0", @@ -32,7 +33,8 @@ "express": "^4.17.1", "knex": "^2.5.1", "pg": "^8.11.3", - "starknet": "5.25.0" + "starknet": "5.25.0", + "zod": "^3.22.4" }, "devDependencies": { "@snapshot-labs/eslint-config": "0.1.0-beta.13", @@ -44,6 +46,6 @@ "nodemon": "^3.0.1", "prettier": "^3.1.0", "ts-node": "^10.9.1", - "typescript": "^5.2.2" + "typescript": "^5.3.3" } } diff --git a/apps/mana/src/eth/dependencies.ts b/apps/mana/src/eth/dependencies.ts index 31cbcf020..5ba6a792c 100644 --- a/apps/mana/src/eth/dependencies.ts +++ b/apps/mana/src/eth/dependencies.ts @@ -1,11 +1,11 @@ import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { Wallet } from '@ethersproject/wallet'; -const DEFAULT_INDEX = 0; -const SPACES_INDICIES = { - '0x65e4329e8c0fba31883b98e2cf3e81d3cdcac780': 1, // SekhmetDAO - '0x4d95a8be4f1d24d50cc0d7b12f5576fa4bbd892b': 2 // Labs -}; +export const DEFAULT_INDEX = 0; +export const SPACES_INDICIES = new Map([ + ['0x65e4329e8c0fba31883b98e2cf3e81d3cdcac780', 1], // SekhmetDAO + ['0x4d95a8be4f1d24d50cc0d7b12f5576fa4bbd892b', 2] // Labs +]); export function getEthereumWallet(mnemonic: string, index: number) { const path = `m/44'/60'/0'/0/${index}`; @@ -20,7 +20,7 @@ export const createWalletProxy = (mnemonic: string, chainId: number) => { const normalizedSpaceAddress = spaceAddress.toLowerCase(); if (!signers.has(normalizedSpaceAddress)) { - const index = SPACES_INDICIES[normalizedSpaceAddress] || DEFAULT_INDEX; + const index = SPACES_INDICIES.get(normalizedSpaceAddress) || DEFAULT_INDEX; const wallet = getEthereumWallet(mnemonic, index); signers.set(normalizedSpaceAddress, wallet.connect(provider)); } diff --git a/apps/mana/src/eth/index.ts b/apps/mana/src/eth/index.ts index 3d4a5e802..38b98119f 100644 --- a/apps/mana/src/eth/index.ts +++ b/apps/mana/src/eth/index.ts @@ -1,19 +1,31 @@ import express from 'express'; +import z from 'zod'; import { createNetworkHandler, NETWORKS } from './rpc'; -import { getEthereumWallet } from './dependencies'; -import { DEFAULT_INDEX, SPACES_INDICIES } from '../stark/dependencies'; +import { rpcError } from '../utils'; +import { getEthereumWallet, DEFAULT_INDEX, SPACES_INDICIES } from './dependencies'; -const router = express.Router(); +const jsonRpcRequestSchema = z.object({ + id: z.number(), + method: z.enum(['send', 'execute', 'executeQueuedProposal']), + params: z.any() +}); const handlers = Object.fromEntries( Object.keys(NETWORKS).map(chainId => [chainId, createNetworkHandler(parseInt(chainId, 10))]) ); +const router = express.Router(); + router.post('/:chainId?', (req, res) => { const chainId = req.params.chainId || '5'; + + const parsed = jsonRpcRequestSchema.safeParse(req.body); + if (!parsed.success) return rpcError(res, 400, parsed.error, 0); + const { id, method, params } = parsed.data; + const handler = handlers[chainId]; + if (!handler) return rpcError(res, 404, new Error('Unsupported chainId'), id); - const { id, method, params } = req.body; handler[method](id, params, res); }); @@ -22,7 +34,7 @@ router.get('/relayers', (req, res) => { const defaultRelayer = getEthereumWallet(mnemonic, DEFAULT_INDEX).address; const relayers = Object.fromEntries( - Object.entries(SPACES_INDICIES).map(([spaceAddress, index]) => { + Array.from(SPACES_INDICIES).map(([spaceAddress, index]) => { const { address } = getEthereumWallet(mnemonic, index); return [spaceAddress, address]; }) diff --git a/apps/mana/src/eth/rpc.ts b/apps/mana/src/eth/rpc.ts index edd3de543..32eb44e5d 100644 --- a/apps/mana/src/eth/rpc.ts +++ b/apps/mana/src/eth/rpc.ts @@ -1,3 +1,4 @@ +import { Response } from 'express'; import { clients, evmPolygon, @@ -5,29 +6,30 @@ import { evmMainnet, evmGoerli, evmSepolia, - evmLineaGoerli + evmLineaGoerli, + EvmNetworkConfig } from '@snapshot-labs/sx'; import { createWalletProxy } from './dependencies'; import { rpcError, rpcSuccess } from '../utils'; -export const NETWORKS = { - 137: evmPolygon, - 42161: evmArbitrum, - 1: evmMainnet, - 5: evmGoerli, - 11155111: evmSepolia, - 59140: evmLineaGoerli -} as const; +export const NETWORKS = new Map([ + [137, evmPolygon], + [42161, evmArbitrum], + [1, evmMainnet], + [5, evmGoerli], + [11155111, evmSepolia], + [59140, evmLineaGoerli] +]); export const createNetworkHandler = (chainId: number) => { - const networkConfig = NETWORKS[chainId]; + const networkConfig = NETWORKS.get(chainId); if (!networkConfig) throw new Error('Unsupported chainId'); const getWallet = createWalletProxy(process.env.ETH_MNEMONIC || '', chainId); - const client = new clients.EvmEthereumTx({ networkConfig: networkConfig }); + const client = new clients.EvmEthereumTx({ networkConfig }); - async function send(id, params, res) { + async function send(id: number, params: any, res: Response) { try { const { signatureData, data } = params.envelope; const { types } = signatureData; @@ -67,7 +69,7 @@ export const createNetworkHandler = (chainId: number) => { } } - async function execute(id, params, res) { + async function execute(id: number, params: any, res: Response) { try { const { space, proposalId, executionParams } = params; const signer = getWallet(space); @@ -85,7 +87,7 @@ export const createNetworkHandler = (chainId: number) => { } } - async function executeQueuedProposal(id, params, res) { + async function executeQueuedProposal(id: number, params: any, res: Response) { try { const { space, executionStrategy, executionParams } = params; const signer = getWallet(space); diff --git a/apps/mana/src/knex.ts b/apps/mana/src/knex.ts index 0bb16c630..05eced4ca 100644 --- a/apps/mana/src/knex.ts +++ b/apps/mana/src/knex.ts @@ -30,8 +30,8 @@ export default knex({ database: connectionConfig.path[0], user: connectionConfig.user, password: connectionConfig.password, - host: connectionConfig.hosts[0].name, - port: connectionConfig.hosts[0].port, + host: connectionConfig?.hosts[0]?.name, + port: connectionConfig?.hosts[0]?.port, ssl: Object.keys(sslConfig).length > 0 ? sslConfig : undefined } }); diff --git a/apps/mana/src/stark/dependencies.ts b/apps/mana/src/stark/dependencies.ts index f8d6703a7..4dd1dd901 100644 --- a/apps/mana/src/stark/dependencies.ts +++ b/apps/mana/src/stark/dependencies.ts @@ -6,14 +6,13 @@ import { NonceManager } from './nonce-manager'; const basePath = "m/44'/9004'/0'/0"; const contractAXclassHash = '0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003'; -const NODE_URLS = { - [constants.StarknetChainId.SN_MAIN]: process.env.STARKNET_MAINNET_RPC_URL, - [constants.StarknetChainId.SN_GOERLI]: process.env.STARKNET_GOERLI_RPC_URL, - [constants.StarknetChainId.SN_SEPOLIA]: process.env.STARKNET_SEPOLIA_RPC_URL -}; - +const NODE_URLS = new Map([ + [constants.StarknetChainId.SN_MAIN, process.env.STARKNET_MAINNET_RPC_URL], + [constants.StarknetChainId.SN_GOERLI, process.env.STARKNET_GOERLI_RPC_URL], + [constants.StarknetChainId.SN_SEPOLIA, process.env.STARKNET_SEPOLIA_RPC_URL] +]); export function getProvider(chainId: string) { - return new RpcProvider({ nodeUrl: NODE_URLS[chainId] }); + return new RpcProvider({ nodeUrl: NODE_URLS.get(chainId) }); } export function getStarknetAccount(mnemonic: string, index: number) { @@ -38,16 +37,16 @@ export function getStarknetAccount(mnemonic: string, index: number) { } export const DEFAULT_INDEX = 1; -export const SPACES_INDICIES = { - '0x040e337fb53973b08343ce983369c1d9e6249ba011e929347288e4d8b590d048': 2 -}; +export const SPACES_INDICIES = new Map([ + ['0x040e337fb53973b08343ce983369c1d9e6249ba011e929347288e4d8b590d048', 2] +]); export function createAccountProxy(mnemonic: string, provider: RpcProvider) { const accounts = new Map(); return (spaceAddress: string) => { const normalizedSpaceAddress = validateAndParseAddress(spaceAddress); - const index = SPACES_INDICIES[normalizedSpaceAddress] || DEFAULT_INDEX; + const index = SPACES_INDICIES.get(normalizedSpaceAddress) || DEFAULT_INDEX; if (!accounts.has(index)) { const { address, privateKey } = getStarknetAccount(mnemonic, index); diff --git a/apps/mana/src/stark/herodotus.ts b/apps/mana/src/stark/herodotus.ts index 1753294b6..597615459 100644 --- a/apps/mana/src/stark/herodotus.ts +++ b/apps/mana/src/stark/herodotus.ts @@ -4,25 +4,40 @@ import { clients } from '@snapshot-labs/sx'; import * as db from '../db'; import { getClient } from './networks'; -const HERODOTUS_API_KEY = process.env.HERODOTUS_API_KEY || ''; -const HERODOTUS_MAPPING = { - [constants.StarknetChainId.SN_MAIN]: { - DESTINATION_CHAIN_ID: 'STARKNET', - ACCUMULATES_CHAIN_ID: '1', - FEE: '100000' - }, - [constants.StarknetChainId.SN_GOERLI]: { - DESTINATION_CHAIN_ID: 'SN_GOERLI', - ACCUMULATES_CHAIN_ID: '5', - FEE: '0' - }, - [constants.StarknetChainId.SN_SEPOLIA]: { - DESTINATION_CHAIN_ID: 'SN_SEPOLIA', - ACCUMULATES_CHAIN_ID: '11155111', - FEE: '0' - } +type HerodotusConfig = { + DESTINATION_CHAIN_ID: string; + ACCUMULATES_CHAIN_ID: string; + FEE: string; }; +const HERODOTUS_API_KEY = process.env.HERODOTUS_API_KEY || ''; +const HERODOTUS_MAPPING = new Map([ + [ + constants.StarknetChainId.SN_MAIN, + { + DESTINATION_CHAIN_ID: 'STARKNET', + ACCUMULATES_CHAIN_ID: '1', + FEE: '100000' + } + ], + [ + constants.StarknetChainId.SN_GOERLI, + { + DESTINATION_CHAIN_ID: 'SN_GOERLI', + ACCUMULATES_CHAIN_ID: '5', + FEE: '0' + } + ], + [ + constants.StarknetChainId.SN_SEPOLIA, + { + DESTINATION_CHAIN_ID: 'SN_SEPOLIA', + ACCUMULATES_CHAIN_ID: '11155111', + FEE: '0' + } + ] +]); + const controller = new clients.HerodotusController(); type ApiProposal = { @@ -56,7 +71,7 @@ async function getStatus(id: string) { } async function submitBatch(proposal: ApiProposal) { - const mapping = HERODOTUS_MAPPING[proposal.chainId]; + const mapping = HERODOTUS_MAPPING.get(proposal.chainId); if (!mapping) throw new Error('Invalid chainId'); const { DESTINATION_CHAIN_ID, ACCUMULATES_CHAIN_ID, FEE } = mapping; @@ -132,6 +147,7 @@ export async function registerProposal(proposal: ApiProposal) { export async function processProposal(proposal: DbProposal) { if (!proposal.herodotusId) { const [, l1TokenAddress] = proposal.id.split('-'); + if (!l1TokenAddress) throw new Error('Invalid proposal id'); return submitBatch({ ...proposal, @@ -154,7 +170,7 @@ export async function processProposal(proposal: DbProposal) { const { getAccount } = getClient(proposal.chainId); const { account, nonceManager } = getAccount('0x0'); - const mapping = HERODOTUS_MAPPING[proposal.chainId]; + const mapping = HERODOTUS_MAPPING.get(proposal.chainId); if (!mapping) throw new Error('Invalid chainId'); const { DESTINATION_CHAIN_ID, ACCUMULATES_CHAIN_ID } = mapping; diff --git a/apps/mana/src/stark/index.ts b/apps/mana/src/stark/index.ts index f26854946..20f7779fb 100644 --- a/apps/mana/src/stark/index.ts +++ b/apps/mana/src/stark/index.ts @@ -1,19 +1,32 @@ import express from 'express'; +import z from 'zod'; import { createNetworkHandler } from './rpc'; +import { rpcError } from '../utils'; import { NETWORKS } from './networks'; import { DEFAULT_INDEX, SPACES_INDICIES, getStarknetAccount } from './dependencies'; -const router = express.Router(); +const jsonRpcRequestSchema = z.object({ + id: z.number(), + method: z.enum(['send', 'registerTransaction', 'registerProposal']), + params: z.any() +}); const handlers = Object.fromEntries( Object.keys(NETWORKS).map(chainId => [chainId, createNetworkHandler(chainId)]) ); +const router = express.Router(); + router.post('/:chainId', (req, res) => { const chainId = req.params.chainId; + + const parsed = jsonRpcRequestSchema.safeParse(req.body); + if (!parsed.success) return rpcError(res, 400, parsed.error, 0); + const { id, method, params } = parsed.data; + const handler = handlers[chainId]; + if (!handler) return rpcError(res, 404, new Error('Unsupported chainId'), id); - const { id, method, params } = req.body; handler[method](id, params, res); }); @@ -22,7 +35,7 @@ router.get('/relayers', (req, res) => { const defaultRelayer = getStarknetAccount(mnemonic, DEFAULT_INDEX).address; const relayers = Object.fromEntries( - Object.entries(SPACES_INDICIES).map(([spaceAddress, index]) => { + Array.from(SPACES_INDICIES).map(([spaceAddress, index]) => { const { address } = getStarknetAccount(mnemonic, index); return [spaceAddress, address]; }) diff --git a/apps/mana/src/stark/networks.ts b/apps/mana/src/stark/networks.ts index 60172290c..ab5dfb476 100644 --- a/apps/mana/src/stark/networks.ts +++ b/apps/mana/src/stark/networks.ts @@ -1,20 +1,26 @@ import { Account, RpcProvider, constants } from 'starknet'; -import { clients, starknetMainnet, starknetGoerli, starknetSepolia } from '@snapshot-labs/sx'; +import { + clients, + starknetMainnet, + starknetGoerli, + starknetSepolia, + NetworkConfig +} from '@snapshot-labs/sx'; import { getProvider, createAccountProxy } from './dependencies'; import { NonceManager } from './nonce-manager'; -export const NETWORKS = { +export const NETWORKS: Record = { [constants.StarknetChainId.SN_MAIN]: starknetMainnet, [constants.StarknetChainId.SN_GOERLI]: starknetGoerli, [constants.StarknetChainId.SN_SEPOLIA]: starknetSepolia -} as const; +}; const clientsMap = new Map< string, { provider: RpcProvider; client: clients.StarknetTx; - getAccount: (spaceAddress) => { account: Account; nonceManager: NonceManager }; + getAccount: (spaceAddress: string) => { account: Account; nonceManager: NonceManager }; } >(); diff --git a/apps/mana/src/stark/rpc.ts b/apps/mana/src/stark/rpc.ts index ecf846cbe..957f530ab 100644 --- a/apps/mana/src/stark/rpc.ts +++ b/apps/mana/src/stark/rpc.ts @@ -1,3 +1,4 @@ +import { Response } from 'express'; import { NETWORKS, getClient } from './networks'; import * as db from '../db'; import * as herodotus from './herodotus'; @@ -9,7 +10,7 @@ export const createNetworkHandler = (chainId: string) => { const { client, getAccount } = getClient(chainId); - async function send(id, params, res) { + async function send(id: number, params: any, res: Response) { try { const { signatureData, data } = params.envelope; const { address, primaryType, message } = signatureData; @@ -52,7 +53,7 @@ export const createNetworkHandler = (chainId: string) => { } } - async function registerTransaction(id, params, res) { + async function registerTransaction(id: number, params: any, res: Response) { try { const { type, hash, payload } = params; @@ -67,7 +68,7 @@ export const createNetworkHandler = (chainId: string) => { } } - async function registerProposal(id, params, res) { + async function registerProposal(id: number, params: any, res: Response) { try { const { l1TokenAddress, strategyAddress, snapshotTimestamp } = params; diff --git a/apps/mana/src/utils.ts b/apps/mana/src/utils.ts index b80d109b9..699d3f696 100644 --- a/apps/mana/src/utils.ts +++ b/apps/mana/src/utils.ts @@ -1,4 +1,6 @@ -export function rpcSuccess(res, result, id) { +import { Response } from 'express'; + +export function rpcSuccess(res: Response, result: any, id: number) { res.json({ jsonrpc: '2.0', result, @@ -6,7 +8,7 @@ export function rpcSuccess(res, result, id) { }); } -export function rpcError(res, code, e, id) { +export function rpcError(res: Response, code: number, e: unknown, id: number) { res.status(code).json({ jsonrpc: '2.0', error: { diff --git a/apps/mana/tsconfig.json b/apps/mana/tsconfig.json index fe97d19f6..7b37ef05c 100644 --- a/apps/mana/tsconfig.json +++ b/apps/mana/tsconfig.json @@ -1,14 +1,10 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { "target": "esnext", "module": "Node16", "rootDir": "./", "outDir": "./dist", - "esModuleInterop": true, - "strict": true, - "noImplicitAny": false, - "resolveJsonModule": true, - "moduleResolution": "node16", - "skipLibCheck": true + "moduleResolution": "node16" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..ad6e0edb4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "strict": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/yarn.lock b/yarn.lock index 790affbc2..c2d3aef9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2782,6 +2782,13 @@ dependencies: "@types/node" "*" +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + "@types/debug@^4.1.7": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -12591,7 +12598,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5.2.2: +typescript@^5.2.2, typescript@^5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== @@ -13520,7 +13527,7 @@ zen-observable@0.8.15: resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== -zod@^3.21.4: +zod@^3.21.4, zod@^3.22.4: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==