-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
5924e8f
commit 173c892
Showing
14 changed files
with
297 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,46 @@ | ||
import { Address, getAddress } from 'viem' | ||
import { Address, getAddress, isAddress, zeroAddress } from 'viem' | ||
import { chains } from './chains' | ||
import type { ChainId, PrefixedAddress } from './types' | ||
|
||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Route> { | ||
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<StartingPoint | Waypoint> { | ||
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 = { | ||
...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<number> { | ||
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', | ||
], | ||
}) | ||
) | ||
} |
Oops, something went wrong.