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/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/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/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/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/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/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/client.ts b/packages/provider/src/client.ts index d2883ec1c..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,13 +490,36 @@ 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 }) + 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() + + 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) + })), + capabilities: { + ...(options?.preconditions ? { preconditions: options.preconditions.map(encodePrecondition) } : undefined) + } + } + ], + chainId + }) + )[ethers.toQuantity(chainId)] } async getWalletContext(): Promise { 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/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/provider/src/transports/wallet-request-handler.ts b/packages/provider/src/transports/wallet-request-handler.ts index 911e9c089..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 { @@ -36,6 +37,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 +426,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 +437,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 +478,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 +559,171 @@ 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, capabilities } = 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 (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, preconditions } + ) + ) + } + + if (preconditions) { + throw new Error(`wallet_sendCalls preconditions not supported`) + } + + 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 }, + ...(this.prompter && { preconditions: { supported: true, versions: ['1.0'] } }) + } + ]) + ) + + default: + return {} + } + } + // smart wallet method case 'sequence_getWalletContext': { return account.contexts @@ -862,17 +1026,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; preconditions?: Precondition[] } + ): 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; 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..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 @@ -75,6 +74,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/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/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/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/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 { 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 81891927c..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 @@ -293,7 +296,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 +478,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 +1947,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 +9849,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 +9870,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 +9912,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 +9926,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