Skip to content

Commit

Permalink
eip-7795
Browse files Browse the repository at this point in the history
  • Loading branch information
attente committed Jan 14, 2025
1 parent a93c6a9 commit 53edcaa
Show file tree
Hide file tree
Showing 14 changed files with 569 additions and 95 deletions.
6 changes: 3 additions & 3 deletions packages/account/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,9 @@ export class Account {
return this.buildBootstrapTransactions(status, chainId)
}

async doBootstrap(chainId: ethers.BigNumberish, feeQuote?: FeeQuote, prestatus?: AccountStatus) {
async doBootstrap(chainId: ethers.BigNumberish, quote?: FeeQuote, prestatus?: AccountStatus) {
const bootstrapTxs = await this.bootstrapTransactions(chainId, prestatus)
return this.relayer(chainId).relay({ ...bootstrapTxs, chainId }, feeQuote)
return this.relayer(chainId).relay({ ...bootstrapTxs, chainId }, { quote })
}

signMessage(
Expand Down Expand Up @@ -926,7 +926,7 @@ export class Account {
const decoratedBundle = await this.decorateTransactions(signedBundle, status, chainId)
callback?.(decoratedBundle)

return this.relayer(chainId).relay(decoratedBundle, quote, undefined, projectAccessKey)
return this.relayer(chainId).relay(decoratedBundle, { projectAccessKey, quote })
}

async fillGasLimits(
Expand Down
13 changes: 10 additions & 3 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@
"test": "echo",
"typecheck": "tsc --noEmit"
},
"dependencies": {},
"peerDependencies": {},
"devDependencies": {},
"dependencies": {
"@0xsequence/network": "workspace:*",
"@0xsequence/relayer": "workspace:*"
},
"peerDependencies": {
"ethers": ">=6"
},
"devDependencies": {
"ethers": "6.13.4"
},
"files": [
"src",
"dist"
Expand Down
16 changes: 14 additions & 2 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
export * from './api.gen'
export * from './preconditions'

import { API as ApiRpc } from './api.gen'
import { API, SolveArgs, SolveReturn } from './api.gen'
import { Preconditions, encodePreconditions } from './preconditions'

export class SequenceAPIClient extends ApiRpc {
export class SequenceAPIClient extends API {
constructor(
hostname: string,
public projectAccessKey?: string,
public jwtAuth?: string
) {
super(hostname.endsWith('/') ? hostname.slice(0, -1) : hostname, fetch)

this.fetch = this._fetch

const solve = this.solve
this.solve = (
args: SolveArgs | { preconditions: Preconditions },
headers?: object,
signal?: AbortSignal
): Promise<SolveReturn> => {
return solve({ preconditions: encodePreconditions(args.preconditions) }, headers, signal)
}
}

_fetch = (input: RequestInfo, init?: RequestInit): Promise<Response> => {
Expand Down
61 changes: 61 additions & 0 deletions packages/api/src/preconditions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { networks } from '@0xsequence/network'
import { ChainPreconditions, encodeChainPreconditions, isChainPreconditions } from '@0xsequence/relayer'
import { ethers } from 'ethers'

import { TransactionPreconditions } from '.'

export interface Preconditions {
chain?: { [chainId: string]: ChainPreconditions }
}

export function isPreconditions(preconditions: any): preconditions is Preconditions {
if (typeof preconditions !== 'object') {
return false
}
if (!preconditions) {
return false
}
switch (typeof preconditions.chain) {
case 'undefined':
return true
case 'object':
return (
preconditions.chain &&
Object.entries(preconditions.chain).every(
([chainId, preconditions]) => isChainId(chainId) && isChainPreconditions(preconditions)
)
)
default:
return false
}
}

export function encodePreconditions(preconditions: Preconditions): TransactionPreconditions {
return {
chain:
preconditions.chain &&
Object.fromEntries(
Object.entries(preconditions.chain).map(([chainId, preconditions]) => [
encodeChainId(chainId),
encodeChainPreconditions(preconditions)
])
)
}
}

function isChainId(chainId: any): boolean {
try {
ethers.getBigInt(chainId)
return true
} catch {
return typeof chainId === 'string' && Object.values(networks).some(({ name }) => name === chainId)
}
}

function encodeChainId(chainId: string): string {
try {
return ethers.getBigInt(chainId).toString()
} catch {
return Object.entries(networks).find(([chainId, { name }]) => name === chainId)![0]
}
}
1 change: 1 addition & 0 deletions packages/provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"@0xsequence/abi": "workspace:*",
"@0xsequence/account": "workspace:*",
"@0xsequence/api": "workspace:*",
"@0xsequence/auth": "workspace:*",
"@0xsequence/core": "workspace:*",
"@0xsequence/migration": "workspace:*",
Expand Down
9 changes: 7 additions & 2 deletions packages/provider/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Preconditions } from '@0xsequence/api'
import { NetworkConfig } from '@0xsequence/network'
import {
ConnectDetails,
Expand Down Expand Up @@ -490,7 +491,10 @@ export class SequenceClient {
})
}

async sendTransaction(tx: ethers.TransactionRequest[] | ethers.TransactionRequest, options?: OptionalChainId): Promise<string> {
async sendTransaction(
tx: ethers.TransactionRequest[] | ethers.TransactionRequest,
options?: OptionalChainId & { transactionPreconditions?: Preconditions }
): Promise<string> {
const transactions = Array.isArray(tx) ? tx : [tx]
const chainId = options?.chainId ?? this.getChainId()

Expand All @@ -508,7 +512,8 @@ export class SequenceClient {
value: value !== undefined && value !== null ? ethers.toQuantity(value) : undefined,
data: data || undefined,
chainId: ethers.toQuantity(chainId)
}))
})),
capabilities: { transactionPreconditions: options?.transactionPreconditions }
}
],
chainId
Expand Down
32 changes: 27 additions & 5 deletions packages/provider/src/transports/wallet-request-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Account, AccountStatus } from '@0xsequence/account'
import { Preconditions, isPreconditions } from '@0xsequence/api'
import { signAuthorization, AuthorizationOptions } from '@0xsequence/auth'
import { commons } from '@0xsequence/core'
import {
Expand Down Expand Up @@ -568,7 +569,7 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq
throw new Error(`${params.length} parameters for wallet_sendCalls request`)
}

const { version, from, calls } = params[0]
const { version, from, calls, capabilities } = params[0]

switch (version) {
case '1.0':
Expand Down Expand Up @@ -602,11 +603,26 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq
throw new Error(`wallet_sendCalls call '${JSON.stringify(invalidCall)}' is invalid`)
}

let transactionPreconditions: Preconditions | undefined
if (capabilities !== undefined) {
if (typeof capabilities !== 'object') {
throw new Error(`wallet_sendCalls capabilities '${JSON.stringify(capabilities)}' are invalid`)
}
if (capabilities.transactionPreconditions !== undefined) {
if (!isPreconditions(capabilities.transactionPreconditions)) {
throw new Error(
`wallet_sendCalls transaction preconditions '${JSON.stringify(capabilities.transactionPreconditions)}' are invalid`
)
}
transactionPreconditions = capabilities.transactionPreconditions
}
}

if (this.prompter) {
return JSON.stringify(
await this.prompter.promptSendTransaction(
calls.map((call: any) => ({ ...call, chainId: call.chainId !== undefined ? Number(call.chainId) : undefined })),
request
{ ...request, transactionPreconditions }
)
)
}
Expand Down Expand Up @@ -693,7 +709,13 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq
switch (address) {
case account.address:
return Object.fromEntries(
account.networks.map(({ chainId }) => [ethers.toQuantity(chainId), { atomicBatch: { supported: true } }])
account.networks.map(({ chainId }) => [
ethers.toQuantity(chainId),
{
atomicBatch: { supported: true },
transactionPreconditions: this.prompter ? { supported: true, versions: ['1.0'] } : undefined
}
])
)

default:
Expand Down Expand Up @@ -1004,11 +1026,11 @@ export interface WalletUserPrompter {
promptSignMessage(message: MessageToSign, origin?: string, projectAccessKey?: string): Promise<string>
promptSignTransaction(
transactions: Array<{ chainId?: number; transactions: commons.transaction.Transactionish }>,
options?: { origin?: string; projectAccessKey?: string }
options?: { origin?: string; projectAccessKey?: string; transactionPreconditions?: Preconditions }
): Promise<string[]>
promptSendTransaction(
transactions: Array<{ chainId?: number; transactions: commons.transaction.Transactionish }>,
options?: { origin?: string; projectAccessKey?: string }
options?: { origin?: string; projectAccessKey?: string; transactionPreconditions?: Preconditions }
): Promise<string[]>
promptConfirmWalletDeploy(chainId: number, origin?: string): Promise<boolean>
promptChangeNetwork(chainId: number): Promise<boolean>
Expand Down
25 changes: 13 additions & 12 deletions packages/relayer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { commons } from '@0xsequence/core'
import { ethers } from 'ethers'

import { ChainPreconditions } from './preconditions'
import { proto } from './rpc-relayer'

import { commons } from '@0xsequence/core'
export * from './local-relayer'
export * from './preconditions'
export * from './provider-relayer'
export * from './rpc-relayer'

export { proto as RpcRelayerProto } from './rpc-relayer'
export type FeeOption = proto.FeeOption
export type SimulateResult = proto.SimulateResult

export interface Relayer {
// simulate returns the execution results for a list of transactions.
Expand Down Expand Up @@ -38,10 +48,8 @@ export interface Relayer {
// The quote should be the one returned from getFeeOptions, if any.
// waitForReceipt must default to true.
relay(
signedTxs: commons.transaction.IntendedTransactionBundle,
quote?: FeeQuote,
waitForReceipt?: boolean,
projectAccessKey?: string
transactions: commons.transaction.IntendedTransactionBundle,
options?: { projectAccessKey?: string; quote?: FeeQuote; preconditions?: ChainPreconditions; waitForReceipt?: boolean }
): Promise<commons.transaction.TransactionResponse>

// wait for transaction confirmation
Expand All @@ -56,13 +64,6 @@ export interface Relayer {
): Promise<commons.transaction.TransactionResponse>
}

export * from './local-relayer'
export * from './provider-relayer'
export * from './rpc-relayer'
export { proto as RpcRelayerProto } from './rpc-relayer'
export type SimulateResult = proto.SimulateResult
export type FeeOption = proto.FeeOption

// A fee quote is simply an opaque value that can be obtained via Relayer.getFeeOptions(), and
// returned back to the same relayer via Relayer.relay(). Fee quotes should be treated as an
// implementation detail of the relayer that produces them.
Expand Down
23 changes: 13 additions & 10 deletions packages/relayer/src/local-relayer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ethers } from 'ethers'
import { commons } from '@0xsequence/core'
import { logger } from '@0xsequence/utils'
import { FeeOption, FeeQuote, Relayer } from '.'
import { ethers } from 'ethers'

import { ChainPreconditions, FeeOption, FeeQuote, Relayer } from '.'
import { ProviderRelayer, ProviderRelayerOptions } from './provider-relayer'
import { commons } from '@0xsequence/core'

export type LocalRelayerOptions = Omit<ProviderRelayerOptions, 'provider'> & {
signer: ethers.Signer
Expand Down Expand Up @@ -46,29 +47,31 @@ export class LocalRelayer extends ProviderRelayer implements Relayer {
}

async relay(
signedTxs: commons.transaction.IntendedTransactionBundle,
quote?: FeeQuote,
waitForReceipt: boolean = true
transactions: commons.transaction.IntendedTransactionBundle,
options?: { projectAccessKey?: string; quote?: FeeQuote; preconditions?: ChainPreconditions; waitForReceipt?: boolean }
): Promise<commons.transaction.TransactionResponse<ethers.TransactionReceipt>> {
if (quote !== undefined) {
if (options?.quote) {
logger.warn(`LocalRelayer doesn't accept fee quotes`)
}
if (options?.preconditions) {
logger.warn(`LocalRelayer doesn't accept transaction preconditions`)
}

const data = commons.transaction.encodeBundleExecData(signedTxs)
const data = commons.transaction.encodeBundleExecData(transactions)

// TODO: think about computing gas limit individually, summing together and passing across
// NOTE: we expect that all txns have set their gasLimit ahead of time through proper estimation
// const gasLimit = signedTxs.transactions.reduce((sum, tx) => sum + tx.gasLimit, 0n)
// txRequest.gasLimit = gasLimit

const responsePromise = this.signer.sendTransaction({
to: signedTxs.entrypoint,
to: transactions.entrypoint,
data,
...this.txnOptions,
gasLimit: 9000000
})

if (waitForReceipt) {
if (options?.waitForReceipt !== false) {
const response: commons.transaction.TransactionResponse = await responsePromise
response.receipt = await response.wait()
return response
Expand Down
Loading

0 comments on commit 53edcaa

Please sign in to comment.