/** * WagmiEventHandler * * Handles wallet event tracking by hooking into Wagmi v2's config.subscribe() * and TanStack Query's MutationCache. This replaces the EIP-1193 provider * wrapping approach when Wagmi mode is enabled. */ import { FormoAnalytics } from "../FormoAnalytics"; import { QueryClient, WagmiConfig } from "./types"; /** * How long the markers stay trustworthy after the last handler goes away. * * A marker only means "already announced" while the SDK is actually watching * wagmi. With no handler mounted nothing observes a disconnect, so a wallet * that disconnects and reconnects in that window would otherwise look * unchanged and its genuine connect would be swallowed for the rest of the * page load. * * Rebuilding the SDK tears the old handler down and builds a new one, and * `FormoAnalytics.init()` is async, so the gap is real but short. This grace * period spans a rebuild and little else: markers survive it, and a longer * unmount discards them so the next connect is treated as new. */ export declare const MARKER_GRACE_MS = 3000; /** Test hook. Real page loads reset this naturally. */ export declare function __resetSeededWallet(): void; export declare class WagmiEventHandler { private formo; private wagmiConfig; private queryClient?; private unsubscribers; private trackingState; /** * Set by `cleanup()`. Every deferred emission checks it, so a handler that * has been torn down cannot push an event into a queue the replacement SDK * instance no longer owns. */ private disposed; /** * Drop markers for wallets wagmi no longer holds, and report whether any * announced wallet is still connected. * * Called when a handler mounts. Cancelling the expiry timer on the strength * of "some connection exists" let a marker for a wallet that had since * disconnected survive indefinitely, suppressing its genuine reconnect. */ private pruneMarkersForLostWallets; /** Identifies this handler's destination for emit-ownership. */ private ownerKey?; /** * Whether this handler is the one allowed to emit for its destination. * * A handler that lost the race still tracks state, so it is ready to take * over the moment the owner goes away; it just does not emit. */ private get isEmittingOwner(); /** Reentrancy guard for `reconcileWithLiveState()`. */ private reconciling; /** * Ticket for each active-connection transition. Emissions are awaited, and * wagmi can move again in that window; a continuation whose ticket is no * longer current must not write stale state over the newer one. */ private transitionGeneration; /** * A chain reported while wagmi was not `connected`, so the chain callback * had to drop it. Replayed by `reconcileWithLiveState()`. */ private pendingChainId?; /** * A `disconnected` status that arrived while the lock was held. Final-state * reconciliation cannot see a disconnect/reconnect cycle that completed * inside that window, so this records that one happened. */ private missedDisconnect; /** * Track processed mutation states to prevent duplicate event emissions * Key format: `${mutationId}:${status}` */ private processedMutations; /** * Track processed query states to prevent duplicate event emissions * Key format: `${queryHash}:${status}` */ private processedQueries; /** * Store transaction details from BROADCASTED events for use in CONFIRMED/REVERTED * Key: transactionHash, Value: transaction details including the original sender address */ /** * Shared per destination, not per handler. * * A broadcast observed by handler A, followed by a replacement B taking * over before the receipt arrives, used to lose the confirmation entirely: * A no longer emits because it is not the owner, and B had no record of the * broadcast to match the receipt against. */ private get pendingTransactions(); /** Broadcast batches awaiting `callsStatus`, shared like the map above. */ private get pendingBatches(); constructor(formoAnalytics: FormoAnalytics, wagmiConfig: WagmiConfig, queryClient?: QueryClient); /** * Set up listeners for wallet connection, disconnection, and chain changes */ private setupConnectionListeners; /** * Adopt a connection that already existed when this handler was created. * * `config.subscribe` reports *changes* only, so a wallet restored by wagmi's * mount-time `reconnect()` is invisible to a handler built afterwards. That * is the normal case whenever the host app loads the SDK lazily (dynamic * import, `requestIdleCallback`) and can also happen on a plain mount, since * `FormoAnalytics.init()` and wagmi's reconnect are both async and race. * * Without this seed the whole session is lost: no connect event, and * `lastAddress` stays undefined so the signature and transaction handlers * drop their events too, and the eventual disconnect carries neither address * nor chain id. * * `subscribe(..., { fireImmediately: true })` is not a substitute. It reports * the current value with no distinct previous value, so the connect branch's * `prevStatus !== "connected"` test never passes, and wagmi v3 does not * honour the option at all. * * Deliberately synchronous, and deliberately does NOT take the * `isProcessing` lock. Everything that mutates tracking state runs before * any await, so no status change can interleave with it. Holding the lock * across the `connect()` emission instead would make `handleStatusChange()` * drop - not defer - a disconnect or wallet switch that lands in that * window, which is exactly when a lazily loaded SDK is racing app activity. * * A genuine `connected` transition cannot double-emit alongside this seed. * `config.subscribe` only fires on change, and the connect branch treats a * re-entry for an already-tracked address as a no-op (or a chain change). */ private seedFromCurrentState; /** * Re-assert the tracked wallet into central state. * * Needed when an awaited emission clears the central namespace after a newer * transition has already adopted a different wallet: the two then disagree, * and `trackEvent()` reads the central one. */ private restoreCentralStateFromTracking; /** * Drop the wallet this handler is attributing events to. * * Used whenever central state refuses to hold the wallet - an excluded path * or chain, or an opt-out. Keeping the address privately would let the * mutation handlers label signatures and transactions with an identity the * SDK has deliberately declined to know. */ private releaseTrackedWallet; /** * Adopt the live connection if one exists and nothing is tracked yet. * * `seedFromCurrentState()` runs once at construction and can legitimately * decline: wagmi may still be filling `connections`, the chain may not have * arrived, or tracking may be suppressed because the app happens to be on an * excluded path. `config.subscribe` only reports *changes*, so an unchanged * connection would then stay invisible for the rest of the page load and * every later signature and transaction would be dropped. * * Idempotent. The page-load marker keeps it from re-emitting a connect for a * wallet already announced, so this is safe to call from any listener. */ retryAdoption(): void; /** Whether the wallet this handler tracks is the one wagmi currently has. */ private tracksLiveWallet; /** * Put the tracked wallet back into central state without emitting anything. * * Used when the two have diverged for a reason that is not a wallet change: * an opt-out `reset()`, or a stale disconnect completing after a newer * transition adopted a different wallet. */ private resyncCentralState; /** Whether the wallet currently tracked has already had a connect emitted. */ private isCurrentWalletAnnounced; /** * Handle status changes (connect/disconnect) */ private handleStatusChange; /** * Re-run the status logic when the handler's view diverges from wagmi. * * `handleStatusChange()` drops - rather than defers - anything that arrives * while it holds `isProcessing`, and it holds that lock across an awaited * `connect()` / `disconnect()` emission. A wallet that reconnects inside * that window would otherwise leave the handler believing it is * disconnected for the rest of the page load, silently dropping every later * signature and transaction. The mirror case leaves a disconnected wallet * marked connected. * * Converges: each pass either adopts the live wallet or releases the tracked * one, so the next pass finds the two in agreement and returns. */ private reconcileWithLiveState; /** * Handle a switch to a different account on an already-connected wallet. * * Only fires for a genuine in-place switch. A fresh connect and a disconnect * both move `state.status`, and that listener is registered first, so it has * already recorded the new address synchronously by the time this runs. The * `lastAddress` check below is what makes the two paths mutually exclusive. */ private handleActiveAddressChangeEntryKick; private handleActiveAddressChange; /** * Record and emit a chain move for the wallet already being tracked. * * Shared by `handleChainChange()` and the connector-fallback path, which * has to apply the chain itself: the chain callback defers while the new * connection is not adopted yet, so nothing else would. */ private applyChainForTrackedWallet; /** * Handle chain ID changes */ private handleChainChange; /** * Set up mutation tracking for signatures and transactions */ private setupMutationTracking; /** * Set up query tracking for transaction confirmations * Listens for waitForTransactionReceipt queries to detect CONFIRMED status */ private setupQueryTracking; /** * Handle query cache events (transaction confirmations) */ private handleQueryEvent; /** * Settle a just-registered batch from a status query that already ran. * * Best-effort by design: the minimal QueryClient interface the SDK * accepts is not guaranteed to expose cache lookup, and a missing * `getAll` just means settlement waits for the next query event, which * is where it normally comes from anyway. */ private settleFromCachedCallsStatus; /** * Settle an EIP-5792 batch from a `callsStatus` query. * * Only batches whose broadcast this SDK observed are settled: the batch id * must be in `pendingBatches`, for the same reason receipt queries are * gated on an observed hash - queries are visible to any code sharing the * QueryClient, and emitting for an id we never saw broadcast would let a * forged query invent transactions. * * Outcome semantics are shared with the EIP-1193 path (`src/evm/batch.ts`): * per-call receipts outrank the batch verdict, an atomic batch's single * receipt reaches every call, and a 600 leaves receipt-less calls * unsettled rather than guessed. */ private handleCallsStatusQuery; /** * Handle waitForTransactionReceipt query completion * Emits CONFIRMED or REVERTED transaction status */ private handleTransactionReceiptQuery; /** * Handle mutation cache events (signatures, transactions) */ private handleMutationEvent; /** * Handle signature mutations (signMessage, signTypedData) */ /** * Is a PENDING wagmi mutation already covering this wallet request? * * The hybrid-capture dedup. TanStack dispatches `pending` BEFORE the * mutationFn issues the wallet call (verified against query-core), so a * hook-driven request always finds its mutation here and the request * wrapper stands down; an imperative viem call never does, and the * wrapper captures it. Matching is by mutation type, refined with cheap * parameter checks where the shapes allow, and errs toward NOT skipping: * a duplicate is visible and diagnosable, a silent loss is neither. */ hasMatchingPendingMutation(method: string, params: unknown[]): boolean; /** * Wallet attribution for mutation- and query-derived events. * * Mid-session events (signatures, transactions) fire after the peer * lookup has resolved, so a WalletConnect connector names its actual * signer here - the first observable consumer of the peer cache. */ private mutationAttribution; private handleSignatureMutation; /** * Handle transaction mutations (sendTransaction, writeContract) */ private handleTransactionMutation; /** * One `transaction` event per call in an EIP-5792 batch, wagmi path. * * Mirrors `EvmRequestTracker.trackBatchedCalls` exactly: the CALL is the * unit of attribution, so each call gets its own STARTED at pending and * BROADCASTED (with `batch_id`) when the wallet returns an id. The batch's * on-chain outcome arrives through the `callsStatus` query, handled in * `handleCallsStatusQuery`. * * Rejection matches the 1193 path's rule: only a user rejection (4001 * anywhere in the error chain) marks the calls rejected - one dismissal * dismisses the whole prompt, so every call in it is rejected, and * reporting only the first would undercount. Any other error (a wallet * without EIP-5792 support, a transport failure) emits nothing further: * inventing a rejection the user never made would be worse. */ private handleSendCallsMutation; /** * Get the current Wagmi state * Supports both getState() method and direct state property access * for compatibility with different Wagmi wrappers (RainbowKit, etc.) */ private getState; /** * Chain of the connection that is currently active. * * Authoritative, because `state.chainId` is a single global value. With * `syncConnectedChain: false` it stays on the chain the APP selected while * the connection reports the chain the WALLET is actually on, so seeding * from the global could label a wallet on an excluded chain as an allowed * one and send events the exclusion forbids. */ private getActiveConnectionChainId; /** * Get the currently connected address from Wagmi state */ private getConnectedAddress; /** * Whether any live connection still holds this address. * * Distinguishes "the user switched away from this account" (gone from every * connection) from "this account merely stopped being the current one" * (still connected through its own connector). */ private isAddressConnected; /** * A specific connector's name and rdns, for the fallback-wrapped * provider's request-derived events. * * The NAME is exactly what the hook path emits for that connector * (`getConnectorName`: the live WalletConnect peer for a WalletConnect * connector, the connector's own name otherwise), so a branded connector * keeps its name on both paths and a session that changes wallets * renames on both. The rdns is the connector's when wagmi knows it as * ONE value (EIP-6963 discovered connectors carry theirs, some as a * one-element list); a connector that matches several rdns values does * not say which one this provider is, so nothing is passed and the * registry keeps the sniffed one. */ private attributionForConnector; /** * Resolver handed to the registry at wrap time, read at each request * start. BOUND to the connector whose `getProvider` resolved to the * wrapped provider, not to whichever connector is current: an app can * keep a wallet client over a previous connector's provider and issue a * request through it after switching, and that request belongs to the * wallet that handles it. The peer name is still read live. A provider * SHARED by two connectors is re-bound when the new connector's * resolution lands (one microtask for an injected connector); a request * issued inside that gap keeps the previous label. */ private boundConnectorAttribution; /** `getConnectorName` for a SPECIFIC connector, current or not. */ private nameForConnector; /** * Get the connector name from Wagmi state */ private getConnectorName; /** * Start resolving the wallet behind a WalletConnect connection. * * Fire-and-forget on purpose: emission paths are synchronous by design * and must never wait (see the connect marker comment). Called at the * START of the status/address flows rather than only at read time - the * lookup is one microtask for an initialised connector, and the emission * sits behind several awaits, so kicking early usually means even the * FIRST connect names the real wallet. When the race is lost the event * honestly says "WalletConnect" and every later event names the peer. */ /** * Install the request wrapper on the active connector's provider. * * This is what lets wagmi mode capture IMPERATIVE viem calls * (walletClient.sendTransaction / .signMessage / .writeContract / raw * request), which create no mutation and were silently lost. Hook-driven * calls stay owned by the mutation handlers via the pending-mutation * dedup. Fire-and-forget per connection; a provider that cannot be * produced simply keeps mutation-only capture. */ /** * Connections THIS instance has wrapped. Deliberately per-instance, not * module-level: a module-level guard let a rebuilt SDK instance skip * re-wrapping, so ownership stayed with the torn-down instance and every * capture died in its closed queue - and it also stopped a second * write-key instance from ever registering. Re-wrapping is safe and * cheap: the request tracker rebinds ownership of an intact wrapper. */ /** * Latest wrap attempt per connector. Every kick re-resolves * getProvider() - a connector can hand out a REPLACEMENT provider after * a reconnect, so a once-only guard would leave the new session's * provider unwrapped forever. Re-wrapping is idempotent: the request * tracker rebinds ownership of an intact wrapper. The epoch lets a slow * resolution from a superseded kick (an earlier session, an earlier * chain) be discarded instead of overwriting fresher state. */ private wrapEpochs; /** Consecutive automatic wrap retries per connector; see below. */ private wrapRetryCounts; /** Pending retry timers, cancelled by cleanup(). */ private wrapRetryTimers; /** * Bumped on every observed disconnect. Every wrap attempt captures it * at kick time and its resolution stands down on a mismatch, so a * disconnect invalidates ALL pending wraps at once - including ones * whose getProvider() had not resolved yet, which no per-connector * bookkeeping can reach because nothing has been recorded for them. */ private wrapSessionGeneration; /** The active connector this instance wrapped, and its provider, for * chain updates. Kept as a pair so a chain report is only ever applied * to the provider of the connector it describes. Keyed by connector, * not by connection record: wagmi REPLACES the connection object on * every account or chain update, so record identity is not stable. */ private fallbackConnector?; private fallbackProvider?; private wrapActiveConnectorProvider; private kickWalletConnectPeerLookup; /** * Clean up all subscriptions */ cleanup(): void; } //# sourceMappingURL=WagmiEventHandler.d.ts.map