@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
@@ -94,6 +94,39 @@ var FormoAnalytics = /** @class */ (function () {
94
94
  evm: {},
95
95
  solana: {},
96
96
  };
97
+ /**
98
+ * Last known chain per tracked provider, fed by `chainChanged` / `connect`
99
+ * and by a one-off probe at tracking time. Read synchronously when labelling
100
+ * an autocaptured event, so the wallet's request transport is never used for
101
+ * analytics. Weak so an untracked provider can be collected.
102
+ */
103
+ /**
104
+ * The connect this SDK has actually REPORTED for a provider.
105
+ *
106
+ * Deduplicating on "is an address known" was wrong in both directions.
107
+ * `onConnected` and `onAccountsChanged` both observe one connection, so
108
+ * something has to stop them double-reporting - but an address can be
109
+ * present without a connect ever having been sent: restored from the
110
+ * active-wallet cookie, or reported with an unresolved chain and then
111
+ * refused by `tracking.excludeChains`. Suppressing on address presence lost
112
+ * the connect entirely in those cases.
113
+ *
114
+ * Records what was reported, so a later event can be recognised as a
115
+ * genuine improvement on it rather than a duplicate.
116
+ */
117
+ this._announcedConnect = new WeakMap();
118
+ this._providerChainIds = new WeakMap();
119
+ /**
120
+ * Bumped on every chain observation, PER PROVIDER. An `eth_chainId` answer
121
+ * that resolves after a newer observation for the same provider must not
122
+ * overwrite it.
123
+ *
124
+ * Deliberately per provider, not per SDK instance: a global counter meant
125
+ * any activity on wallet B discarded a perfectly valid in-flight answer for
126
+ * wallet A, leaving A at chain 0 - and, since an unresolved chain fails
127
+ * closed, dropping all of A's events whenever `excludeChains` is set.
128
+ */
129
+ this._providerChainGenerations = new WeakMap();
97
130
  this._providerListenersMap = new Map();
98
131
  /**
99
132
  * EIP-6963 provider details discovered through the browser
@@ -222,11 +255,37 @@ var FormoAnalytics = /** @class */ (function () {
222
255
  return this._chainState.evm.provider;
223
256
  },
224
257
  set: function (value) {
258
+ var previous = this._chainState.evm.provider;
259
+ // A provider that stops being the active one has, from this SDK's point of
260
+ // view, ended its connection - so the connect it reported must stop
261
+ // counting. Otherwise toggling between two installed wallets silently
262
+ // loses every connect after the first: A's stale record suppresses the
263
+ // connect when the user comes back to it.
264
+ //
265
+ // Done in the setter rather than at each switch site because the active
266
+ // provider is reassigned from several paths - `accountsChanged`,
267
+ // `chainChanged`, `connect`, `handleProviderMismatch`, `untrackProvider` -
268
+ // and any one of them missed would reopen the same hole.
269
+ this.forgetConnectRecord(previous, value);
225
270
  this._chainState.evm.provider = value;
226
271
  },
227
272
  enumerable: false,
228
273
  configurable: true
229
274
  });
275
+ /**
276
+ * Drop a provider's reported-connect record when it stops being active.
277
+ *
278
+ * The active provider is displaced from several places, and not all of them
279
+ * go through the `_provider` setter: `setChainState()` writes
280
+ * `_chainState.evm.provider` directly and `clearChainState()` replaces the
281
+ * whole namespace. Guarding only the setter left A's record alive across a
282
+ * real A-to-B switch, so returning to A was suppressed.
283
+ */
284
+ FormoAnalytics.prototype.forgetConnectRecord = function (previous, next) {
285
+ if (previous && previous !== next) {
286
+ this._announcedConnect.delete(previous);
287
+ }
288
+ };
230
289
  Object.defineProperty(FormoAnalytics.prototype, "_evmAddress", {
231
290
  get: function () {
232
291
  return this._chainState.evm.address;
@@ -843,6 +902,18 @@ var FormoAnalytics = /** @class */ (function () {
843
902
  * Opt out of tracking.
844
903
  * @returns {void}
845
904
  */
905
+ /**
906
+ * Whether an event carrying this chain would currently be sent.
907
+ *
908
+ * Exposed for integrations that keep their own "already reported" state.
909
+ * `syncWalletState()` can accept a wallet that `trackEvent()` then drops -
910
+ * `tracking: false`, or a chain in `excludeChains` - and an integration that
911
+ * marked it as reported would stay silent about that wallet even after the
912
+ * configuration changed to allow it.
913
+ */
914
+ FormoAnalytics.prototype.willTrackEvent = function (chainId) {
915
+ return this.shouldTrack(chainId);
916
+ };
846
917
  FormoAnalytics.prototype.optOutTracking = function () {
847
918
  logger.info("Opting out of tracking");
848
919
  // Set opt-out flag in persistent storage using direct cookie access
@@ -860,9 +931,15 @@ var FormoAnalytics = /** @class */ (function () {
860
931
  * @returns {void}
861
932
  */
862
933
  FormoAnalytics.prototype.optInTracking = function () {
934
+ var _a;
863
935
  logger.info("Opting back into tracking");
864
936
  // Remove opt-out flag
865
937
  removeConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY);
938
+ // A wallet connected while opted out was declined by syncWalletState, and
939
+ // an unchanged wagmi connection produces no status or chain update to
940
+ // retry on. Without this, opting back in leaves that wallet invisible for
941
+ // the rest of the page load.
942
+ (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
866
943
  logger.info("Successfully opted back into tracking");
867
944
  };
868
945
  /**
@@ -904,20 +981,43 @@ var FormoAnalytics = /** @class */ (function () {
904
981
  // This ensures currentAddress, currentChainId, and _provider are always up-to-date
905
982
  // Event emission is controlled conditionally inside the handlers
906
983
  this.registerAccountsChangedListener(provider);
907
- // Register other listeners based on autocapture configuration
908
- if (this.isAutocaptureEnabled("chain")) {
909
- this.registerChainChangedListener(provider);
910
- }
984
+ // `chainChanged` and `connect` are registered UNCONDITIONALLY: they are
985
+ // how this provider's chain is observed, and every signature and
986
+ // transaction has to be labelled with it. Gating registration on
987
+ // `autocapture.chain` conflated observing a chain with reporting one, so
988
+ // `{ chain: false, signature: true }` left the chain frozen at whatever
989
+ // was first seen - a switch to an excluded chain went unnoticed and its
990
+ // signatures were emitted under the old, allowed chain. Whether an
991
+ // event is emitted is decided inside each handler.
992
+ this.registerChainChangedListener(provider);
911
993
  if (this.isAutocaptureEnabled("connect")) {
912
994
  this.registerConnectListener(provider);
913
995
  }
996
+ else {
997
+ // Observation only. The full connect handler calls `getAddress()`,
998
+ // which issues `eth_accounts`, and nothing analytics-only may go on
999
+ // the wallet's transport - a stalled request there sits in front of
1000
+ // the next signature the dapp makes. The chain rides along on the
1001
+ // event itself, so it costs nothing to record.
1002
+ this.registerConnectChainObserver(provider);
1003
+ }
1004
+ // Seed the chain from the provider's own synchronous state if it exposes
1005
+ // one. Deliberately a property read and never an RPC: see
1006
+ // resolveChainIdForProvider for why nothing analytics-only may go on the
1007
+ // wallet's transport.
1008
+ this.seedProviderChainFromState(provider);
914
1009
  if (this.isAutocaptureEnabled("signature") || this.isAutocaptureEnabled("transaction")) {
915
1010
  this.registerRequestListeners(provider);
916
1011
  }
917
1012
  else {
918
1013
  logger.debug("TrackProvider: Skipping request wrapping (both signature and transaction autocapture disabled)");
919
1014
  }
920
- if (this.isAutocaptureEnabled("disconnect")) {
1015
+ // Registered UNCONDITIONALLY: this listener also ends the provider's
1016
+ // reported-connect record, and with `{ connect: true, disconnect: false }`
1017
+ // a wallet that disconnected and reconnected would otherwise find its
1018
+ // old record still standing and have the new connect suppressed. Whether
1019
+ // a disconnect EVENT is emitted is decided inside the handler.
1020
+ {
921
1021
  this.registerDisconnectListener(provider);
922
1022
  }
923
1023
  // Only add to tracked providers after all listeners are successfully registered
@@ -1011,6 +1111,10 @@ var FormoAnalytics = /** @class */ (function () {
1011
1111
  evmChainId: this._evmChainId,
1012
1112
  providerMatch: this._provider === provider,
1013
1113
  });
1114
+ // The reported connect ends with the connection, so a genuine
1115
+ // reconnect later reports again rather than being taken for a
1116
+ // duplicate.
1117
+ this._announcedConnect.delete(provider);
1014
1118
  if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 5];
1015
1119
  _a.label = 1;
1016
1120
  case 1:
@@ -1159,9 +1263,7 @@ var FormoAnalytics = /** @class */ (function () {
1159
1263
  if (this._provider === provider && address === this._evmAddress) {
1160
1264
  return [2 /*return*/];
1161
1265
  }
1162
- return [4 /*yield*/, this.getCurrentChainId(provider)];
1163
- case 26:
1164
- nextChainId = _a.sent();
1266
+ nextChainId = this.resolveChainIdForProvider(provider);
1165
1267
  wasDisconnected = !this._evmAddress;
1166
1268
  // Update state regardless of whether connect *event* tracking is enabled,
1167
1269
  // so disconnect events keep valid address/chainId values. (excludeChains is
@@ -1175,7 +1277,8 @@ var FormoAnalytics = /** @class */ (function () {
1175
1277
  }
1176
1278
  providerInfo = this.getProviderInfo(provider);
1177
1279
  effectiveChainId = nextChainId || 0;
1178
- if (this.isAutocaptureEnabled("connect")) {
1280
+ if (this.isAutocaptureEnabled("connect") &&
1281
+ this.shouldReportConnect(provider, address)) {
1179
1282
  logger.info("OnAccountsChanged: Detected wallet connection, emitting connect event", {
1180
1283
  chainId: nextChainId,
1181
1284
  address: address,
@@ -1187,6 +1290,7 @@ var FormoAnalytics = /** @class */ (function () {
1187
1290
  if (effectiveChainId === 0) {
1188
1291
  logger.info("OnAccountsChanged: Using fallback chainId 0 for connect event");
1189
1292
  }
1293
+ this.markConnectReported(provider, address, effectiveChainId);
1190
1294
  this.connect({
1191
1295
  chainId: effectiveChainId,
1192
1296
  address: address,
@@ -1224,46 +1328,140 @@ var FormoAnalytics = /** @class */ (function () {
1224
1328
  };
1225
1329
  FormoAnalytics.prototype.onChainChanged = function (provider, chainIdHex) {
1226
1330
  return __awaiter(this, void 0, void 0, function () {
1227
- var nextChainId;
1331
+ var nextChainId, error_3;
1228
1332
  return __generator(this, function (_a) {
1229
- logger.info("onChainChanged", chainIdHex);
1230
- nextChainId = parseChainId(chainIdHex);
1231
- // Only handle chain changes for the active provider (or if none is set yet)
1232
- if (this.isProviderMismatch(provider)) {
1233
- this.handleProviderMismatch(provider);
1234
- }
1235
- // Chain changes only matter for connected users
1236
- if (!this._evmAddress) {
1237
- logger.info("OnChainChanged: No current address, user appears disconnected");
1238
- return [2 /*return*/, Promise.resolve()];
1239
- }
1240
- // Set provider if none exists
1241
- if (!this._provider) {
1242
- this._provider = provider;
1243
- }
1244
- this.setChainState('evm', { chainId: nextChainId });
1245
- try {
1246
- // This is just a chain change since we already confirmed _evmAddress exists
1247
- if (this.isAutocaptureEnabled("chain")) {
1248
- return [2 /*return*/, this.chain({
1333
+ switch (_a.label) {
1334
+ case 0:
1335
+ logger.info("onChainChanged", chainIdHex);
1336
+ nextChainId = parseChainId(chainIdHex);
1337
+ // Record it for THIS provider regardless of which one is active. This is
1338
+ // the only way an autocaptured event from a non-active wallet can learn
1339
+ // its chain without putting an RPC on that wallet's transport.
1340
+ this.rememberProviderChain(provider, nextChainId);
1341
+ // Beyond that, a chain event from a NON-active provider is observation
1342
+ // only when chain autocapture is off.
1343
+ //
1344
+ // This listener is now registered unconditionally, so that a signature can
1345
+ // be labelled with its signer's chain. `handleProviderMismatch()` treats a
1346
+ // chain event from another wallet as a wallet switch and clears the active
1347
+ // wallet's address and chain. That is the established behaviour of the
1348
+ // chain feature and stays exactly as it was, but it must not start firing
1349
+ // for apps that never asked for chain tracking: a second wallet switching
1350
+ // network would silently erase the active wallet's attribution.
1351
+ // Observation only when chain autocapture is off, whether or not an
1352
+ // active provider has been established yet.
1353
+ //
1354
+ // `isProviderMismatch()` is false while `_provider` is undefined, which is
1355
+ // exactly the state left by restoring a wallet from the active-wallet
1356
+ // cookie. A background wallet's `chainChanged` could therefore claim the
1357
+ // active slot and overwrite the restored wallet's chain - suppressing
1358
+ // allowed events, or letting excluded ones through. The active provider is
1359
+ // established by an actual account/connect/request association, not by
1360
+ // another wallet changing network.
1361
+ if (!this.isAutocaptureEnabled("chain") && provider !== this._provider) {
1362
+ return [2 /*return*/];
1363
+ }
1364
+ // Only handle chain changes for the active provider (or if none is set yet)
1365
+ if (this.isProviderMismatch(provider)) {
1366
+ this.handleProviderMismatch(provider);
1367
+ }
1368
+ // Chain changes only matter for connected users
1369
+ if (!this._evmAddress) {
1370
+ logger.info("OnChainChanged: No current address, user appears disconnected");
1371
+ return [2 /*return*/, Promise.resolve()];
1372
+ }
1373
+ // Set provider if none exists
1374
+ if (!this._provider) {
1375
+ this._provider = provider;
1376
+ }
1377
+ this.setChainState('evm', { chainId: nextChainId });
1378
+ _a.label = 1;
1379
+ case 1:
1380
+ _a.trys.push([1, 5, , 6]);
1381
+ if (!this.isAutocaptureEnabled("chain")) return [3 /*break*/, 3];
1382
+ // Awaited, so a failing emission is caught below rather than escaping
1383
+ // as an unhandled rejection out of the provider's event listener.
1384
+ // `return`ing the promise left the catch here unreachable.
1385
+ return [4 /*yield*/, this.chain({
1249
1386
  chainId: nextChainId,
1250
1387
  address: this._evmAddress,
1251
1388
  })];
1252
- }
1253
- else {
1389
+ case 2:
1390
+ // Awaited, so a failing emission is caught below rather than escaping
1391
+ // as an unhandled rejection out of the provider's event listener.
1392
+ // `return`ing the promise left the catch here unreachable.
1393
+ _a.sent();
1394
+ return [3 /*break*/, 4];
1395
+ case 3:
1254
1396
  logger.debug("OnChainChanged: Chain event skipped (autocapture.chain: false)", {
1255
1397
  chainId: this._evmChainId,
1256
1398
  address: this._evmAddress,
1257
1399
  });
1258
- }
1259
- }
1260
- catch (error) {
1261
- logger.error("OnChainChanged: Failed to emit chain event:", error);
1400
+ _a.label = 4;
1401
+ case 4: return [3 /*break*/, 6];
1402
+ case 5:
1403
+ error_3 = _a.sent();
1404
+ logger.error("OnChainChanged: Failed to emit chain event:", error_3);
1405
+ return [3 /*break*/, 6];
1406
+ case 6: return [2 /*return*/];
1262
1407
  }
1263
- return [2 /*return*/];
1264
1408
  });
1265
1409
  });
1266
1410
  };
1411
+ /**
1412
+ * Record a provider's chain from its `connect` event, and nothing else.
1413
+ *
1414
+ * Used when connect autocapture is off. `connect` carries `chainId` in its
1415
+ * payload, so this needs no RPC - unlike the full handler, which resolves
1416
+ * the account.
1417
+ */
1418
+ FormoAnalytics.prototype.registerConnectChainObserver = function (provider) {
1419
+ var _this = this;
1420
+ var listener = function () {
1421
+ var args = [];
1422
+ for (var _i = 0; _i < arguments.length; _i++) {
1423
+ args[_i] = arguments[_i];
1424
+ }
1425
+ var connection = args[0];
1426
+ if (typeof (connection === null || connection === void 0 ? void 0 : connection.chainId) !== "string")
1427
+ return;
1428
+ _this.rememberProviderChain(provider, parseChainId(connection.chainId));
1429
+ };
1430
+ provider.on("connect", listener);
1431
+ this.addProviderListener(provider, "connect", listener);
1432
+ };
1433
+ /**
1434
+ * Whether a connect for this wallet still needs reporting.
1435
+ *
1436
+ * True when nothing has been reported for this provider, or when the account
1437
+ * changed. A wallet already reported is not reported again.
1438
+ *
1439
+ * Deliberately does NOT re-report to correct a chain. When `accountsChanged`
1440
+ * wins the race on a provider that exposes no synchronous `chainId`, the
1441
+ * connect carries 0 - honestly, since the chain is unknown at that instant -
1442
+ * and the `connect` payload that follows knows the real one. Emitting again
1443
+ * to relabel would mean two connects for one connection, which is the bug
1444
+ * this whole path exists to prevent. That payload still corrects
1445
+ * `currentChainId`, so everything after it is attributed properly.
1446
+ */
1447
+ FormoAnalytics.prototype.shouldReportConnect = function (provider, address) {
1448
+ var reported = this._announcedConnect.get(provider);
1449
+ if (!reported)
1450
+ return true;
1451
+ return reported.address.toLowerCase() !== address.toLowerCase();
1452
+ };
1453
+ /**
1454
+ * Record a connect as reported - but only if it will actually be sent.
1455
+ *
1456
+ * `connect()` passes through `shouldTrack()`, which refuses an unresolvable
1457
+ * chain when `tracking.excludeChains` is configured. Marking a refused event
1458
+ * as reported would suppress the authoritative one that follows.
1459
+ */
1460
+ FormoAnalytics.prototype.markConnectReported = function (provider, address, chainId) {
1461
+ if (!this.willTrackEvent(chainId))
1462
+ return;
1463
+ this._announcedConnect.set(provider, { address: address, chainId: chainId });
1464
+ };
1267
1465
  FormoAnalytics.prototype.registerConnectListener = function (provider) {
1268
1466
  var _this = this;
1269
1467
  logger.info("registerConnectListener");
@@ -1288,6 +1486,9 @@ var FormoAnalytics = /** @class */ (function () {
1288
1486
  case 0:
1289
1487
  if (this._provider !== provider)
1290
1488
  return [2 /*return*/];
1489
+ // As in the accountsChanged disconnect path: the reported connect ends
1490
+ // with the connection.
1491
+ this._announcedConnect.delete(provider);
1291
1492
  logger.info("OnDisconnect: Wallet disconnect event received, current state:", {
1292
1493
  currentAddress: this._evmAddress,
1293
1494
  currentChainId: this._evmChainId,
@@ -1335,6 +1536,8 @@ var FormoAnalytics = /** @class */ (function () {
1335
1536
  if (!(connection === null || connection === void 0 ? void 0 : connection.chainId) || typeof connection.chainId !== "string")
1336
1537
  return [2 /*return*/];
1337
1538
  chainId = parseChainId(connection.chainId);
1539
+ // Record it for this provider before anything can bail out below.
1540
+ this.rememberProviderChain(provider, chainId);
1338
1541
  return [4 /*yield*/, this.getAddress(provider)];
1339
1542
  case 2:
1340
1543
  address = _a.sent();
@@ -1359,8 +1562,18 @@ var FormoAnalytics = /** @class */ (function () {
1359
1562
  });
1360
1563
  }
1361
1564
  }
1362
- // Conditionally emit connect event based on tracking configuration
1363
- if (isActiveProvider && this._evmAddress) {
1565
+ // Conditionally emit connect event based on tracking configuration.
1566
+ //
1567
+ // Both handlers observe one connection, so `shouldReportConnect()`
1568
+ // decides which of them reports it. It keys on what was actually
1569
+ // REPORTED, not on whether an address is known: an address can be
1570
+ // present with no connect ever sent - restored from the active-wallet
1571
+ // cookie, or reported with an unresolved chain and then refused by
1572
+ // `excludeChains` - and this payload carries the authoritative chain,
1573
+ // so it must be able to supersede such a report.
1574
+ if (isActiveProvider &&
1575
+ this._evmAddress &&
1576
+ this.shouldReportConnect(provider, address)) {
1364
1577
  providerInfo = this.getProviderInfo(provider);
1365
1578
  effectiveChainId = chainId || 0;
1366
1579
  if (this.isAutocaptureEnabled("connect")) {
@@ -1375,6 +1588,7 @@ var FormoAnalytics = /** @class */ (function () {
1375
1588
  if (effectiveChainId === 0) {
1376
1589
  logger.info("OnConnected: Using fallback chainId 0 for connect event");
1377
1590
  }
1591
+ this.markConnectReported(provider, address, effectiveChainId);
1378
1592
  this.connect({
1379
1593
  chainId: effectiveChainId,
1380
1594
  address: address,
@@ -1431,162 +1645,209 @@ var FormoAnalytics = /** @class */ (function () {
1431
1645
  }
1432
1646
  var request = provider.request.bind(provider);
1433
1647
  var wrappedRequest = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
1434
- var capturedChainId_1, _c, response_1, error_3, rpcError, transactionHash_1, error_4, rpcError;
1648
+ var generation_1, responsePromise, capturedChainId_1, response_1, error_4, rpcError, txPromise, txChainId_1, transactionHash_1, error_5, rpcError;
1435
1649
  var _this = this;
1650
+ var _c;
1436
1651
  var method = _b.method, params = _b.params;
1437
1652
  return __generator(this, function (_d) {
1438
1653
  switch (_d.label) {
1439
1654
  case 0:
1655
+ // Learn the chain from a call the APP was making anyway.
1656
+ //
1657
+ // A standards-compliant provider need not expose a synchronous `chainId`
1658
+ // property, and if it connected before the SDK initialised, its
1659
+ // `connect` event is never replayed. Such a provider stayed unknown -
1660
+ // reported as chain 0, and with `excludeChains` configured its events
1661
+ // were dropped even on an allowed chain. This adds no request of its
1662
+ // own; it only reads the answer to one the dapp already sent.
1663
+ if (method === "eth_chainId") {
1664
+ generation_1 = (_c = this._providerChainGenerations.get(provider)) !== null && _c !== void 0 ? _c : 0;
1665
+ return [2 /*return*/, request({ method: method, params: params }).then(function (result) {
1666
+ var _a;
1667
+ // A `chainChanged` for THIS provider may have landed while this was
1668
+ // in flight. It is newer by definition, so it must not be
1669
+ // overwritten by this answer.
1670
+ if (generation_1 === ((_a = _this._providerChainGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) &&
1671
+ typeof result === "string") {
1672
+ _this.rememberProviderChain(provider, parseChainId(result));
1673
+ }
1674
+ return result;
1675
+ })];
1676
+ }
1440
1677
  if (!(Array.isArray(params) &&
1441
- ["eth_signTypedData_v4", "personal_sign"].includes(method))) return [3 /*break*/, 6];
1678
+ ["eth_signTypedData_v4", "personal_sign"].includes(method))) return [3 /*break*/, 4];
1442
1679
  if (!this.isAutocaptureEnabled("signature")) {
1443
1680
  logger.debug("Signature event skipped (autocapture.signature: false)", { method: method });
1444
1681
  return [2 /*return*/, request({ method: method, params: params })];
1445
1682
  }
1446
- _c = this._evmChainId;
1447
- if (_c) return [3 /*break*/, 2];
1448
- return [4 /*yield*/, this.getCurrentChainId(provider)];
1449
- case 1:
1450
- _c = (_d.sent());
1451
- _d.label = 2;
1452
- case 2:
1453
- capturedChainId_1 = _c;
1683
+ responsePromise = request({ method: method, params: params });
1684
+ // Attach a no-op handler now so a rejection arriving before the await
1685
+ // below is never reported as unhandled. The real handling is there.
1686
+ responsePromise.catch(function () { return undefined; });
1687
+ capturedChainId_1 = this.resolveChainIdForProvider(provider);
1454
1688
  // Fire-and-forget tracking
1455
1689
  (function () { return __awaiter(_this, void 0, void 0, function () {
1690
+ var e_4;
1456
1691
  return __generator(this, function (_a) {
1457
- try {
1458
- this.signature(__assign({ status: SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1)));
1459
- }
1460
- catch (e) {
1461
- logger.error("Formo: Failed to track signature request", e);
1692
+ switch (_a.label) {
1693
+ case 0:
1694
+ _a.trys.push([0, 2, , 3]);
1695
+ return [4 /*yield*/, this.signature(__assign({ status: SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
1696
+ case 1:
1697
+ _a.sent();
1698
+ return [3 /*break*/, 3];
1699
+ case 2:
1700
+ e_4 = _a.sent();
1701
+ logger.error("Formo: Failed to track signature request", e_4);
1702
+ return [3 /*break*/, 3];
1703
+ case 3: return [2 /*return*/];
1462
1704
  }
1463
- return [2 /*return*/];
1464
1705
  });
1465
1706
  }); })();
1466
- _d.label = 3;
1467
- case 3:
1468
- _d.trys.push([3, 5, , 6]);
1469
- return [4 /*yield*/, request({ method: method, params: params })];
1470
- case 4:
1471
- response_1 = (_d.sent());
1707
+ _d.label = 1;
1708
+ case 1:
1709
+ _d.trys.push([1, 3, , 4]);
1710
+ return [4 /*yield*/, responsePromise];
1711
+ case 2:
1712
+ response_1 = _d.sent();
1472
1713
  // Track signature confirmation only for truthy responses
1473
1714
  if (response_1) {
1474
1715
  (function () { return __awaiter(_this, void 0, void 0, function () {
1716
+ var e_5;
1475
1717
  return __generator(this, function (_a) {
1476
- try {
1477
- this.signature(__assign({ status: SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1)));
1478
- }
1479
- catch (e) {
1480
- logger.error("Formo: Failed to track signature confirmation", e);
1718
+ switch (_a.label) {
1719
+ case 0:
1720
+ _a.trys.push([0, 2, , 3]);
1721
+ return [4 /*yield*/, this.signature(__assign({ status: SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)))];
1722
+ case 1:
1723
+ _a.sent();
1724
+ return [3 /*break*/, 3];
1725
+ case 2:
1726
+ e_5 = _a.sent();
1727
+ logger.error("Formo: Failed to track signature confirmation", e_5);
1728
+ return [3 /*break*/, 3];
1729
+ case 3: return [2 /*return*/];
1481
1730
  }
1482
- return [2 /*return*/];
1483
1731
  });
1484
1732
  }); })();
1485
1733
  }
1486
1734
  return [2 /*return*/, response_1];
1487
- case 5:
1488
- error_3 = _d.sent();
1489
- rpcError = error_3;
1735
+ case 3:
1736
+ error_4 = _d.sent();
1737
+ rpcError = error_4;
1490
1738
  if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
1491
1739
  // Use the already cast rpcError to avoid duplication
1492
1740
  (function () { return __awaiter(_this, void 0, void 0, function () {
1741
+ var e_6;
1493
1742
  return __generator(this, function (_a) {
1494
- try {
1495
- this.signature(__assign({ status: SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1)));
1496
- }
1497
- catch (e) {
1498
- logger.error("Formo: Failed to track signature rejection", e);
1743
+ switch (_a.label) {
1744
+ case 0:
1745
+ _a.trys.push([0, 2, , 3]);
1746
+ return [4 /*yield*/, this.signature(__assign({ status: SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
1747
+ case 1:
1748
+ _a.sent();
1749
+ return [3 /*break*/, 3];
1750
+ case 2:
1751
+ e_6 = _a.sent();
1752
+ logger.error("Formo: Failed to track signature rejection", e_6);
1753
+ return [3 /*break*/, 3];
1754
+ case 3: return [2 /*return*/];
1499
1755
  }
1500
- return [2 /*return*/];
1501
1756
  });
1502
1757
  }); })();
1503
1758
  }
1504
- throw error_3;
1505
- case 6:
1759
+ throw error_4;
1760
+ case 4:
1506
1761
  if (!(Array.isArray(params) &&
1507
1762
  method === "eth_sendTransaction" &&
1508
- params[0])) return [3 /*break*/, 10];
1763
+ params[0])) return [3 /*break*/, 8];
1509
1764
  if (!this.isAutocaptureEnabled("transaction")) {
1510
1765
  logger.debug("Transaction event skipped (autocapture.transaction: false)", { method: method });
1511
1766
  return [2 /*return*/, request({ method: method, params: params })];
1512
1767
  }
1768
+ txPromise = request({ method: method, params: params });
1769
+ txPromise.catch(function () { return undefined; });
1770
+ txChainId_1 = this.resolveChainIdForProvider(provider);
1513
1771
  (function () { return __awaiter(_this, void 0, void 0, function () {
1514
- var payload, e_4;
1772
+ var payload, e_7;
1515
1773
  return __generator(this, function (_a) {
1516
1774
  switch (_a.label) {
1517
1775
  case 0:
1518
- _a.trys.push([0, 2, , 3]);
1519
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider)];
1776
+ _a.trys.push([0, 3, , 4]);
1777
+ return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1520
1778
  case 1:
1521
1779
  payload = _a.sent();
1522
- this.transaction(__assign({ status: TransactionStatus.STARTED }, payload));
1523
- return [3 /*break*/, 3];
1780
+ return [4 /*yield*/, this.transaction(__assign({ status: TransactionStatus.STARTED }, payload))];
1524
1781
  case 2:
1525
- e_4 = _a.sent();
1526
- logger.error("Formo: Failed to track transaction start", e_4);
1527
- return [3 /*break*/, 3];
1528
- case 3: return [2 /*return*/];
1782
+ _a.sent();
1783
+ return [3 /*break*/, 4];
1784
+ case 3:
1785
+ e_7 = _a.sent();
1786
+ logger.error("Formo: Failed to track transaction start", e_7);
1787
+ return [3 /*break*/, 4];
1788
+ case 4: return [2 /*return*/];
1529
1789
  }
1530
1790
  });
1531
1791
  }); })();
1532
- _d.label = 7;
1533
- case 7:
1534
- _d.trys.push([7, 9, , 10]);
1535
- return [4 /*yield*/, request({
1536
- method: method,
1537
- params: params,
1538
- })];
1539
- case 8:
1540
- transactionHash_1 = (_d.sent());
1792
+ _d.label = 5;
1793
+ case 5:
1794
+ _d.trys.push([5, 7, , 8]);
1795
+ return [4 /*yield*/, txPromise];
1796
+ case 6:
1797
+ transactionHash_1 = _d.sent();
1541
1798
  (function () { return __awaiter(_this, void 0, void 0, function () {
1542
- var payload, e_5;
1799
+ var payload, e_8;
1543
1800
  return __generator(this, function (_a) {
1544
1801
  switch (_a.label) {
1545
1802
  case 0:
1546
- _a.trys.push([0, 2, , 3]);
1547
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider)];
1803
+ _a.trys.push([0, 3, , 4]);
1804
+ return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1548
1805
  case 1:
1549
1806
  payload = _a.sent();
1550
- this.transaction(__assign(__assign({ status: TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }));
1807
+ return [4 /*yield*/, this.transaction(__assign(__assign({ status: TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }))];
1808
+ case 2:
1809
+ _a.sent();
1551
1810
  // Start async polling for transaction receipt
1552
1811
  this.pollTransactionReceipt(provider, transactionHash_1, payload);
1553
- return [3 /*break*/, 3];
1554
- case 2:
1555
- e_5 = _a.sent();
1556
- logger.error("Formo: Failed to track transaction broadcast", e_5);
1557
- return [3 /*break*/, 3];
1558
- case 3: return [2 /*return*/];
1812
+ return [3 /*break*/, 4];
1813
+ case 3:
1814
+ e_8 = _a.sent();
1815
+ logger.error("Formo: Failed to track transaction broadcast", e_8);
1816
+ return [3 /*break*/, 4];
1817
+ case 4: return [2 /*return*/];
1559
1818
  }
1560
1819
  });
1561
1820
  }); })();
1562
1821
  return [2 /*return*/, transactionHash_1];
1563
- case 9:
1564
- error_4 = _d.sent();
1565
- rpcError = error_4;
1822
+ case 7:
1823
+ error_5 = _d.sent();
1824
+ rpcError = error_5;
1566
1825
  if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
1567
1826
  // Use the already cast rpcError to avoid duplication
1568
1827
  (function () { return __awaiter(_this, void 0, void 0, function () {
1569
- var payload, e_6;
1828
+ var payload, e_9;
1570
1829
  return __generator(this, function (_a) {
1571
1830
  switch (_a.label) {
1572
1831
  case 0:
1573
- _a.trys.push([0, 2, , 3]);
1574
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider)];
1832
+ _a.trys.push([0, 3, , 4]);
1833
+ return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1575
1834
  case 1:
1576
1835
  payload = _a.sent();
1577
- this.transaction(__assign({ status: TransactionStatus.REJECTED }, payload));
1578
- return [3 /*break*/, 3];
1836
+ return [4 /*yield*/, this.transaction(__assign({ status: TransactionStatus.REJECTED }, payload))];
1579
1837
  case 2:
1580
- e_6 = _a.sent();
1581
- logger.error("Formo: Failed to track transaction rejection", e_6);
1582
- return [3 /*break*/, 3];
1583
- case 3: return [2 /*return*/];
1838
+ _a.sent();
1839
+ return [3 /*break*/, 4];
1840
+ case 3:
1841
+ e_9 = _a.sent();
1842
+ logger.error("Formo: Failed to track transaction rejection", e_9);
1843
+ return [3 /*break*/, 4];
1844
+ case 4: return [2 /*return*/];
1584
1845
  }
1585
1846
  });
1586
1847
  }); })();
1587
1848
  }
1588
- throw error_4;
1589
- case 10: return [2 /*return*/, request({ method: method, params: params })];
1849
+ throw error_5;
1850
+ case 8: return [2 /*return*/, request({ method: method, params: params })];
1590
1851
  }
1591
1852
  });
1592
1853
  }); };
@@ -1604,9 +1865,16 @@ var FormoAnalytics = /** @class */ (function () {
1604
1865
  };
1605
1866
  FormoAnalytics.prototype.onLocationChange = function () {
1606
1867
  return __awaiter(this, void 0, void 0, function () {
1607
- return __generator(this, function (_a) {
1868
+ var _a;
1869
+ return __generator(this, function (_b) {
1608
1870
  if (this._currentUrl !== window.location.href) {
1609
1871
  this._currentUrl = window.location.href;
1872
+ // Host/path exclusions are evaluated per navigation, so a SPA can leave
1873
+ // an excluded route and become trackable without any wallet event
1874
+ // firing. The wagmi handler only observes *changes*, so an unchanged
1875
+ // connection it was forced to decline earlier would stay invisible for
1876
+ // the rest of the page load. Give it a chance to adopt it now.
1877
+ (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1610
1878
  this.trackPageHit();
1611
1879
  }
1612
1880
  return [2 /*return*/];
@@ -1677,7 +1945,7 @@ var FormoAnalytics = /** @class */ (function () {
1677
1945
  if (_this._pageHooksDisposed)
1678
1946
  return;
1679
1947
  (function () { return __awaiter(_this, void 0, void 0, function () {
1680
- var e_7;
1948
+ var e_10;
1681
1949
  return __generator(this, function (_a) {
1682
1950
  switch (_a.label) {
1683
1951
  case 0:
@@ -1690,8 +1958,8 @@ var FormoAnalytics = /** @class */ (function () {
1690
1958
  _a.sent();
1691
1959
  return [3 /*break*/, 3];
1692
1960
  case 2:
1693
- e_7 = _a.sent();
1694
- logger.error("Formo: Failed to track page hit", e_7);
1961
+ e_10 = _a.sent();
1962
+ logger.error("Formo: Failed to track page hit", e_10);
1695
1963
  return [3 /*break*/, 3];
1696
1964
  case 3: return [2 /*return*/];
1697
1965
  }
@@ -1704,12 +1972,17 @@ var FormoAnalytics = /** @class */ (function () {
1704
1972
  };
1705
1973
  FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1706
1974
  return __awaiter(this, void 0, void 0, function () {
1707
- var error_5;
1975
+ var error_6;
1708
1976
  return __generator(this, function (_a) {
1709
1977
  switch (_a.label) {
1710
1978
  case 0:
1711
1979
  _a.trys.push([0, 2, , 3]);
1712
- if (!this.shouldTrack()) {
1980
+ // Gate on the chain the event actually carries. `connect`, `disconnect`,
1981
+ // `chain`, `signature` and `transaction` all put one in the payload, and
1982
+ // it is authoritative: it can name a provider or a wagmi mutation chain
1983
+ // that is not the active one. Events without a chain (page, track,
1984
+ // identify) fall back to the central value.
1985
+ if (!this.shouldTrack(payload === null || payload === void 0 ? void 0 : payload.chainId)) {
1713
1986
  logger.info("Skipping ".concat(type, " event due to tracking configuration"));
1714
1987
  return [2 /*return*/];
1715
1988
  }
@@ -1718,8 +1991,8 @@ var FormoAnalytics = /** @class */ (function () {
1718
1991
  _a.sent();
1719
1992
  return [3 /*break*/, 3];
1720
1993
  case 2:
1721
- error_5 = _a.sent();
1722
- logger.error("Error tracking event:", error_5);
1994
+ error_6 = _a.sent();
1995
+ logger.error("Error tracking event:", error_6);
1723
1996
  return [3 /*break*/, 3];
1724
1997
  case 3: return [2 /*return*/];
1725
1998
  }
@@ -1756,16 +2029,24 @@ var FormoAnalytics = /** @class */ (function () {
1756
2029
  * an options object with `excludeChains` set, and only once a chain id is
1757
2030
  * known.
1758
2031
  */
1759
- FormoAnalytics.prototype.isCurrentChainExcluded = function () {
2032
+ FormoAnalytics.prototype.isCurrentChainExcluded = function (eventChainId) {
1760
2033
  if (this.options.tracking === null ||
1761
2034
  typeof this.options.tracking !== "object" ||
1762
2035
  Array.isArray(this.options.tracking)) {
1763
2036
  return false;
1764
2037
  }
1765
2038
  var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1766
- return (excludeChains.length > 0 &&
1767
- !!this.currentChainId &&
1768
- excludeChains.includes(this.currentChainId));
2039
+ if (excludeChains.length === 0)
2040
+ return false;
2041
+ // Mirrors `shouldTrack()`: the event's own chain wins when it has one,
2042
+ // and an unresolvable chain on a known wallet counts as excluded rather
2043
+ // than allowed.
2044
+ var chainToCheck = eventChainId !== null && eventChainId !== void 0 ? eventChainId : this.currentChainId;
2045
+ if (chainToCheck === 0)
2046
+ return true;
2047
+ if (chainToCheck === undefined)
2048
+ return false;
2049
+ return excludeChains.includes(chainToCheck);
1769
2050
  };
1770
2051
  /**
1771
2052
  * Whether the current environment is excluded from tracking - the visitor's
@@ -1860,7 +2141,7 @@ var FormoAnalytics = /** @class */ (function () {
1860
2141
  * Determines if tracking should be enabled based on configuration and consent
1861
2142
  * @returns {boolean} True if tracking should be enabled
1862
2143
  */
1863
- FormoAnalytics.prototype.shouldTrack = function () {
2144
+ FormoAnalytics.prototype.shouldTrack = function (eventChainId) {
1864
2145
  // First check if user has opted out of tracking
1865
2146
  if (this.hasOptedOutTracking()) {
1866
2147
  return false;
@@ -1879,11 +2160,42 @@ var FormoAnalytics = /** @class */ (function () {
1879
2160
  if (this.isCurrentEnvironmentExcluded()) {
1880
2161
  return false;
1881
2162
  }
1882
- // Check chainId exclusions
1883
- if (excludeChains.length > 0 &&
1884
- this.currentChainId &&
1885
- excludeChains.includes(this.currentChainId)) {
1886
- return false;
2163
+ // Check chainId exclusions.
2164
+ //
2165
+ // The event's OWN chain wins over `currentChainId`. They differ whenever
2166
+ // the event did not come from the active provider: a second wallet
2167
+ // signing through its own provider, or a wagmi mutation that names an
2168
+ // explicit chain. Reading only the central field there would emit an
2169
+ // event that is labelled with an excluded chain, which is precisely what
2170
+ // the exclusion forbids - and would drop an allowed event whenever the
2171
+ // active provider happened to sit on an excluded chain.
2172
+ var chainToCheck = eventChainId !== null && eventChainId !== void 0 ? eventChainId : this.currentChainId;
2173
+ if (excludeChains.length > 0) {
2174
+ // Fail CLOSED on an unknown chain. `resolveChainIdForProvider` reports
2175
+ // 0 when it has never heard a chain from the signing wallet, and 0 is
2176
+ // in no exclusion list, so treating it as "not excluded" would let
2177
+ // exactly the events an operator excluded through - the wallet on the
2178
+ // excluded chain is often the one we know least about. An explicit
2179
+ // exclusion is a directive, so an unresolvable chain is refused.
2180
+ //
2181
+ // This covers the central value too, not just an explicit event
2182
+ // chain: `page`, `track` and `identify` carry no chain of their own
2183
+ // and fall back to `currentChainId`, so an unknown chain there would
2184
+ // otherwise send wallet-attributed events for a wallet that may well
2185
+ // be sitting on an excluded chain.
2186
+ // Deliberately keyed on 0 and NOT on `undefined`. 0 is the explicit
2187
+ // "we asked and could not tell" marker. `undefined` means no chain
2188
+ // state yet, which is a legitimate transient - the Privy path
2189
+ // reconciles a Solana wallet through exactly that state - and
2190
+ // refusing it would drop real events.
2191
+ //
2192
+ // `backfillActiveWallet()` never persists 0, so an unresolvable chain
2193
+ // cannot leak into `currentChainId` and reach the unscoped events
2194
+ // (page / track / identify) that fall back to it.
2195
+ if (chainToCheck === 0)
2196
+ return false;
2197
+ if (chainToCheck && excludeChains.includes(chainToCheck))
2198
+ return false;
1887
2199
  }
1888
2200
  // If nothing is excluded, tracking is enabled
1889
2201
  return true;
@@ -1967,7 +2279,7 @@ var FormoAnalytics = /** @class */ (function () {
1967
2279
  _this.trackProviders(newDetails);
1968
2280
  // Detect newly discovered wallets (session de-dupes) with error handling
1969
2281
  (function () { return __awaiter(_this, void 0, void 0, function () {
1970
- var e_8;
2282
+ var e_11;
1971
2283
  return __generator(this, function (_a) {
1972
2284
  switch (_a.label) {
1973
2285
  case 0:
@@ -1977,8 +2289,8 @@ var FormoAnalytics = /** @class */ (function () {
1977
2289
  _a.sent();
1978
2290
  return [3 /*break*/, 3];
1979
2291
  case 2:
1980
- e_8 = _a.sent();
1981
- logger.error("Formo: Failed to detect wallets", e_8);
2292
+ e_11 = _a.sent();
2293
+ logger.error("Formo: Failed to detect wallets", e_11);
1982
2294
  return [3 /*break*/, 3];
1983
2295
  case 3: return [2 /*return*/];
1984
2296
  }
@@ -2171,6 +2483,105 @@ var FormoAnalytics = /** @class */ (function () {
2171
2483
  });
2172
2484
  });
2173
2485
  };
2486
+ /**
2487
+ * Resolve the chain an autocaptured request actually ran on.
2488
+ *
2489
+ * `_evmChainId` is maintained by `chainChanged` from whichever provider is
2490
+ * currently active. When a request arrives from a *different* tracked
2491
+ * provider - which happens whenever a visitor has two wallets installed -
2492
+ * that cached value describes the wrong wallet, and tagging the event with
2493
+ * it silently mis-attributes the chain.
2494
+ *
2495
+ * Answered entirely from a per-provider snapshot. This is deliberately
2496
+ * SYNCHRONOUS and never issues an RPC.
2497
+ *
2498
+ * An earlier version called `eth_chainId` on the signing provider and
2499
+ * time-boxed it with `Promise.race`. That is not safe: the race abandons the
2500
+ * SDK's promise but cannot cancel the provider's request. On a transport
2501
+ * that serializes - WalletConnect's relay socket, the very case this path
2502
+ * exists to serve - an abandoned lookup stays at the head of the wallet's
2503
+ * queue, and every later RPC the dapp makes queues behind it until reload.
2504
+ * Mislabelling a chain is a reporting defect; wedging the user's wallet is
2505
+ * not acceptable to avoid one.
2506
+ *
2507
+ * When nothing is known, this reports 0 ("unknown") rather than guessing
2508
+ * with the active provider's chain, which is known-wrong for another wallet.
2509
+ */
2510
+ FormoAnalytics.prototype.resolveChainIdForProvider = function (provider) {
2511
+ if (provider) {
2512
+ var known = this._providerChainIds.get(provider);
2513
+ if (known)
2514
+ return known;
2515
+ // Only the active provider's chain is described by the central cache.
2516
+ if (provider === this._provider && this._evmChainId) {
2517
+ return this._evmChainId;
2518
+ }
2519
+ // A tracked provider we have never heard a chain from. Deliberately no
2520
+ // fall back to `_evmChainId`: it belongs to a different wallet.
2521
+ return 0;
2522
+ }
2523
+ return this._evmChainId || 0;
2524
+ };
2525
+ /**
2526
+ * Record a provider's chain. Fed by `chainChanged` and `connect`, and by the
2527
+ * one-off probe at tracking time - never from inside a user request.
2528
+ */
2529
+ FormoAnalytics.prototype.rememberProviderChain = function (provider, chainId) {
2530
+ if (!provider || !chainId)
2531
+ return;
2532
+ // Any observation is newer than an `eth_chainId` still in flight for this
2533
+ // provider.
2534
+ this.bumpProviderChainGeneration(provider);
2535
+ this._providerChainIds.set(provider, chainId);
2536
+ // If this IS the active provider, central state has to follow. Recording
2537
+ // it only per provider left `currentChainId` on a chain restored from a
2538
+ // previous session, and the unscoped events that fall back to it were sent
2539
+ // despite an exclusion covering the chain the wallet was really on.
2540
+ //
2541
+ // Guarded on EVM already being the active namespace. `setChainState()`
2542
+ // makes whichever namespace it touches active, so syncing here
2543
+ // unconditionally let a background EVM wallet's chain report steal the
2544
+ // active slot from a live Solana wallet, and every later page or track
2545
+ // event was attributed to the wrong wallet entirely.
2546
+ if (provider === this._provider &&
2547
+ this._activeNamespace === "evm" &&
2548
+ this._evmChainId !== chainId) {
2549
+ this.setChainState('evm', { chainId: chainId });
2550
+ }
2551
+ };
2552
+ /** Advance and return this provider's chain-observation generation. */
2553
+ FormoAnalytics.prototype.bumpProviderChainGeneration = function (provider) {
2554
+ var _a;
2555
+ var next = ((_a = this._providerChainGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) + 1;
2556
+ this._providerChainGenerations.set(provider, next);
2557
+ return next;
2558
+ };
2559
+ /**
2560
+ * Seed a provider's chain from whatever it already exposes synchronously.
2561
+ *
2562
+ * Most EIP-1193 implementations carry a `chainId` property (MetaMask,
2563
+ * WalletConnect, Coinbase). Reading it costs nothing and cannot block.
2564
+ *
2565
+ * There is deliberately no RPC fallback. An earlier version probed with
2566
+ * `eth_chainId` when a provider was first tracked, on the theory that
2567
+ * tracking time is off the user's critical path. It is not: a serialized
2568
+ * transport has ONE queue, so a stalled probe sits in front of every later
2569
+ * signature and transaction the dapp makes. It could also land out of order
2570
+ * - a slow probe response overwriting a newer `chainChanged` - and relabel
2571
+ * events onto a chain the wallet had already left.
2572
+ *
2573
+ * A provider that exposes nothing stays unknown until it emits
2574
+ * `chainChanged` or `connect`, and unknown is reported honestly as 0.
2575
+ */
2576
+ FormoAnalytics.prototype.seedProviderChainFromState = function (provider) {
2577
+ var raw = provider.chainId;
2578
+ var chainId = typeof raw === "string"
2579
+ ? parseChainId(raw)
2580
+ : typeof raw === "number"
2581
+ ? raw
2582
+ : undefined;
2583
+ this.rememberProviderChain(provider, chainId);
2584
+ };
2174
2585
  FormoAnalytics.prototype.getCurrentChainId = function (provider) {
2175
2586
  return __awaiter(this, void 0, void 0, function () {
2176
2587
  var p, chainIdHex, err_5;
@@ -2206,7 +2617,7 @@ var FormoAnalytics = /** @class */ (function () {
2206
2617
  };
2207
2618
  FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
2208
2619
  // Intentionally not read. Kept for positional call-site arity.
2209
- _response, chainId) {
2620
+ _response, chainId, provider) {
2210
2621
  var _a;
2211
2622
  var rawAddress = method === "personal_sign"
2212
2623
  ? params[1]
@@ -2216,7 +2627,11 @@ var FormoAnalytics = /** @class */ (function () {
2216
2627
  throw new Error("Invalid address in signature payload: ".concat(rawAddress));
2217
2628
  }
2218
2629
  var effectiveChainId = (_a = chainId !== null && chainId !== void 0 ? chainId : this._evmChainId) !== null && _a !== void 0 ? _a : undefined;
2219
- this.backfillActiveWallet(validAddress, effectiveChainId);
2630
+ // Only the active provider may write central wallet state - see the same
2631
+ // guard in buildTransactionEventPayload.
2632
+ if (!provider || provider === this._provider || !this._provider) {
2633
+ this.backfillActiveWallet(validAddress, effectiveChainId, provider);
2634
+ }
2220
2635
  var basePayload = {
2221
2636
  chainId: effectiveChainId,
2222
2637
  address: validAddress,
@@ -2228,34 +2643,37 @@ var FormoAnalytics = /** @class */ (function () {
2228
2643
  // eth_signTypedData*: params[1] is the full EIP-712 struct.
2229
2644
  return __assign(__assign({}, basePayload), { message: params[1] });
2230
2645
  };
2231
- FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider) {
2646
+ FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider,
2647
+ /**
2648
+ * Chain resolved once for this request's whole lifecycle. Passing it keeps
2649
+ * every status of one transaction on the same chain even if the user
2650
+ * switches network while the wallet prompt is open.
2651
+ */
2652
+ capturedChainId) {
2232
2653
  return __awaiter(this, void 0, void 0, function () {
2233
- var _a, data, from, to, value, validAddress, chainId, _b;
2234
- return __generator(this, function (_c) {
2235
- switch (_c.label) {
2236
- case 0:
2237
- _a = params[0], data = _a.data, from = _a.from, to = _a.to, value = _a.value;
2238
- validAddress = validateAndChecksumAddress(from);
2239
- if (!validAddress) {
2240
- throw new Error("Invalid address in transaction payload: ".concat(from));
2241
- }
2242
- _b = this._evmChainId;
2243
- if (_b) return [3 /*break*/, 2];
2244
- return [4 /*yield*/, this.getCurrentChainId(provider)];
2245
- case 1:
2246
- _b = (_c.sent());
2247
- _c.label = 2;
2248
- case 2:
2249
- chainId = _b;
2250
- this.backfillActiveWallet(validAddress, chainId);
2251
- return [2 /*return*/, {
2252
- chainId: chainId,
2253
- data: data,
2254
- address: validAddress,
2255
- to: to,
2256
- value: value,
2257
- }];
2654
+ var _a, data, from, to, value, validAddress, chainId;
2655
+ return __generator(this, function (_b) {
2656
+ _a = params[0], data = _a.data, from = _a.from, to = _a.to, value = _a.value;
2657
+ validAddress = validateAndChecksumAddress(from);
2658
+ if (!validAddress) {
2659
+ throw new Error("Invalid address in transaction payload: ".concat(from));
2660
+ }
2661
+ chainId = capturedChainId !== null && capturedChainId !== void 0 ? capturedChainId : this.resolveChainIdForProvider(provider);
2662
+ // Only the ACTIVE provider may write central wallet state. A request from
2663
+ // a second, non-active wallet would otherwise overwrite the active
2664
+ // provider's address and chain, and every later request through the active
2665
+ // provider would then trust the other wallet's chain - persistent
2666
+ // mis-attribution, and a way around `excludeChains`.
2667
+ if (!provider || provider === this._provider || !this._provider) {
2668
+ this.backfillActiveWallet(validAddress, chainId, provider);
2258
2669
  }
2670
+ return [2 /*return*/, {
2671
+ chainId: chainId,
2672
+ data: data,
2673
+ address: validAddress,
2674
+ to: to,
2675
+ value: value,
2676
+ }];
2259
2677
  });
2260
2678
  });
2261
2679
  };
@@ -2267,16 +2685,51 @@ var FormoAnalytics = /** @class */ (function () {
2267
2685
  * social-login wrappers). If `accountsChanged` later fires it overwrites this
2268
2686
  * value in the normal way; existing connections are never clobbered.
2269
2687
  */
2270
- FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId) {
2688
+ FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId, provider) {
2689
+ // Refuse a chain the provider has since moved off.
2690
+ //
2691
+ // A request captures its chain once and reuses that snapshot for every
2692
+ // status it emits, which is right for the event payload - a confirmation
2693
+ // must not be relabelled mid-flight. But writing that captured value back
2694
+ // into central state on the LATER statuses restored a chain the wallet had
2695
+ // already left, and the unscoped events that fall back to it then bypassed
2696
+ // an exclusion that should have caught them.
2697
+ if (provider && chainId !== undefined && chainId !== 0) {
2698
+ var current = this._providerChainIds.get(provider);
2699
+ if (current !== undefined && current !== chainId) {
2700
+ chainId = current;
2701
+ }
2702
+ }
2703
+ // `0` is kept deliberately. It means "could not resolve", and persisting
2704
+ // it is what lets the exclusion gate refuse the unscoped events - `page`,
2705
+ // `track`, `identify` - that carry no chain of their own and fall back to
2706
+ // `currentChainId`. Erasing it to `undefined` looked tidier but removed
2707
+ // the only marker distinguishing "unknown" from "no wallet yet", and those
2708
+ // events then went out attributed to a wallet that might be sitting on an
2709
+ // excluded chain.
2271
2710
  // Never learn identity while suppressed (opt-out / timezone / excluded host
2272
2711
  // or path). A signature/transaction observed on an excluded route must not
2273
- // populate currentAddress for later allowed-page events. backfill only ever
2274
- // *adds* an address (it no-ops when one is already known), so there is no
2275
- // stale state to clear here.
2712
+ // populate currentAddress for later allowed-page events.
2276
2713
  if (this.isTrackingSuppressed())
2277
2714
  return;
2278
- if (this._evmAddress)
2715
+ var known = this._evmAddress;
2716
+ if (known) {
2717
+ // Same wallet, newer chain: correct it rather than returning.
2718
+ //
2719
+ // A persisted wallet restores a chain from a previous session. If the
2720
+ // provider has since moved - to an excluded chain, or to one we cannot
2721
+ // resolve - the autocaptured event is gated correctly, but the stale
2722
+ // restored chain stayed in `currentChainId` and the unscoped events
2723
+ // that fall back to it went out under an exclusion that should have
2724
+ // caught them.
2725
+ if (chainId !== undefined &&
2726
+ known.toLowerCase() === address.toLowerCase() &&
2727
+ this._evmChainId !== chainId) {
2728
+ this.setChainState('evm', { address: known, chainId: chainId });
2729
+ }
2730
+ // A DIFFERENT address is another wallet's business; never overwrite.
2279
2731
  return;
2732
+ }
2280
2733
  this.setChainState('evm', { address: address, chainId: chainId });
2281
2734
  };
2282
2735
  /**
@@ -2307,7 +2760,7 @@ var FormoAnalytics = /** @class */ (function () {
2307
2760
  if (!provider)
2308
2761
  return [2 /*return*/];
2309
2762
  poll = function () { return __awaiter(_this, void 0, void 0, function () {
2310
- var receipt, e_9;
2763
+ var receipt, e_12;
2311
2764
  return __generator(this, function (_a) {
2312
2765
  switch (_a.label) {
2313
2766
  case 0:
@@ -2331,8 +2784,8 @@ var FormoAnalytics = /** @class */ (function () {
2331
2784
  }
2332
2785
  return [3 /*break*/, 3];
2333
2786
  case 2:
2334
- e_9 = _a.sent();
2335
- logger.error("Error polling transaction receipt", e_9);
2787
+ e_12 = _a.sent();
2788
+ logger.error("Error polling transaction receipt", e_12);
2336
2789
  return [3 /*break*/, 3];
2337
2790
  case 3:
2338
2791
  attempts++;
@@ -2462,6 +2915,9 @@ var FormoAnalytics = /** @class */ (function () {
2462
2915
  ns.chainId = namespaceOrChainId;
2463
2916
  }
2464
2917
  if (namespace === 'evm' && 'provider' in update) {
2918
+ // Displacing the active provider ends its connection as far as this SDK
2919
+ // is concerned, so the connect it reported stops counting.
2920
+ this.forgetConnectRecord(ns.provider, update.provider);
2465
2921
  ns.provider = update.provider;
2466
2922
  }
2467
2923
  this._activeNamespace = namespace;
@@ -2475,6 +2931,8 @@ var FormoAnalytics = /** @class */ (function () {
2475
2931
  ? namespaceOrChainId
2476
2932
  : this.getNamespace(namespaceOrChainId);
2477
2933
  if (namespace === 'evm') {
2934
+ // Same rule: wiping the namespace drops the active provider.
2935
+ this.forgetConnectRecord(this._chainState.evm.provider, undefined);
2478
2936
  this._chainState.evm = {};
2479
2937
  }
2480
2938
  else {