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, + }, }, ] 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 b40b138f8..2e6074364 100644 --- a/packages/wallet/wdk/src/sequence/recovery.ts +++ b/packages/wallet/wdk/src/sequence/recovery.ts @@ -1,12 +1,16 @@ 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' +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. @@ -514,9 +518,16 @@ 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, provider: Provider.from(RpcTransport.fromHttp(network.rpcUrl)), })) @@ -526,73 +537,167 @@ 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) { + await Promise.all( + providers.map(async ({ chainId, provider, multicall3Address }) => { try { - const totalPayloads = await Extensions.Recovery.totalQueuedPayloads( - provider, - this.shared.sequence.extensions.recovery, - wallet, - signer.address, - ) - - for (let i = 0n; i < totalPayloads; i++) { - const payloadHash = await Extensions.Recovery.queuedPayloadHashOf( - provider, - this.shared.sequence.extensions.recovery, - wallet, - signer.address, - i, - ) - - const timestamp = await Extensions.Recovery.timestampForQueuedPayload( + 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, - this.shared.sequence.extensions.recovery, + 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}` - - // Create a new payload - const payloadEntry: QueuedRecoveryPayload = { - id, - index: i, - recoveryModule: this.shared.sequence.extensions.recovery, - 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', err) + console.error(`Recovery.fetchQueuedPayloads error for chainId ${chainId}:`, err) } + }), + ) + + return payloads + } + + private async fetchTotalQueuedPayloadsBatched( + provider: Provider.Provider, + recoveryExtension: Address.Address, + wallet: Address.Address, + signers: RecoverySigner[], + multicall3Address: Address.Address, + ): 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: multicall3Address, + 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) + }) + } + + 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 payloads + return result } async encodeRecoverySignature(imageHash: Hex.Hex, signer: Address.Address) {