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