import { createPublicClient, getContract, http, type Abi, type Address, type GetContractReturnType, type Hex, type PublicClient, } from 'viem'; import { KEY_REGISTRY_ABI, SETTLEMENT_ABI, VALSET_DRIVER_ABI, VOTING_POWER_PROVIDER_ABI, } from './abis/index.js'; import type { CrossChainAddress, OperatorVotingPower, OperatorWithKeys } from './types/index.js'; import type { BlockTagPreference } from './utils/core.js'; const getReadContract = ( client: PublicClient, address: Address, abi: TAbi ): GetContractReturnType => getContract({ address, abi, client, }); /** @notice Single multicall request entry. */ export type MulticallRequest = { address: Address; abi: Abi; functionName: string; args: readonly unknown[]; }; type DriverReadMethod = | 'getCurrentEpoch' | 'getCurrentEpochDuration' | 'getCurrentEpochStart' | 'getNextEpoch' | 'getNextEpochDuration' | 'getNextEpochStart' | 'getEpochStart' | 'getEpochDuration' | 'getEpochIndex'; type DriverReadArgsMap = { getCurrentEpoch: []; getCurrentEpochDuration: []; getCurrentEpochStart: []; getNextEpoch: []; getNextEpochDuration: []; getNextEpochStart: []; getEpochStart: [number]; getEpochDuration: [number]; getEpochIndex: [number]; }; export const DRIVER_METHOD = { CURRENT_EPOCH: 'getCurrentEpoch', CURRENT_EPOCH_DURATION: 'getCurrentEpochDuration', CURRENT_EPOCH_START: 'getCurrentEpochStart', NEXT_EPOCH: 'getNextEpoch', NEXT_EPOCH_DURATION: 'getNextEpochDuration', NEXT_EPOCH_START: 'getNextEpochStart', EPOCH_START: 'getEpochStart', EPOCH_DURATION: 'getEpochDuration', EPOCH_INDEX: 'getEpochIndex', } as const; /** @notice Build a map of PublicClient instances keyed by chainId from RPC URLs. */ export const buildClientMap = async ( rpcUrls: readonly string[] ): Promise> => { const clients = new Map(); await Promise.all( rpcUrls.map(async url => { const client = createPublicClient({ transport: http(url), batch: { multicall: { deployless: true } }, }); const chainId = await client.getChainId(); clients.set(chainId, client); }) ); return clients; }; /** @notice Retrieve a client by chainId or throw if it is missing. */ export const getClientOrThrow = ( clients: Map, chainId: number ): PublicClient => { const client = clients.get(chainId); if (!client) { throw new Error(`No client for chain ID ${chainId}`); } return client; }; const getDriverContract = ( client: PublicClient, driver: CrossChainAddress ): GetContractReturnType => getReadContract(client, driver.address, VALSET_DRIVER_ABI); export const readDriverNumber = async ({ client, driver, method, blockTag, args, }: { client: PublicClient; driver: CrossChainAddress; method: M; blockTag: BlockTagPreference; args?: DriverReadArgsMap[M]; }): Promise => { const driverContract = getDriverContract(client, driver); const reader = ( driverContract.read as unknown as Record< DriverReadMethod, (...params: unknown[]) => Promise > )[method]; const result = args && args.length > 0 ? await reader(args, { blockTag }) : await reader({ blockTag }); return Number(result); }; export const readDriverNumbersBatch = async ({ client, driver, method, blockTag, argsList, }: { client: PublicClient; driver: CrossChainAddress; method: M; blockTag: BlockTagPreference; argsList: readonly DriverReadArgsMap[M][]; }): Promise => { if (argsList.length === 0) return []; const requests: MulticallRequest[] = argsList.map(args => ({ address: driver.address, abi: VALSET_DRIVER_ABI, functionName: method, args: (args ?? []) as readonly unknown[], })); const results = await executeChunkedMulticall({ client, requests, blockTag, }); return results.map(result => Number(result)); }; export const readDriverConfigAt = async ( client: PublicClient, driver: CrossChainAddress, epochStart: number, blockTag: BlockTagPreference ): Promise => { const driverContract = getDriverContract(client, driver); return driverContract.read.getConfigAt([epochStart], { blockTag, }); }; export const readDriverConfigAtBatch = async ( client: PublicClient, driver: CrossChainAddress, epochStarts: readonly number[], blockTag: BlockTagPreference ): Promise => { if (epochStarts.length === 0) return []; const uniqueEpochStarts: number[] = []; const epochIndexByStart = new Map(); for (const epochStart of epochStarts) { if (!epochIndexByStart.has(epochStart)) { epochIndexByStart.set(epochStart, uniqueEpochStarts.length); uniqueEpochStarts.push(epochStart); } } const requests: MulticallRequest[] = uniqueEpochStarts.map(epochStart => ({ address: driver.address, abi: VALSET_DRIVER_ABI, functionName: 'getConfigAt', args: [epochStart], })); const uniqueResults = await executeChunkedMulticall({ client, requests, blockTag, }); return epochStarts.map(epochStart => uniqueResults[epochIndexByStart.get(epochStart)!]); }; /** @notice Execute multicall requests using viem batching defaults. */ export const executeChunkedMulticall = async ({ client, requests, blockTag, }: { client: PublicClient; requests: readonly MulticallRequest[]; blockTag: BlockTagPreference; }): Promise => { if (requests.length === 0) return []; const run = async (batch: readonly MulticallRequest[]): Promise => { try { return (await client.multicall({ allowFailure: false, blockTag, contracts: batch.map(item => ({ address: item.address, abi: item.abi, functionName: item.functionName as never, args: item.args, })), })) as readonly unknown[]; } catch (error) { const message = error instanceof Error ? error.message : String(error); if (batch.length > 1) { const mid = Math.ceil(batch.length / 2); const left = await run(batch.slice(0, mid)); const right = await run(batch.slice(mid)); return [...left, ...right]; } const first = batch[0]; const label = first ? `${first.functionName} @ ${first.address}` : 'multicall'; // eslint-disable-next-line no-console console.warn(`Multicall failed for ${label} (requests ${batch.length}): ${message}`); throw error; } }; const rawResult = await run(requests); return rawResult as unknown as T[]; }; export const fetchNetworkIdentifiers = async ({ client, driver, blockTag, }: { client: PublicClient; driver: CrossChainAddress; blockTag: BlockTagPreference; }): Promise<{ network: Address; subnetwork: Hex }> => { const results = await client.multicall({ allowFailure: false, blockTag, contracts: [ { address: driver.address, abi: VALSET_DRIVER_ABI, functionName: 'NETWORK', }, { address: driver.address, abi: VALSET_DRIVER_ABI, functionName: 'SUBNETWORK', }, ], }); return { network: results[0] as Address, subnetwork: results[1] as Hex, }; }; export const fetchSettlementDomain = async ({ client, settlement, blockTag, }: { client: PublicClient; settlement: CrossChainAddress; blockTag: BlockTagPreference; }): Promise<{ fields: string; name: string; version: string; chainId: bigint; verifyingContract: Address; salt: Hex; extensions: bigint[]; }> => { const settlementContract = getReadContract(client, settlement.address, SETTLEMENT_ABI); const domainTuple = await settlementContract.read.eip712Domain({ blockTag }); const [fields, name, version, chainId, verifyingContract, salt, extensions] = domainTuple as readonly [Hex, string, string, bigint, Address, Hex, readonly bigint[]]; return { fields: fields as string, name, version, chainId, verifyingContract, salt, extensions: [...extensions] as bigint[], }; }; export const fetchVotingPowers = async ({ client, provider, blockTag, timestamp, useMulticall, }: { client: PublicClient; provider: CrossChainAddress; blockTag: BlockTagPreference; timestamp: bigint; useMulticall: boolean; }): Promise => { const timestampSeconds = BigInt(timestamp); const providerContract = getReadContract(client, provider.address, VOTING_POWER_PROVIDER_ABI); if (useMulticall) { const operators = await providerContract.read.getOperatorsAt( [timestampSeconds] as unknown as Parameters< typeof providerContract.read.getOperatorsAt >[0], { blockTag, } ); if (operators.length === 0) return []; const operatorList = Array.from(operators, operator => operator as Address); const results = await Promise.all( operatorList.map(operator => providerContract.read.getOperatorVotingPowersAt( [operator, '0x' as Hex, timestampSeconds] as unknown as Parameters< typeof providerContract.read.getOperatorVotingPowersAt >[0], { blockTag, } ) ) ); return operatorList.map((operator, index) => { const vaults = (results[index] ?? []) as readonly { vault: Address; value: bigint }[]; return { operator, vaults: Array.from(vaults, v => ({ vault: v.vault as Address, votingPower: v.value, })), }; }); } const votingPowers = (await providerContract.read.getVotingPowersAt( [[], timestampSeconds] as unknown as Parameters< typeof providerContract.read.getVotingPowersAt >[0], { blockTag, } )) as readonly { operator: Address; vaults: readonly { vault: Address; value: bigint }[] }[]; return votingPowers.map(vp => ({ operator: vp.operator, vaults: vp.vaults.map(v => ({ vault: v.vault, votingPower: v.value, })), })); }; export const fetchVotingPowersBatch = async ({ client, provider, blockTag, timestamps, useMulticall, }: { client: PublicClient; provider: CrossChainAddress; blockTag: BlockTagPreference; timestamps: readonly bigint[]; useMulticall: boolean; }): Promise => { if (timestamps.length === 0) return []; const providerContract = getReadContract(client, provider.address, VOTING_POWER_PROVIDER_ABI); if (!useMulticall) { const values = await Promise.all( timestamps.map(timestampSeconds => providerContract.read.getVotingPowersAt( [[], timestampSeconds] as unknown as Parameters< typeof providerContract.read.getVotingPowersAt >[0], { blockTag, } ) ) ); return values.map(votingPowers => ( votingPowers as readonly { operator: Address; vaults: readonly { vault: Address; value: bigint }[]; }[] ).map(vp => ({ operator: vp.operator, vaults: vp.vaults.map(v => ({ vault: v.vault, votingPower: v.value, })), })) ); } const results = await executeChunkedMulticall< readonly { operator: Address; vaults: readonly { vault: Address; value: bigint }[]; }[] >({ client, requests: timestamps.map(timestampSeconds => ({ address: provider.address, abi: VOTING_POWER_PROVIDER_ABI, functionName: 'getVotingPowersAt', args: [[], timestampSeconds], })), blockTag, }); return results.map(votingPowers => (votingPowers ?? []).map(vp => ({ operator: vp.operator, vaults: vp.vaults.map(v => ({ vault: v.vault, votingPower: v.value, })), })) ); }; export const fetchKeysAt = async ({ client, provider, blockTag, timestamp, useMulticall, }: { client: PublicClient; provider: CrossChainAddress; blockTag: BlockTagPreference; timestamp: bigint; useMulticall: boolean; }): Promise => { const timestampNumber = Number(timestamp); const keyRegistry = getReadContract(client, provider.address, KEY_REGISTRY_ABI); if (useMulticall) { const operators = await keyRegistry.read.getKeysOperatorsAt([timestampNumber] as const, { blockTag, }); if (operators.length === 0) return []; const operatorList = Array.from(operators, operator => operator as Address); const results = await Promise.all( operatorList.map(operator => client.readContract({ address: provider.address, abi: KEY_REGISTRY_ABI, functionName: 'getKeysAt', args: [operator, timestampNumber] as const, blockTag, }) ) ); return operatorList.map((operator, index) => { const operatorKeys = (results[index] ?? []) as readonly { tag: number | bigint; payload: Hex; }[]; return { operator, keys: Array.from(operatorKeys, key => ({ tag: typeof key.tag === 'bigint' ? Number(key.tag) : key.tag, payload: key.payload as Hex, })), }; }); } const keys = (await client.readContract({ address: provider.address, abi: KEY_REGISTRY_ABI, functionName: 'getKeysAt', args: [timestampNumber] as const, blockTag, })) as readonly { operator: Address; keys: readonly { tag: number | bigint; payload: Hex }[]; }[]; return keys.map(k => ({ operator: k.operator, keys: Array.from(k.keys, key => ({ tag: typeof key.tag === 'bigint' ? Number(key.tag) : key.tag, payload: key.payload, })), })); }; export const fetchKeysAtBatch = async ({ client, provider, blockTag, timestamps, useMulticall, }: { client: PublicClient; provider: CrossChainAddress; blockTag: BlockTagPreference; timestamps: readonly bigint[]; useMulticall: boolean; }): Promise => { if (timestamps.length === 0) return []; const timestampNumbers = timestamps.map(timestamp => Number(timestamp)); const fetchDirect = async (): Promise => { const results = await Promise.all( timestampNumbers.map(timestampNumber => client.readContract({ address: provider.address, abi: KEY_REGISTRY_ABI, functionName: 'getKeysAt', args: [timestampNumber] as const, blockTag, }) ) ); return results.map(entries => ( entries as readonly { operator: Address; keys: readonly { tag: number | bigint; payload: Hex }[]; }[] ).map(k => ({ operator: k.operator, keys: Array.from(k.keys, key => ({ tag: typeof key.tag === 'bigint' ? Number(key.tag) : key.tag, payload: key.payload, })), })) ); }; if (!useMulticall) { return fetchDirect(); } const results = await executeChunkedMulticall< readonly { operator: Address; keys: readonly { tag: number | bigint; payload: Hex }[]; }[] >({ client, requests: timestampNumbers.map(timestampNumber => ({ address: provider.address, abi: KEY_REGISTRY_ABI, functionName: 'getKeysAt', args: [timestampNumber], })), blockTag, }); return results.map(entries => (entries ?? []).map(k => ({ operator: k.operator, keys: Array.from(k.keys, key => ({ tag: typeof key.tag === 'bigint' ? Number(key.tag) : key.tag, payload: key.payload, })), })) ); }; export type { DriverReadArgsMap, DriverReadMethod };