From 173c892394378aaff698484e9cde5f4296351cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crist=C3=B3v=C3=A3o?= Date: Wed, 8 Jan 2025 18:00:34 +0100 Subject: [PATCH] feat: normalize routes (#27) This PR normalizes routes before planning, in a way that will fill in threshold if that number is missing. (Addresses the always propose bug) closes #26 --- bun.lockb | Bin 70790 -> 73707 bytes package.json | 16 ++-- src/addresses.test.ts | 26 +++++- src/addresses.ts | 47 ++++++----- src/encode/execTransaction.ts | 2 +- src/encode/execTransactionWithRole.ts | 8 +- src/execute/execute.ts | 10 +-- src/execute/normalizeRoute.test.ts | 45 +++++++++++ src/execute/normalizeRoute.ts | 109 ++++++++++++++++++++++++++ src/execute/options.ts | 49 ++++++++++++ src/execute/plan.test.ts | 38 ++++----- src/execute/plan.ts | 3 + src/execute/safeTransaction.ts | 55 ++----------- src/execute/types.ts | 10 --- 14 files changed, 297 insertions(+), 121 deletions(-) create mode 100644 src/execute/normalizeRoute.test.ts create mode 100644 src/execute/normalizeRoute.ts create mode 100644 src/execute/options.ts diff --git a/bun.lockb b/bun.lockb index c344283745d7bd56db3f15e2f0a06317f2626bc6..b0e72b1f3f85516b21a059e71e327e054bfb2441 100755 GIT binary patch delta 15210 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 delta 13863 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 I7-{qW04779Gynhq diff --git a/package.json b/package.json index c3c1680..77e920f 100644 --- a/package.json +++ b/package.json @@ -28,18 +28,18 @@ }, "devDependencies": { "@gnosis-guild/zodiac-core": "^2.0.4", - "@types/bun": "^1.1.12", + "@types/bun": "^1.1.15", "@types/wait-on": "^5.3.4", - "prettier": "^3.3.3", + "prettier": "^3.4.2", "tsup": "^8.1.0", - "typescript": "^5.6.3", + "typescript": "^5.7.2", "wait-on": "8.0.1" }, "dependencies": { - "@safe-global/api-kit": "^2.5.3", - "@safe-global/protocol-kit": "^5.0.3", - "@safe-global/safe-deployments": "^1.37.13", - "@safe-global/types-kit": "^1.0.0", - "viem": "^2.21.38" + "@safe-global/api-kit": "^2.5.6", + "@safe-global/protocol-kit": "^5.1.1", + "@safe-global/safe-deployments": "^1.37.22", + "@safe-global/types-kit": "^1.0.1", + "viem": "^2.22.4" } } \ No newline at end of file diff --git a/src/addresses.test.ts b/src/addresses.test.ts index 6033998..46e8ca1 100644 --- a/src/addresses.test.ts +++ b/src/addresses.test.ts @@ -1,6 +1,30 @@ import { describe, expect, it } from 'bun:test' -import { parsePrefixedAddress } from './addresses' +import { parsePrefixedAddress, splitPrefixedAddress } from './addresses' import { zeroAddress } from 'viem' +import { PrefixedAddress } from './types' + +describe('splitPrefixedAddress', () => { + it('correctly splits a prefixed address with a chain shortName', () => { + expect(splitPrefixedAddress(`eth:${zeroAddress}`)).toEqual([1, zeroAddress]) + }) + + it('correctly splits a prefixed address with eoa', () => { + expect(splitPrefixedAddress(`eoa:${zeroAddress}`)).toEqual([ + undefined, + zeroAddress, + ]) + }) + + it('throws error when prefix chain shortName is unknown', () => { + expect(() => + splitPrefixedAddress(`abc:${zeroAddress}` as PrefixedAddress) + ).toThrow() + }) + + it('correctly splits a simple address', () => { + expect(splitPrefixedAddress(zeroAddress)).toEqual([undefined, zeroAddress]) + }) +}) describe('parsePrefixedAddress', () => { it('returns the address part of a prefixed address', () => { diff --git a/src/addresses.ts b/src/addresses.ts index 2494b9d..10159a2 100644 --- a/src/addresses.ts +++ b/src/addresses.ts @@ -1,4 +1,4 @@ -import { Address, getAddress } from 'viem' +import { Address, getAddress, isAddress, zeroAddress } from 'viem' import { chains } from './chains' import type { ChainId, PrefixedAddress } from './types' @@ -6,36 +6,41 @@ export const formatPrefixedAddress = ( chainId: ChainId | undefined, address: Address ) => { - const chain = chainId && chains.find((chain) => chain.chainId === chainId) + const chain = chains.find((chain) => chain.chainId === chainId) - if (!chain && chainId) { - throw new Error(`Unsupported chain ID: ${chainId}`) + if (chainId && !chain) { + throw new Error(`Unsupported chainId: ${chainId}`) } const prefix = chain ? chain.shortName : 'eoa' return `${prefix}:${getAddress(address)}` as PrefixedAddress } -export const splitPrefixedAddress = (prefixedAddress: PrefixedAddress) => { - const [prefix, address] = prefixedAddress.split(':') - const chain = - prefix !== 'eoa' - ? chains.find(({ shortName }) => shortName === prefix) - : undefined - if (!chain && prefix !== 'eoa') { - throw new Error(`Unknown chain prefix: ${prefix}`) +export const splitPrefixedAddress = ( + prefixedAddress: PrefixedAddress | Address +): [ChainId | undefined, Address] => { + if (prefixedAddress.length == zeroAddress.length) { + if (!isAddress(prefixedAddress)) { + throw new Error(`Not an Address: ${prefixedAddress}`) + } + return [undefined, getAddress(prefixedAddress)] + } else { + if (prefixedAddress.indexOf(':') == -1) { + throw new Error(`Unsupported PrefixedAddress format: ${prefixedAddress}`) + } + const [prefix, address] = prefixedAddress.split(':') + const chain = chains.find(({ shortName }) => shortName === prefix) + if (prefix && prefix != 'eoa' && !chain) { + throw new Error(`Unsupported chain shortName: ${prefix}`) + } + + return [chain?.chainId, getAddress(address)] as const } - const checksummedAddress = getAddress(address) as `0x${string}` - return [chain?.chainId, checksummedAddress] as const } export const parsePrefixedAddress = ( prefixedAddress: PrefixedAddress | Address -) => { - if (!prefixedAddress.includes(':')) { - return getAddress(prefixedAddress) as `0x${string}` - } - - const [, address] = prefixedAddress.split(':') - return getAddress(address) as `0x${string}` +): Address => { + const [, address] = splitPrefixedAddress(prefixedAddress) + return address } diff --git a/src/encode/execTransaction.ts b/src/encode/execTransaction.ts index 1936307..9c99895 100644 --- a/src/encode/execTransaction.ts +++ b/src/encode/execTransaction.ts @@ -18,7 +18,7 @@ export default function encodeExecTransaction({ args: [ safeTransaction.to, BigInt(safeTransaction.value), - safeTransaction.data as Hex, + safeTransaction.data, safeTransaction.operation, BigInt(safeTransaction.safeTxGas), BigInt(safeTransaction.baseGas), diff --git a/src/encode/execTransactionWithRole.ts b/src/encode/execTransactionWithRole.ts index dcf0455..ecf819d 100644 --- a/src/encode/execTransactionWithRole.ts +++ b/src/encode/execTransactionWithRole.ts @@ -85,9 +85,9 @@ export default function encodeExecTransactionWithRole({ abi: ROLES_V1_ABI, functionName: 'execTransactionWithRole', args: [ - transaction.to as `0x${string}`, + transaction.to, BigInt(transaction.value), - transaction.data as `0x${string}`, + transaction.data, transaction.operation || 0, Number(BigInt(role)), true, @@ -97,9 +97,9 @@ export default function encodeExecTransactionWithRole({ abi: ROLES_V2_ABI, functionName: 'execTransactionWithRole', args: [ - transaction.to as `0x${string}`, + transaction.to, BigInt(transaction.value), - transaction.data as `0x${string}`, + transaction.data, transaction.operation || 0, role, true, diff --git a/src/execute/execute.ts b/src/execute/execute.ts index 9981463..ce7ddc7 100644 --- a/src/execute/execute.ts +++ b/src/execute/execute.ts @@ -1,4 +1,3 @@ -import assert from 'assert' import { Address, Chain, @@ -7,7 +6,6 @@ import { defineChain, getAddress, hashTypedData, - isAddress, } from 'viem' import { type Eip1193Provider } from '@safe-global/protocol-kit' import SafeApiKit from '@safe-global/api-kit' @@ -68,7 +66,7 @@ export const execute = async ( const [relayer] = (await provider.request({ // message can be relayed by any account, request one method: 'eth_accounts', - })) as string[] + })) as Address[] const walletClient = getWalletClient({ chain: action.chain, @@ -98,12 +96,6 @@ export const execute = async ( break } case ExecutionActionType.PROPOSE_TRANSACTION: { - const [relayer] = (await provider.request({ - method: 'eth_accounts', - })) as string[] - - assert(isAddress(relayer)) - const { chain, safe, safeTransaction, proposer } = action const previousOutput = state[i - 1] diff --git a/src/execute/normalizeRoute.test.ts b/src/execute/normalizeRoute.test.ts new file mode 100644 index 0000000..4c1b47a --- /dev/null +++ b/src/execute/normalizeRoute.test.ts @@ -0,0 +1,45 @@ +import assert from 'assert' + +import { expect, describe, it } from 'bun:test' + +import { privateKeyToAccount } from 'viem/accounts' +import { randomHash, testClient } from '../../test/client' +import { deploySafe } from '../../test/avatar' +import { eoaSafe } from '../../test/routes' +import { AccountType, ChainId } from '../types' +import { normalizeRoute } from './normalizeRoute' + +describe('normalizeRoute', () => { + it('queries and patches missing threshold in a SAFE account', async () => { + const signer = privateKeyToAccount(randomHash()) + const signer2 = privateKeyToAccount(randomHash()) + const signer3 = privateKeyToAccount(randomHash()) + const signer4 = privateKeyToAccount(randomHash()) + + const safe = await deploySafe({ + owners: [ + signer.address, + signer2.address, + signer3.address, + signer4.address, + ], + creationNonce: BigInt(randomHash()), + threshold: 3, + }) + + let route = eoaSafe({ + eoa: signer.address, + safe, + }) + + assert(route.waypoints[1].account.type == AccountType.SAFE) + ;(route.waypoints[1].account as any).threshold = undefined + assert(route.waypoints[1].account.threshold == undefined) + + route = await normalizeRoute(route, { + providers: { [testClient.chain.id as ChainId]: testClient }, + }) + assert(route.waypoints[1].account.type == AccountType.SAFE) + expect(route.waypoints[1].account.threshold).toEqual(3) + }) +}) diff --git a/src/execute/normalizeRoute.ts b/src/execute/normalizeRoute.ts new file mode 100644 index 0000000..980e4de --- /dev/null +++ b/src/execute/normalizeRoute.ts @@ -0,0 +1,109 @@ +import { encodeFunctionData, getAddress, parseAbi } from 'viem' + +import { formatPrefixedAddress, splitPrefixedAddress } from '../addresses' + +import { + Account, + AccountType, + ChainId, + Connection, + PrefixedAddress, + Route, + StartingPoint, + Waypoint, +} from '../types' +import { getEip1193Provider, Options } from './options' + +export async function normalizeRoute( + route: Route, + options?: Options +): Promise { + const waypoints = await Promise.all( + route.waypoints.map((w) => normalizeWaypoint(w, options)) + ) + + return { + id: route.id, + initiator: normalizePrefixedAddress(route.initiator), + avatar: normalizePrefixedAddress(route.avatar), + waypoints: waypoints as [StartingPoint, ...Waypoint[]], + } +} + +export async function normalizeWaypoint( + waypoint: StartingPoint | Waypoint, + options?: Options +): Promise { + waypoint = { + ...waypoint, + account: await normalizeAccount(waypoint.account, options), + } + + if ('connection' in waypoint) { + waypoint = { + ...waypoint, + connection: normalizeConnection(waypoint.connection as Connection), + } + } + + return waypoint +} + +async function normalizeAccount( + account: Account, + options?: Options +): Promise { + account = { + ...account, + address: getAddress(account.address), + prefixedAddress: normalizePrefixedAddress(account.prefixedAddress), + } + + if ( + account.type == AccountType.SAFE && + typeof account.threshold != 'number' + ) { + account.threshold = await fetchThreshold(account, options) + } + + return account +} + +function normalizeConnection(connection: Connection): Connection { + return { + ...connection, + from: normalizePrefixedAddress(connection.from), + } +} + +function normalizePrefixedAddress( + prefixedAddress: PrefixedAddress +): PrefixedAddress { + const [chainId, address] = splitPrefixedAddress(prefixedAddress) + return formatPrefixedAddress(chainId, address) +} + +async function fetchThreshold( + account: Account, + options?: Options +): Promise { + const [chainId, safe] = splitPrefixedAddress(account.prefixedAddress) + const provider = getEip1193Provider({ chainId: chainId as ChainId, options }) + + return Number( + await provider.request({ + method: 'eth_call', + params: [ + { + to: safe, + data: encodeFunctionData({ + abi: parseAbi(['function getThreshold() view returns (uint256)']), + functionName: 'getThreshold', + args: [], + }), + }, + 'latest', + ], + }) + ) +} diff --git a/src/execute/options.ts b/src/execute/options.ts new file mode 100644 index 0000000..9ff8d87 --- /dev/null +++ b/src/execute/options.ts @@ -0,0 +1,49 @@ +import { createPublicClient, http } from 'viem' + +import { Eip1193Provider } from '@safe-global/protocol-kit' + +import { chains, defaultRpc } from '../chains' + +import { ChainId, PrefixedAddress } from '../types' +import { SafeTransactionProperties } from './types' + +export interface Options { + providers?: { + [chainId in ChainId]?: string | Eip1193Provider + } + safeTransactionProperties?: { + [safe: PrefixedAddress]: SafeTransactionProperties + } +} + +export function getEip1193Provider({ + chainId, + options, +}: { + chainId: ChainId + options?: Options +}): Eip1193Provider { + const chain = chainId && chains.find((chain) => chain.chainId === chainId) + if (!chain) { + throw new Error(`Unsupported chainId: ${chainId}`) + } + + const passedIn = Boolean( + options && options.providers && options.providers[chainId] + ) + + let urlOrProvider + if (passedIn) { + urlOrProvider = options!.providers![chainId]! + } else { + urlOrProvider = defaultRpc[chainId]! + } + + if (typeof urlOrProvider == 'string') { + return createPublicClient({ + transport: http(urlOrProvider), + }) as Eip1193Provider + } else { + return urlOrProvider + } +} diff --git a/src/execute/plan.test.ts b/src/execute/plan.test.ts index 340d99b..4b5eb52 100644 --- a/src/execute/plan.test.ts +++ b/src/execute/plan.test.ts @@ -69,7 +69,7 @@ describe('plan', () => { [ { data: '0x', - to: receiver.address as `0x{string}`, + to: receiver.address, value: parseEther('1'), operation: OperationType.Call, }, @@ -126,7 +126,7 @@ describe('plan', () => { [ { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('1'), operation: OperationType.Call, }, @@ -192,7 +192,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('1'), operation: OperationType.Call, } @@ -231,7 +231,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('1'), operation: OperationType.Call, } @@ -277,7 +277,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver as `0x${string}`, + to: receiver, value: parseEther('1'), operation: OperationType.Call, } @@ -333,7 +333,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver as `0x${string}`, + to: receiver, value: parseEther('1'), operation: OperationType.Call, } @@ -436,7 +436,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('1'), operation: OperationType.Call, } @@ -517,7 +517,7 @@ describe('plan', () => { [ { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('1'), operation: OperationType.Call, }, @@ -592,7 +592,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -658,7 +658,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -733,7 +733,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -825,7 +825,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -917,7 +917,7 @@ describe('plan', () => { }) const route = eoaRolesSafeOwnsSafe({ - eoa: member.address as `0x${string}`, + eoa: member.address, roles, roleId, safe1, @@ -926,7 +926,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -999,7 +999,7 @@ describe('plan', () => { }) const route = eoaRolesSafeOwnsSafe({ - eoa: member.address as `0x${string}`, + eoa: member.address, roles, roleId, safe1, @@ -1008,7 +1008,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -1106,7 +1106,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } @@ -1156,7 +1156,7 @@ describe('plan', () => { ).toEqual(parseEther('0.123')) }) - it('plans and executes independently', async () => { + it.only('plans and executes independently', async () => { const owner = privateKeyToAccount(randomHash()) const eoa = privateKeyToAccount(randomHash()) const receiver = privateKeyToAccount(randomHash()) @@ -1204,7 +1204,7 @@ describe('plan', () => { const transaction: MetaTransactionRequest = { data: '0x', - to: receiver.address as `0x${string}`, + to: receiver.address, value: parseEther('0.123'), operation: OperationType.Call, } diff --git a/src/execute/plan.ts b/src/execute/plan.ts index 0042a32..1f293e0 100644 --- a/src/execute/plan.ts +++ b/src/execute/plan.ts @@ -4,6 +4,7 @@ import { Eip1193Provider } from '@safe-global/protocol-kit' import { createPreApprovedSignature } from './signatures' import { encodeMultiSend } from './multisend' +import { normalizeRoute } from './normalizeRoute' import { prepareSafeTransaction } from './safeTransaction' import { splitPrefixedAddress } from '../addresses' @@ -64,6 +65,8 @@ export const planExecution = async ( route: Route, options: Options = {} ): Promise => { + route = await normalizeRoute(route, options) + // encode batch using the appropriate multiSend contract address const lastRolesAccount = route.waypoints.findLast( (wp) => wp.account.type === AccountType.ROLES diff --git a/src/execute/safeTransaction.ts b/src/execute/safeTransaction.ts index c9e2682..10e7a85 100644 --- a/src/execute/safeTransaction.ts +++ b/src/execute/safeTransaction.ts @@ -1,18 +1,14 @@ import { Address, - createPublicClient, encodeFunctionData, getAddress, - http, parseAbi, zeroAddress, } from 'viem' import { OperationType } from '@safe-global/types-kit' -import { Eip1193Provider } from '@safe-global/protocol-kit' import { formatPrefixedAddress } from '../addresses' - -import { chains, defaultRpc } from '../chains' +import { getEip1193Provider, Options } from './options' import { ChainId, @@ -20,16 +16,6 @@ import { PrefixedAddress, SafeTransactionRequest, } from '../types' -import { SafeTransactionProperties } from './types' - -interface Options { - providers?: { - [chainId in ChainId]?: string | Eip1193Provider - } - safeTransactionProperties?: { - [safe: PrefixedAddress]: SafeTransactionProperties - } -} export async function prepareSafeTransaction({ chainId, @@ -43,8 +29,13 @@ export async function prepareSafeTransaction({ options?: Options }): Promise { const provider = getEip1193Provider({ chainId, options }) + + const key1 = formatPrefixedAddress(chainId, safe) + const key2 = key1.toLowerCase() as PrefixedAddress + const defaults = - options?.safeTransactionProperties?.[formatPrefixedAddress(chainId, safe)] + options?.safeTransactionProperties?.[key1] || + options?.safeTransactionProperties?.[key2] const avatarAbi = parseAbi(['function nonce() view returns (uint256)']) @@ -80,35 +71,3 @@ export async function prepareSafeTransaction({ nonce: Number(defaults?.nonce || nonce), } } - -function getEip1193Provider({ - chainId, - options, -}: { - chainId: ChainId - options?: Options -}): Eip1193Provider { - const chain = chainId && chains.find((chain) => chain.chainId === chainId) - if (!chain) { - throw new Error(`Unsupported chain ID: ${chainId}`) - } - - const passedIn = Boolean( - options && options.providers && options.providers[chainId] - ) - - let urlOrProvider - if (passedIn) { - urlOrProvider = options!.providers![chainId]! - } else { - urlOrProvider = defaultRpc[chainId]! - } - - if (typeof urlOrProvider == 'string') { - return createPublicClient({ - transport: http(urlOrProvider), - }) as Eip1193Provider - } else { - return urlOrProvider - } -} diff --git a/src/execute/types.ts b/src/execute/types.ts index eac0f60..bd7991a 100644 --- a/src/execute/types.ts +++ b/src/execute/types.ts @@ -20,15 +20,6 @@ export interface ExecuteTransactionAction { transaction: TransactionRequest } -/** Represents a signature to be produced for the given message by the specified account */ -export interface SignMessageAction { - type: ExecutionActionType.SIGN_MESSAGE - chain: ChainId - from: PrefixedAddress - - message: string -} - interface TypedDataField { name: string type: string @@ -79,7 +70,6 @@ export type ExecutionAction = | ExecuteTransactionAction | SafeTransactionAction | ProposeTransactionAction - | SignMessageAction | SignTypedDataAction /**