From e0dc186508c52a979e88d0b95959d7c22b5e9750 Mon Sep 17 00:00:00 2001 From: William Hua Date: Wed, 27 Nov 2024 17:58:01 -0500 Subject: [PATCH 01/11] provider: eip-5792 support --- .../json-rpc/middleware/signing-provider.ts | 8 +- packages/provider/src/provider.ts | 5 + .../src/transports/wallet-request-handler.ts | 187 +++++++++++++++--- 3 files changed, 174 insertions(+), 26 deletions(-) diff --git a/packages/network/src/json-rpc/middleware/signing-provider.ts b/packages/network/src/json-rpc/middleware/signing-provider.ts index 47947725b..923100d4c 100644 --- a/packages/network/src/json-rpc/middleware/signing-provider.ts +++ b/packages/network/src/json-rpc/middleware/signing-provider.ts @@ -26,7 +26,13 @@ export const SignerJsonRpcMethods = [ 'wallet_switchEthereumChain', 'wallet_registerOnboarding', 'wallet_watchAsset', - 'wallet_scanQRCode' + 'wallet_scanQRCode', + + // EIP-5792 + 'wallet_sendCalls', + 'wallet_getCallsStatus', + 'wallet_showCallsStatus', + 'wallet_getCapabilities' ] export class SigningProvider implements JsonRpcMiddlewareHandler { diff --git a/packages/provider/src/provider.ts b/packages/provider/src/provider.ts index b5070f5ff..76f2de067 100644 --- a/packages/provider/src/provider.ts +++ b/packages/provider/src/provider.ts @@ -319,6 +319,11 @@ export class SequenceProvider extends ethers.AbstractProvider implements ISequen method === 'eth_signTypedData' || method === 'eth_signTypedData_v4' || method === 'personal_sign' || + // EIP-5792 + method === 'wallet_sendCalls' || + method === 'wallet_getCallsStatus' || + method === 'wallet_showCallsStatus' || + method === 'wallet_getCapabilities' || // These methods will use EIP-6492 // but this is handled directly by the wallet method === 'sequence_sign' || diff --git a/packages/provider/src/transports/wallet-request-handler.ts b/packages/provider/src/transports/wallet-request-handler.ts index 911e9c089..cd9efc99e 100644 --- a/packages/provider/src/transports/wallet-request-handler.ts +++ b/packages/provider/src/transports/wallet-request-handler.ts @@ -36,6 +36,10 @@ import { prefixEIP191Message } from '../utils' const SIGNER_READY_TIMEOUT = 10000 +const mainModule = new ethers.Interface([ + 'function selfExecute((bool delegateCall, bool revertOnError, uint256 gasLimit, address target, uint256 value, bytes data)[] calldata transactions)' +]) + export interface WalletSignInOptions { connect?: boolean defaultNetworkId?: number @@ -421,7 +425,7 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq case 'eth_sendTransaction': { // https://eth.wiki/json-rpc/API#eth_sendtransaction - const transactionParams = fromExtended(request.params![0]).map(tx => { + const transaction = fromExtended(request.params![0]).map(tx => { // eth_sendTransaction uses 'gas' // ethers and sequence use 'gasLimit' if ('gas' in tx && tx.gasLimit === undefined) { @@ -432,21 +436,18 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq return tx }) - validateTransactionRequest(account.address, transactionParams) + validateTransactionRequest(account.address, transaction) let txnHash = '' if (this.prompter === null) { // prompter is null, so we'll send from here - const txnResponse = await account.sendTransaction(transactionParams, request.chainId ?? this.defaultChainId()) + const txnResponse = await account.sendTransaction(transaction, request.chainId ?? this.defaultChainId()) txnHash = txnResponse?.hash ?? '' } else { // prompt user to provide the response - txnHash = await this.prompter.promptSendTransaction( - transactionParams, - request.chainId, - request.origin, - request.projectAccessKey - ) + txnHash = ( + await this.prompter.promptSendTransaction([{ chainId: request.chainId, transactions: transaction }], request) + )[0] } if (txnHash) { @@ -476,12 +477,9 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq // we will want to resolveProperties the big number values to hex strings return await account.signTransactions(transaction, request.chainId ?? this.defaultChainId()) } else { - return await this.prompter.promptSignTransaction( - transaction, - request.chainId, - request.origin, - request.projectAccessKey - ) + return ( + await this.prompter.promptSignTransaction([{ chainId: request.chainId, transactions: transaction }], request) + )[0] } } @@ -560,6 +558,149 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq return null // success } + case 'wallet_sendCalls': { + const { params } = request + + if (!params) { + throw new Error('no parameters for wallet_sendCalls request') + } + if (params.length !== 1) { + throw new Error(`${params.length} parameters for wallet_sendCalls request`) + } + + const { version, from, calls } = params[0] + + switch (version) { + case '1.0': + break + default: + throw new Error(`wallet_sendCalls version '${version}' not supported`) + } + + if (!ethers.isAddress(from)) { + throw new Error(`wallet_sendCalls from address '${from}' is not an address`) + } + + const invalidCall = calls.find((call: any) => { + if ( + (call.to !== undefined && !ethers.isAddress(call.to)) || + (call.value !== undefined && !ethers.isHexString(call.value)) || + (call.data !== undefined && !ethers.isHexString(call.data, true)) || + (call.chainId !== undefined && !ethers.isHexString(call.chainId)) + ) { + return true + } + + try { + validateTransactionRequest(account.address, call) + return false + } catch { + return true + } + }) + if (invalidCall) { + throw new Error(`wallet_sendCalls call '${JSON.stringify(invalidCall)}' is invalid`) + } + + if (this.prompter) { + return JSON.stringify( + await this.prompter.promptSendTransaction( + calls.map((call: any) => ({ ...call, chainId: call.chainId !== undefined ? Number(call.chainId) : undefined })), + request + ) + ) + } + + const chainIds = [] + const chainCalls = new Map>() + + for (const call of calls) { + const chainId = BigInt(call.chainId ?? request.chainId ?? this.defaultChainId()) + + let calls = chainCalls.get(chainId) + if (!calls) { + calls = [] + chainCalls.set(chainId, calls) + chainIds.push(chainId) + } + + calls.push(call) + } + + const metaTxns = new Map( + await Promise.all( + chainIds.map(async chainId => { + const calls = chainCalls.get(chainId)! + + const transaction = + calls.length <= 1 + ? calls + : { + to: account.address, + data: mainModule.encodeFunctionData('selfExecute', [ + commons.transaction.sequenceTxAbiEncode( + calls.map(({ to, value, data }) => ({ + to: to ?? ethers.ZeroAddress, + value, + data, + revertOnError: true + })) + ) + ]) + } + + const response = await account.sendTransaction(transaction, chainId) + const metaTxn = response?.hash + + if (metaTxn) { + return [chainId, metaTxn] as const + } + + throw new Error('transaction rejected by user') + }) + ) + ) + + return JSON.stringify( + calls.map((call: any) => metaTxns.get(BigInt(call.chainId ?? request.chainId ?? this.defaultChainId()))) + ) + } + + case 'wallet_getCallsStatus': { + throw new Error('not implemented') + } + + case 'wallet_showCallsStatus': { + throw new Error('not implemented') + } + + case 'wallet_getCapabilities': { + const { params } = request + + if (!params) { + throw new Error('no parameters for wallet_getCapabilities request') + } + if (params.length !== 1) { + throw new Error(`${params.length} parameters for wallet_getCapabilities request`) + } + + const [address] = params + + if (!ethers.isAddress(address)) { + throw new Error(`wallet_getCapabilities '${address}' is not an address`) + } + + switch (address) { + case account.address: + return Object.fromEntries( + account.networks.map(({ chainId }) => [ethers.toQuantity(chainId), { atomicBatch: { supported: true } }]) + ) + + default: + return {} + } + } + // smart wallet method case 'sequence_getWalletContext': { return account.contexts @@ -862,17 +1003,13 @@ export interface WalletUserPrompter { promptSignInConnect(connectOptions?: ConnectOptions): Promise promptSignMessage(message: MessageToSign, origin?: string, projectAccessKey?: string): Promise promptSignTransaction( - txn: commons.transaction.Transactionish, - chainId?: number, - origin?: string, - projectAccessKey?: string - ): Promise + transactions: Array<{ chainId?: number; transactions: commons.transaction.Transactionish }>, + options?: { origin?: string; projectAccessKey?: string } + ): Promise promptSendTransaction( - txn: commons.transaction.Transactionish, - chainId?: number, - origin?: string, - projectAccessKey?: string - ): Promise + transactions: Array<{ chainId?: number; transactions: commons.transaction.Transactionish }>, + options?: { origin?: string; projectAccessKey?: string } + ): Promise promptConfirmWalletDeploy(chainId: number, origin?: string): Promise promptChangeNetwork(chainId: number): Promise } From e408c8c6657a4bd9247401c66a56cac9539f33a1 Mon Sep 17 00:00:00 2001 From: William Hua Date: Thu, 28 Nov 2024 11:34:30 -0500 Subject: [PATCH 02/11] provider: use eip-5792 --- packages/provider/src/client.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/provider/src/client.ts b/packages/provider/src/client.ts index d2883ec1c..daad98d3b 100644 --- a/packages/provider/src/client.ts +++ b/packages/provider/src/client.ts @@ -491,12 +491,29 @@ export class SequenceClient { } async sendTransaction(tx: ethers.TransactionRequest[] | ethers.TransactionRequest, options?: OptionalChainId): Promise { - const sequenceTxs = Array.isArray(tx) ? tx : [tx] - const extendedTxs = toExtended(sequenceTxs) - - this.analytics?.track({ event: 'SEND_TRANSACTION_REQUEST', props: { chainId: `${options?.chainId || this.getChainId()}` } }) - - return this.request({ method: 'eth_sendTransaction', params: [extendedTxs], chainId: options?.chainId }) + const transactions = Array.isArray(tx) ? tx : [tx] + const chainId = options?.chainId ?? this.getChainId() + + this.analytics?.track({ event: 'SEND_TRANSACTION_REQUEST', props: { chainId: chainId.toString() } }) + + return JSON.parse( + await this.request({ + method: 'wallet_sendCalls', + params: [ + { + version: '1.0', + from: this.getAddress(), + calls: transactions.map(({ to, value, data }) => ({ + to: to ? ethers.resolveAddress(to) : undefined, + value: value !== undefined && value !== null ? ethers.toQuantity(value) : undefined, + data: data || undefined, + chainId: ethers.toQuantity(chainId) + })) + } + ], + chainId + }) + )[ethers.toQuantity(chainId)] } async getWalletContext(): Promise { From 1b898dedd4ae6a613602e6dcf544fbc6dc6c171a Mon Sep 17 00:00:00 2001 From: William Hua Date: Tue, 14 Jan 2025 18:08:46 +0100 Subject: [PATCH 03/11] eip-7795 --- packages/api/package.json | 12 +- packages/api/src/index.ts | 42 ++- packages/provider/package.json | 1 + .../src/transports/wallet-request-handler.ts | 33 ++- packages/relayer/src/index.ts | 1 + packages/relayer/src/precondition.ts | 274 ++++++++++++++++++ pnpm-lock.yaml | 23 +- 7 files changed, 369 insertions(+), 17 deletions(-) create mode 100644 packages/relayer/src/precondition.ts diff --git a/packages/api/package.json b/packages/api/package.json index a279cd3d6..47e826f0f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -12,9 +12,15 @@ "test": "echo", "typecheck": "tsc --noEmit" }, - "dependencies": {}, - "peerDependencies": {}, - "devDependencies": {}, + "dependencies": { + "@0xsequence/relayer": "workspace:*" + }, + "peerDependencies": { + "ethers": ">=6" + }, + "devDependencies": { + "ethers": "6.13.4" + }, "files": [ "src", "dist" diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index e7b9ab042..10e7f3d19 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,8 +1,13 @@ -export * from './api.gen' +import { + Precondition as ChainPrecondition, + encodePrecondition as encodeChainPrecondition, + isPrecondition as isChainPrecondition +} from '@0xsequence/relayer' +import { ethers } from 'ethers' -import { API as ApiRpc } from './api.gen' +import * as proto from './api.gen' -export class SequenceAPIClient extends ApiRpc { +export class SequenceAPIClient extends proto.API { constructor( hostname: string, public projectAccessKey?: string, @@ -34,3 +39,34 @@ export class SequenceAPIClient extends ApiRpc { return fetch(input, init) } } + +export * from './api.gen' + +export type Precondition = { chainId: ethers.BigNumberish } & ChainPrecondition + +export function isPrecondition(precondition: any): precondition is Precondition { + return ( + typeof precondition === 'object' && precondition && isBigNumberish(precondition.chainId) && isChainPrecondition(precondition) + ) +} + +export function encodePrecondition(precondition: Precondition): proto.Precondition { + const { type, precondition: args } = encodeChainPrecondition(precondition) + delete args.chainId + return { type, chainID: encodeBigNumberish(precondition.chainId), precondition: args } +} + +function isBigNumberish(value: any): value is ethers.BigNumberish { + try { + ethers.toBigInt(value) + return true + } catch { + return false + } +} + +function encodeBigNumberish( + value: T +): T extends ethers.BigNumberish ? string : undefined { + return value !== undefined ? ethers.toBigInt(value).toString() : (undefined as any) +} diff --git a/packages/provider/package.json b/packages/provider/package.json index 22a1f7cf2..677224976 100644 --- a/packages/provider/package.json +++ b/packages/provider/package.json @@ -25,6 +25,7 @@ "dependencies": { "@0xsequence/abi": "workspace:*", "@0xsequence/account": "workspace:*", + "@0xsequence/api": "workspace:*", "@0xsequence/auth": "workspace:*", "@0xsequence/core": "workspace:*", "@0xsequence/migration": "workspace:*", diff --git a/packages/provider/src/transports/wallet-request-handler.ts b/packages/provider/src/transports/wallet-request-handler.ts index cd9efc99e..b1bae7c8b 100644 --- a/packages/provider/src/transports/wallet-request-handler.ts +++ b/packages/provider/src/transports/wallet-request-handler.ts @@ -1,4 +1,5 @@ import { Account, AccountStatus } from '@0xsequence/account' +import { Precondition, isPrecondition } from '@0xsequence/api' import { signAuthorization, AuthorizationOptions } from '@0xsequence/auth' import { commons } from '@0xsequence/core' import { @@ -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': @@ -602,15 +603,31 @@ export class WalletRequestHandler implements EIP1193Provider, ProviderMessageReq throw new Error(`wallet_sendCalls call '${JSON.stringify(invalidCall)}' is invalid`) } + if (capabilities !== undefined && typeof capabilities !== 'object') { + throw new Error(`wallet_sendCalls capabilities '${JSON.stringify(capabilities)}' is invalid`) + } + + let preconditions: Precondition[] | undefined + if (capabilities.preconditions !== undefined) { + if (!(capabilities.preconditions instanceof Array) || !capabilities.preconditions.every(isPrecondition)) { + throw new Error(`wallet_sendCalls preconditions '${JSON.stringify(capabilities.preconditions)}' is invalid`) + } + preconditions = capabilities.preconditions + } + 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, preconditions } ) ) } + if (preconditions) { + throw new Error(`wallet_sendCalls preconditions not supported`) + } + const chainIds = [] const chainCalls = new Map>() @@ -693,7 +710,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 }, + ...(this.prompter && { preconditions: { supported: true, versions: ['1.0'] } }) + } + ]) ) default: @@ -1004,11 +1027,11 @@ export interface WalletUserPrompter { promptSignMessage(message: MessageToSign, origin?: string, projectAccessKey?: string): Promise promptSignTransaction( transactions: Array<{ chainId?: number; transactions: commons.transaction.Transactionish }>, - options?: { origin?: string; projectAccessKey?: string } + options?: { origin?: string; projectAccessKey?: string; preconditions?: Precondition[] } ): Promise promptSendTransaction( transactions: Array<{ chainId?: number; transactions: commons.transaction.Transactionish }>, - options?: { origin?: string; projectAccessKey?: string } + options?: { origin?: string; projectAccessKey?: string; preconditions?: Precondition[] } ): Promise promptConfirmWalletDeploy(chainId: number, origin?: string): Promise promptChangeNetwork(chainId: number): Promise diff --git a/packages/relayer/src/index.ts b/packages/relayer/src/index.ts index 1fcdb1774..1d592e5e7 100644 --- a/packages/relayer/src/index.ts +++ b/packages/relayer/src/index.ts @@ -75,6 +75,7 @@ export interface Relayer { } export * from './local-relayer' +export * from './precondition' export * from './provider-relayer' export * from './rpc-relayer' export { proto as RpcRelayerProto } from './rpc-relayer' diff --git a/packages/relayer/src/precondition.ts b/packages/relayer/src/precondition.ts new file mode 100644 index 000000000..d49927d85 --- /dev/null +++ b/packages/relayer/src/precondition.ts @@ -0,0 +1,274 @@ +import { ethers } from 'ethers' + +import { proto } from './rpc-relayer' + +export type Precondition = + | NativeBalancePrecondition + | Erc20BalancePrecondition + | Erc20ApprovalPrecondition + | Erc721OwnershipPrecondition + | Erc721ApprovalPrecondition + | Erc1155BalancePrecondition + | Erc1155ApprovalPrecondition + +export function isPrecondition(precondition: any): precondition is Precondition { + return [ + isNativeBalancePrecondition, + isErc20BalancePrecondition, + isErc20ApprovalPrecondition, + isErc721OwnershipPrecondition, + isErc721ApprovalPrecondition, + isErc1155BalancePrecondition, + isErc1155ApprovalPrecondition + ].some(predicate => predicate(precondition)) +} + +export function encodePrecondition(precondition: Precondition): proto.Precondition { + if (isNativeBalancePrecondition(precondition)) { + return encodeNativeBalancePrecondition(precondition) + } else if (isErc20BalancePrecondition(precondition)) { + return encodeErc20BalancePrecondition(precondition) + } else if (isErc20ApprovalPrecondition(precondition)) { + return encodeErc20ApprovalPrecondition(precondition) + } else if (isErc721OwnershipPrecondition(precondition)) { + return encodeErc721OwnershipPrecondition(precondition) + } else if (isErc721ApprovalPrecondition(precondition)) { + return encodeErc721ApprovalPrecondition(precondition) + } else if (isErc1155BalancePrecondition(precondition)) { + return encodeErc1155BalancePrecondition(precondition) + } else if (isErc1155ApprovalPrecondition(precondition)) { + return encodeErc1155ApprovalPrecondition(precondition) + } else { + throw new Error('unreachable') + } +} + +type NativeBalancePrecondition = { + type: 'native-balance' + address: `0x${string}` + min?: ethers.BigNumberish + max?: ethers.BigNumberish +} + +function isNativeBalancePrecondition(precondition: any): precondition is NativeBalancePrecondition { + return ( + typeof precondition === 'object' && + precondition && + precondition.type === 'native-balance' && + ethers.isAddress(precondition.address) && + (precondition.min === undefined || isBigNumberish(precondition.min)) && + (precondition.max === undefined || isBigNumberish(precondition.max)) + ) +} + +function encodeNativeBalancePrecondition(precondition: NativeBalancePrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { + ...precondition, + type: undefined, + min: encodeBigNumberish(precondition.min), + max: encodeBigNumberish(precondition.max) + } + } +} + +type Erc20BalancePrecondition = { + type: 'erc20-balance' + address: `0x${string}` + token: `0x${string}` + min?: ethers.BigNumberish + max?: ethers.BigNumberish +} + +function isErc20BalancePrecondition(precondition: any): precondition is Erc20BalancePrecondition { + return ( + typeof precondition === 'object' && + precondition && + precondition.type === 'erc20-balance' && + ethers.isAddress(precondition.address) && + ethers.isAddress(precondition.token) && + (precondition.min === undefined || isBigNumberish(precondition.min)) && + (precondition.max === undefined || isBigNumberish(precondition.max)) + ) +} + +function encodeErc20BalancePrecondition(precondition: Erc20BalancePrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { + ...precondition, + type: undefined, + min: encodeBigNumberish(precondition.min), + max: encodeBigNumberish(precondition.max) + } + } +} + +type Erc20ApprovalPrecondition = { + type: 'erc20-approval' + address: `0x${string}` + token: `0x${string}` + operator: `0x${string}` + min: ethers.BigNumberish +} + +function isErc20ApprovalPrecondition(precondition: any): precondition is Erc20ApprovalPrecondition { + return ( + typeof precondition === 'object' && + precondition && + precondition.type === 'erc20-approval' && + ethers.isAddress(precondition.address) && + ethers.isAddress(precondition.token) && + ethers.isAddress(precondition.operator) && + isBigNumberish(precondition.min) + ) +} + +function encodeErc20ApprovalPrecondition(precondition: Erc20ApprovalPrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { ...precondition, type: undefined, min: encodeBigNumberish(precondition.min) } + } +} + +type Erc721OwnershipPrecondition = { + type: 'erc721-ownership' + address: `0x${string}` + token: `0x${string}` + tokenId: ethers.BigNumberish + owned?: boolean +} + +function isErc721OwnershipPrecondition(precondition: any): precondition is Erc721OwnershipPrecondition { + return ( + typeof precondition === 'object' && + precondition.type === 'erc721-ownership' && + ethers.isAddress(precondition.address) && + ethers.isAddress(precondition.token) && + isBigNumberish(precondition.tokenId) && + (precondition.owned === undefined || typeof precondition.owned === 'boolean') + ) +} + +function encodeErc721OwnershipPrecondition(precondition: Erc721OwnershipPrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { + ...precondition, + type: undefined, + tokenId: encodeBigNumberish(precondition.tokenId), + owned: precondition.owned !== false + } + } +} + +type Erc721ApprovalPrecondition = { + type: 'erc721-approval' + address: `0x${string}` + token: `0x${string}` + tokenId: ethers.BigNumberish + operator: `0x${string}` +} + +function isErc721ApprovalPrecondition(precondition: any): precondition is Erc721ApprovalPrecondition { + return ( + typeof precondition === 'object' && + precondition.type === 'erc721-approval' && + ethers.isAddress(precondition.address) && + ethers.isAddress(precondition.token) && + isBigNumberish(precondition.tokenId) && + ethers.isAddress(precondition.operator) + ) +} + +function encodeErc721ApprovalPrecondition(precondition: Erc721ApprovalPrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { ...precondition, type: undefined, tokenId: encodeBigNumberish(precondition.tokenId) } + } +} + +type Erc1155BalancePrecondition = { + type: 'erc1155-balance' + address: `0x${string}` + token: `0x${string}` + tokenId: ethers.BigNumberish + min?: ethers.BigNumberish + max?: ethers.BigNumberish +} + +function isErc1155BalancePrecondition(precondition: any): precondition is Erc1155BalancePrecondition { + return ( + typeof precondition === 'object' && + precondition && + precondition.type === 'erc1155-balance' && + ethers.isAddress(precondition.address) && + ethers.isAddress(precondition.token) && + isBigNumberish(precondition.tokenId) && + (precondition.min === undefined || isBigNumberish(precondition.min)) && + (precondition.max === undefined || isBigNumberish(precondition.max)) + ) +} + +function encodeErc1155BalancePrecondition(precondition: Erc1155BalancePrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { + ...precondition, + type: undefined, + tokenId: encodeBigNumberish(precondition.tokenId), + min: encodeBigNumberish(precondition.min), + max: encodeBigNumberish(precondition.max) + } + } +} + +type Erc1155ApprovalPrecondition = { + type: 'erc1155-approval' + address: `0x${string}` + token: `0x${string}` + tokenId: ethers.BigNumberish + operator: `0x${string}` + min: ethers.BigNumberish +} + +function isErc1155ApprovalPrecondition(precondition: any): precondition is Erc1155ApprovalPrecondition { + return ( + typeof precondition === 'object' && + precondition && + precondition.type === 'erc1155-approval' && + ethers.isAddress(precondition.address) && + ethers.isAddress(precondition.token) && + isBigNumberish(precondition.tokenId) && + ethers.isAddress(precondition.operator) && + isBigNumberish(precondition.min) + ) +} + +function encodeErc1155ApprovalPrecondition(precondition: Erc1155ApprovalPrecondition): proto.Precondition { + return { + type: precondition.type, + precondition: { + ...precondition, + type: undefined, + tokenId: encodeBigNumberish(precondition.tokenId), + min: encodeBigNumberish(precondition.min) + } + } +} + +function isBigNumberish(value: any): value is ethers.BigNumberish { + try { + ethers.toBigInt(value) + return true + } catch { + return false + } +} + +function encodeBigNumberish( + value: T +): T extends ethers.BigNumberish ? string : undefined { + return value !== undefined ? ethers.toBigInt(value).toString() : (undefined as any) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81891927c..56fdc2b59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -293,7 +293,15 @@ importers: specifier: ^15.1.0 version: 15.1.0 - packages/api: {} + packages/api: + dependencies: + '@0xsequence/relayer': + specifier: workspace:* + version: link:../relayer + devDependencies: + ethers: + specifier: 6.13.4 + version: 6.13.4(bufferutil@4.0.8)(utf-8-validate@6.0.3) packages/auth: dependencies: @@ -467,6 +475,9 @@ importers: '@0xsequence/account': specifier: workspace:* version: link:../account + '@0xsequence/api': + specifier: workspace:* + version: link:../api '@0xsequence/auth': specifier: workspace:* version: link:../auth @@ -1933,7 +1944,7 @@ packages: '@nomicfoundation/hardhat-ethers@3.0.8': resolution: {integrity: sha512-zhOZ4hdRORls31DTOqg+GmEZM0ujly8GGIuRY7t7szEk2zW/arY1qDug/py8AEktT00v5K+b6RvbVog+va51IA==} peerDependencies: - ethers: 6.13.4 + ethers: ^6.1.0 hardhat: ^2.0.0 '@nomicfoundation/hardhat-ignition-ethers@0.15.6': @@ -9835,7 +9846,7 @@ snapshots: '@nomicfoundation/hardhat-ethers@3.0.8(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.14(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.9)(typescript@5.6.3))(typescript@5.6.3)(utf-8-validate@5.0.10))': dependencies: - debug: 4.3.7(supports-color@6.1.0) + debug: 4.4.0 ethers: 6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) hardhat: 2.22.14(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.9)(typescript@5.6.3))(typescript@5.6.3)(utf-8-validate@5.0.10) lodash.isequal: 4.5.0 @@ -9856,7 +9867,7 @@ snapshots: '@nomicfoundation/ignition-core': 0.15.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@nomicfoundation/ignition-ui': 0.15.6 chalk: 4.1.2 - debug: 4.3.7(supports-color@6.1.0) + debug: 4.4.0 fs-extra: 10.1.0 hardhat: 2.22.14(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.9)(typescript@5.6.3))(typescript@5.6.3)(utf-8-validate@5.0.10) json5: 2.2.3 @@ -9898,7 +9909,7 @@ snapshots: '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 - debug: 4.3.7(supports-color@6.1.0) + debug: 4.4.0 hardhat: 2.22.14(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.9)(typescript@5.6.3))(typescript@5.6.3)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 semver: 6.3.1 @@ -9912,7 +9923,7 @@ snapshots: '@ethersproject/address': 5.6.1 '@nomicfoundation/solidity-analyzer': 0.1.2 cbor: 9.0.2 - debug: 4.3.7(supports-color@6.1.0) + debug: 4.4.0 ethers: 6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) fs-extra: 10.1.0 immer: 10.0.2 From cb8c2806a72dae0440b18f7a8975e825b330af28 Mon Sep 17 00:00:00 2001 From: William Hua Date: Fri, 17 Jan 2025 10:35:42 +0100 Subject: [PATCH 04/11] stack@eip-7795 bindings --- packages/api/src/api.gen.ts | 50 +++++++++++++++++-- .../relayer/src/rpc-relayer/relayer.gen.ts | 10 +++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/api/src/api.gen.ts b/packages/api/src/api.gen.ts index 711f1f184..21c16bbd7 100644 --- a/packages/api/src/api.gen.ts +++ b/packages/api/src/api.gen.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// sequence-api v0.4.0 24814ebb88457c0545aa80e8388cb0f08ec59bec +// sequence-api v0.4.0 41111b03b412959cee78aeeaa326584819b68a62 // -- // Code generated by webrpc-gen@v0.20.3 with typescript generator. DO NOT EDIT. // @@ -12,7 +12,7 @@ export const WebRPCVersion = 'v1' export const WebRPCSchemaVersion = 'v0.4.0' // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = '24814ebb88457c0545aa80e8388cb0f08ec59bec' +export const WebRPCSchemaHash = '41111b03b412959cee78aeeaa326584819b68a62' // // Types @@ -148,6 +148,22 @@ export interface TupleComponent { value: any } +export interface Precondition { + type: string + chainID: string + precondition: any +} + +export interface Solution { + transactions: Array +} + +export interface Transactions { + chainID: string + transactions: Array + preconditions?: Array +} + export interface Transaction { delegateCall: boolean revertOnError: boolean @@ -155,7 +171,11 @@ export interface Transaction { target: string value: string data: string - call?: ContractCall +} + +export interface SolutionPrecondition { + type: string + precondition: any } export interface UserStorage { @@ -555,6 +575,7 @@ export interface API { getSwapPrice(args: GetSwapPriceArgs, headers?: object, signal?: AbortSignal): Promise getSwapPrices(args: GetSwapPricesArgs, headers?: object, signal?: AbortSignal): Promise getSwapQuote(args: GetSwapQuoteArgs, headers?: object, signal?: AbortSignal): Promise + satisfy(args: SatisfyArgs, headers?: object, signal?: AbortSignal): Promise listCurrencyGroups(headers?: object, signal?: AbortSignal): Promise addOffchainInventory( args: AddOffchainInventoryArgs, @@ -1066,6 +1087,14 @@ export interface GetSwapQuoteArgs { export interface GetSwapQuoteReturn { swapQuote: SwapQuote } +export interface SatisfyArgs { + wallet: string + preconditions: Array +} + +export interface SatisfyReturn { + solutions: Array +} export interface ListCurrencyGroupsArgs {} export interface ListCurrencyGroupsReturn { @@ -2141,6 +2170,21 @@ export class API implements API { ) } + satisfy = (args: SatisfyArgs, headers?: object, signal?: AbortSignal): Promise => { + return this.fetch(this.url('Satisfy'), createHTTPRequest(args, headers, signal)).then( + res => { + return buildResponse(res).then(_data => { + return { + solutions: >_data.solutions + } + }) + }, + error => { + throw WebrpcRequestFailedError.new({ cause: `fetch(): ${error.message || ''}` }) + } + ) + } + listCurrencyGroups = (headers?: object, signal?: AbortSignal): Promise => { return this.fetch(this.url('ListCurrencyGroups'), createHTTPRequest({}, headers, signal)).then( res => { diff --git a/packages/relayer/src/rpc-relayer/relayer.gen.ts b/packages/relayer/src/rpc-relayer/relayer.gen.ts index 27714d417..4ac626f76 100644 --- a/packages/relayer/src/rpc-relayer/relayer.gen.ts +++ b/packages/relayer/src/rpc-relayer/relayer.gen.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// sequence-relayer v0.4.1 dd95e21fa884c6564199bc3dd5f588534827ffe2 +// sequence-relayer v0.4.1 71ba2b7611bb610981221020466fb0c705f5f97b // -- // Code generated by webrpc-gen@v0.20.3 with typescript generator. DO NOT EDIT. // @@ -12,7 +12,7 @@ export const WebRPCVersion = 'v1' export const WebRPCSchemaVersion = 'v0.4.1' // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = 'dd95e21fa884c6564199bc3dd5f588534827ffe2' +export const WebRPCSchemaHash = '71ba2b7611bb610981221020466fb0c705f5f97b' // // Types @@ -185,6 +185,11 @@ export interface MetaTxnReceiptLog { data: string } +export interface Precondition { + type: string + precondition: any +} + export interface Transaction { txnHash?: string blockNumber: number @@ -353,6 +358,7 @@ export interface SendMetaTxnArgs { call: MetaTxn quote?: string projectID?: number + preconditions?: Array } export interface SendMetaTxnReturn { From f0fd1f4dbc0ad2e3d682c02b03d8a24dad6c9ab6 Mon Sep 17 00:00:00 2001 From: William Hua Date: Fri, 17 Jan 2025 11:02:43 +0100 Subject: [PATCH 05/11] fixup! eip-7795 --- packages/account/package.json | 1 + packages/account/src/account.ts | 62 +++++++++++++---------- packages/account/src/signer.ts | 16 ++---- packages/auth/src/session.ts | 2 +- packages/provider/src/client.ts | 18 ++++--- packages/provider/src/signer.ts | 18 ++++--- packages/relayer/src/index.ts | 11 ++-- packages/relayer/src/local-relayer.ts | 21 ++++---- packages/relayer/src/provider-relayer.ts | 12 ++--- packages/relayer/src/rpc-relayer/index.ts | 41 +++++++-------- packages/wallet/src/wallet.ts | 42 ++++++++------- pnpm-lock.yaml | 3 ++ 12 files changed, 131 insertions(+), 116 deletions(-) diff --git a/packages/account/package.json b/packages/account/package.json index 613adeed5..58f75aefd 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@0xsequence/abi": "workspace:*", + "@0xsequence/api": "workspace:*", "@0xsequence/core": "workspace:*", "@0xsequence/migration": "workspace:*", "@0xsequence/network": "workspace:*", diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index 0a486c473..4daaf5ad4 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -1,4 +1,5 @@ import { walletContracts } from '@0xsequence/abi' +import { Precondition } from '@0xsequence/api' import { commons, universal } from '@0xsequence/core' import { migrator, defaults, version } from '@0xsequence/migration' import { ChainId, NetworkConfig } from '@0xsequence/network' @@ -783,9 +784,9 @@ export class Account { return this.buildBootstrapTransactions(status, chainId) } - async doBootstrap(chainId: ethers.BigNumberish, feeQuote?: FeeQuote, prestatus?: AccountStatus) { + async doBootstrap(chainId: ethers.BigNumberish, prestatus?: AccountStatus) { const bootstrapTxs = await this.bootstrapTransactions(chainId, prestatus) - return this.relayer(chainId).relay({ ...bootstrapTxs, chainId }, feeQuote) + return this.relayer(chainId).relay({ ...bootstrapTxs, chainId }) } signMessage( @@ -910,23 +911,28 @@ export class Account { } async sendSignedTransactions( - signedBundle: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[], + transactions: commons.transaction.IntendedTransactionBundle | commons.transaction.IntendedTransactionBundle[], chainId: ethers.BigNumberish, - quote?: FeeQuote, - pstatus?: AccountStatus, - callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void, - projectAccessKey?: string + options?: { + projectAccessKey?: string + quote?: FeeQuote + preconditions?: Precondition[] + waitForReceipt?: boolean + status?: AccountStatus + callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void + } ): Promise { - if (!Array.isArray(signedBundle)) { - return this.sendSignedTransactions([signedBundle], chainId, quote, pstatus, callback, projectAccessKey) + if (!Array.isArray(transactions)) { + return this.sendSignedTransactions([transactions], chainId, options) } - const status = pstatus || (await this.status(chainId)) + + const status = options?.status ?? (await this.status(chainId)) this.mustBeFullyMigrated(status) - const decoratedBundle = await this.decorateTransactions(signedBundle, status, chainId) - callback?.(decoratedBundle) + const decorated = await this.decorateTransactions(transactions, status, chainId) + options?.callback?.(decorated) - return this.relayer(chainId).relay(decoratedBundle, quote, undefined, projectAccessKey) + return this.relayer(chainId).relay(decorated, options) } async fillGasLimits( @@ -1008,32 +1014,36 @@ export class Account { } async sendTransaction( - txs: commons.transaction.Transactionish, + transactions: commons.transaction.Transactionish, chainId: ethers.BigNumberish, - quote?: FeeQuote, - skipPreDecorate: boolean = false, - callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void, options?: { + projectAccessKey?: string + quote?: FeeQuote + preconditions?: Precondition[] + waitForReceipt?: boolean + status?: AccountStatus + callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void nonceSpace?: ethers.BigNumberish serial?: boolean - projectAccessKey?: string + skipPredecorate?: boolean } ): Promise { - const status = await this.status(chainId) - - const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId) - const hasTxs = commons.transaction.fromTransactionish(this.address, predecorated).length > 0 - const signed = hasTxs ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined + const status = options?.status ?? (await this.status(chainId)) + const predecorated = options?.skipPredecorate + ? transactions + : await this.predecorateTransactions(transactions, status, chainId) + const hasTransactions = commons.transaction.fromTransactionish(this.address, predecorated).length > 0 + const signed = hasTransactions ? await this.signTransactions(predecorated, chainId, undefined, options) : undefined const childBundles = await this.orchestrator.predecorateSignedTransactions({ chainId }) const bundles: commons.transaction.SignedTransactionBundle[] = [] - if (signed !== undefined && signed.transactions.length > 0) { + if (signed && signed.transactions.length > 0) { bundles.push(signed) } - bundles.push(...childBundles.filter(b => b.transactions.length > 0)) + bundles.push(...childBundles.filter(b => b.transactions.length)) - return this.sendSignedTransactions(bundles, chainId, quote, undefined, callback, options?.projectAccessKey) + return this.sendSignedTransactions(bundles, chainId, options) } async signTypedData( diff --git a/packages/account/src/signer.ts b/packages/account/src/signer.ts index 56b4b16ab..95c9fe314 100644 --- a/packages/account/src/signer.ts +++ b/packages/account/src/signer.ts @@ -145,18 +145,10 @@ export class AccountSigner implements ethers.AbstractSigner { const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)] - return this.account.sendTransaction( - finalTransactions, - this.chainId, - prepare.feeQuote, - undefined, - undefined, - this.options?.nonceSpace !== undefined - ? { - nonceSpace: this.options.nonceSpace - } - : undefined - ) as Promise // Will always have a transaction response + return this.account.sendTransaction(finalTransactions, this.chainId, { + quote: prepare.feeQuote, + nonceSpace: this.options?.nonceSpace + }) as Promise // Will always have a transaction response } getBalance(blockTag?: ethers.BlockTag | undefined): Promise { diff --git a/packages/auth/src/session.ts b/packages/auth/src/session.ts index 18f08a6e1..7ef60325a 100644 --- a/packages/auth/src/session.ts +++ b/packages/auth/src/session.ts @@ -248,7 +248,7 @@ export class Session { // we could speed this up by sending the migration alongside the jwt request // and letting the API validate it offchain. if (status.onChain.version !== status.version) { - await account.doBootstrap(referenceChainId, undefined, status) + await account.doBootstrap(referenceChainId, status) } const prevConfig = status.config diff --git a/packages/provider/src/client.ts b/packages/provider/src/client.ts index daad98d3b..29becde99 100644 --- a/packages/provider/src/client.ts +++ b/packages/provider/src/client.ts @@ -1,4 +1,8 @@ +import { Precondition, encodePrecondition } from '@0xsequence/api' +import { commons, VERSION } from '@0xsequence/core' import { NetworkConfig } from '@0xsequence/network' +import { TypedData } from '@0xsequence/utils' +import { ethers } from 'ethers' import { ConnectDetails, ConnectOptions, @@ -15,11 +19,7 @@ import { isProviderTransport, messageToBytes } from '.' -import { commons, VERSION } from '@0xsequence/core' -import { TypedData } from '@0xsequence/utils' -import { toExtended } from './extended' import { Analytics, setupAnalytics } from './analytics' -import { ethers } from 'ethers' /** * This session class is meant to persist the state of the wallet connection @@ -490,7 +490,10 @@ export class SequenceClient { }) } - async sendTransaction(tx: ethers.TransactionRequest[] | ethers.TransactionRequest, options?: OptionalChainId): Promise { + async sendTransaction( + tx: ethers.TransactionRequest[] | ethers.TransactionRequest, + options?: OptionalChainId & { preconditions?: Precondition[] } + ): Promise { const transactions = Array.isArray(tx) ? tx : [tx] const chainId = options?.chainId ?? this.getChainId() @@ -508,7 +511,10 @@ export class SequenceClient { value: value !== undefined && value !== null ? ethers.toQuantity(value) : undefined, data: data || undefined, chainId: ethers.toQuantity(chainId) - })) + })), + capabilities: { + ...(options?.preconditions ? { preconditions: options.preconditions.map(encodePrecondition) } : undefined) + } } ], chainId diff --git a/packages/provider/src/signer.ts b/packages/provider/src/signer.ts index 168b1ddee..c9b2080c7 100644 --- a/packages/provider/src/signer.ts +++ b/packages/provider/src/signer.ts @@ -1,12 +1,13 @@ +import { Precondition } from '@0xsequence/api' +import { commons } from '@0xsequence/core' +import { ChainIdLike, NetworkConfig } from '@0xsequence/network' import { ethers } from 'ethers' -import { SequenceProvider, SingleNetworkSequenceProvider } from './provider' import { SequenceClient } from './client' -import { commons } from '@0xsequence/core' -import { ChainIdLike, NetworkConfig } from '@0xsequence/network' +import { SequenceProvider, SingleNetworkSequenceProvider } from './provider' +import { OptionalChainIdLike, OptionalEIP6492 } from './types' import { resolveArrayProperties } from './utils' import { WalletUtils } from './utils/index' -import { OptionalChainIdLike, OptionalEIP6492 } from './types' export interface ISequenceSigner extends Omit { getProvider(): SequenceProvider @@ -36,7 +37,7 @@ export interface ISequenceSigner extends Omit { // It supports any kind of transaction, including regular ethers transactions, and Sequence transactions. sendTransaction( transaction: ethers.TransactionRequest[] | ethers.TransactionRequest, - options?: OptionalChainIdLike + options?: OptionalChainIdLike & { preconditions?: Precondition[] } ): Promise utils: WalletUtils @@ -123,10 +124,13 @@ export class SequenceSigner implements ISequenceSigner { return this.provider.getProvider(chainId) } - async sendTransaction(transaction: ethers.TransactionRequest[] | ethers.TransactionRequest, options?: OptionalChainIdLike) { + async sendTransaction( + transaction: ethers.TransactionRequest[] | ethers.TransactionRequest, + options?: OptionalChainIdLike & { preconditions?: Precondition[] } + ) { const chainId = this.useChainId(options?.chainId) const resolved = await resolveArrayProperties(transaction) - const txHash = await this.client.sendTransaction(resolved, { chainId }) + const txHash = await this.client.sendTransaction(resolved, { chainId, preconditions: options?.preconditions }) const provider = this.getProvider(chainId) try { diff --git a/packages/relayer/src/index.ts b/packages/relayer/src/index.ts index 1d592e5e7..abdc40874 100644 --- a/packages/relayer/src/index.ts +++ b/packages/relayer/src/index.ts @@ -1,7 +1,8 @@ +import { commons } from '@0xsequence/core' import { ethers } from 'ethers' -import { proto } from './rpc-relayer' -import { commons } from '@0xsequence/core' +import { Precondition } from './precondition' +import { proto } from './rpc-relayer' export interface Relayer { // simulate returns the execution results for a list of transactions. @@ -38,10 +39,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?: Precondition[]; waitForReceipt?: boolean } ): Promise // wait for transaction confirmation diff --git a/packages/relayer/src/local-relayer.ts b/packages/relayer/src/local-relayer.ts index 7b170ce75..bee22d46a 100644 --- a/packages/relayer/src/local-relayer.ts +++ b/packages/relayer/src/local-relayer.ts @@ -1,8 +1,8 @@ -import { ethers } from 'ethers' -import { logger } from '@0xsequence/utils' -import { FeeOption, FeeQuote, proto, Relayer } from '.' -import { ProviderRelayer, ProviderRelayerOptions } from './provider-relayer' import { commons } from '@0xsequence/core' +import { logger } from '@0xsequence/utils' +import { ethers } from 'ethers' + +import { FeeOption, FeeQuote, Precondition, ProviderRelayer, ProviderRelayerOptions, Relayer, proto } from '.' export type LocalRelayerOptions = Omit & { signer: ethers.Signer @@ -46,15 +46,14 @@ 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?: Precondition[]; waitForReceipt?: boolean } ): Promise> { - if (quote !== undefined) { + if (options?.quote) { logger.warn(`LocalRelayer doesn't accept fee quotes`) } - 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 @@ -62,13 +61,13 @@ export class LocalRelayer extends ProviderRelayer implements Relayer { // 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 diff --git a/packages/relayer/src/provider-relayer.ts b/packages/relayer/src/provider-relayer.ts index c4acdb02a..931abf6dc 100644 --- a/packages/relayer/src/provider-relayer.ts +++ b/packages/relayer/src/provider-relayer.ts @@ -1,8 +1,9 @@ -import { ethers } from 'ethers' import { walletContracts } from '@0xsequence/abi' -import { FeeOption, FeeQuote, proto, Relayer, SimulateResult } from '.' -import { logger, Optionals } from '@0xsequence/utils' import { commons } from '@0xsequence/core' +import { logger, Optionals } from '@0xsequence/utils' +import { ethers } from 'ethers' + +import { FeeOption, FeeQuote, Precondition, proto, Relayer, SimulateResult } from '.' const DEFAULT_GAS_LIMIT = 800000n @@ -54,9 +55,8 @@ export abstract class ProviderRelayer implements Relayer { abstract gasRefundOptions(address: string, ...transactions: commons.transaction.Transaction[]): Promise abstract relay( - signedTxs: commons.transaction.IntendedTransactionBundle, - quote?: FeeQuote, - waitForReceipt?: boolean + transactions: commons.transaction.IntendedTransactionBundle, + options?: { projectAccessKey?: string; quote?: FeeQuote; preconditions?: Precondition[]; waitForReceipt?: boolean } ): Promise abstract getTransactionCost( diff --git a/packages/relayer/src/rpc-relayer/index.ts b/packages/relayer/src/rpc-relayer/index.ts index 7a7a81c61..a63236366 100644 --- a/packages/relayer/src/rpc-relayer/index.ts +++ b/packages/relayer/src/rpc-relayer/index.ts @@ -1,8 +1,9 @@ -import { ethers } from 'ethers' -import { FeeOption, FeeQuote, Relayer, SimulateResult } from '..' -import * as proto from './relayer.gen' import { commons } from '@0xsequence/core' import { bigintReplacer, getFetchRequest, logger, toHexString } from '@0xsequence/utils' +import { ethers } from 'ethers' + +import { FeeOption, FeeQuote, Precondition, Relayer, SimulateResult } from '..' +import * as proto from './relayer.gen' export { proto } @@ -190,19 +191,17 @@ export class RpcRelayer implements Relayer { } async relay( - signedTxs: commons.transaction.IntendedTransactionBundle, - quote?: FeeQuote, - waitForReceipt: boolean = true, - projectAccessKey?: string + transactions: commons.transaction.IntendedTransactionBundle, + options?: { projectAccessKey?: string; quote?: FeeQuote; preconditions?: Precondition[]; waitForReceipt?: boolean } ): Promise> { logger.info( - `[rpc-relayer/relay] relaying signed meta-transactions ${JSON.stringify(signedTxs, bigintReplacer)} with quote ${JSON.stringify(quote, bigintReplacer)}` + `[rpc-relayer/relay] relaying signed meta-transactions ${JSON.stringify(transactions, bigintReplacer)} with quote ${JSON.stringify(options?.quote, bigintReplacer)}` ) let typecheckedQuote: string | undefined - if (quote !== undefined) { - if (typeof quote._quote === 'string') { - typecheckedQuote = quote._quote + if (options?.quote) { + if (typeof options.quote._quote === 'string') { + typecheckedQuote = options.quote._quote } else { logger.warn('[rpc-relayer/relay] ignoring invalid fee quote') } @@ -213,28 +212,24 @@ export class RpcRelayer implements Relayer { throw new Error('provider is not set') } - const data = commons.transaction.encodeBundleExecData(signedTxs) + const data = commons.transaction.encodeBundleExecData(transactions) const metaTxn = await this.service.sendMetaTxn( { - call: { - walletAddress: signedTxs.intent.wallet, - contract: signedTxs.entrypoint, - input: data - }, + call: { walletAddress: transactions.intent.wallet, contract: transactions.entrypoint, input: data }, quote: typecheckedQuote }, - { ...(projectAccessKey ? { 'X-Access-Key': projectAccessKey } : undefined) } + { ...(options?.projectAccessKey ? { 'X-Access-Key': options.projectAccessKey } : undefined) } ) logger.info(`[rpc-relayer/relay] got relay result ${JSON.stringify(metaTxn, bigintReplacer)}`) - if (waitForReceipt) { - return this.wait(signedTxs.intent.id) + if (options?.waitForReceipt !== false) { + return this.wait(transactions.intent.id) } else { const response = { - hash: signedTxs.intent.id, + hash: transactions.intent.id, confirmations: 0, - from: signedTxs.intent.wallet, + from: transactions.intent.wallet, wait: (_confirmations?: number): Promise => Promise.reject(new Error('impossible')) } @@ -243,7 +238,7 @@ export class RpcRelayer implements Relayer { throw new Error('cannot wait for receipt, relayer has no provider set') } - const waitResponse = await this.wait(signedTxs.intent.id) + const waitResponse = await this.wait(transactions.intent.id) const transactionHash = waitResponse.receipt?.transactionHash if (!transactionHash) { diff --git a/packages/wallet/src/wallet.ts b/packages/wallet/src/wallet.ts index 554ad292a..3272e7004 100644 --- a/packages/wallet/src/wallet.ts +++ b/packages/wallet/src/wallet.ts @@ -1,10 +1,10 @@ -import { ethers } from 'ethers' +import { walletContracts } from '@0xsequence/abi' import { commons, v1, v2 } from '@0xsequence/core' import { ChainId } from '@0xsequence/network' +import { FeeQuote, Precondition, Relayer } from '@0xsequence/relayer' import { SignatureOrchestrator, SignerState, Status } from '@0xsequence/signhub' import { encodeTypedDataDigest, subDigestOf } from '@0xsequence/utils' -import { FeeQuote, Relayer } from '@0xsequence/relayer' -import { walletContracts } from '@0xsequence/abi' +import { ethers } from 'ethers' import { resolveArrayProperties } from './utils' @@ -202,18 +202,18 @@ export class Wallet< async deploy(metadata?: commons.WalletDeployMetadata): Promise { const deployTx = await this.buildDeployTransaction(metadata) - if (deployTx === undefined) { - // Already deployed - return + if (!deployTx) { + return // already deployed + } + + if (!this.relayer) { + throw new Error('no relayer') } - if (!this.relayer) throw new Error('Wallet deploy requires a relayer') + return this.relayer.relay({ ...deployTx, chainId: this.chainId, - intent: { - id: ethers.hexlify(ethers.randomBytes(32)), - wallet: this.address - } + intent: { id: ethers.hexlify(ethers.randomBytes(32)), wallet: this.address } }) } @@ -394,11 +394,14 @@ export class Wallet< } async sendSignedTransaction( - signedBundle: commons.transaction.IntendedTransactionBundle, - quote?: FeeQuote + transactions: commons.transaction.IntendedTransactionBundle, + options?: { projectAccessKey?: string; quote?: FeeQuote; preconditions?: Precondition[]; waitForReceipt?: boolean } ): Promise { - if (!this.relayer) throw new Error('Wallet sendTransaction requires a relayer') - return this.relayer.relay(signedBundle, quote) + if (!this.relayer) { + throw new Error('no relayer') + } + + return this.relayer.relay(transactions, options) } // sendTransaction will dispatch the transaction to the relayer for submission to the network. @@ -408,9 +411,12 @@ export class Wallet< // By default, nonces are generated randomly and assigned so transactioned can be executed // in parallel. However, if you'd like to execute serially, pass { serial: true } as an option. async sendTransaction( - txs: commons.transaction.Transactionish, + transactions: commons.transaction.Transactionish, options?: { + projectAccessKey?: string quote?: FeeQuote + preconditions?: Precondition[] + waitForReceipt?: boolean nonce?: ethers.BigNumberish serial?: boolean } @@ -427,9 +433,9 @@ export class Wallet< nonce = this.randomNonce() } - const signed = await this.signTransactions(txs, nonce) + const signed = await this.signTransactions(transactions, nonce) const decorated = await this.decorateTransactions(signed) - return this.sendSignedTransaction(decorated, options?.quote) + return this.sendSignedTransaction(decorated, options) } async fillGasLimits(txs: commons.transaction.Transactionish): Promise { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56fdc2b59..1859eec48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,6 +255,9 @@ importers: '@0xsequence/abi': specifier: workspace:* version: link:../abi + '@0xsequence/api': + specifier: workspace:* + version: link:../api '@0xsequence/core': specifier: workspace:* version: link:../core From 77e2f46ea3345acedd4e3971b642268f62133a94 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:02:17 +0700 Subject: [PATCH 06/11] Update packages/relayer/src/precondition.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- packages/relayer/src/precondition.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/relayer/src/precondition.ts b/packages/relayer/src/precondition.ts index d49927d85..1ae53bcd0 100644 --- a/packages/relayer/src/precondition.ts +++ b/packages/relayer/src/precondition.ts @@ -267,8 +267,6 @@ function isBigNumberish(value: any): value is ethers.BigNumberish { } } -function encodeBigNumberish( - value: T -): T extends ethers.BigNumberish ? string : undefined { - return value !== undefined ? ethers.toBigInt(value).toString() : (undefined as any) +function encodeBigNumberish(value: ethers.BigNumberish | undefined): string | undefined { + return value !== undefined ? ethers.toBigInt(value).toString() : undefined } From 63f869975c96924be654e881a700a60362c9e758 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:13:21 +0700 Subject: [PATCH 07/11] Add SequenceBatchService for transaction batching Implement SequenceBatchService for token approval and calls. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .../dapp-client/src/utils/sequenceHelper.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/wallet/dapp-client/src/utils/sequenceHelper.ts diff --git a/packages/wallet/dapp-client/src/utils/sequenceHelper.ts b/packages/wallet/dapp-client/src/utils/sequenceHelper.ts new file mode 100644 index 000000000..e4d712fe2 --- /dev/null +++ b/packages/wallet/dapp-client/src/utils/sequenceHelper.ts @@ -0,0 +1,36 @@ +// src/utils/sequenceHelper.ts +import { sequence } from '@0xsequence/provider' +import { ethers } from 'ethers' + +export class SequenceBatchService { + private signer: sequence.WalletSigner + + constructor(signer: sequence.WalletSigner) { + this.signer = signer + } + + /** + * Helper function Approve + Action Atomic Batch + */ + async executeApproveAndCall( + tokenAddress: string, + spender: string, + approveAmount: ethers.BigNumberish, + targetContract: string, + callData: string + ) { + const tokenInterface = new ethers.utils.Interface([ + 'function approve(address spender, uint256 amount) returns (bool)' + ]) + + const dataApprove = tokenInterface.encodeFunctionData('approve', [spender, approveAmount]) + + // Call Payload Batch Array + const txBatch = [ + { to: tokenAddress, data: dataApprove, value: 0 }, + { to: targetContract, data: callData, value: 0 } + ] + + return await this.signer.sendTransaction(txBatch) + } +} From 8be805e68900de321e8b4e187a2d2d0ab2c8be3c Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:19:48 +0700 Subject: [PATCH 08/11] Add TypeScript interfaces for batch calls and ERC20 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- packages/services/builder/src/types.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/services/builder/src/types.ts diff --git a/packages/services/builder/src/types.ts b/packages/services/builder/src/types.ts new file mode 100644 index 000000000..953dcbbcd --- /dev/null +++ b/packages/services/builder/src/types.ts @@ -0,0 +1,19 @@ +import { BigNumberish, BytesLike } from 'ethers' + +export interface BatchCall { + to: string + data: BytesLike + value?: BigNumberish +} + +export interface ERC20ApproveParams { + tokenAddress: string + spender: string + amount: BigNumberish +} + +export interface ERC20TransferParams { + tokenAddress: string + recipient: string + amount: BigNumberish +} From 3d2ea66039b46bcb13ed66cbd86e50f5ce03fe8e Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:26:01 +0700 Subject: [PATCH 09/11] Implement SequenceBatchBuilder for batch transactions Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .../builder/src/SequenceBatchBuilder.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/services/builder/src/SequenceBatchBuilder.ts diff --git a/packages/services/builder/src/SequenceBatchBuilder.ts b/packages/services/builder/src/SequenceBatchBuilder.ts new file mode 100644 index 000000000..bc7137446 --- /dev/null +++ b/packages/services/builder/src/SequenceBatchBuilder.ts @@ -0,0 +1,84 @@ +import { sequence } from '@0xsequence/provider' +import { ethers } from 'ethers' +import { BatchCall, ERC20ApproveParams, ERC20TransferParams } from './types' + +export class SequenceBatchBuilder { + private signer: sequence.WalletSigner + private queue: BatchCall[] = [] + + private static erc20Interface = new ethers.utils.Interface([ + 'function approve(address spender, uint256 amount) returns (bool)', + 'function transfer(address to, uint256 amount) returns (bool)' + ]) + + constructor(signer: sequence.WalletSigner) { + this.signer = signer + } + + /** + * clean Queue + */ + public clear(): this { + this.queue = [] + return this + } + + /** + * add Custom Contract Call in Batch Queue + */ + public addCall(call: BatchCall): this { + this.queue.push({ + to: call.to, + data: call.data || '0x', + value: call.value || 0 + }) + return this + } + + /** + * add Approve token ERC-20/BEP-20 + */ + public addERC20Approve(params: ERC20ApproveParams): this { + const data = SequenceBatchBuilder.erc20Interface.encodeFunctionData('approve', [ + params.spender, + params.amount + ]) + return this.addCall({ to: params.tokenAddress, data, value: 0 }) + } + + /** + * add Transfer token ERC-20/BEP-20 + */ + public addERC20Transfer(params: ERC20TransferParams): this { + const data = SequenceBatchBuilder.erc20Interface.encodeFunctionData('transfer', [ + params.recipient, + params.amount + ]) + return this.addCall({ to: params.tokenAddress, data, value: 0 }) + } + + /** + * pull list Calldata Queue all (Simulate on Tenderly/Phalcon) + */ + public getBatchQueue(): BatchCall[] { + return [...this.queue] + } + + /** + * sent Execute Batch Sequence Relayer + */ + public async execute(): Promise { + if (this.queue.length === 0) { + throw new Error('Batch Queue is empty. Add at least one transaction before executing.') + } + + console.log(`Executing batch of ${this.queue.length} transactions...`) + + // Sequence Signer Array on Transaction Payload + const tx = await this.signer.sendTransaction(this.queue) + + // Clear queue + this.clear() + return tx + } +} From 8e5b0e57afc6b837a3d6fb11dacdaf0df26c7ee9 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:34:00 +0700 Subject: [PATCH 10/11] Add example for SequenceBatchBuilder usage This file contains an example of using SequenceBatchBuilder to approve and transfer ERC20 tokens on the BSC network. Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- packages/services/builder/src/Example.ts | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/services/builder/src/Example.ts diff --git a/packages/services/builder/src/Example.ts b/packages/services/builder/src/Example.ts new file mode 100644 index 000000000..eb94cd66a --- /dev/null +++ b/packages/services/builder/src/Example.ts @@ -0,0 +1,43 @@ +import { sequence } from '@0xsequence/provider' +import { ethers } from 'ethers' +import { SequenceBatchBuilder } from './services/sequence/SequenceBatchBuilder' + +async function runExample() { + const wallet = await sequence.initWallet('mainnet') + const signer = wallet.getSigner(56) // BSC Chain ID: 56 + + const busdAddress = '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56' + const dexRouter = '0x1111111254fb6c44bac0bed2854e76f90643097d' + const recipient = '0x9a72807e1BC8A5e1E178f51E26239d58F511EB3D' + + // Initialize Helper + const batchBuilder = new SequenceBatchBuilder(signer) + + // batchBuilder (Chainable) + batchBuilder + .addERC20Approve({ + tokenAddress: busdAddress, + spender: dexRouter, + amount: ethers.utils.parseUnits('100.0', 18) + }) + .addERC20Transfer({ + tokenAddress: busdAddress, + recipient: recipient, + amount: ethers.utils.parseUnits('50.0', 18) + }) + + // (Optional) pull Payload out Simulate on Tenderly + const payloadForSimulation = batchBuilder.getBatchQueue() + console.log('Payload for Simulation:', JSON.stringify(payloadForSimulation, null, 2)) + + // 3. sent Execute + try { + const tx = await batchBuilder.execute() + console.log('Batch Transaction Submitted! Tx Hash:', tx.hash) + + const receipt = await tx.wait() + console.log('✅ Success in Block:', receipt.blockNumber) + } catch (error) { + console.error('❌ Batch Execution Failed:', error) + } +} From 17496f8e82edbec9e0f62e72b75accc441f17cca Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:39:43 +0700 Subject: [PATCH 11/11] Implement BatchWalletUI for multi-action transactions Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- wagmi-project/src/BatchWalletUI.tsx | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 wagmi-project/src/BatchWalletUI.tsx diff --git a/wagmi-project/src/BatchWalletUI.tsx b/wagmi-project/src/BatchWalletUI.tsx new file mode 100644 index 000000000..7b27e2d78 --- /dev/null +++ b/wagmi-project/src/BatchWalletUI.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react' +import { ethers } from 'ethers' +import { SequenceBatchBuilder } from './services/sequence/SequenceBatchBuilder' + +export const BatchWalletUI = ({ signer }: { signer: any }) => { + const [builder] = useState(() => new SequenceBatchBuilder(signer)) + const [queue, setQueue] = useState([]) + const [loading, setLoading] = useState(false) + + // Input Form States + const [tokenAddress, setTokenAddress] = useState('') + const [recipient, setRecipient] = useState('') + const [amount, setAmount] = useState('') + + // 1. add Queue (update Builder and UI) + const handleAddTransfer = () => { + if (!tokenAddress || !recipient || !amount) return + + builder.addERC20Transfer({ + tokenAddress, + recipient, + amount: ethers.utils.parseUnits(amount, 18) + }) + + // pull list queue update UI + setQueue(builder.getBatchQueue()) + setRecipient('') + setAmount('') + } + + // 2. sent Execute sign Batch + const handleExecute = async () => { + try { + setLoading(true) + const tx = await builder.execute() + alert(`Tx Submitted: ${tx.hash}`) + setQueue([]) // clean UI + } catch (err: any) { + alert(`Error: ${err.message}`) + } finally { + setLoading(false) + } + } + + return ( +
+

🚀 Sequence Multi-Action Batch Sender

+ + {/* Form data */} +
+ setTokenAddress(e.target.value)} + /> + setRecipient(e.target.value)} + /> + setAmount(e.target.value)} + /> + +
+ + {/* list data Queue */} +

Pending Batch Items ({queue.length})

+
    + {queue.map((item, index) => ( +
  • + Target: {item.to} | Value: {item.value.toString()} | Data: {item.data.slice(0, 10)}... +
  • + ))} +
+ + {/* sent Action */} + {queue.length > 0 && ( + + )} +
+ ) +}