import { EIP6963ProviderDetail } from "mipd"; import { Address, ChainID, EIP1193Provider, IFormoEventProperties } from "../types"; import { WalletStateStore } from "../wallet/WalletStateStore"; import { EvmProviderRegistry } from "./EvmProviderRegistry"; import { AutocaptureEventType } from "../tracking/TrackingPolicy"; /** What the tracker needs from the SDK that owns it. */ export interface EvmEventTrackerDeps { /** Whether this wallet event kind is enabled for autocapture. */ isAutocaptureEnabled(eventType: AutocaptureEventType): boolean; /** Visitor-level or page-level suppression: never LEARN a wallet. */ isTrackingSuppressed(): boolean; /** Whether an event on this chain would actually be sent. */ willTrackEvent(chainId?: ChainID): boolean; /** Retry detect events after the active chain changes. */ retryDetection(): void; /** In wagmi mode the SDK does not wrap providers itself. */ isWagmiMode(): boolean; /** Emission. The tracker decides WHEN; the SDK owns the event API. */ connect(params: { chainId: ChainID; address: Address; }, properties?: IFormoEventProperties): Promise; disconnect(params?: { chainId?: ChainID; address?: Address; }): Promise; chain(params: { chainId: ChainID; address?: Address; }, properties?: IFormoEventProperties): Promise; /** * `rdns` is the session dedup key for detect, so it must reach `detect()` * exactly as the wallet announced it. Required here rather than optional * so no caller can quietly substitute a placeholder for a missing value. */ detect(params: { providerName: string; rdns: string; }): Promise; /** * Install the request wrapper that captures signatures and transactions. * * Still owned by the SDK: it is the next thing to move, and keeping it out * of this change keeps the diff reviewable. */ registerRequestListeners(provider: EIP1193Provider): boolean; } export declare class EvmEventTracker { private readonly wallet; private readonly registry; private readonly deps; /** * Providers adopted through `registerProvider` rather than discovered. * Announcement-driven cleanup must not touch them: they are never in an * announcement list, so "missing from the announcement" is their normal * state, not evidence of removal. A Set rather than a WeakSet because * the corrected-detect pass iterates it; the registry's detail list * holds these providers strongly for the instance's life anyway. */ private externallyRegistered; /** * Registered providers whose session this SDK has NOT yet learned. * * Only these are retried on page hits and opt-in. Re-running adoption for * every registered provider looked idempotent but was not: adoption is * the same path a live `accountsChanged` takes, and that path treats a * provider with a different address from the active one as a wallet * switch. With two registered providers, or one registered next to a * discovered wallet, every page hit emitted a disconnect and a connect * that no user action caused. * * A provider enters at registration (it may have no session yet, or be * refused because tracking is suppressed), again whenever a handler * refuses its signal while suppressed (noted on entry AND at the * suppressed commit, since the handlers gate on the active provider and * re-check suppression after an await), and again for all of them when * an opt-out purges identity. It leaves the first time a handler commits * its session unsuppressed, or when it is untracked. Retrying reads the * provider's SYNCHRONOUS accounts only - no RPC - so a registered * provider whose session never fires `accountsChanged` (a wallet that * signals `connect` alone while `autocapture.connect` is off, which * installs a chain-only observer) is still adopted on the next hit. "No * RPC" holds for the pending provider itself; the handler's switch * arbitration may still ask the ACTIVE provider for its accounts, as it * does for any live signal. * * Replay goes through the accounts handler, which has live wallet-switch * semantics: a different provider with a different address is a switch. * So a pending provider is replayed only while no OTHER wallet is active * and known - it waits, at no cost, until that wallet is gone - unless * its own signal was refused (`latestRefused`): then the replay does * exactly what the live signal would have done, switch included. */ private pendingAdoptions; /** * The pending provider whose own live signal was refused while * suppressed, LATEST only. The SDK follows one active wallet and the * newest signal wins, so replaying only the last refused signal reaches * the state the live signals would have reached, without the switches * in between; an earlier refused provider stays pending, unprivileged. */ private latestRefused?; private dropRefusal; /** * An opt-out purges wallet identity. Every registered session is then * unknown again (pending; no refusal is ADDED, so the opt-in replay of a * merely connected wallet does not switch, while a refusal already * standing from an excluded route keeps its meaning), and the * opt-in retry must re-learn all of them: with the active wallet's * address gone, the accounts handler cannot even tell a second wallet's * accounts apart from the active one's, and would ignore them. */ markRegisteredAdoptionsPending(): void; /** * Remember a registered provider's signal that suppression refuses. * * Replay privilege (`latestRefused`) goes only to a provider whose * session the replay can actually read - synchronous accounts - so an * accountless `connect` (a chain-only observation, or a session still * pairing) cannot displace an earlier refusal that is adoptable. */ private noteRefusalIfSuppressed; /** * Registered providers whose session ended and have not signalled a * new one. Some providers keep stale synchronous `accounts` after a * disconnect; replaying those would re-install a session that is over. * Any later session signal (connect, accountsChanged) lifts this. */ private awaitingNewSession; /** A handler has learned this provider's session; nothing is pending. */ private settleAdoption; /** * A registered provider's session has ended. Its NEXT session is not * yet learned, and may announce itself in a way no handler adopts (a * `connect` alone while `autocapture.connect` is off installs a * chain-only observer), so it is pending again for the page-hit retry. */ private reopenAdoption; /** One retry scan at a time; a call during a scan queues one more. */ private retryInFlight; private retryRequested; /** * The connect this SDK has already reported for a provider. * * Connection REPORTING, which is why it lives with the handlers rather than * with wallet state. The store tells us when a provider stops being active, * which is when a record must lapse. */ private _announcedConnect; /** * The EIP-6963 discovery subscription, so teardown can release it. Left * live, a disposed SDK kept reacting to every later wallet announcement: * wrapping providers and emitting detect events from an instance the host * had already replaced. */ private unsubscribeDiscovery?; /** Discovery store retained for complete listener cleanup. */ private discoveryStore?; constructor(wallet: WalletStateStore, registry: EvmProviderRegistry, deps: EvmEventTrackerDeps); /** Stop listening for wallet announcements. Called from SDK teardown. */ cleanup(): void; /** Set by `cleanup()`; every awaited continuation checks it. */ private disposed; /** Bumped when a registered provider's session ends. See reopenAdoption. */ private sessionGenerations; private sessionGeneration; /** Drop a provider's reported connect. Called when it stops being active. */ forgetAnnouncedConnect(provider: EIP1193Provider): void; /** * Helper method to check if a provider is different from the currently active one * @param provider The provider to check * @returns true if there's a provider mismatch, false otherwise */ private isProviderMismatch; /** * Track an EIP-1193 provider by wrapping its request method and adding event listeners * Note: This is only used in non-Wagmi mode. When Wagmi is enabled, all tracking * happens through Wagmi's connector system instead of EIP-1193/EIP-6963. * @param provider The EIP-1193 provider to track */ trackEIP1193Provider(provider: EIP1193Provider): void; trackProviders(providers: readonly EIP6963ProviderDetail[]): void; /** * Adopt a provider the page constructed rather than announced. * * Discovery only ever sees EIP-6963 announcements and `window.ethereum`. * A WalletConnect or Ledger provider is a constructed object that does * neither, so without this entry point its whole session is invisible - * the P-2403 gap. The pipeline from here on is the same one every * discovered provider takes: registry, detect event, listeners, request * wrapper. * * A session that already exists at registration is seeded from the * provider's SYNCHRONOUS `accounts` state (WalletConnect exposes it), via * the same accounts-arrival path a live `accountsChanged` takes. No RPC: * nothing analytics-only may go on a wallet's transport, and * WalletConnect's serialised relay socket is the very case that rule * exists for. */ adoptExternalProvider(detail: EIP6963ProviderDetail): boolean; /** * Finish what registration could not. * * A registered provider's session may not have existed at registration, * or its adoption was refused because tracking was suppressed (opt-out, * excluded route) - and an existing session may never emit another * accountsChanged, so nothing else would ever retry. Called when * suppression can have ended (opt-in, page navigation). * * Adoption is retried ONLY for providers not yet adopted * (`pendingAdoptions`), only from their synchronous accounts, and only * once suppression has actually ended. The corrected-detect pass below * runs for every registered provider: it is deduplicated per session by * rdns, so repeating it is free. */ retryExternalAdoptions(): void; private registerAccountsChangedListener; private onAccountsChanged; /** * Handles changes to the accounts of a given EIP-1193 provider. * * @param provider - The EIP-1193 provider whose accounts have changed. * @param accounts - The new array of account addresses. An empty array indicates a disconnect. * @returns A promise that resolves when the account change has been processed. * * If the accounts array is empty and the provider is the active provider, this method triggers * a disconnect flow. Otherwise, it updates the state to reflect the new accounts as needed. */ private _handleAccountsChanged; private registerChainChangedListener; private onChainChanged; /** * Record a provider's chain from its `connect` event, and nothing else. * * Used when connect autocapture is off. `connect` carries `chainId` in its * payload, so this needs no RPC - unlike the full handler, which resolves * the account. */ private registerConnectChainObserver; /** * Whether a connect for this wallet still needs reporting. * * True when nothing has been reported for this provider, or when the account * changed. A wallet already reported is not reported again. * * Deliberately does NOT re-report to correct a chain. When `accountsChanged` * wins the race on a provider that exposes no synchronous `chainId`, the * connect carries 0 - honestly, since the chain is unknown at that instant - * and the `connect` payload that follows knows the real one. Emitting again * to relabel would mean two connects for one connection, which is the bug * this whole path exists to prevent. That payload still corrects * `currentChainId`, so everything after it is attributed properly. */ private shouldReportConnect; /** * Record a connect as reported - but only if it will actually be sent. * * `connect()` passes through `shouldTrack()`, which refuses an unresolvable * chain when `tracking.excludeChains` is configured. Marking a refused event * as reported would suppress the authoritative one that follows. */ private markConnectReported; private registerConnectListener; private registerDisconnectListener; private onConnected; /** Whether Formo owns provider lifecycle tracking. */ private tracksDiscovered; getProviders(): Promise; detectWallets(providers: readonly EIP6963ProviderDetail[]): Promise; detectableProviders(): readonly EIP6963ProviderDetail[]; /** * Seed a provider's chain from whatever it already exposes synchronously. * * Most EIP-1193 implementations carry a `chainId` property (MetaMask, * WalletConnect, Coinbase). Reading it costs nothing and cannot block. * * There is deliberately no RPC fallback. An earlier version probed with * `eth_chainId` when a provider was first tracked, on the theory that * tracking time is off the user's critical path. It is not: a serialized * transport has ONE queue, so a stalled probe sits in front of every later * signature and transaction the dapp makes. It could also land out of order * - a slow probe response overwriting a newer `chainChanged` - and relabel * events onto a chain the wallet had already left. * * A provider that exposes nothing stays unknown until it emits * `chainChanged` or `connect`, and unknown is reported honestly as 0. */ private seedProviderChainFromState; untrackProvider(provider: EIP1193Provider): void; /** * Clean up providers that are no longer available * This helps maintain consistent state and prevents memory leaks */ private cleanupUnavailableProviders; } //# sourceMappingURL=EvmEventTracker.d.ts.map