Skip to content

Commit

Permalink
fix: generally handle addresses in lowercase representation (#35)
Browse files Browse the repository at this point in the history
- BREAKING CHANGE: `formatPrefixedAddress` renamed to `prefixAddress`
- BREAKING CHANGE: `parsePrefixedAddress` renamed to `unprefixAddress`
- BREAKING CHANGE: prefixed addresses are all lowercase now

resolves #31
  • Loading branch information
cristovaoth authored and jfschwarz committed Jan 30, 2025
1 parent 946b81b commit 53299bb
Show file tree
Hide file tree
Showing 21 changed files with 239 additions and 204 deletions.
Binary file modified bun.lockb
Binary file not shown.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
},
"dependencies": {
"@epic-web/invariant": "1.0.0",
"@safe-global/api-kit": "^2.5.6",
"@safe-global/protocol-kit": "^5.1.1",
"@safe-global/safe-deployments": "^1.37.22",
"@safe-global/api-kit": "^2.5.7",
"@safe-global/protocol-kit": "^5.2.0",
"@safe-global/safe-deployments": "^1.37.26",
"@safe-global/types-kit": "^1.0.1",
"viem": "^2.22.17"
}
Expand Down
8 changes: 5 additions & 3 deletions src/addresses.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'bun:test'
import { parsePrefixedAddress, splitPrefixedAddress } from './addresses'
import { zeroAddress } from 'viem'

import { unprefixAddress, splitPrefixedAddress } from './addresses'

import { PrefixedAddress } from './types'

describe('splitPrefixedAddress', () => {
Expand Down Expand Up @@ -28,10 +30,10 @@ describe('splitPrefixedAddress', () => {

describe('parsePrefixedAddress', () => {
it('returns the address part of a prefixed address', () => {
expect(parsePrefixedAddress(`eth:${zeroAddress}`)).toEqual(zeroAddress)
expect(unprefixAddress(`eth:${zeroAddress}`)).toEqual(zeroAddress)
})

it('is the identify function when the input is already an address', () => {
expect(parsePrefixedAddress(zeroAddress)).toEqual(zeroAddress)
expect(unprefixAddress(zeroAddress)).toEqual(zeroAddress)
})
})
56 changes: 32 additions & 24 deletions src/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,54 @@
import { Address, getAddress, isAddress, zeroAddress } from 'viem'
import { getAddress, isAddress, zeroAddress } from 'viem'
import { chains } from './chains'
import type { ChainId, PrefixedAddress } from './types'

export const formatPrefixedAddress = (
import type { Address, ChainId, PrefixedAddress } from './types'

export function prefixAddress(
chainId: ChainId | undefined,
address: Address
) => {
): PrefixedAddress {
const chain = chains.find((chain) => chain.chainId === chainId)

if (chainId && !chain) {
throw new Error(`Unsupported chainId: ${chainId}`)
}

if (!isAddress(address)) {
throw new Error(`Not an Address: "${address}"`)
}

const prefix = chain ? chain.shortName : 'eoa'
return `${prefix}:${getAddress(address)}` as PrefixedAddress
return `${prefix}:${getAddress(address).toLowerCase()}` as PrefixedAddress
}

export const splitPrefixedAddress = (
export function unprefixAddress(
prefixedAddress: PrefixedAddress | Address
): [ChainId | undefined, Address] => {
): Address {
const [, address] = splitPrefixedAddress(prefixedAddress)
return address
}

export function splitPrefixedAddress(
prefixedAddress: PrefixedAddress | Address
): [ChainId | undefined, Address] {
// without prefix
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 [undefined, getAddress(prefixedAddress).toLowerCase() as Address]
}

return [chain?.chainId, getAddress(address)] as const
if (prefixedAddress.indexOf(':') == -1) {
throw new Error(`Unsupported PrefixedAddress format: ${prefixedAddress}`)
}
}

export const parsePrefixedAddress = (
prefixedAddress: PrefixedAddress | Address
): Address => {
const [, address] = splitPrefixedAddress(prefixedAddress)
return address
// with prefix
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).toLowerCase() as Address] as const
}
4 changes: 2 additions & 2 deletions src/eip712.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, it, expect } from 'bun:test'
import {
Address,
encodeFunctionData,
getAddress,
Hash,
Expand All @@ -17,9 +16,10 @@ import { typedDataForSafeTransaction } from './eip712'

import { deploySafe } from '../test/avatar'
import { testClient } from '../test/client'
import { Address } from './types'

const makeAddress = (number: number): Address =>
getAddress(toHex(number, { size: 20 }))
getAddress(toHex(number, { size: 20 })).toLowerCase() as Address

const safeAbi = parseAbi([
'function getTransactionHash(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce) view returns (bytes32)',
Expand Down
4 changes: 3 additions & 1 deletion src/eip712.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Address, Hex, zeroAddress } from 'viem'
import { Hex, zeroAddress } from 'viem'
import { OperationType } from '@safe-global/types-kit'

import { Address } from './types'

export function typedDataForSafeTransaction({
chainId,
safeAddress,
Expand Down
3 changes: 1 addition & 2 deletions src/execute/execute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Address,
Chain,
createWalletClient,
custom,
Expand All @@ -20,7 +19,7 @@ import {
type ExecutionPlan,
type ExecutionState,
} from './types'
import type { ChainId } from '../types'
import type { Address, ChainId } from '../types'

/**
* Executes the given plan, continuing from the given state. Mutates the state array to track execution progress.
Expand Down
22 changes: 12 additions & 10 deletions src/execute/multisend.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { concat, encodeFunctionData, encodePacked, hexToBytes } from 'viem'
import { OperationType } from '@safe-global/types-kit'
import { MetaTransactionRequest } from '../types'

import { Address, MetaTransactionRequest } from '../types'

export const encodeMultiSend = (
transactions: readonly MetaTransactionRequest[],
preferredAddresses: `0x${string}`[] = []
preferredAddresses: Address[] = []
): MetaTransactionRequest => {
if (transactions.length === 0) {
throw new Error('No transactions to encode')
Expand Down Expand Up @@ -61,35 +62,36 @@ const encodeMultiSendData = (
})
}

const MULTI_SEND_141 = '0x38869bf66a61cf6bdb996a6ae40d5853fd43b526'
const MULTI_SEND_CALLONLY_141 = '0x9641d764fc13c8b624c04430c7356c1c7c8102e2'
const MULTI_SEND_141: Address = '0x38869bf66a61cf6bdb996a6ae40d5853fd43b526'
const MULTI_SEND_CALLONLY_141: Address =
'0x9641d764fc13c8b624c04430c7356c1c7c8102e2'

const KNOWN_MULTI_SEND_ADDRESSES = [
const KNOWN_MULTI_SEND_ADDRESSES: Address[] = [
MULTI_SEND_141,
'0xa238cbeb142c10ef7ad8442c6d1f9e89e07e7761', // MultiSend 1.3.0
'0x998739bfdaadde7c933b942a68053933098f9eda', // MultiSend 1.3.0 alternative
'0x8d29be29923b68abfdd21e541b9374737b49cdad', // MultiSend 1.1.1
]
const KNOWN_MULTI_SEND_CALL_ONLY_ADDRESSES = [
const KNOWN_MULTI_SEND_CALL_ONLY_ADDRESSES: Address[] = [
MULTI_SEND_CALLONLY_141,
'0x40a2accbd92bca938b02010e17a5b8929b49130d', // MultiSendCallOnly 1.3.0
'0xa1dabef33b3b82c7814b6d82a79e50f4ac44102b', // MultiSendCallOnly 1.3.0 alternative
]

const multiSendAddress = (
transactions: readonly MetaTransactionRequest[],
preferredAddresses: `0x${string}`[] = []
): `0x${string}` => {
preferredAddresses: Address[] = []
): Address => {
const callOnly = transactions.every(
(tx) => tx.operation === OperationType.Call
)

const preferredAddress: `0x${string}` | undefined =
const preferredAddress: Address | undefined =
preferredAddresses.find((a) =>
(callOnly
? KNOWN_MULTI_SEND_CALL_ONLY_ADDRESSES
: KNOWN_MULTI_SEND_ADDRESSES
).includes(a.toLowerCase())
).includes(a)
) || preferredAddresses[0]

if (
Expand Down
33 changes: 19 additions & 14 deletions src/execute/normalizeRoute.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { encodeFunctionData, getAddress, parseAbi } from 'viem'

import { formatPrefixedAddress, splitPrefixedAddress } from '../addresses'
import { prefixAddress, splitPrefixedAddress } from '../addresses'

import {
Account,
AccountType,
Address,
ChainId,
Connection,
PrefixedAddress,
Expand Down Expand Up @@ -34,19 +35,13 @@ export async function normalizeWaypoint(
waypoint: StartingPoint | Waypoint,
options?: Options
): Promise<StartingPoint | Waypoint> {
waypoint = {
return {
...waypoint,
account: await normalizeAccount(waypoint.account, options),
...('connection' in waypoint
? { connection: normalizeConnection(waypoint.connection as Connection) }
: {}),
}

if ('connection' in waypoint) {
waypoint = {
...waypoint,
connection: normalizeConnection(waypoint.connection as Connection),
}
}

return waypoint
}

async function normalizeAccount(
Expand All @@ -55,20 +50,30 @@ async function normalizeAccount(
): Promise<Account> {
account = {
...account,
address: getAddress(account.address),
address: normalizeAddress(account.address),
prefixedAddress: normalizePrefixedAddress(account.prefixedAddress),
...('multisend' in account
? { multisend: account.multisend.map(normalizeAddress) }
: {}),
}

if (
account.type == AccountType.SAFE &&
typeof account.threshold != 'number'
) {
account.threshold = await fetchThreshold(account, options)
account = {
...account,
threshold: await fetchThreshold(account, options),
}
}

return account
}

function normalizeAddress(address: any): Address {
return getAddress(address).toLowerCase() as Address
}

function normalizeConnection(connection: Connection): Connection {
return {
...connection,
Expand All @@ -80,7 +85,7 @@ function normalizePrefixedAddress(
prefixedAddress: PrefixedAddress
): PrefixedAddress {
const [chainId, address] = splitPrefixedAddress(prefixedAddress)
return formatPrefixedAddress(chainId, address)
return prefixAddress(chainId, address)
}

async function fetchThreshold(
Expand Down
12 changes: 4 additions & 8 deletions src/execute/options.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Address, createPublicClient, http } from 'viem'
import { createPublicClient, http } from 'viem'

import { Eip1193Provider } from '@safe-global/protocol-kit'

import { chains, defaultRpc } from '../chains'
import { formatPrefixedAddress } from '../addresses'
import { prefixAddress } from '../addresses'

import { ChainId, PrefixedAddress } from '../types'
import { Address, ChainId, PrefixedAddress } from '../types'
import { SafeTransactionProperties } from './types'

export interface Options {
Expand Down Expand Up @@ -58,14 +58,10 @@ export function nonceConfig({
safe: Address
options?: Options
}): 'enqueue' | 'override' | number {
const key1 = formatPrefixedAddress(chainId, safe)
const key2 = key1.toLocaleLowerCase() as PrefixedAddress

const properties =
options &&
options.safeTransactionProperties &&
(options.safeTransactionProperties[key1] ||
options.safeTransactionProperties[key2])
options.safeTransactionProperties[prefixAddress(chainId, safe)]

if (typeof properties?.nonce == 'undefined') {
return 'enqueue'
Expand Down
Loading

0 comments on commit 53299bb

Please sign in to comment.