Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new options for Safe transaction nonce: override and enqueue #28

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/execute/options.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createPublicClient, http } from 'viem'
import { Address, createPublicClient, http } from 'viem'

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

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

import { ChainId, PrefixedAddress } from '../types'
import { SafeTransactionProperties } from './types'
Expand Down Expand Up @@ -47,3 +48,28 @@ export function getEip1193Provider({
return urlOrProvider
}
}

export function nonceConfig({
chainId,
safe,
options,
}: {
chainId: ChainId
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])

if (typeof properties?.nonce == 'undefined') {
return 'enqueue'
}

return properties.nonce
}
58 changes: 56 additions & 2 deletions src/execute/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { planExecution } from './plan'
import { execute } from './execute'
import encodeExecTransaction from '../encode/execTransaction'

type NonceConfig = number | 'enqueue' | 'override'

const withPrefix = (address: Address) =>
formatPrefixedAddress(testClient.chain.id, address)

Expand Down Expand Up @@ -81,6 +83,7 @@ describe('plan', () => {
[formatPrefixedAddress(chainId, safe)]: {
proposeOnly: false,
onchainSignature: false,
nonce: 'override' as NonceConfig,
},
},
}
Expand Down Expand Up @@ -138,6 +141,7 @@ describe('plan', () => {
[formatPrefixedAddress(chainId, safe)]: {
proposeOnly: false,
onchainSignature: false,
nonce: 'override' as NonceConfig,
},
},
}
Expand Down Expand Up @@ -200,7 +204,10 @@ describe('plan', () => {
const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { proposeOnly: true },
[withPrefix(safe)]: {
proposeOnly: true,
nonce: 'override' as NonceConfig,
},
},
})

Expand Down Expand Up @@ -239,6 +246,11 @@ describe('plan', () => {
const chainId = testClient.chain.id
const plan = await planExecution([transaction], route, {
providers: { [chainId]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: {
nonce: 'override' as NonceConfig,
},
},
})

expect(plan).toHaveLength(2)
Expand Down Expand Up @@ -285,6 +297,10 @@ describe('plan', () => {
// plan a transfer of 1 eth into receiver
const plan = await planExecution([transaction], route, {
providers: { [chainId]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe1)]: { nonce: 'override' as NonceConfig },
[withPrefix(safe2)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(3)
Expand Down Expand Up @@ -341,6 +357,10 @@ describe('plan', () => {
// plan a transfer of 1 eth into receiver
const plan = await planExecution([transaction], route, {
providers: { [chainId]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe1)]: { nonce: 'override' as NonceConfig },
[withPrefix(safe2)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(3)
Expand Down Expand Up @@ -444,6 +464,10 @@ describe('plan', () => {
// plan a transfer of 1 eth into receiver
const plan = await planExecution([transaction], route, {
providers: { [chainId]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe1)]: { nonce: 'override' as NonceConfig },
[withPrefix(safe2)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(2)
Expand Down Expand Up @@ -525,6 +549,10 @@ describe('plan', () => {
route,
{
providers: { [chainId]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe1)]: { nonce: 'override' as NonceConfig },
[withPrefix(safe2)]: { nonce: 'override' as NonceConfig },
},
}
)

Expand Down Expand Up @@ -599,6 +627,9 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(1)
Expand Down Expand Up @@ -665,6 +696,9 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(1)
Expand Down Expand Up @@ -740,6 +774,9 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(2)
Expand Down Expand Up @@ -832,6 +869,9 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(2)
Expand Down Expand Up @@ -933,6 +973,10 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe1)]: { nonce: 'override' as NonceConfig },
[withPrefix(safe2)]: { nonce: 'override' as NonceConfig },
},
})

expect(await testClient.getBalance({ address: safe2 })).toEqual(
Expand Down Expand Up @@ -1015,6 +1059,10 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe1)]: { nonce: 'override' as NonceConfig },
[withPrefix(safe2)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(2)
Expand Down Expand Up @@ -1113,6 +1161,9 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(2)
Expand Down Expand Up @@ -1156,7 +1207,7 @@ describe('plan', () => {
).toEqual(parseEther('0.123'))
})

it.only('plans and executes independently', async () => {
it('plans and executes independently', async () => {
const owner = privateKeyToAccount(randomHash())
const eoa = privateKeyToAccount(randomHash())
const receiver = privateKeyToAccount(randomHash())
Expand Down Expand Up @@ -1211,6 +1262,9 @@ describe('plan', () => {

const plan = await planExecution([transaction], route, {
providers: { [testClient.chain.id]: testClient as Eip1193Provider },
safeTransactionProperties: {
[withPrefix(safe)]: { nonce: 'override' as NonceConfig },
},
})

expect(plan).toHaveLength(2)
Expand Down
91 changes: 74 additions & 17 deletions src/execute/safeTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import assert from 'assert'
import {
Address,
encodeFunctionData,
getAddress,
parseAbi,
zeroAddress,
} from 'viem'
import SafeApiKit from '@safe-global/api-kit'
import { OperationType } from '@safe-global/types-kit'

import { formatPrefixedAddress } from '../addresses'
import { getEip1193Provider, Options } from './options'
import { getEip1193Provider, nonceConfig, Options } from './options'

import {
ChainId,
Expand All @@ -28,18 +30,61 @@ export async function prepareSafeTransaction({
transaction: MetaTransactionRequest
options?: Options
}): Promise<SafeTransactionRequest> {
const provider = getEip1193Provider({ chainId, options })

const key1 = formatPrefixedAddress(chainId, safe)
const key2 = key1.toLowerCase() as PrefixedAddress

const defaults =
options?.safeTransactionProperties?.[key1] ||
options?.safeTransactionProperties?.[key2]

return {
to: transaction.to,
value: transaction.value,
data: transaction.data,
operation: transaction.operation ?? OperationType.Call,
safeTxGas: BigInt(defaults?.safeTxGas || 0),
baseGas: BigInt(defaults?.baseGas || 0),
gasPrice: BigInt(defaults?.gasPrice || 0),
gasToken: getAddress(defaults?.gasToken || zeroAddress),
refundReceiver: getAddress(defaults?.refundReceiver || zeroAddress),
nonce: await nonce({ chainId, safe, options }),
}
}

async function nonce({
chainId,
safe,
options,
}: {
chainId: ChainId
safe: Address
options?: Options
}): Promise<number> {
const config = nonceConfig({ chainId, safe, options })
if (config == 'enqueue') {
return fetchQueueNonce({ chainId, safe })
} else if (config == 'override') {
return fetchOnChainNonce({ chainId, safe, options })
} else {
const nonce = config
assert(typeof nonce == 'number')
return nonce
}
}

async function fetchOnChainNonce({
chainId,
safe,
options,
}: {
chainId: ChainId
safe: Address
options?: Options
}): Promise<number> {
const provider = getEip1193Provider({ chainId, options })
const avatarAbi = parseAbi(['function nonce() view returns (uint256)'])

const nonce = BigInt(
const nonce = Number(
(await provider.request({
method: 'eth_call',
params: [
Expand All @@ -56,18 +101,30 @@ export async function prepareSafeTransaction({
})) as string
)

return {
to: transaction.to,
value: transaction.value,
data: transaction.data,
operation: transaction.operation ?? OperationType.Call,
safeTxGas: BigInt(defaults?.safeTxGas || 0),
baseGas: BigInt(defaults?.baseGas || 0),
gasPrice: BigInt(defaults?.gasPrice || 0),
gasToken: getAddress(defaults?.gasToken || zeroAddress) as `0x${string}`,
refundReceiver: getAddress(
defaults?.refundReceiver || zeroAddress
) as `0x${string}`,
nonce: Number(defaults?.nonce || nonce),
return nonce
}

async function fetchQueueNonce({
chainId,
safe,
}: {
chainId: ChainId
safe: Address
}): Promise<number> {
const apiKit = initApiKit(chainId)

const nonce = await apiKit.getNextNonce(safe)

return nonce
}

// TODO: remove this once https://github.com/safe-global/safe-core-sdk/issues/514 is closed
const initApiKit = (chainId: ChainId): SafeApiKit => {
// @ts-expect-error SafeApiKit is only available as a CJS module. That doesn't play super nice with us being ESM.
if (SafeApiKit.default) {
// @ts-expect-error See above
return new SafeApiKit.default({ chainId: BigInt(chainId) })
}

return new SafeApiKit({ chainId: BigInt(chainId) })
}
7 changes: 6 additions & 1 deletion src/execute/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export type ExecutionPlan = [ExecutionAction, ...ExecutionAction[]]
export type ExecutionState = `0x${string}`[]

export interface SafeTransactionProperties
extends SafeTransactionOptionalProps {
extends Omit<SafeTransactionOptionalProps, 'nonce'> {
/**
* If a Safe transaction is executable, only approve/propose the transaction,
* but don't execute it. Anyone will be able to trigger execution.
Expand All @@ -100,4 +100,9 @@ export interface SafeTransactionProperties
* on-chain
**/
onchainSignature?: boolean
/**
* Defines the method used to derive the safeTransaction's nonce. Enqueue gets it
* from the txService. Override gets it from onchain. A concrete nonce can be provided
*/
nonce?: 'enqueue' | 'override' | number
}
Loading