From e43217f1094a05733f1c713145293125079ae496 Mon Sep 17 00:00:00 2001 From: Corban Riley Date: Fri, 13 Feb 2026 16:28:07 -0500 Subject: [PATCH 1/4] Use multicall3 to aggregate queued payload checking across N signers --- packages/wallet/wdk/src/sequence/recovery.ts | 84 ++++++++++++++++---- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/packages/wallet/wdk/src/sequence/recovery.ts b/packages/wallet/wdk/src/sequence/recovery.ts index b40b138f8..83cf297b8 100644 --- a/packages/wallet/wdk/src/sequence/recovery.ts +++ b/packages/wallet/wdk/src/sequence/recovery.ts @@ -1,12 +1,19 @@ import { Envelope } from '@0xsequence/wallet-core' import { Config, Constants, Extensions, GenericTree, Payload } from '@0xsequence/wallet-primitives' -import { Address, Hex, Provider, RpcTransport } from 'ox' +import { Abi, AbiFunction, Address, Hex, Provider, RpcTransport } from 'ox' import { MnemonicHandler } from './handlers/mnemonic.js' import { Shared } from './manager.js' import { Actions, Module } from './types/index.js' import { QueuedRecoveryPayload } from './types/recovery.js' import { Kinds, RecoverySigner } from './types/signer.js' +// Multicall3 is deployed at a deterministic address on virtually all EVM chains +const MULTICALL3_ADDRESS: Address.Address = '0xcA11bde05977b3631167028862bE2a173976CA11' + +const AGGREGATE3 = Abi.from([ + 'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) external payable returns ((bool success, bytes returnData)[])', +])[0]! + export interface RecoveryInterface { /** * Retrieves the list of configured recovery signers for a given wallet. @@ -526,22 +533,30 @@ export class Recovery implements RecoveryInterface { return [] } + const recoveryExtension = this.shared.sequence.extensions.recovery const payloads: QueuedRecoveryPayload[] = [] - for (const signer of signers) { - for (const { chainId, provider } of providers) { - try { - const totalPayloads = await Extensions.Recovery.totalQueuedPayloads( - provider, - this.shared.sequence.extensions.recovery, - wallet, - signer.address, - ) - + for (const { chainId, provider } of providers) { + try { + // Batch all totalQueuedPayloads calls for every signer into a single Multicall3 request. + // This reduces N signer calls per network down to 1 call per network. + const totalPayloadsBySigner = await this.fetchTotalQueuedPayloadsBatched( + provider, + recoveryExtension, + wallet, + signers, + ) + + for (let s = 0; s < signers.length; s++) { + const signer = signers[s]! + const totalPayloads = totalPayloadsBySigner[s]! + if (totalPayloads === 0n) continue + + // Only make individual calls for the rare case where payloads actually exist for (let i = 0n; i < totalPayloads; i++) { const payloadHash = await Extensions.Recovery.queuedPayloadHashOf( provider, - this.shared.sequence.extensions.recovery, + recoveryExtension, wallet, signer.address, i, @@ -549,7 +564,7 @@ export class Recovery implements RecoveryInterface { const timestamp = await Extensions.Recovery.timestampForQueuedPayload( provider, - this.shared.sequence.extensions.recovery, + recoveryExtension, wallet, signer.address, payloadHash, @@ -570,11 +585,10 @@ export class Recovery implements RecoveryInterface { // The id is the index + signer address + chainId + wallet address const id = `${i}-${signer.address}-${chainId}-${wallet}` - // Create a new payload const payloadEntry: QueuedRecoveryPayload = { id, index: i, - recoveryModule: this.shared.sequence.extensions.recovery, + recoveryModule: recoveryExtension, wallet: wallet, signer: signer.address, chainId, @@ -586,15 +600,51 @@ export class Recovery implements RecoveryInterface { payloads.push(payloadEntry) } - } catch (err) { - console.error('Recovery.fetchQueuedPayloads error', err) } + } catch (err) { + console.error('Recovery.fetchQueuedPayloads error', err) } } return payloads } + private async fetchTotalQueuedPayloadsBatched( + provider: Provider.Provider, + recoveryExtension: Address.Address, + wallet: Address.Address, + signers: RecoverySigner[], + ): Promise { + const calls = signers.map((signer) => ({ + target: recoveryExtension, + allowFailure: true, + callData: AbiFunction.encodeData(Extensions.Recovery.TOTAL_QUEUED_PAYLOADS, [wallet, signer.address]), + })) + + const response = await provider.request({ + method: 'eth_call', + params: [ + { + to: MULTICALL3_ADDRESS, + data: AbiFunction.encodeData(AGGREGATE3, [calls]), + }, + 'latest', + ], + }) + + const results = AbiFunction.decodeResult(AGGREGATE3, response) as readonly { + success: boolean + returnData: Hex.Hex + }[] + + return results.map((result) => { + if (!result.success || result.returnData === '0x') { + return 0n + } + return Hex.toBigInt(result.returnData) + }) + } + async encodeRecoverySignature(imageHash: Hex.Hex, signer: Address.Address) { const genericTree = await this.shared.sequence.stateProvider.getTree(imageHash) if (!genericTree) { From 0a6cd154be84f52d524bc8df8be02b5c888680f9 Mon Sep 17 00:00:00 2001 From: Corban Riley Date: Tue, 17 Feb 2026 12:20:46 -0500 Subject: [PATCH 2/4] Adding contracts object to Network type to store per network contract configs for common contracts like ensUniversalResolver and multicall3 --- .../relayer/src/relayer/rpc-relayer/index.ts | 12 +- packages/wallet/primitives/src/network.ts | 156 +++++++++++++++++- 2 files changed, 161 insertions(+), 7 deletions(-) diff --git a/packages/services/relayer/src/relayer/rpc-relayer/index.ts b/packages/services/relayer/src/relayer/rpc-relayer/index.ts index d4cac8e71..6a54057d5 100644 --- a/packages/services/relayer/src/relayer/rpc-relayer/index.ts +++ b/packages/services/relayer/src/relayer/rpc-relayer/index.ts @@ -49,12 +49,14 @@ const networkToChain = (network: Network.Network): Chain => { }, } : undefined, - contracts: network.ensAddress - ? { - ensUniversalResolver: { - address: network.ensAddress as `0x${string}`, + contracts: network.contracts + ? Object.entries(network.contracts).reduce( + (acc, [name, address]) => { + acc[name] = { address } + return acc }, - } + {} as Record, + ) : undefined, } as Chain } diff --git a/packages/wallet/primitives/src/network.ts b/packages/wallet/primitives/src/network.ts index e3ddfb6d8..d971f412e 100644 --- a/packages/wallet/primitives/src/network.ts +++ b/packages/wallet/primitives/src/network.ts @@ -1,3 +1,8 @@ +import { Address } from 'ox' + +const DEFAULT_MULTICALL3_ADDRESS: Address.Address = '0xcA11bde05977b3631167028862bE2a173976CA11' +const SEQUENCE_MULTICALL3_ADDRESS: Address.Address = '0xae96419a81516f063744206d4b5E36f3168280f8' + export enum NetworkType { MAINNET = 'mainnet', TESTNET = 'testnet', @@ -21,8 +26,11 @@ export interface Network { name: string decimals: number } - ensAddress?: string deprecated?: true + contracts?: { + multicall3?: Address.Address + ensUniversalResolver?: Address.Address + } } export const ChainId = { @@ -151,7 +159,10 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, - ensAddress: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + ensUniversalResolver: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', + }, }, { chainId: ChainId.SEPOLIA, @@ -169,6 +180,9 @@ export const ALL: Network[] = [ name: 'Sepolia Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.POLYGON, @@ -186,6 +200,9 @@ export const ALL: Network[] = [ name: 'POL', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.POLYGON_AMOY, @@ -203,6 +220,9 @@ export const ALL: Network[] = [ name: 'Amoy POL', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.POLYGON_ZKEVM, @@ -220,6 +240,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.BSC, @@ -237,6 +260,9 @@ export const ALL: Network[] = [ name: 'BNB', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.BSC_TESTNET, @@ -254,6 +280,9 @@ export const ALL: Network[] = [ name: 'Testnet BNB', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.OPTIMISM, @@ -271,6 +300,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.OPTIMISM_SEPOLIA, @@ -288,6 +320,9 @@ export const ALL: Network[] = [ name: 'Sepolia Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.ARBITRUM, @@ -305,6 +340,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.ARBITRUM_SEPOLIA, @@ -322,6 +360,9 @@ export const ALL: Network[] = [ name: 'Sepolia Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.ARBITRUM_NOVA, @@ -339,6 +380,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.AVALANCHE, @@ -356,6 +400,9 @@ export const ALL: Network[] = [ name: 'AVAX', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.AVALANCHE_TESTNET, @@ -373,6 +420,9 @@ export const ALL: Network[] = [ name: 'Testnet AVAX', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.GNOSIS, @@ -390,6 +440,9 @@ export const ALL: Network[] = [ name: 'XDAI', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.BASE, @@ -407,6 +460,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.BASE_SEPOLIA, @@ -424,6 +480,9 @@ export const ALL: Network[] = [ name: 'Sepolia Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.HOMEVERSE, @@ -441,6 +500,9 @@ export const ALL: Network[] = [ name: 'OAS', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.HOMEVERSE_TESTNET, @@ -458,6 +520,9 @@ export const ALL: Network[] = [ name: 'Testnet OAS', decimals: 18, }, + contracts: { + multicall3: SEQUENCE_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.XAI, @@ -475,6 +540,9 @@ export const ALL: Network[] = [ name: 'XAI', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.XAI_SEPOLIA, @@ -492,6 +560,9 @@ export const ALL: Network[] = [ name: 'Sepolia XAI', decimals: 18, }, + contracts: { + multicall3: SEQUENCE_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.B3, @@ -509,6 +580,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.B3_SEPOLIA, @@ -526,6 +600,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.APECHAIN, @@ -543,6 +620,9 @@ export const ALL: Network[] = [ name: 'ApeCoin', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.APECHAIN_TESTNET, @@ -560,6 +640,9 @@ export const ALL: Network[] = [ name: 'ApeCoin', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.BLAST, @@ -577,6 +660,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.BLAST_SEPOLIA, @@ -594,6 +680,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.TELOS, @@ -611,6 +700,9 @@ export const ALL: Network[] = [ name: 'TLOS', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.TELOS_TESTNET, @@ -628,6 +720,9 @@ export const ALL: Network[] = [ name: 'TLOS', decimals: 18, }, + contracts: { + multicall3: SEQUENCE_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.SKALE_NEBULA, @@ -645,6 +740,9 @@ export const ALL: Network[] = [ name: 'SKALE Fuel', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.SKALE_NEBULA_TESTNET, @@ -662,6 +760,9 @@ export const ALL: Network[] = [ name: 'SKALE Fuel', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.SONEIUM, @@ -679,6 +780,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.SONEIUM_MINATO, @@ -696,6 +800,9 @@ export const ALL: Network[] = [ name: 'Ether', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.TOY_TESTNET, @@ -713,6 +820,9 @@ export const ALL: Network[] = [ name: 'TOY', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.IMMUTABLE_ZKEVM, @@ -730,6 +840,9 @@ export const ALL: Network[] = [ name: 'IMX', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.IMMUTABLE_ZKEVM_TESTNET, @@ -747,6 +860,9 @@ export const ALL: Network[] = [ name: 'IMX', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.MOONBEAM, @@ -764,6 +880,9 @@ export const ALL: Network[] = [ name: 'GLMR', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.MOONBASE_ALPHA, @@ -781,6 +900,9 @@ export const ALL: Network[] = [ name: 'GLMR', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.ETHERLINK, @@ -798,6 +920,9 @@ export const ALL: Network[] = [ name: 'Tez', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.ETHERLINK_SHADOWNET_TESTNET, @@ -815,6 +940,9 @@ export const ALL: Network[] = [ name: 'Tez', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.MONAD, @@ -832,6 +960,9 @@ export const ALL: Network[] = [ name: 'MON', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { chainId: ChainId.MONAD_TESTNET, @@ -849,6 +980,9 @@ export const ALL: Network[] = [ name: 'MON', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { @@ -867,6 +1001,9 @@ export const ALL: Network[] = [ name: 'SOMI', decimals: 18, }, + contracts: { + multicall3: SEQUENCE_MULTICALL3_ADDRESS, + }, }, { @@ -885,6 +1022,9 @@ export const ALL: Network[] = [ name: 'STT', decimals: 18, }, + contracts: { + multicall3: SEQUENCE_MULTICALL3_ADDRESS, + }, }, { @@ -903,6 +1043,9 @@ export const ALL: Network[] = [ name: 'TCENT', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { @@ -921,6 +1064,9 @@ export const ALL: Network[] = [ name: 'ETH', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { @@ -939,6 +1085,9 @@ export const ALL: Network[] = [ name: 'SAND', decimals: 18, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, { @@ -957,6 +1106,9 @@ export const ALL: Network[] = [ name: 'USDC', decimals: 6, }, + contracts: { + multicall3: DEFAULT_MULTICALL3_ADDRESS, + }, }, ] From c9e88e4f6503dcca631399fe12a6e9ec1d59def7 Mon Sep 17 00:00:00 2001 From: Corban Riley Date: Tue, 17 Feb 2026 12:22:16 -0500 Subject: [PATCH 3/4] Parallelzing fetched queued payloads and adding fallback for when multicall3 batching either fails or is not configured on the network --- packages/wallet/wdk/src/sequence/recovery.ts | 173 ++++++++++++------- 1 file changed, 111 insertions(+), 62 deletions(-) diff --git a/packages/wallet/wdk/src/sequence/recovery.ts b/packages/wallet/wdk/src/sequence/recovery.ts index 83cf297b8..41e9eb4e7 100644 --- a/packages/wallet/wdk/src/sequence/recovery.ts +++ b/packages/wallet/wdk/src/sequence/recovery.ts @@ -7,9 +7,6 @@ import { Actions, Module } from './types/index.js' import { QueuedRecoveryPayload } from './types/recovery.js' import { Kinds, RecoverySigner } from './types/signer.js' -// Multicall3 is deployed at a deterministic address on virtually all EVM chains -const MULTICALL3_ADDRESS: Address.Address = '0xcA11bde05977b3631167028862bE2a173976CA11' - const AGGREGATE3 = Abi.from([ 'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) external payable returns ((bool success, bytes returnData)[])', ])[0]! @@ -524,6 +521,7 @@ export class Recovery implements RecoveryInterface { .filter((network) => (chainId ? network.chainId === chainId : true)) .map((network) => ({ chainId: network.chainId, + multicall3Address: network.contracts?.multicall3, provider: Provider.from(RpcTransport.fromHttp(network.rpcUrl)), })) @@ -536,75 +534,102 @@ export class Recovery implements RecoveryInterface { const recoveryExtension = this.shared.sequence.extensions.recovery const payloads: QueuedRecoveryPayload[] = [] - for (const { chainId, provider } of providers) { - try { - // Batch all totalQueuedPayloads calls for every signer into a single Multicall3 request. - // This reduces N signer calls per network down to 1 call per network. - const totalPayloadsBySigner = await this.fetchTotalQueuedPayloadsBatched( - provider, - recoveryExtension, - wallet, - signers, - ) - - for (let s = 0; s < signers.length; s++) { - const signer = signers[s]! - const totalPayloads = totalPayloadsBySigner[s]! - if (totalPayloads === 0n) continue - - // Only make individual calls for the rare case where payloads actually exist - for (let i = 0n; i < totalPayloads; i++) { - const payloadHash = await Extensions.Recovery.queuedPayloadHashOf( - provider, - recoveryExtension, - wallet, - signer.address, - i, - ) - - const timestamp = await Extensions.Recovery.timestampForQueuedPayload( + await Promise.all( + providers.map(async ({ chainId, provider, multicall3Address }) => { + try { + let totalPayloadsBySigner: bigint[] + + if (multicall3Address) { + try { + // Batch all totalQueuedPayloads calls for every signer into a single Multicall3 request. + // This reduces N signer calls per network down to 1 call per network. + totalPayloadsBySigner = await this.fetchTotalQueuedPayloadsBatched( + provider, + recoveryExtension, + wallet, + signers, + multicall3Address, + ) + } catch (err) { + console.error( + `Recovery.fetchQueuedPayloads multicall3 failed for chainId ${chainId}, retrying with individual calls:`, + err, + ) + totalPayloadsBySigner = await this.fetchTotalQueuedPayloadsFallback( + provider, + recoveryExtension, + wallet, + signers, + ) + } + } else { + totalPayloadsBySigner = await this.fetchTotalQueuedPayloadsFallback( provider, recoveryExtension, wallet, - signer.address, - payloadHash, + signers, ) + } - const payload = await this.shared.sequence.stateProvider.getPayload(payloadHash) + for (let s = 0; s < signers.length; s++) { + const signer = signers[s]! + const totalPayloads = totalPayloadsBySigner[s]! + if (totalPayloads === 0n) continue + + // Only make individual calls for the rare case where payloads actually exist + for (let i = 0n; i < totalPayloads; i++) { + const payloadHash = await Extensions.Recovery.queuedPayloadHashOf( + provider, + recoveryExtension, + wallet, + signer.address, + i, + ) + + const timestamp = await Extensions.Recovery.timestampForQueuedPayload( + provider, + recoveryExtension, + wallet, + signer.address, + payloadHash, + ) + + const payload = await this.shared.sequence.stateProvider.getPayload(payloadHash) + + // If ready, we need to check if it was executed already + // for this, we check if the wallet nonce for the given space + // is greater than the nonce in the payload + if (timestamp < Date.now() / 1000 && payload && Payload.isCalls(payload.payload)) { + const nonce = await this.shared.modules.wallets.getNonce(chainId, wallet, payload.payload.space) + if (nonce > i) { + continue + } + } - // If ready, we need to check if it was executed already - // for this, we check if the wallet nonce for the given space - // is greater than the nonce in the payload - if (timestamp < Date.now() / 1000 && payload && Payload.isCalls(payload.payload)) { - const nonce = await this.shared.modules.wallets.getNonce(chainId, wallet, payload.payload.space) - if (nonce > i) { - continue + // The id is the index + signer address + chainId + wallet address + const id = `${i}-${signer.address}-${chainId}-${wallet}` + + const payloadEntry: QueuedRecoveryPayload = { + id, + index: i, + recoveryModule: recoveryExtension, + wallet: wallet, + signer: signer.address, + chainId, + startTimestamp: timestamp, + endTimestamp: timestamp + signer.requiredDeltaTime, + payloadHash, + payload: payload?.payload, } - } - // The id is the index + signer address + chainId + wallet address - const id = `${i}-${signer.address}-${chainId}-${wallet}` - - const payloadEntry: QueuedRecoveryPayload = { - id, - index: i, - recoveryModule: recoveryExtension, - wallet: wallet, - signer: signer.address, - chainId, - startTimestamp: timestamp, - endTimestamp: timestamp + signer.requiredDeltaTime, - payloadHash, - payload: payload?.payload, + payloads.push(payloadEntry) } - - payloads.push(payloadEntry) } + } catch (err) { + console.error(`Recovery.fetchQueuedPayloads error for chainId ${chainId}:`, err) } - } catch (err) { - console.error('Recovery.fetchQueuedPayloads error', err) - } - } + }), + ) return payloads } @@ -614,6 +639,7 @@ export class Recovery implements RecoveryInterface { recoveryExtension: Address.Address, wallet: Address.Address, signers: RecoverySigner[], + multicall3Address: Address.Address, ): Promise { const calls = signers.map((signer) => ({ target: recoveryExtension, @@ -625,7 +651,7 @@ export class Recovery implements RecoveryInterface { method: 'eth_call', params: [ { - to: MULTICALL3_ADDRESS, + to: multicall3Address, data: AbiFunction.encodeData(AGGREGATE3, [calls]), }, 'latest', @@ -645,6 +671,29 @@ export class Recovery implements RecoveryInterface { }) } + private fetchTotalQueuedPayloadsFallback = async ( + provider: Provider.Provider, + recoveryExtension: Address.Address, + wallet: Address.Address, + signers: RecoverySigner[], + ): Promise => { + const result: bigint[] = signers.map(() => 0n) + + // Fallback to individual calls if the multicall3 call fails + for (let s = 0; s < signers.length; s++) { + const signer = signers[s]! + const totalPayloads = await Extensions.Recovery.totalQueuedPayloads( + provider, + recoveryExtension, + wallet, + signer.address, + ) + result[s] = totalPayloads + } + + return result + } + async encodeRecoverySignature(imageHash: Hex.Hex, signer: Address.Address) { const genericTree = await this.shared.sequence.stateProvider.getTree(imageHash) if (!genericTree) { From 31201f3994d7e05f3f7ea43535fe087f09601725 Mon Sep 17 00:00:00 2001 From: Corban Riley Date: Tue, 17 Feb 2026 12:33:57 -0500 Subject: [PATCH 4/4] Add includeTestnets option to RecoverySettings, default: false so fetchingQueuedPayloads can skip testnets to reduce requests --- packages/wallet/wdk/src/sequence/manager.ts | 2 ++ packages/wallet/wdk/src/sequence/recovery.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/wallet/wdk/src/sequence/manager.ts b/packages/wallet/wdk/src/sequence/manager.ts index 80ca1653c..783dfc201 100644 --- a/packages/wallet/wdk/src/sequence/manager.ts +++ b/packages/wallet/wdk/src/sequence/manager.ts @@ -233,6 +233,7 @@ export const ManagerOptionsDefaults = { defaultRecoverySettings: { requiredDeltaTime: 2592000n, // 30 days (in seconds) minTimestamp: 0n, + includeTestnets: false, }, multiInjectedProviderDiscovery: true, @@ -369,6 +370,7 @@ export function applyManagerOptionsDefaults(options?: ManagerOptions): ResolvedM export type RecoverySettings = { requiredDeltaTime: bigint minTimestamp: bigint + includeTestnets?: boolean } export type Databases = { diff --git a/packages/wallet/wdk/src/sequence/recovery.ts b/packages/wallet/wdk/src/sequence/recovery.ts index 41e9eb4e7..2e6074364 100644 --- a/packages/wallet/wdk/src/sequence/recovery.ts +++ b/packages/wallet/wdk/src/sequence/recovery.ts @@ -518,7 +518,13 @@ export class Recovery implements RecoveryInterface { async fetchQueuedPayloads(wallet: Address.Address, chainId?: number): Promise { // Create providers for each network const providers = this.shared.sequence.networks - .filter((network) => (chainId ? network.chainId === chainId : true)) + .filter((network) => + chainId + ? network.chainId === chainId + : !this.shared.sequence.defaultRecoverySettings.includeTestnets + ? network.type !== 'testnet' + : true, + ) .map((network) => ({ chainId: network.chainId, multicall3Address: network.contracts?.multicall3,