@formo/analytics 1.35.1 → 1.35.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +134 -0
  2. package/dist/cjs/src/FormoAnalytics.js +636 -178
  3. package/dist/cjs/src/constants/base.d.ts +0 -1
  4. package/dist/cjs/src/constants/base.js +1 -3
  5. package/dist/cjs/src/event/EventFactory.js +1 -1
  6. package/dist/cjs/src/utils/mergeDeepRight.d.ts +22 -0
  7. package/dist/cjs/src/utils/mergeDeepRight.js +54 -0
  8. package/dist/cjs/src/version.d.ts +1 -1
  9. package/dist/cjs/src/version.js +1 -1
  10. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +193 -1
  11. package/dist/cjs/src/wagmi/WagmiEventHandler.js +1317 -77
  12. package/dist/cjs/src/wagmi/types.d.ts +6 -0
  13. package/dist/esm/src/FormoAnalytics.d.ts +134 -0
  14. package/dist/esm/src/FormoAnalytics.js +636 -178
  15. package/dist/esm/src/constants/base.d.ts +0 -1
  16. package/dist/esm/src/constants/base.js +0 -2
  17. package/dist/esm/src/event/EventFactory.js +1 -1
  18. package/dist/esm/src/utils/mergeDeepRight.d.ts +22 -0
  19. package/dist/esm/src/utils/mergeDeepRight.js +51 -0
  20. package/dist/esm/src/version.d.ts +1 -1
  21. package/dist/esm/src/version.js +1 -1
  22. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +193 -1
  23. package/dist/esm/src/wagmi/WagmiEventHandler.js +1315 -76
  24. package/dist/esm/src/wagmi/types.d.ts +6 -0
  25. package/dist/index.umd.min.js +1 -1
  26. package/package.json +1 -10
  27. package/dist/cjs/src/ramda/internal/_curry1.d.ts +0 -10
  28. package/dist/cjs/src/ramda/internal/_curry1.js +0 -26
  29. package/dist/cjs/src/ramda/internal/_curry2.d.ts +0 -10
  30. package/dist/cjs/src/ramda/internal/_curry2.js +0 -43
  31. package/dist/cjs/src/ramda/internal/_curry3.d.ts +0 -10
  32. package/dist/cjs/src/ramda/internal/_curry3.js +0 -74
  33. package/dist/cjs/src/ramda/internal/_has.d.ts +0 -2
  34. package/dist/cjs/src/ramda/internal/_has.js +0 -7
  35. package/dist/cjs/src/ramda/internal/_isObject.d.ts +0 -2
  36. package/dist/cjs/src/ramda/internal/_isObject.js +0 -7
  37. package/dist/cjs/src/ramda/internal/_isPlaceholder.d.ts +0 -2
  38. package/dist/cjs/src/ramda/internal/_isPlaceholder.js +0 -7
  39. package/dist/cjs/src/ramda/mergeDeepRight.d.ts +0 -3
  40. package/dist/cjs/src/ramda/mergeDeepRight.js +0 -14
  41. package/dist/cjs/src/ramda/mergeDeepWithKey.d.ts +0 -3
  42. package/dist/cjs/src/ramda/mergeDeepWithKey.js +0 -20
  43. package/dist/cjs/src/ramda/mergeWithKey.d.ts +0 -3
  44. package/dist/cjs/src/ramda/mergeWithKey.js +0 -26
  45. package/dist/esm/src/ramda/internal/_curry1.d.ts +0 -10
  46. package/dist/esm/src/ramda/internal/_curry1.js +0 -20
  47. package/dist/esm/src/ramda/internal/_curry2.d.ts +0 -10
  48. package/dist/esm/src/ramda/internal/_curry2.js +0 -37
  49. package/dist/esm/src/ramda/internal/_curry3.d.ts +0 -10
  50. package/dist/esm/src/ramda/internal/_curry3.js +0 -68
  51. package/dist/esm/src/ramda/internal/_has.d.ts +0 -2
  52. package/dist/esm/src/ramda/internal/_has.js +0 -4
  53. package/dist/esm/src/ramda/internal/_isObject.d.ts +0 -2
  54. package/dist/esm/src/ramda/internal/_isObject.js +0 -4
  55. package/dist/esm/src/ramda/internal/_isPlaceholder.d.ts +0 -2
  56. package/dist/esm/src/ramda/internal/_isPlaceholder.js +0 -4
  57. package/dist/esm/src/ramda/mergeDeepRight.d.ts +0 -3
  58. package/dist/esm/src/ramda/mergeDeepRight.js +0 -9
  59. package/dist/esm/src/ramda/mergeDeepWithKey.d.ts +0 -3
  60. package/dist/esm/src/ramda/mergeDeepWithKey.js +0 -15
  61. package/dist/esm/src/ramda/mergeWithKey.d.ts +0 -3
  62. package/dist/esm/src/ramda/mergeWithKey.js +0 -21
@@ -70,6 +70,259 @@ var RESERVED_FIELDS = new Set([
70
70
  "function_name",
71
71
  "function_args",
72
72
  ]);
73
+ /**
74
+ * Connections already announced to a destination during this page load.
75
+ *
76
+ * Deliberately module scoped rather than per handler or per FormoAnalytics
77
+ * instance. The duplicate this prevents comes from the SDK instance itself
78
+ * being rebuilt - a provider remount, an options change, HMR - while the
79
+ * wallet never disconnected, and per-instance state cannot see that.
80
+ *
81
+ * A real disconnect clears the entry, so a genuine reconnect within the same
82
+ * page load still emits. A new page load starts with a fresh module scope.
83
+ */
84
+ var announcedConnections = new Set();
85
+ /**
86
+ * Connections announced, keyed by the wagmi connection OBJECT.
87
+ *
88
+ * Wagmi builds a fresh connection object on every `connect()`, and replaces it
89
+ * whenever anything about the connection changes. So finding the SAME object
90
+ * still in the store is proof that nothing happened in between - no
91
+ * disconnect, no reconnect - and the marker can be trusted with no time limit
92
+ * at all. That covers the case the grace period alone gets wrong: an SDK
93
+ * unmounted for a long time while the wallet simply stayed connected.
94
+ *
95
+ * The address-keyed set above remains the fallback for the genuinely
96
+ * ambiguous case, where the object was replaced but a chain switch and a
97
+ * reconnect are indistinguishable after the fact.
98
+ *
99
+ * Weak, so a dead connection is collected rather than pinned.
100
+ */
101
+ var announcedByConnection = new WeakMap();
102
+ /**
103
+ * Keyed by write key as well as address: two SDK instances for different write
104
+ * keys are separate analytics destinations with separate queues, so the second
105
+ * must still receive its own connect for the same wallet.
106
+ *
107
+ * The connector uid is NOT part of the key. Wagmi keeps that uid stable across
108
+ * a disconnect and reconnect through the same connector, so including it never
109
+ * distinguished one connection occurrence from the next. The observation
110
+ * window below is what makes a reconnect emit again.
111
+ */
112
+ var announceKey = function (writeKey, address) {
113
+ return "".concat(writeKey, ":").concat(address.toLowerCase());
114
+ };
115
+ /**
116
+ * How long the markers stay trustworthy after the last handler goes away.
117
+ *
118
+ * A marker only means "already announced" while the SDK is actually watching
119
+ * wagmi. With no handler mounted nothing observes a disconnect, so a wallet
120
+ * that disconnects and reconnects in that window would otherwise look
121
+ * unchanged and its genuine connect would be swallowed for the rest of the
122
+ * page load.
123
+ *
124
+ * Rebuilding the SDK tears the old handler down and builds a new one, and
125
+ * `FormoAnalytics.init()` is async, so the gap is real but short. This grace
126
+ * period spans a rebuild and little else: markers survive it, and a longer
127
+ * unmount discards them so the next connect is treated as new.
128
+ */
129
+ export var MARKER_GRACE_MS = 3000;
130
+ /**
131
+ * Liveness is tracked PER WRITE KEY, because markers are.
132
+ *
133
+ * A global count would let one destination keep another's markers alive: with
134
+ * destination B still mounted, A's would never expire, so a wallet that
135
+ * disconnected and reconnected while A was unmounted would have its genuine
136
+ * connect suppressed when A came back.
137
+ */
138
+ var liveHandlers = new Map();
139
+ var markerExpiry = new Map();
140
+ /**
141
+ * @param adoptsLiveConnection whether the arriving handler actually found the
142
+ * announced connection still in place. Only then may it cancel the expiry:
143
+ * a replacement that mounts over a DISCONNECTED store has observed nothing,
144
+ * so cancelling would strand the markers for the rest of the page load and
145
+ * suppress a genuine connect hours later.
146
+ */
147
+ function retainMarkers(writeKey, adoptsLiveConnection) {
148
+ var _a;
149
+ liveHandlers.set(writeKey, ((_a = liveHandlers.get(writeKey)) !== null && _a !== void 0 ? _a : 0) + 1);
150
+ if (!adoptsLiveConnection)
151
+ return;
152
+ var pending = markerExpiry.get(writeKey);
153
+ if (pending) {
154
+ clearTimeout(pending);
155
+ markerExpiry.delete(writeKey);
156
+ }
157
+ }
158
+ function releaseMarkers(writeKey) {
159
+ var _a, _b, _c;
160
+ var remaining = Math.max(0, ((_a = liveHandlers.get(writeKey)) !== null && _a !== void 0 ? _a : 0) - 1);
161
+ liveHandlers.set(writeKey, remaining);
162
+ var wasLast = remaining === 0;
163
+ if (remaining > 0 || markerExpiry.has(writeKey))
164
+ return wasLast;
165
+ var timer = setTimeout(function () {
166
+ markerExpiry.delete(writeKey);
167
+ var prefix = "".concat(writeKey, ":");
168
+ announcedConnections.forEach(function (key) {
169
+ if (key.startsWith(prefix))
170
+ announcedConnections.delete(key);
171
+ });
172
+ }, MARKER_GRACE_MS);
173
+ // Never hold a Node process (or a test run) open for this.
174
+ (_c = (_b = timer).unref) === null || _c === void 0 ? void 0 : _c.call(_b);
175
+ markerExpiry.set(writeKey, timer);
176
+ return wasLast;
177
+ }
178
+ /**
179
+ * The one handler allowed to EMIT for a given destination.
180
+ *
181
+ * Two handlers can be alive at once over the same wagmi config and write key:
182
+ * Strict Mode, HMR, or an options change whose replacement mounts before the
183
+ * old one is torn down - which the marker grace period explicitly supports.
184
+ * Non-owners still track state, so whichever survives cleanup is already
185
+ * correct and takes over immediately; they simply do not emit.
186
+ */
187
+ var emittingOwners = new Map();
188
+ /** Stable per-config id, so the owner key can span SDK instances. */
189
+ var configIds = new WeakMap();
190
+ var nextConfigId = 0;
191
+ var ownerKey = function (writeKey, config) {
192
+ var id = configIds.get(config);
193
+ if (!id) {
194
+ id = "cfg".concat((nextConfigId += 1));
195
+ configIds.set(config, id);
196
+ }
197
+ return "".concat(writeKey, ":").concat(id);
198
+ };
199
+ /**
200
+ * Pending transactions, shared per destination rather than per handler.
201
+ *
202
+ * A broadcast observed by handler A, followed by a replacement B taking over
203
+ * before the receipt arrives, otherwise loses the confirmation entirely: A no
204
+ * longer emits because it is not the owner, and B has no record of the
205
+ * broadcast to match the receipt against.
206
+ */
207
+ var pendingTransactionsByDestination = new Map();
208
+ /**
209
+ * Pending records outlive a rebuild by the same grace period as the markers.
210
+ * The ordinary rebuild is cleanup THEN remount, so dropping them the instant
211
+ * the last handler goes would lose the receipt for anything broadcast just
212
+ * before teardown.
213
+ */
214
+ var pendingTransactionExpiry = new Map();
215
+ function schedulePendingTransactionExpiry(key) {
216
+ var _a, _b;
217
+ if (pendingTransactionExpiry.has(key))
218
+ return;
219
+ var timer = setTimeout(function () {
220
+ pendingTransactionExpiry.delete(key);
221
+ pendingTransactionsByDestination.delete(key);
222
+ }, MARKER_GRACE_MS);
223
+ (_b = (_a = timer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
224
+ pendingTransactionExpiry.set(key, timer);
225
+ }
226
+ function cancelPendingTransactionExpiry(key) {
227
+ var timer = pendingTransactionExpiry.get(key);
228
+ if (!timer)
229
+ return;
230
+ clearTimeout(timer);
231
+ pendingTransactionExpiry.delete(key);
232
+ }
233
+ /**
234
+ * Has this destination already announced this wallet on this very connection?
235
+ *
236
+ * `exact` means the wagmi connection object is unchanged since the
237
+ * announcement, which is proof nothing happened in between and holds however
238
+ * long the SDK was away. `recent` means the object was replaced but the
239
+ * announcement is still inside the observation window.
240
+ */
241
+ function announcementState(key, connection) {
242
+ var _a;
243
+ if (connection && ((_a = announcedByConnection.get(connection)) === null || _a === void 0 ? void 0 : _a.has(key))) {
244
+ return "exact";
245
+ }
246
+ return announcedConnections.has(key) ? "recent" : "none";
247
+ }
248
+ /**
249
+ * Undo an announcement claim whose emission failed.
250
+ *
251
+ * `markAnnounced()` is called before the event is handed to FormoAnalytics,
252
+ * so the claim exists while the emission is in flight and a concurrent
253
+ * handler cannot double-report. If that emission then fails, nothing was
254
+ * reported - and leaving the claim standing means a retry or a replacement
255
+ * handler treats the wallet as done, losing its connect for the rest of the
256
+ * page load even after the queue recovers.
257
+ */
258
+ function releaseAnnouncement(key, connection) {
259
+ var _a;
260
+ announcedConnections.delete(key);
261
+ if (connection)
262
+ (_a = announcedByConnection.get(connection)) === null || _a === void 0 ? void 0 : _a.delete(key);
263
+ }
264
+ /**
265
+ * Record a connection as announced.
266
+ *
267
+ * Deliberately unbounded. Entries are removed when the wallet disconnects,
268
+ * and pruned against the live connections whenever a handler mounts, so the
269
+ * set can only hold wallets that are actually connected right now. An earlier
270
+ * revision carried a size cap as a backstop; with both of those in place it
271
+ * was unreachable, and the test covering it passed with the cap removed.
272
+ */
273
+ function markAnnounced(key, connection) {
274
+ var _a;
275
+ if (connection) {
276
+ var keys = (_a = announcedByConnection.get(connection)) !== null && _a !== void 0 ? _a : new Set();
277
+ keys.add(key);
278
+ announcedByConnection.set(connection, keys);
279
+ }
280
+ announcedConnections.add(key);
281
+ }
282
+ /** Test hook. Real page loads reset this naturally. */
283
+ export function __resetSeededWallet() {
284
+ emittingOwners.clear();
285
+ pendingTransactionExpiry.forEach(function (t) { return clearTimeout(t); });
286
+ pendingTransactionExpiry.clear();
287
+ pendingTransactionsByDestination.clear();
288
+ announcedConnections.clear();
289
+ liveHandlers.clear();
290
+ markerExpiry.forEach(function (timer) { return clearTimeout(timer); });
291
+ markerExpiry.clear();
292
+ }
293
+ /**
294
+ * wagmi accepts an `account` as either a bare address or a viem Account
295
+ * object. Normalise both to an address, or undefined when absent.
296
+ */
297
+ /**
298
+ * Coerce a wagmi/viem chain id to a number.
299
+ *
300
+ * An EIP-712 domain carries whatever the caller put in it, and viem accepts a
301
+ * hex string or a bigint there as well as a number. Passing those straight
302
+ * through would put a non-number in the event payload and defeat the numeric
303
+ * comparison in `tracking.excludeChains`.
304
+ */
305
+ function normalizeChainId(value) {
306
+ if (typeof value === "number")
307
+ return Number.isFinite(value) ? value : undefined;
308
+ if (typeof value === "bigint")
309
+ return Number(value);
310
+ if (typeof value === "string" && value.trim() !== "") {
311
+ var parsed = value.startsWith("0x") ? Number(value) : Number(value);
312
+ return Number.isFinite(parsed) ? parsed : undefined;
313
+ }
314
+ return undefined;
315
+ }
316
+ function resolveAccountAddress(account) {
317
+ if (typeof account === "string")
318
+ return account;
319
+ if (account && typeof account === "object") {
320
+ var address = account.address;
321
+ if (typeof address === "string")
322
+ return address;
323
+ }
324
+ return undefined;
325
+ }
73
326
  /**
74
327
  * Clean up old entries from a Set to prevent memory leaks.
75
328
  * Removes oldest entries when size exceeds maxSize.
@@ -94,6 +347,26 @@ var WagmiEventHandler = /** @class */ (function () {
94
347
  this.trackingState = {
95
348
  isProcessing: false,
96
349
  };
350
+ /**
351
+ * Set by `cleanup()`. Every deferred emission checks it, so a handler that
352
+ * has been torn down cannot push an event into a queue the replacement SDK
353
+ * instance no longer owns.
354
+ */
355
+ this.disposed = false;
356
+ /** Reentrancy guard for `reconcileWithLiveState()`. */
357
+ this.reconciling = false;
358
+ /**
359
+ * Ticket for each active-connection transition. Emissions are awaited, and
360
+ * wagmi can move again in that window; a continuation whose ticket is no
361
+ * longer current must not write stale state over the newer one.
362
+ */
363
+ this.transitionGeneration = 0;
364
+ /**
365
+ * A `disconnected` status that arrived while the lock was held. Final-state
366
+ * reconciliation cannot see a disconnect/reconnect cycle that completed
367
+ * inside that window, so this records that one happened.
368
+ */
369
+ this.missedDisconnect = false;
97
370
  /**
98
371
  * Track processed mutation states to prevent duplicate event emissions
99
372
  * Key format: `${mutationId}:${status}`
@@ -104,17 +377,35 @@ var WagmiEventHandler = /** @class */ (function () {
104
377
  * Key format: `${queryHash}:${status}`
105
378
  */
106
379
  this.processedQueries = new Set();
107
- /**
108
- * Store transaction details from BROADCASTED events for use in CONFIRMED/REVERTED
109
- * Key: transactionHash, Value: transaction details including the original sender address
110
- */
111
- this.pendingTransactions = new Map();
112
380
  this.formo = formoAnalytics;
113
381
  this.wagmiConfig = wagmiConfig;
114
382
  this.queryClient = queryClient;
115
383
  logger.info("WagmiEventHandler: Initializing Wagmi integration");
384
+ // Keep the page-load markers alive across an SDK rebuild. Must run before
385
+ // the seed, so a handler created moments after its predecessor was torn
386
+ // down still sees what that predecessor announced.
387
+ // Only a handler that finds an ANNOUNCED wallet still connected may hold
388
+ // the markers open. Mounting over a disconnected store has observed
389
+ // nothing; mounting over a DIFFERENT wallet is worse still - it would
390
+ // preserve the previous wallet's marker and suppress its genuine
391
+ // reconnect for the rest of the page load. Markers for wallets no longer
392
+ // in `state.connections` are dropped outright.
393
+ var livesOverConnection = this.pruneMarkersForLostWallets();
394
+ retainMarkers(this.formo.writeKey, livesOverConnection);
395
+ // Claim the right to emit for this destination. The NEWEST handler always
396
+ // wins, deliberately: an app that rebuilds without calling `cleanup()`
397
+ // leaks the old handler, and letting that stale one keep ownership would
398
+ // silence the live one for the rest of the page load - far worse than the
399
+ // duplicate events this is here to prevent.
400
+ this.ownerKey = ownerKey(this.formo.writeKey, wagmiConfig);
401
+ emittingOwners.set(this.ownerKey, this);
402
+ // A remount for this destination cancels the pending-record expiry the
403
+ // previous handler started.
404
+ cancelPendingTransactionExpiry(this.ownerKey);
116
405
  // Set up connection/disconnection/chain listeners
117
406
  this.setupConnectionListeners();
407
+ // Adopt a wallet that connected before this handler existed.
408
+ this.seedFromCurrentState();
118
409
  // Set up mutation and query tracking if QueryClient is provided
119
410
  if (this.queryClient) {
120
411
  this.setupMutationTracking();
@@ -124,6 +415,88 @@ var WagmiEventHandler = /** @class */ (function () {
124
415
  logger.warn("WagmiEventHandler: QueryClient not provided, signature and transaction events will not be tracked");
125
416
  }
126
417
  }
418
+ /**
419
+ * Drop markers for wallets wagmi no longer holds, and report whether any
420
+ * announced wallet is still connected.
421
+ *
422
+ * Called when a handler mounts. Cancelling the expiry timer on the strength
423
+ * of "some connection exists" let a marker for a wallet that had since
424
+ * disconnected survive indefinitely, suppressing its genuine reconnect.
425
+ */
426
+ WagmiEventHandler.prototype.pruneMarkersForLostWallets = function () {
427
+ var _this = this;
428
+ var state;
429
+ try {
430
+ state = this.getState();
431
+ }
432
+ catch (_a) {
433
+ return false;
434
+ }
435
+ var prefix = "".concat(this.formo.writeKey, ":");
436
+ var live = new Set();
437
+ state.connections.forEach(function (connection) {
438
+ connection.accounts.forEach(function (a) {
439
+ return live.add(announceKey(_this.formo.writeKey, a));
440
+ });
441
+ });
442
+ var announcedStillConnected = false;
443
+ announcedConnections.forEach(function (key) {
444
+ if (!key.startsWith(prefix))
445
+ return;
446
+ if (live.has(key))
447
+ announcedStillConnected = true;
448
+ else
449
+ announcedConnections.delete(key);
450
+ });
451
+ return announcedStillConnected;
452
+ };
453
+ Object.defineProperty(WagmiEventHandler.prototype, "isEmittingOwner", {
454
+ /**
455
+ * Whether this handler is the one allowed to emit for its destination.
456
+ *
457
+ * A handler that lost the race still tracks state, so it is ready to take
458
+ * over the moment the owner goes away; it just does not emit.
459
+ */
460
+ get: function () {
461
+ if (!this.ownerKey)
462
+ return true;
463
+ var owner = emittingOwners.get(this.ownerKey);
464
+ if (!owner) {
465
+ // Nobody holds it - the previous owner was torn down. Claim it.
466
+ emittingOwners.set(this.ownerKey, this);
467
+ return true;
468
+ }
469
+ return owner === this;
470
+ },
471
+ enumerable: false,
472
+ configurable: true
473
+ });
474
+ Object.defineProperty(WagmiEventHandler.prototype, "pendingTransactions", {
475
+ /**
476
+ * Store transaction details from BROADCASTED events for use in CONFIRMED/REVERTED
477
+ * Key: transactionHash, Value: transaction details including the original sender address
478
+ */
479
+ /**
480
+ * Shared per destination, not per handler.
481
+ *
482
+ * A broadcast observed by handler A, followed by a replacement B taking
483
+ * over before the receipt arrives, used to lose the confirmation entirely:
484
+ * A no longer emits because it is not the owner, and B had no record of the
485
+ * broadcast to match the receipt against.
486
+ */
487
+ get: function () {
488
+ var _a;
489
+ var key = (_a = this.ownerKey) !== null && _a !== void 0 ? _a : "";
490
+ var map = pendingTransactionsByDestination.get(key);
491
+ if (!map) {
492
+ map = new Map();
493
+ pendingTransactionsByDestination.set(key, map);
494
+ }
495
+ return map;
496
+ },
497
+ enumerable: false,
498
+ configurable: true
499
+ });
127
500
  /**
128
501
  * Set up listeners for wallet connection, disconnection, and chain changes
129
502
  */
@@ -135,83 +508,832 @@ var WagmiEventHandler = /** @class */ (function () {
135
508
  _this.handleStatusChange(status, prevStatus);
136
509
  });
137
510
  this.unsubscribers.push(statusUnsubscribe);
138
- // Subscribe to chain ID changes
139
- var chainIdUnsubscribe = this.wagmiConfig.subscribe(function (state) { return state.chainId; }, function (chainId, prevChainId) {
511
+ // Subscribe to chain ID changes.
512
+ //
513
+ // Selects the ACTIVE CONNECTION's chain, with the global as a fallback -
514
+ // the same rule every read site here uses. Selecting only `state.chainId`
515
+ // meant that with several connections, or with `syncConnectedChain: false`,
516
+ // the active connection could move to a new chain while the global stayed
517
+ // put: no callback ran, and both the tracked chain and Formo's central
518
+ // chain silently went stale for every later event.
519
+ var chainIdUnsubscribe = this.wagmiConfig.subscribe(function (state) { var _a; return (_a = _this.getActiveConnectionChainId(state)) !== null && _a !== void 0 ? _a : state.chainId; }, function (chainId, prevChainId) {
140
520
  _this.handleChainChange(chainId, prevChainId);
141
521
  });
142
522
  this.unsubscribers.push(chainIdUnsubscribe);
523
+ // Subscribe to the active connection as a whole: its connector id AND its
524
+ // address, as one string.
525
+ //
526
+ // `state.status` is a single global value, so it stays "connected" when a
527
+ // user switches account inside an already-connected wallet. Without this
528
+ // the switch is invisible: no event, and the tracked address goes stale
529
+ // and mis-attributes every later signature and transaction.
530
+ //
531
+ // The connector id has to be in the key too. Selecting the address alone
532
+ // missed the case where two connectors hold the SAME account - MetaMask
533
+ // and Rabby over one hardware wallet, say - and the current one
534
+ // disconnects: wagmi falls back to the other, the address is unchanged, so
535
+ // nothing fired and the disconnect was lost for good.
536
+ var connectionUnsubscribe = this.wagmiConfig.subscribe(function (state) { var _a, _b; return "".concat((_a = state.current) !== null && _a !== void 0 ? _a : "", "|").concat((_b = _this.getConnectedAddress(state)) !== null && _b !== void 0 ? _b : ""); }, function (key, prevKey) {
537
+ var _a = key.split("|"), address = _a[1];
538
+ var _b = (prevKey !== null && prevKey !== void 0 ? prevKey : "|").split("|"), prevAddress = _b[1];
539
+ _this.handleActiveAddressChange(address || undefined, prevAddress || undefined);
540
+ });
541
+ this.unsubscribers.push(connectionUnsubscribe);
143
542
  logger.info("WagmiEventHandler: Connection listeners set up successfully");
144
543
  };
544
+ /**
545
+ * Adopt a connection that already existed when this handler was created.
546
+ *
547
+ * `config.subscribe` reports *changes* only, so a wallet restored by wagmi's
548
+ * mount-time `reconnect()` is invisible to a handler built afterwards. That
549
+ * is the normal case whenever the host app loads the SDK lazily (dynamic
550
+ * import, `requestIdleCallback`) and can also happen on a plain mount, since
551
+ * `FormoAnalytics.init()` and wagmi's reconnect are both async and race.
552
+ *
553
+ * Without this seed the whole session is lost: no connect event, and
554
+ * `lastAddress` stays undefined so the signature and transaction handlers
555
+ * drop their events too, and the eventual disconnect carries neither address
556
+ * nor chain id.
557
+ *
558
+ * `subscribe(..., { fireImmediately: true })` is not a substitute. It reports
559
+ * the current value with no distinct previous value, so the connect branch's
560
+ * `prevStatus !== "connected"` test never passes, and wagmi v3 does not
561
+ * honour the option at all.
562
+ *
563
+ * Deliberately synchronous, and deliberately does NOT take the
564
+ * `isProcessing` lock. Everything that mutates tracking state runs before
565
+ * any await, so no status change can interleave with it. Holding the lock
566
+ * across the `connect()` emission instead would make `handleStatusChange()`
567
+ * drop - not defer - a disconnect or wallet switch that lands in that
568
+ * window, which is exactly when a lazily loaded SDK is racing app activity.
569
+ *
570
+ * A genuine `connected` transition cannot double-emit alongside this seed.
571
+ * `config.subscribe` only fires on change, and the connect branch treats a
572
+ * re-entry for an already-tracked address as a no-op (or a chain change).
573
+ */
574
+ WagmiEventHandler.prototype.seedFromCurrentState = function () {
575
+ var _a, _b;
576
+ try {
577
+ var state = this.getState();
578
+ if (state.status !== "connected") {
579
+ return;
580
+ }
581
+ var address = this.getConnectedAddress(state);
582
+ // The active connection's chain is authoritative. `state.chainId` is a
583
+ // single global value that can lag or describe a different connection,
584
+ // and with `syncConnectedChain: false` it stays on the chain the APP
585
+ // selected while the connection reports what the WALLET is on.
586
+ var chainId = (_a = this.getActiveConnectionChainId(state)) !== null && _a !== void 0 ? _a : state.chainId;
587
+ if (!address || chainId === undefined) {
588
+ logger.debug("WagmiEventHandler: Connected at init but address or chainId is missing, nothing to seed", { address: address, chainId: chainId });
589
+ return;
590
+ }
591
+ logger.info("WagmiEventHandler: Adopting connection that predates this handler", { address: address, chainId: chainId });
592
+ // Sync central state first so tracking.excludeChains is enforced even
593
+ // when connect autocapture is disabled.
594
+ this.formo.syncWalletState({ chainId: chainId, address: address });
595
+ // Only adopt the wallet privately if central state actually accepted it.
596
+ // While tracking is suppressed (opt-out, excluded host or path)
597
+ // syncWalletState deliberately refuses to learn a wallet. Retaining it
598
+ // here anyway would let the mutation handlers attribute signatures and
599
+ // transactions to an address the SDK has decided it must not know -
600
+ // including on a chain that excludeChains rejects, since that check
601
+ // reads the central field rather than the event payload.
602
+ //
603
+ // Compared case-insensitively: syncWalletState stores the checksummed
604
+ // form, which need not match the casing wagmi reported.
605
+ if (((_b = this.formo.currentAddress) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== address.toLowerCase()) {
606
+ logger.debug("WagmiEventHandler: Central state declined the connection, not seeding", { address: address, chainId: chainId });
607
+ return;
608
+ }
609
+ // Adopt the tracking state either way - the mutation handlers need an
610
+ // address - but only emit the first time this page load adopts this
611
+ // wallet. A rebuilt SDK instance over an unchanged connection is a
612
+ // lifecycle event, not a user action.
613
+ this.trackingState.lastAddress = address;
614
+ this.trackingState.lastChainId = chainId;
615
+ this.trackingState.lastStatus = state.status;
616
+ // Always record the connection, including on the deduplicated path
617
+ // below. A replacement handler that adopts a live connection must be
618
+ // able to clear the right marker when it later sees the disconnect.
619
+ this.trackingState.lastConnectionId = state.current;
620
+ if (!this.formo.isAutocaptureEnabled("connect") || !this.isEmittingOwner) {
621
+ // Nothing was announced, so nothing may be marked. Marking here would
622
+ // make a later rebuild with connect autocapture enabled - which is
623
+ // exactly how FormoAnalyticsProvider applies an options change - find
624
+ // the marker and stay silent about a wallet it never reported.
625
+ logger.debug("WagmiEventHandler: Connect autocapture disabled, adopted without emitting", { address: address, chainId: chainId });
626
+ return;
627
+ }
628
+ // Nothing that will not actually be sent may be marked. `syncWalletState`
629
+ // accepts a wallet that `trackEvent()` then silently drops - `tracking:
630
+ // false`, or a chain in `excludeChains` - and marking it would make the
631
+ // rebuild that turns tracking back on find the marker and stay quiet
632
+ // about the wallet for the rest of the page load.
633
+ if (!this.formo.willTrackEvent(chainId)) {
634
+ logger.debug("WagmiEventHandler: Connect would not be tracked, adopted without emitting", { address: address, chainId: chainId });
635
+ return;
636
+ }
637
+ var walletKey_1 = announceKey(this.formo.writeKey, address);
638
+ var connection_1 = state.current
639
+ ? state.connections.get(state.current)
640
+ : undefined;
641
+ if (announcementState(walletKey_1, connection_1) !== "none") {
642
+ logger.debug("WagmiEventHandler: Wallet already adopted this page load, not re-emitting connect", { address: address });
643
+ return;
644
+ }
645
+ markAnnounced(walletKey_1, connection_1);
646
+ {
647
+ var connectorName = this.getConnectorName(state);
648
+ // Invoked synchronously, only the completion is fire-and-forget.
649
+ //
650
+ // Deferring the call itself to a microtask and guarding it on
651
+ // `disposed` loses the event outright: a rebuild that tears this
652
+ // handler down before the microtask runs leaves the marker standing,
653
+ // so the replacement handler suppresses its own connect while this one
654
+ // declines to emit. Nobody reports the wallet. Calling now means the
655
+ // marker and the emission are decided together, and a rebuild can only
656
+ // arrive after the event is already handed to FormoAnalytics.
657
+ void Promise.resolve(this.formo.connect({ chainId: chainId, address: address }, __assign({}, (connectorName && { providerName: connectorName })))).catch(function (error) {
658
+ // The marker is a claim, not a record. If the emission failed the
659
+ // wallet was never reported, so release it - otherwise a retry or a
660
+ // replacement handler treats it as done and the connect is lost for
661
+ // the rest of the page load even once the queue recovers.
662
+ releaseAnnouncement(walletKey_1, connection_1);
663
+ logger.error("WagmiEventHandler: Error emitting seeded connect event:", error);
664
+ });
665
+ }
666
+ }
667
+ catch (error) {
668
+ logger.error("WagmiEventHandler: Error seeding from current state:", error);
669
+ }
670
+ };
671
+ /**
672
+ * Re-assert the tracked wallet into central state.
673
+ *
674
+ * Needed when an awaited emission clears the central namespace after a newer
675
+ * transition has already adopted a different wallet: the two then disagree,
676
+ * and `trackEvent()` reads the central one.
677
+ */
678
+ WagmiEventHandler.prototype.restoreCentralStateFromTracking = function () {
679
+ var _a;
680
+ var _b = this.trackingState, lastAddress = _b.lastAddress, lastChainId = _b.lastChainId;
681
+ if (!lastAddress || lastChainId === undefined)
682
+ return;
683
+ // Compares the CHAIN as well as the address.
684
+ //
685
+ // A chain switch on an excluded path is deliberately refused by
686
+ // `syncWalletState()`, so central state keeps the old chain while
687
+ // `lastChainId` moves on. Matching on the address alone returned here,
688
+ // and after navigating back to an allowed path the events that fall back
689
+ // to the central chain were gated against a chain the wallet had left -
690
+ // bypassing an exclusion covering where it actually was.
691
+ if (((_a = this.formo.currentAddress) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === lastAddress.toLowerCase() &&
692
+ this.formo.currentChainId === lastChainId) {
693
+ return;
694
+ }
695
+ logger.info("WagmiEventHandler: Restoring central state a stale disconnect cleared", { address: lastAddress, chainId: lastChainId });
696
+ this.formo.syncWalletState({ chainId: lastChainId, address: lastAddress });
697
+ };
698
+ /**
699
+ * Drop the wallet this handler is attributing events to.
700
+ *
701
+ * Used whenever central state refuses to hold the wallet - an excluded path
702
+ * or chain, or an opt-out. Keeping the address privately would let the
703
+ * mutation handlers label signatures and transactions with an identity the
704
+ * SDK has deliberately declined to know.
705
+ */
706
+ WagmiEventHandler.prototype.releaseTrackedWallet = function () {
707
+ var previous = this.trackingState.lastAddress;
708
+ this.trackingState.lastAddress = undefined;
709
+ this.trackingState.lastChainId = undefined;
710
+ this.trackingState.lastConnectionId = undefined;
711
+ if (previous) {
712
+ announcedConnections.delete(announceKey(this.formo.writeKey, previous));
713
+ }
714
+ };
715
+ /**
716
+ * Adopt the live connection if one exists and nothing is tracked yet.
717
+ *
718
+ * `seedFromCurrentState()` runs once at construction and can legitimately
719
+ * decline: wagmi may still be filling `connections`, the chain may not have
720
+ * arrived, or tracking may be suppressed because the app happens to be on an
721
+ * excluded path. `config.subscribe` only reports *changes*, so an unchanged
722
+ * connection would then stay invisible for the rest of the page load and
723
+ * every later signature and transaction would be dropped.
724
+ *
725
+ * Idempotent. The page-load marker keeps it from re-emitting a connect for a
726
+ * wallet already announced, so this is safe to call from any listener.
727
+ */
728
+ WagmiEventHandler.prototype.retryAdoption = function () {
729
+ if (this.disposed)
730
+ return;
731
+ // The cached wallet may no longer be the live one. While tracking is
732
+ // suppressed a user can disconnect, or switch account inside the same
733
+ // wallet - which moves only the connection's accounts, so neither the
734
+ // status nor the chain subscription fires and nothing tells this handler.
735
+ // Restoring the cached wallet then resurrects an address the user has
736
+ // left, and the marker makes the check below return before the live one
737
+ // is ever adopted.
738
+ if (this.trackingState.lastAddress && !this.tracksLiveWallet()) {
739
+ logger.debug("WagmiEventHandler: Tracked wallet is no longer the live one, releasing before retry", { tracked: this.trackingState.lastAddress });
740
+ this.releaseTrackedWallet();
741
+ }
742
+ // Central state can have been cleared underneath a wallet this handler is
743
+ // still tracking: `optOutTracking()` calls `reset()`, which wipes
744
+ // `currentAddress` / `currentChainId` while the handler keeps its wallet.
745
+ // Left unreconciled, later events carry no wallet at all - and
746
+ // `shouldTrack()` sees no chain, so `excludeChains` stops excluding.
747
+ this.resyncCentralState();
748
+ // Adopted is not the same as announced. A wallet connected on an excluded
749
+ // chain, or while tracking was off, is adopted so mutations can be
750
+ // attributed, but its connect is deliberately not emitted or marked. If
751
+ // this returned merely because an address is present, that wallet would
752
+ // never be reported once the chain or configuration allowed it.
753
+ if (this.trackingState.lastAddress && this.isCurrentWalletAnnounced()) {
754
+ return;
755
+ }
756
+ this.seedFromCurrentState();
757
+ };
758
+ /** Whether the wallet this handler tracks is the one wagmi currently has. */
759
+ WagmiEventHandler.prototype.tracksLiveWallet = function () {
760
+ var tracked = this.trackingState.lastAddress;
761
+ if (!tracked)
762
+ return false;
763
+ try {
764
+ var state = this.getState();
765
+ if (state.status !== "connected")
766
+ return false;
767
+ var live = this.getConnectedAddress(state);
768
+ return !!live && live.toLowerCase() === tracked.toLowerCase();
769
+ }
770
+ catch (error) {
771
+ logger.error("WagmiEventHandler: Error reading state to compare wallets:", error);
772
+ return false;
773
+ }
774
+ };
775
+ /**
776
+ * Put the tracked wallet back into central state without emitting anything.
777
+ *
778
+ * Used when the two have diverged for a reason that is not a wallet change:
779
+ * an opt-out `reset()`, or a stale disconnect completing after a newer
780
+ * transition adopted a different wallet.
781
+ */
782
+ WagmiEventHandler.prototype.resyncCentralState = function () {
783
+ var _a;
784
+ var _b = this.trackingState, lastAddress = _b.lastAddress, lastChainId = _b.lastChainId;
785
+ if (!lastAddress || lastChainId === undefined)
786
+ return;
787
+ // Compares the CHAIN as well as the address.
788
+ //
789
+ // A chain switch on an excluded path is deliberately refused by
790
+ // `syncWalletState()`, so central state keeps the old chain while
791
+ // `lastChainId` moves on. Matching on the address alone returned here,
792
+ // and after navigating back to an allowed path the events that fall back
793
+ // to the central chain were gated against a chain the wallet had left -
794
+ // bypassing an exclusion covering where it actually was.
795
+ if (((_a = this.formo.currentAddress) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === lastAddress.toLowerCase() &&
796
+ this.formo.currentChainId === lastChainId) {
797
+ return;
798
+ }
799
+ logger.info("WagmiEventHandler: Re-syncing central state for a tracked wallet", {
800
+ address: lastAddress,
801
+ chainId: lastChainId,
802
+ });
803
+ this.formo.syncWalletState({ chainId: lastChainId, address: lastAddress });
804
+ };
805
+ /** Whether the wallet currently tracked has already had a connect emitted. */
806
+ WagmiEventHandler.prototype.isCurrentWalletAnnounced = function () {
807
+ var address = this.trackingState.lastAddress;
808
+ if (!address)
809
+ return false;
810
+ var connection;
811
+ try {
812
+ var state = this.getState();
813
+ connection = state.current
814
+ ? state.connections.get(state.current)
815
+ : undefined;
816
+ }
817
+ catch (_a) {
818
+ /* fall back to the address-keyed marker */
819
+ }
820
+ return (announcementState(announceKey(this.formo.writeKey, address), connection) !==
821
+ "none");
822
+ };
145
823
  /**
146
824
  * Handle status changes (connect/disconnect)
147
825
  */
148
826
  WagmiEventHandler.prototype.handleStatusChange = function (status, prevStatus) {
149
827
  return __awaiter(this, void 0, void 0, function () {
150
- var state, address, chainId, connectorName, error_1;
151
- return __generator(this, function (_a) {
152
- switch (_a.label) {
828
+ var state, address, chainId, disconnectedAddress, disconnectedChainId, walletKey, connection, connectorName, error_1, error_2;
829
+ var _a, _b, _c, _d;
830
+ return __generator(this, function (_e) {
831
+ switch (_e.label) {
153
832
  case 0:
154
833
  // Prevent concurrent processing
155
834
  if (this.trackingState.isProcessing) {
835
+ // Dropped, not queued - but remember that a disconnect went past.
836
+ //
837
+ // Reconciliation compares only the FINAL state, so a wallet that
838
+ // disconnects and reconnects entirely inside this window looks
839
+ // unchanged and neither its disconnect nor its genuine reconnect is
840
+ // ever reported. Recording the transient lets reconciliation tell that
841
+ // cycle apart from nothing having happened at all.
842
+ if (status === "disconnected") {
843
+ this.missedDisconnect = true;
844
+ }
156
845
  logger.debug("WagmiEventHandler: Already processing status change, skipping");
157
846
  return [2 /*return*/];
158
847
  }
159
848
  this.trackingState.isProcessing = true;
160
- _a.label = 1;
849
+ // A status change outranks any connection transition still in flight. A
850
+ // full disconnect advances no ticket of its own, so without this an older
851
+ // fallback continuation could resume and announce a wallet wagmi no longer
852
+ // has.
853
+ this.transitionGeneration += 1;
854
+ _e.label = 1;
161
855
  case 1:
162
- _a.trys.push([1, 7, 8, 9]);
856
+ _e.trys.push([1, 14, 15, 16]);
163
857
  state = this.getState();
164
858
  address = this.getConnectedAddress(state);
165
- chainId = state.chainId;
859
+ chainId = (_a = this.getActiveConnectionChainId(state)) !== null && _a !== void 0 ? _a : state.chainId;
166
860
  logger.info("WagmiEventHandler: Status changed", {
167
861
  status: status,
168
862
  prevStatus: prevStatus,
169
863
  address: address,
170
864
  chainId: chainId,
171
865
  });
172
- if (!(status === "disconnected" && prevStatus === "connected")) return [3 /*break*/, 4];
173
- // Clear central chain state regardless of autocapture so a later
174
- // event can't carry a stale excluded/!excluded chainId.
175
- this.formo.syncWalletState({
176
- chainId: this.trackingState.lastChainId,
177
- });
178
- if (!this.formo.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 3];
866
+ if (!(status === "disconnected" && this.trackingState.lastAddress)) return [3 /*break*/, 4];
867
+ disconnectedAddress = this.trackingState.lastAddress;
868
+ disconnectedChainId = this.trackingState.lastChainId;
869
+ this.trackingState.lastConnectionId = undefined;
870
+ this.trackingState.lastAddress = undefined;
871
+ this.trackingState.lastChainId = undefined;
872
+ this.pendingChainId = undefined;
873
+ this.missedDisconnect = false;
874
+ // A real disconnect ends the adoption, so a genuine reconnect later in
875
+ // this same page load emits again.
876
+ announcedConnections.delete(announceKey(this.formo.writeKey, disconnectedAddress));
877
+ if (!(this.formo.isAutocaptureEnabled("disconnect") && this.isEmittingOwner)) return [3 /*break*/, 3];
179
878
  return [4 /*yield*/, this.formo.disconnect({
180
- chainId: this.trackingState.lastChainId,
181
- address: this.trackingState.lastAddress,
879
+ chainId: disconnectedChainId,
880
+ address: disconnectedAddress,
182
881
  })];
183
882
  case 2:
184
- _a.sent();
185
- _a.label = 3;
883
+ _e.sent();
884
+ return [3 /*break*/, 4];
186
885
  case 3:
187
- this.trackingState.lastAddress = undefined;
188
- this.trackingState.lastChainId = undefined;
189
- _a.label = 4;
886
+ // Nothing to emit, but central state must still be cleared so a
887
+ // later event cannot carry a stale excluded/!excluded chainId.
888
+ this.formo.syncWalletState({ chainId: disconnectedChainId });
889
+ _e.label = 4;
190
890
  case 4:
191
- if (!(status === "connected" && prevStatus !== "connected")) return [3 /*break*/, 6];
192
- if (!(address && chainId !== undefined)) return [3 /*break*/, 6];
891
+ if (!(status === "connected" && prevStatus !== "connected")) return [3 /*break*/, 13];
892
+ if (!(address && chainId !== undefined)) return [3 /*break*/, 13];
893
+ if (!(((_b = this.trackingState.lastAddress) === null || _b === void 0 ? void 0 : _b.toLowerCase()) ===
894
+ address.toLowerCase())) return [3 /*break*/, 9];
895
+ if (!(this.trackingState.lastChainId !== chainId)) return [3 /*break*/, 7];
896
+ if (!(this.pendingChainId === chainId)) return [3 /*break*/, 6];
897
+ this.pendingChainId = undefined;
898
+ logger.info("WagmiEventHandler: Re-entry carries a chain the subscription dropped, emitting it", { address: address, from: this.trackingState.lastChainId, to: chainId });
899
+ return [4 /*yield*/, this.handleChainChange(chainId, this.trackingState.lastChainId)];
900
+ case 5:
901
+ _e.sent();
902
+ this.trackingState.lastStatus = status;
903
+ return [2 /*return*/];
904
+ case 6:
905
+ logger.info("WagmiEventHandler: Tracked wallet re-entered connected on a different chain", { address: address, from: this.trackingState.lastChainId, to: chainId });
906
+ // Sync first, then adopt only if central state accepted it.
907
+ // Writing the private chain first would let the mutation
908
+ // handlers label events with a chain that `shouldTrack()` is
909
+ // still excluding, because that gate reads the central field
910
+ // rather than the event payload.
911
+ this.formo.syncWalletState({ chainId: chainId, address: address });
912
+ if (((_c = this.formo.currentAddress) === null || _c === void 0 ? void 0 : _c.toLowerCase()) ===
913
+ address.toLowerCase()) {
914
+ this.trackingState.lastChainId = chainId;
915
+ }
916
+ else {
917
+ logger.debug("WagmiEventHandler: Central state declined the re-entry chain, dropping the wallet", { address: address, chainId: chainId });
918
+ this.releaseTrackedWallet();
919
+ }
920
+ return [3 /*break*/, 8];
921
+ case 7:
922
+ logger.debug("WagmiEventHandler: Ignoring re-entry to connected for an already tracked wallet", { address: address, chainId: chainId });
923
+ _e.label = 8;
924
+ case 8:
925
+ this.trackingState.lastStatus = status;
926
+ return [2 /*return*/];
927
+ case 9:
928
+ // Sync central state first so tracking.excludeChains is enforced
929
+ // even when connect autocapture is disabled.
930
+ this.formo.syncWalletState({ chainId: chainId, address: address });
931
+ // Same rule as the seed and the account-switch path: while tracking
932
+ // is suppressed, syncWalletState refuses to learn a wallet, and
933
+ // retaining it privately would let the mutation handlers attribute
934
+ // events to an address the SDK declined to know.
935
+ if (((_d = this.formo.currentAddress) === null || _d === void 0 ? void 0 : _d.toLowerCase()) !== address.toLowerCase()) {
936
+ logger.debug("WagmiEventHandler: Central state declined the connection, not adopting", { address: address, chainId: chainId });
937
+ this.trackingState.lastStatus = status;
938
+ return [2 /*return*/];
939
+ }
940
+ // A wallet superseded without an intervening `disconnected` - a
941
+ // `reconnecting` flap landing on a different account - is handled by
942
+ // `reconcileWithLiveState()`, which releases the old wallet (and its
943
+ // marker) before re-seeding. Nothing to do here.
193
944
  this.trackingState.lastAddress = address;
194
945
  this.trackingState.lastChainId = chainId;
195
- // Sync central state unconditionally so tracking.excludeChains
196
- // is enforced even when connect autocapture is disabled.
197
- this.formo.syncWalletState({ chainId: chainId, address: address });
198
- if (!this.formo.isAutocaptureEnabled("connect")) return [3 /*break*/, 6];
946
+ this.trackingState.lastConnectionId = state.current;
947
+ if (!(this.formo.isAutocaptureEnabled("connect") &&
948
+ this.isEmittingOwner &&
949
+ this.formo.willTrackEvent(chainId))) return [3 /*break*/, 13];
950
+ walletKey = announceKey(this.formo.writeKey, address);
951
+ connection = state.current
952
+ ? state.connections.get(state.current)
953
+ : undefined;
954
+ if (announcementState(walletKey, connection) !== "none") {
955
+ logger.debug("WagmiEventHandler: Connect already announced for this wallet, not emitting again", { address: address });
956
+ this.trackingState.lastStatus = status;
957
+ return [2 /*return*/];
958
+ }
959
+ markAnnounced(walletKey, connection);
199
960
  connectorName = this.getConnectorName(state);
961
+ _e.label = 10;
962
+ case 10:
963
+ _e.trys.push([10, 12, , 13]);
200
964
  return [4 /*yield*/, this.formo.connect({ chainId: chainId, address: address }, __assign({}, (connectorName && { providerName: connectorName })))];
201
- case 5:
202
- _a.sent();
203
- _a.label = 6;
204
- case 6:
965
+ case 11:
966
+ _e.sent();
967
+ return [3 /*break*/, 13];
968
+ case 12:
969
+ error_1 = _e.sent();
970
+ // As in the seed: an emission that failed reported nothing, so
971
+ // the claim must not stand.
972
+ releaseAnnouncement(walletKey, connection);
973
+ throw error_1;
974
+ case 13:
205
975
  this.trackingState.lastStatus = status;
206
- return [3 /*break*/, 9];
207
- case 7:
208
- error_1 = _a.sent();
209
- logger.error("WagmiEventHandler: Error handling status change:", error_1);
210
- return [3 /*break*/, 9];
211
- case 8:
976
+ return [3 /*break*/, 16];
977
+ case 14:
978
+ error_2 = _e.sent();
979
+ logger.error("WagmiEventHandler: Error handling status change:", error_2);
980
+ return [3 /*break*/, 16];
981
+ case 15:
212
982
  this.trackingState.isProcessing = false;
983
+ // A status change that arrived while the lock was held was dropped, not
984
+ // queued. Reconcile against live state so the handler cannot be left
985
+ // permanently disagreeing with wagmi.
986
+ this.reconcileWithLiveState();
213
987
  return [7 /*endfinally*/];
214
- case 9: return [2 /*return*/];
988
+ case 16: return [2 /*return*/];
989
+ }
990
+ });
991
+ });
992
+ };
993
+ /**
994
+ * Re-run the status logic when the handler's view diverges from wagmi.
995
+ *
996
+ * `handleStatusChange()` drops - rather than defers - anything that arrives
997
+ * while it holds `isProcessing`, and it holds that lock across an awaited
998
+ * `connect()` / `disconnect()` emission. A wallet that reconnects inside
999
+ * that window would otherwise leave the handler believing it is
1000
+ * disconnected for the rest of the page load, silently dropping every later
1001
+ * signature and transaction. The mirror case leaves a disconnected wallet
1002
+ * marked connected.
1003
+ *
1004
+ * Converges: each pass either adopts the live wallet or releases the tracked
1005
+ * one, so the next pass finds the two in agreement and returns.
1006
+ */
1007
+ WagmiEventHandler.prototype.reconcileWithLiveState = function () {
1008
+ var _this = this;
1009
+ if (this.disposed || this.trackingState.isProcessing)
1010
+ return;
1011
+ if (this.reconciling)
1012
+ return;
1013
+ var state;
1014
+ try {
1015
+ state = this.getState();
1016
+ }
1017
+ catch (error) {
1018
+ logger.error("WagmiEventHandler: Error reading state to reconcile:", error);
1019
+ return;
1020
+ }
1021
+ // `connecting` and `reconnecting` are transient: the wallet has not gone
1022
+ // anywhere, wagmi is mid-handshake. Treating them as disconnected made
1023
+ // reconciliation synthesize a disconnect for an ordinary reconnect flap
1024
+ // and then emit a second connect when it completed - two spurious events
1025
+ // for a wallet that never left.
1026
+ if (state.status !== "connected" && state.status !== "disconnected") {
1027
+ return;
1028
+ }
1029
+ var live = state.status === "connected" ? this.getConnectedAddress(state) : undefined;
1030
+ var tracked = this.trackingState.lastAddress;
1031
+ if (!live && !tracked)
1032
+ return;
1033
+ if (live && tracked) {
1034
+ if (live.toLowerCase() === tracked.toLowerCase()) {
1035
+ if (this.missedDisconnect) {
1036
+ // The wallet went away and came back entirely while the lock was
1037
+ // held. The addresses match, but this is a new session: report the
1038
+ // disconnect, then let the reconnect announce itself.
1039
+ this.missedDisconnect = false;
1040
+ logger.info("WagmiEventHandler: A disconnect/reconnect cycle completed while the lock was held", { address: live });
1041
+ this.reconciling = true;
1042
+ try {
1043
+ void this.handleStatusChange("disconnected", "connected").then(function () {
1044
+ return _this.retryAdoption();
1045
+ });
1046
+ }
1047
+ finally {
1048
+ this.reconciling = false;
1049
+ }
1050
+ return;
1051
+ }
1052
+ // Same wallet. Replay a chain change the chain callback had to drop
1053
+ // because wagmi was still `reconnecting` at the time - the status
1054
+ // callback that would have covered it was then dropped by the
1055
+ // processing guard, so without this both the tracked and the central
1056
+ // chain keep describing the chain the wallet has already left.
1057
+ //
1058
+ // Deliberately keyed on a chain the callback actually dropped rather
1059
+ // than on any difference: in the ordinary flap the chain subscription
1060
+ // still owns the emission, and reconciling on difference alone would
1061
+ // double count it.
1062
+ // A disconnect that completed after a newer transition re-adopted this
1063
+ // same wallet clears the central namespace on its way out. Private
1064
+ // tracking still names the wallet while central state is empty, and
1065
+ // `trackEvent()` reads the central one - so later events lose their
1066
+ // wallet entirely. Put it back.
1067
+ this.resyncCentralState();
1068
+ var dropped = this.pendingChainId;
1069
+ if (dropped !== undefined) {
1070
+ this.pendingChainId = undefined;
1071
+ if (dropped !== this.trackingState.lastChainId) {
1072
+ logger.info("WagmiEventHandler: Replaying a chain change dropped while reconnecting", { address: live, from: this.trackingState.lastChainId, to: dropped });
1073
+ void this.handleChainChange(dropped, this.trackingState.lastChainId);
1074
+ }
1075
+ }
1076
+ return;
1077
+ }
1078
+ // Different wallet. Wagmi moved from one to another while the lock was
1079
+ // held, so BOTH status callbacks were dropped. Returning here would
1080
+ // leave every later signature and transaction attributed to the wallet
1081
+ // the user has already left.
1082
+ logger.info("WagmiEventHandler: Wallet changed while the lock was held, re-adopting", { from: tracked, to: live });
1083
+ this.reconciling = true;
1084
+ try {
1085
+ this.releaseTrackedWallet();
1086
+ this.seedFromCurrentState();
1087
+ }
1088
+ finally {
1089
+ this.reconciling = false;
1090
+ }
1091
+ return;
1092
+ }
1093
+ this.reconciling = true;
1094
+ try {
1095
+ if (live) {
1096
+ logger.info("WagmiEventHandler: Reconnect landed while the lock was held, adopting it", { address: live });
1097
+ this.retryAdoption();
1098
+ }
1099
+ else {
1100
+ logger.info("WagmiEventHandler: Disconnect landed while the lock was held, applying it", { address: tracked });
1101
+ void this.handleStatusChange("disconnected", "connected");
1102
+ }
1103
+ }
1104
+ finally {
1105
+ this.reconciling = false;
1106
+ }
1107
+ };
1108
+ /**
1109
+ * Handle a switch to a different account on an already-connected wallet.
1110
+ *
1111
+ * Only fires for a genuine in-place switch. A fresh connect and a disconnect
1112
+ * both move `state.status`, and that listener is registered first, so it has
1113
+ * already recorded the new address synchronously by the time this runs. The
1114
+ * `lastAddress` check below is what makes the two paths mutually exclusive.
1115
+ */
1116
+ WagmiEventHandler.prototype.handleActiveAddressChange = function (address, prevAddress) {
1117
+ return __awaiter(this, void 0, void 0, function () {
1118
+ var state, trackedConnectionId, trackedConnectionGone, connectionChanged, liveChain, generation, chainId, stillConnected, goneChainId, error_3, liveNow, stillCurrent, walletKey, connection, alreadyAnnounced, connectorName, error_4;
1119
+ var _a, _b, _c, _d, _e, _f;
1120
+ return __generator(this, function (_g) {
1121
+ switch (_g.label) {
1122
+ case 0:
1123
+ state = this.getState();
1124
+ if (state.status !== "connected")
1125
+ return [2 /*return*/];
1126
+ trackedConnectionId = this.trackingState.lastConnectionId;
1127
+ trackedConnectionGone = !!trackedConnectionId && !state.connections.has(trackedConnectionId);
1128
+ connectionChanged = this.trackingState.lastConnectionId !== undefined &&
1129
+ state.current !== undefined &&
1130
+ state.current !== this.trackingState.lastConnectionId;
1131
+ if (!trackedConnectionGone && !connectionChanged) {
1132
+ if (!address || address === prevAddress)
1133
+ return [2 /*return*/];
1134
+ // Already handled by the status listener (fresh connect, or the seed).
1135
+ // Case-insensitive, like every other address comparison here.
1136
+ if (((_a = this.trackingState.lastAddress) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === address.toLowerCase()) {
1137
+ return [2 /*return*/];
1138
+ }
1139
+ }
1140
+ if (!(connectionChanged &&
1141
+ !trackedConnectionGone &&
1142
+ address &&
1143
+ ((_b = this.trackingState.lastAddress) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === address.toLowerCase())) return [3 /*break*/, 3];
1144
+ liveChain = (_c = this.getActiveConnectionChainId(state)) !== null && _c !== void 0 ? _c : state.chainId;
1145
+ logger.info("WagmiEventHandler: Active connection changed for the same account", { address: address, from: this.trackingState.lastConnectionId, to: state.current });
1146
+ this.trackingState.lastConnectionId = state.current;
1147
+ if (!(liveChain !== undefined && liveChain !== this.trackingState.lastChainId)) return [3 /*break*/, 2];
1148
+ return [4 /*yield*/, this.applyChainForTrackedWallet(address, liveChain)];
1149
+ case 1:
1150
+ _g.sent();
1151
+ _g.label = 2;
1152
+ case 2: return [2 /*return*/];
1153
+ case 3:
1154
+ if (!address)
1155
+ return [2 /*return*/];
1156
+ generation = ++this.transitionGeneration;
1157
+ chainId = (_d = this.getActiveConnectionChainId(state)) !== null && _d !== void 0 ? _d : state.chainId;
1158
+ if (chainId === undefined)
1159
+ return [2 /*return*/];
1160
+ if (!trackedConnectionGone) return [3 /*break*/, 14];
1161
+ stillConnected = !!prevAddress && this.isAddressConnected(state, prevAddress);
1162
+ if (!stillConnected) return [3 /*break*/, 7];
1163
+ // The connector went away but the ACCOUNT did not: another live
1164
+ // connection still holds it. Nothing disconnected from the user's
1165
+ // point of view, so no event - just follow the connection.
1166
+ logger.info("WagmiEventHandler: Tracked connector fell away, account still connected elsewhere", { address: prevAddress, connection: state.current });
1167
+ this.trackingState.lastConnectionId = state.current;
1168
+ if (!(((_e = this.trackingState.lastAddress) === null || _e === void 0 ? void 0 : _e.toLowerCase()) === address.toLowerCase())) return [3 /*break*/, 6];
1169
+ if (!(this.trackingState.lastChainId !== chainId)) return [3 /*break*/, 5];
1170
+ return [4 /*yield*/, this.applyChainForTrackedWallet(address, chainId)];
1171
+ case 4:
1172
+ _g.sent();
1173
+ _g.label = 5;
1174
+ case 5: return [2 /*return*/];
1175
+ case 6: return [3 /*break*/, 14];
1176
+ case 7:
1177
+ if (!prevAddress) return [3 /*break*/, 14];
1178
+ logger.info("WagmiEventHandler: Tracked connector disconnected, wagmi fell back to another connection", { disconnected: prevAddress, fallback: address });
1179
+ goneChainId = this.trackingState.lastChainId;
1180
+ this.releaseTrackedWallet();
1181
+ if (!(this.formo.isAutocaptureEnabled("disconnect") && this.isEmittingOwner)) return [3 /*break*/, 12];
1182
+ _g.label = 8;
1183
+ case 8:
1184
+ _g.trys.push([8, 10, , 11]);
1185
+ // Emitted before central state is cleared, so excludeChains can
1186
+ // still gate it on the chain the wallet was actually on.
1187
+ return [4 /*yield*/, this.formo.disconnect({
1188
+ chainId: goneChainId,
1189
+ address: prevAddress,
1190
+ })];
1191
+ case 9:
1192
+ // Emitted before central state is cleared, so excludeChains can
1193
+ // still gate it on the chain the wallet was actually on.
1194
+ _g.sent();
1195
+ return [3 /*break*/, 11];
1196
+ case 10:
1197
+ error_3 = _g.sent();
1198
+ logger.error("WagmiEventHandler: Error tracking connector fallback disconnect:", error_3);
1199
+ return [3 /*break*/, 11];
1200
+ case 11: return [3 /*break*/, 13];
1201
+ case 12:
1202
+ this.formo.syncWalletState({ chainId: goneChainId });
1203
+ _g.label = 13;
1204
+ case 13:
1205
+ liveNow = void 0;
1206
+ try {
1207
+ liveNow = this.getState();
1208
+ }
1209
+ catch (_h) {
1210
+ /* treated as superseded below */
1211
+ }
1212
+ stillCurrent = !this.disposed &&
1213
+ (liveNow === null || liveNow === void 0 ? void 0 : liveNow.status) === "connected" &&
1214
+ liveNow.current === state.current;
1215
+ if (generation !== this.transitionGeneration || !stillCurrent) {
1216
+ logger.debug("WagmiEventHandler: A newer transition superseded this one, stopping", { address: address });
1217
+ // `disconnect()` clears the central wallet namespace as it
1218
+ // completes, and it has just done so on top of whatever the newer
1219
+ // transition adopted. Private tracking still names the new wallet
1220
+ // while central state is empty, which silently drops the address and
1221
+ // chain from every later event. Put back what is actually live.
1222
+ this.restoreCentralStateFromTracking();
1223
+ return [2 /*return*/];
1224
+ }
1225
+ _g.label = 14;
1226
+ case 14:
1227
+ logger.info("WagmiEventHandler: Active account switched", {
1228
+ from: prevAddress,
1229
+ to: address,
1230
+ chainId: chainId,
1231
+ });
1232
+ // Sync central state first so tracking.excludeChains is enforced even when
1233
+ // connect autocapture is disabled.
1234
+ this.formo.syncWalletState({ chainId: chainId, address: address });
1235
+ // Same rule as the seed: while tracking is suppressed, syncWalletState
1236
+ // refuses to learn a wallet, and retaining it privately would let the
1237
+ // mutation handlers attribute events to an address the SDK declined to
1238
+ // know. Compared case-insensitively because the checksummed form is stored.
1239
+ if (((_f = this.formo.currentAddress) === null || _f === void 0 ? void 0 : _f.toLowerCase()) !== address.toLowerCase()) {
1240
+ logger.debug("WagmiEventHandler: Central state declined the switched account, not adopting", { address: address, chainId: chainId });
1241
+ // Drop the previous wallet too. The user has moved off it, so leaving it
1242
+ // in tracking state would attribute this account's later signatures and
1243
+ // transactions to the account they switched away from - worse than
1244
+ // recording nothing.
1245
+ this.releaseTrackedWallet();
1246
+ return [2 /*return*/];
1247
+ }
1248
+ this.trackingState.lastAddress = address;
1249
+ this.trackingState.lastChainId = chainId;
1250
+ this.trackingState.lastConnectionId = state.current;
1251
+ if (!(this.formo.isAutocaptureEnabled("connect") &&
1252
+ this.isEmittingOwner &&
1253
+ this.formo.willTrackEvent(chainId))) return [3 /*break*/, 18];
1254
+ // Move the page-load marker onto the wallet that is now active.
1255
+ //
1256
+ // Gated on ownership as well, like the ordinary connect path. A
1257
+ // non-owner that marked here would make the real owner find the marker
1258
+ // and stay silent, and the non-owner's own emission dies with its queue
1259
+ // when that instance is torn down - losing the connect entirely.
1260
+ //
1261
+ // Only drop the previous wallet's marker when that wallet is genuinely
1262
+ // gone. On an in-place account switch the old account leaves the
1263
+ // connection and must be able to announce itself again later. But with
1264
+ // several connections the previous wallet is often still connected -
1265
+ // it merely stopped being `state.current` - and forgetting it there
1266
+ // would make it re-announce every time wagmi switched back.
1267
+ if (prevAddress && !this.isAddressConnected(state, prevAddress)) {
1268
+ announcedConnections.delete(announceKey(this.formo.writeKey, prevAddress));
1269
+ }
1270
+ walletKey = announceKey(this.formo.writeKey, address);
1271
+ connection = state.current
1272
+ ? state.connections.get(state.current)
1273
+ : undefined;
1274
+ alreadyAnnounced = announcementState(walletKey, connection) !== "none";
1275
+ markAnnounced(walletKey, connection);
1276
+ if (alreadyAnnounced) {
1277
+ // Reached by the connector-fallback path above: wagmi made a
1278
+ // connection active that this page load already reported. Becoming
1279
+ // active is not a new connect.
1280
+ logger.debug("WagmiEventHandler: Fallback connection was already announced, not re-emitting connect", { address: address });
1281
+ return [2 /*return*/];
1282
+ }
1283
+ _g.label = 15;
1284
+ case 15:
1285
+ _g.trys.push([15, 17, , 18]);
1286
+ connectorName = this.getConnectorName(state);
1287
+ return [4 /*yield*/, this.formo.connect({ chainId: chainId, address: address }, __assign({}, (connectorName && { providerName: connectorName })))];
1288
+ case 16:
1289
+ _g.sent();
1290
+ return [3 /*break*/, 18];
1291
+ case 17:
1292
+ error_4 = _g.sent();
1293
+ releaseAnnouncement(walletKey, connection);
1294
+ logger.error("WagmiEventHandler: Error tracking account switch:", error_4);
1295
+ return [3 /*break*/, 18];
1296
+ case 18: return [2 /*return*/];
1297
+ }
1298
+ });
1299
+ });
1300
+ };
1301
+ /**
1302
+ * Record and emit a chain move for the wallet already being tracked.
1303
+ *
1304
+ * Shared by `handleChainChange()` and the connector-fallback path, which
1305
+ * has to apply the chain itself: the chain callback defers while the new
1306
+ * connection is not adopted yet, so nothing else would.
1307
+ */
1308
+ WagmiEventHandler.prototype.applyChainForTrackedWallet = function (address, chainId) {
1309
+ return __awaiter(this, void 0, void 0, function () {
1310
+ var error_5;
1311
+ var _a;
1312
+ return __generator(this, function (_b) {
1313
+ switch (_b.label) {
1314
+ case 0:
1315
+ // Sync central state unconditionally so a switch to an excluded chain is
1316
+ // honored even when chain autocapture is disabled.
1317
+ this.formo.syncWalletState({ chainId: chainId, address: address });
1318
+ if (((_a = this.formo.currentAddress) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== address.toLowerCase()) {
1319
+ logger.debug("WagmiEventHandler: Central state declined the new chain, dropping the wallet", { address: address, chainId: chainId });
1320
+ this.releaseTrackedWallet();
1321
+ return [2 /*return*/];
1322
+ }
1323
+ this.trackingState.lastChainId = chainId;
1324
+ if (!(this.formo.isAutocaptureEnabled("chain") && this.isEmittingOwner)) return [3 /*break*/, 4];
1325
+ _b.label = 1;
1326
+ case 1:
1327
+ _b.trys.push([1, 3, , 4]);
1328
+ return [4 /*yield*/, this.formo.chain({ chainId: chainId, address: address })];
1329
+ case 2:
1330
+ _b.sent();
1331
+ return [3 /*break*/, 4];
1332
+ case 3:
1333
+ error_5 = _b.sent();
1334
+ logger.error("WagmiEventHandler: Error tracking chain change:", error_5);
1335
+ return [3 /*break*/, 4];
1336
+ case 4: return [2 /*return*/];
215
1337
  }
216
1338
  });
217
1339
  });
@@ -221,7 +1343,7 @@ var WagmiEventHandler = /** @class */ (function () {
221
1343
  */
222
1344
  WagmiEventHandler.prototype.handleChainChange = function (chainId, prevChainId) {
223
1345
  return __awaiter(this, void 0, void 0, function () {
224
- var state, address, error_2;
1346
+ var state, address, hadAddress, wasAnnounced, announcedNow;
225
1347
  return __generator(this, function (_a) {
226
1348
  switch (_a.label) {
227
1349
  case 0:
@@ -230,35 +1352,48 @@ var WagmiEventHandler = /** @class */ (function () {
230
1352
  }
231
1353
  state = this.getState();
232
1354
  if (state.status !== "connected") {
1355
+ this.pendingChainId = chainId;
233
1356
  return [2 /*return*/];
234
1357
  }
1358
+ this.pendingChainId = undefined;
235
1359
  address = this.getConnectedAddress(state);
236
1360
  if (!address) {
237
1361
  logger.warn("WagmiEventHandler: Chain changed but no address found");
238
1362
  return [2 /*return*/];
239
1363
  }
1364
+ // Ignore a chain that belongs to a connection this handler has not adopted
1365
+ // yet.
1366
+ //
1367
+ // Both subscriptions fire for one wagmi update and this one can run first.
1368
+ // During a connector fallback that would overwrite `lastChainId` with the
1369
+ // INCOMING connection's chain, so the outgoing wallet's disconnect - which
1370
+ // the connection handler is about to emit - would be labelled with the
1371
+ // chain of the wallet that replaced it, and a spurious `chain` event would
1372
+ // be emitted for a wallet not yet adopted. The connection handler owns
1373
+ // that transition and records the chain itself.
1374
+ if (this.trackingState.lastConnectionId !== undefined &&
1375
+ state.current !== undefined &&
1376
+ state.current !== this.trackingState.lastConnectionId) {
1377
+ logger.debug("WagmiEventHandler: Chain change belongs to a connection not adopted yet, deferring", { chainId: chainId, from: this.trackingState.lastConnectionId, to: state.current });
1378
+ return [2 /*return*/];
1379
+ }
1380
+ hadAddress = !!this.trackingState.lastAddress;
1381
+ wasAnnounced = this.isCurrentWalletAnnounced();
1382
+ if (!hadAddress || !wasAnnounced) {
1383
+ this.retryAdoption();
1384
+ announcedNow = !wasAnnounced && this.isCurrentWalletAnnounced();
1385
+ if (announcedNow)
1386
+ return [2 /*return*/];
1387
+ }
240
1388
  logger.info("WagmiEventHandler: Chain changed", {
241
1389
  chainId: chainId,
242
1390
  prevChainId: prevChainId,
243
1391
  address: address,
244
1392
  });
245
- this.trackingState.lastChainId = chainId;
246
- // Sync central state unconditionally so a chain switch to an
247
- // excluded chain is honored even when chain autocapture is disabled.
248
- this.formo.syncWalletState({ chainId: chainId, address: address });
249
- if (!this.formo.isAutocaptureEnabled("chain")) return [3 /*break*/, 4];
250
- _a.label = 1;
1393
+ return [4 /*yield*/, this.applyChainForTrackedWallet(address, chainId)];
251
1394
  case 1:
252
- _a.trys.push([1, 3, , 4]);
253
- return [4 /*yield*/, this.formo.chain({ chainId: chainId, address: address })];
254
- case 2:
255
1395
  _a.sent();
256
- return [3 /*break*/, 4];
257
- case 3:
258
- error_2 = _a.sent();
259
- logger.error("WagmiEventHandler: Error tracking chain change:", error_2);
260
- return [3 /*break*/, 4];
261
- case 4: return [2 /*return*/];
1396
+ return [2 /*return*/];
262
1397
  }
263
1398
  });
264
1399
  });
@@ -303,6 +1438,11 @@ var WagmiEventHandler = /** @class */ (function () {
303
1438
  if (event.type !== "updated") {
304
1439
  return;
305
1440
  }
1441
+ // Overlapping handlers share one MutationCache/QueryCache, so without
1442
+ // this each of them emits for the same mutation.
1443
+ if (!this.isEmittingOwner) {
1444
+ return;
1445
+ }
306
1446
  var query = event.query;
307
1447
  var queryKey = query.queryKey;
308
1448
  if (!queryKey || queryKey.length === 0) {
@@ -348,7 +1488,7 @@ var WagmiEventHandler = /** @class */ (function () {
348
1488
  * Emits CONFIRMED or REVERTED transaction status
349
1489
  */
350
1490
  WagmiEventHandler.prototype.handleTransactionReceiptQuery = function (query) {
351
- var _a;
1491
+ var _a, _b;
352
1492
  if (!this.formo.isAutocaptureEnabled("transaction")) {
353
1493
  return;
354
1494
  }
@@ -362,7 +1502,8 @@ var WagmiEventHandler = /** @class */ (function () {
362
1502
  // Query key format: ['waitForTransactionReceipt', { hash, chainId, ... }]
363
1503
  var params = queryKey[1];
364
1504
  var transactionHash = params === null || params === void 0 ? void 0 : params.hash;
365
- var chainId = (params === null || params === void 0 ? void 0 : params.chainId) || this.trackingState.lastChainId;
1505
+ // Resolved after the pending lookup below, so the broadcast chain wins.
1506
+ var queryChainId = params === null || params === void 0 ? void 0 : params.chainId;
366
1507
  if (!transactionHash) {
367
1508
  logger.warn("WagmiEventHandler: Transaction receipt query but no hash found");
368
1509
  return;
@@ -382,6 +1523,17 @@ var WagmiEventHandler = /** @class */ (function () {
382
1523
  return;
383
1524
  }
384
1525
  var address = pendingTx.address;
1526
+ // The chain the transaction was actually broadcast on wins, because the
1527
+ // active chain can change between broadcast and receipt and would
1528
+ // otherwise relabel the confirmation.
1529
+ //
1530
+ // Order: a chain the caller named explicitly, then the receipt query's own
1531
+ // chain, then the chain we observed at broadcast time, then the current
1532
+ // one. The inferred broadcast chain sits below the query's because the
1533
+ // query names the chain the receipt was actually read from.
1534
+ var chainId = pendingTx.chainIdWasExplicit
1535
+ ? pendingTx.chainId
1536
+ : (_a = queryChainId !== null && queryChainId !== void 0 ? queryChainId : pendingTx.chainId) !== null && _a !== void 0 ? _a : this.trackingState.lastChainId;
385
1537
  if (!address) {
386
1538
  logger.warn("WagmiEventHandler: Transaction receipt query but no address available");
387
1539
  return;
@@ -403,7 +1555,7 @@ var WagmiEventHandler = /** @class */ (function () {
403
1555
  transactionHash: transactionHash,
404
1556
  address: address,
405
1557
  chainId: chainId,
406
- blockNumber: (_a = receipt === null || receipt === void 0 ? void 0 : receipt.blockNumber) === null || _a === void 0 ? void 0 : _a.toString(),
1558
+ blockNumber: (_b = receipt === null || receipt === void 0 ? void 0 : receipt.blockNumber) === null || _b === void 0 ? void 0 : _b.toString(),
407
1559
  });
408
1560
  this.formo.transaction(__assign(__assign(__assign(__assign(__assign({ status: txStatus, chainId: chainId || 0, address: address, transactionHash: transactionHash }, ((pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.data) && { data: pendingTx.data })), ((pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.to) && { to: pendingTx.to })), ((pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.value) && { value: pendingTx.value })), ((pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.function_name) && { function_name: pendingTx.function_name })), ((pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.function_args) && { function_args: pendingTx.function_args })),
409
1561
  // Spread function args as additional properties (only colliding keys are prefixed)
@@ -422,6 +1574,11 @@ var WagmiEventHandler = /** @class */ (function () {
422
1574
  if (event.type !== "updated") {
423
1575
  return;
424
1576
  }
1577
+ // Overlapping handlers share one MutationCache/QueryCache, so without
1578
+ // this each of them emits for the same mutation.
1579
+ if (!this.isEmittingOwner) {
1580
+ return;
1581
+ }
425
1582
  var mutation = event.mutation;
426
1583
  var mutationKey = mutation.options.mutationKey;
427
1584
  if (!mutationKey || mutationKey.length === 0) {
@@ -462,13 +1619,28 @@ var WagmiEventHandler = /** @class */ (function () {
462
1619
  * Handle signature mutations (signMessage, signTypedData)
463
1620
  */
464
1621
  WagmiEventHandler.prototype.handleSignatureMutation = function (mutationType, mutation) {
1622
+ var _a, _b, _c, _d;
465
1623
  if (!this.formo.isAutocaptureEnabled("signature")) {
466
1624
  return;
467
1625
  }
468
1626
  var state = mutation.state;
469
1627
  var variables = state.variables || {};
470
- var chainId = this.trackingState.lastChainId;
471
- var address = this.trackingState.lastAddress;
1628
+ // An explicit per-call `account` wins over the active connection. wagmi
1629
+ // lets a caller sign with an account other than the current one, and the
1630
+ // tracked connection describes a different wallet in that case.
1631
+ //
1632
+ // For typed data the signed chain lives in the EIP-712 domain, not at the
1633
+ // top level: `signTypedData` takes `{ domain, types, primaryType, message }`
1634
+ // and the domain's `chainId` is what the signature is actually bound to.
1635
+ // Reading only `variables.chainId` labelled such a signature with the
1636
+ // wallet's current chain, which can be a different one entirely.
1637
+ // Falls back to 0 - "could not resolve" - rather than undefined. A caller
1638
+ // can name an account before any connection chain is known, and an
1639
+ // undefined chain slipped past the exclusion gate, which only refuses an
1640
+ // explicit 0. A chain-scoped event whose chain is unknown must fail
1641
+ // closed like every other.
1642
+ var chainId = (_d = (_c = (_b = normalizeChainId((_a = variables.domain) === null || _a === void 0 ? void 0 : _a.chainId)) !== null && _b !== void 0 ? _b : normalizeChainId(variables.chainId)) !== null && _c !== void 0 ? _c : this.trackingState.lastChainId) !== null && _d !== void 0 ? _d : 0;
1643
+ var address = resolveAccountAddress(variables.account) || this.trackingState.lastAddress;
472
1644
  if (!address) {
473
1645
  logger.warn("WagmiEventHandler: Signature event but no address available");
474
1646
  return;
@@ -522,15 +1694,26 @@ var WagmiEventHandler = /** @class */ (function () {
522
1694
  }
523
1695
  var state = mutation.state;
524
1696
  var variables = state.variables || {};
525
- var chainId = this.trackingState.lastChainId || variables.chainId;
526
- // For sendTransaction, user's address is the 'from'
527
- // For writeContract, variables.address is the contract address, not the user
528
- // variables.account can be a string address or an Account object with an address property
529
- var accountValue = variables.account;
530
- var accountAddress = typeof accountValue === "string"
531
- ? accountValue
532
- : accountValue === null || accountValue === void 0 ? void 0 : accountValue.address;
533
- var userAddress = this.trackingState.lastAddress || accountAddress || variables.from;
1697
+ // Explicit per-call values win over the active connection. wagmi lets a
1698
+ // caller target another account or chain, and the tracked connection
1699
+ // describes a different wallet in that case. Fall back to the connection
1700
+ // for the usual call that omits them.
1701
+ //
1702
+ // For sendTransaction the user's address is `from`; for writeContract
1703
+ // `variables.address` is the contract, not the user.
1704
+ // Distinguish an explicitly requested chain from one merely inferred from
1705
+ // the active connection: only the former is authoritative for the receipt.
1706
+ // Normalized: viem accepts a hex string or a bigint for `chainId`, and a
1707
+ // non-number here would both land in the payload and defeat the numeric
1708
+ // `tracking.excludeChains` comparison.
1709
+ var explicitChainId = normalizeChainId(variables.chainId);
1710
+ // Left undefined on purpose. The emission below already coerces with
1711
+ // `chainId || 0`, and defaulting here instead would write 0 into the
1712
+ // pending-transaction record, where an absent chain is what lets the
1713
+ // receipt query's chain win for the confirmation.
1714
+ var chainId = explicitChainId !== null && explicitChainId !== void 0 ? explicitChainId : this.trackingState.lastChainId;
1715
+ var accountAddress = resolveAccountAddress(variables.account);
1716
+ var userAddress = accountAddress || variables.from || this.trackingState.lastAddress;
534
1717
  if (!userAddress) {
535
1718
  logger.warn("WagmiEventHandler: Transaction event but no address available");
536
1719
  return;
@@ -596,7 +1779,7 @@ var WagmiEventHandler = /** @class */ (function () {
596
1779
  // Include the sender address to handle wallet switches between broadcast and confirmation
597
1780
  if (status_2 === TransactionStatus.BROADCASTED && transactionHash) {
598
1781
  var normalizedHash = transactionHash.toLowerCase();
599
- var txDetails = __assign(__assign(__assign(__assign(__assign(__assign({ address: userAddress }, (data && { data: data })), (to && { to: to })), (value && { value: value })), (function_name && { function_name: function_name })), (function_args && { function_args: function_args })), (safeFunctionArgs && { safeFunctionArgs: safeFunctionArgs }));
1782
+ var txDetails = __assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign({ address: userAddress }, (chainId !== undefined && { chainId: chainId })), { chainIdWasExplicit: explicitChainId !== undefined }), (data && { data: data })), (to && { to: to })), (value && { value: value })), (function_name && { function_name: function_name })), (function_args && { function_args: function_args })), (safeFunctionArgs && { safeFunctionArgs: safeFunctionArgs }));
600
1783
  this.pendingTransactions.set(normalizedHash, txDetails);
601
1784
  logger.debug("WagmiEventHandler: Stored pending transaction for confirmation", {
602
1785
  transactionHash: normalizedHash,
@@ -641,6 +1824,21 @@ var WagmiEventHandler = /** @class */ (function () {
641
1824
  chainId: undefined,
642
1825
  };
643
1826
  };
1827
+ /**
1828
+ * Chain of the connection that is currently active.
1829
+ *
1830
+ * Authoritative, because `state.chainId` is a single global value. With
1831
+ * `syncConnectedChain: false` it stays on the chain the APP selected while
1832
+ * the connection reports the chain the WALLET is actually on, so seeding
1833
+ * from the global could label a wallet on an excluded chain as an allowed
1834
+ * one and send events the exclusion forbids.
1835
+ */
1836
+ WagmiEventHandler.prototype.getActiveConnectionChainId = function (state) {
1837
+ var _a;
1838
+ if (!state.current)
1839
+ return undefined;
1840
+ return (_a = state.connections.get(state.current)) === null || _a === void 0 ? void 0 : _a.chainId;
1841
+ };
644
1842
  /**
645
1843
  * Get the currently connected address from Wagmi state
646
1844
  */
@@ -654,6 +1852,24 @@ var WagmiEventHandler = /** @class */ (function () {
654
1852
  }
655
1853
  return connection.accounts[0];
656
1854
  };
1855
+ /**
1856
+ * Whether any live connection still holds this address.
1857
+ *
1858
+ * Distinguishes "the user switched away from this account" (gone from every
1859
+ * connection) from "this account merely stopped being the current one"
1860
+ * (still connected through its own connector).
1861
+ */
1862
+ WagmiEventHandler.prototype.isAddressConnected = function (state, address) {
1863
+ var wanted = address.toLowerCase();
1864
+ var found = false;
1865
+ state.connections.forEach(function (connection) {
1866
+ if (!found &&
1867
+ connection.accounts.some(function (a) { return a.toLowerCase() === wanted; })) {
1868
+ found = true;
1869
+ }
1870
+ });
1871
+ return found;
1872
+ };
657
1873
  /**
658
1874
  * Get the connector name from Wagmi state
659
1875
  */
@@ -669,6 +1885,26 @@ var WagmiEventHandler = /** @class */ (function () {
669
1885
  */
670
1886
  WagmiEventHandler.prototype.cleanup = function () {
671
1887
  logger.debug("WagmiEventHandler: Cleaning up subscriptions");
1888
+ if (!this.disposed) {
1889
+ this.disposed = true;
1890
+ // Nothing in flight may complete against a torn-down handler.
1891
+ this.transitionGeneration += 1;
1892
+ // Give up the right to emit. Any other live handler for this
1893
+ // destination claims it the next time it needs to emit.
1894
+ if (this.ownerKey && emittingOwners.get(this.ownerKey) === this) {
1895
+ emittingOwners.delete(this.ownerKey);
1896
+ }
1897
+ // Start the grace period. If nothing re-mounts, the markers are dropped
1898
+ // so a reconnect that happens while unobserved still emits.
1899
+ var wasLastForDestination = releaseMarkers(this.formo.writeKey);
1900
+ // Drop the shared broadcast records rather than leak them - but only
1901
+ // after the same grace period the markers get. The ordinary rebuild is
1902
+ // cleanup THEN remount, so deleting immediately would lose the receipt
1903
+ // for a transaction broadcast moments before teardown.
1904
+ if (wasLastForDestination && this.ownerKey) {
1905
+ schedulePendingTransactionExpiry(this.ownerKey);
1906
+ }
1907
+ }
672
1908
  for (var _i = 0, _a = this.unsubscribers; _i < _a.length; _i++) {
673
1909
  var unsubscribe = _a[_i];
674
1910
  try {
@@ -681,7 +1917,10 @@ var WagmiEventHandler = /** @class */ (function () {
681
1917
  this.unsubscribers = [];
682
1918
  this.processedMutations.clear();
683
1919
  this.processedQueries.clear();
684
- this.pendingTransactions.clear();
1920
+ // Deliberately NOT clearing `pendingTransactions`: it is shared per
1921
+ // destination, and a replacement handler needs the broadcast records to
1922
+ // match incoming receipts against. It is dropped when the last handler
1923
+ // for the destination goes away (see below).
685
1924
  logger.debug("WagmiEventHandler: Cleanup complete");
686
1925
  };
687
1926
  return WagmiEventHandler;