@formo/analytics 1.36.0 → 1.38.0

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 (44) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +64 -0
  2. package/dist/cjs/src/FormoAnalytics.js +155 -5
  3. package/dist/cjs/src/FormoAnalyticsProvider.js +1 -0
  4. package/dist/cjs/src/evm/EvmEventTracker.d.ts +38 -10
  5. package/dist/cjs/src/evm/EvmEventTracker.js +182 -0
  6. package/dist/cjs/src/evm/EvmProviderRegistry.js +26 -4
  7. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +36 -40
  8. package/dist/cjs/src/evm/EvmRequestTracker.js +216 -115
  9. package/dist/cjs/src/evm/batch.d.ts +94 -0
  10. package/dist/cjs/src/evm/batch.js +130 -0
  11. package/dist/cjs/src/provider/detection.d.ts +30 -0
  12. package/dist/cjs/src/provider/detection.js +62 -0
  13. package/dist/cjs/src/provider/index.d.ts +1 -1
  14. package/dist/cjs/src/provider/index.js +3 -1
  15. package/dist/cjs/src/types/base.d.ts +23 -0
  16. package/dist/cjs/src/types/provider.d.ts +16 -0
  17. package/dist/cjs/src/types/provider.js +17 -1
  18. package/dist/cjs/src/version.d.ts +1 -1
  19. package/dist/cjs/src/version.js +1 -1
  20. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +87 -0
  21. package/dist/cjs/src/wagmi/WagmiEventHandler.js +536 -13
  22. package/dist/esm/src/FormoAnalytics.d.ts +64 -0
  23. package/dist/esm/src/FormoAnalytics.js +155 -5
  24. package/dist/esm/src/FormoAnalyticsProvider.js +1 -0
  25. package/dist/esm/src/evm/EvmEventTracker.d.ts +38 -10
  26. package/dist/esm/src/evm/EvmEventTracker.js +183 -1
  27. package/dist/esm/src/evm/EvmProviderRegistry.js +27 -5
  28. package/dist/esm/src/evm/EvmRequestTracker.d.ts +36 -40
  29. package/dist/esm/src/evm/EvmRequestTracker.js +215 -114
  30. package/dist/esm/src/evm/batch.d.ts +94 -0
  31. package/dist/esm/src/evm/batch.js +123 -0
  32. package/dist/esm/src/provider/detection.d.ts +30 -0
  33. package/dist/esm/src/provider/detection.js +60 -0
  34. package/dist/esm/src/provider/index.d.ts +1 -1
  35. package/dist/esm/src/provider/index.js +1 -1
  36. package/dist/esm/src/types/base.d.ts +23 -0
  37. package/dist/esm/src/types/provider.d.ts +16 -0
  38. package/dist/esm/src/types/provider.js +16 -0
  39. package/dist/esm/src/version.d.ts +1 -1
  40. package/dist/esm/src/version.js +1 -1
  41. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +87 -0
  42. package/dist/esm/src/wagmi/WagmiEventHandler.js +536 -13
  43. package/dist/index.umd.min.js +1 -1
  44. package/package.json +4 -3
@@ -96,6 +96,8 @@ export declare class FormoAnalytics implements IFormoAnalytics {
96
96
  * Call this when destroying the analytics instance
97
97
  * @returns {void}
98
98
  */
99
+ /** Set by cleanup(); a torn-down instance refuses new registrations. */
100
+ private isCleanedUp;
99
101
  cleanup(): void;
100
102
  /**
101
103
  * Emits a connect wallet event.
@@ -373,6 +375,68 @@ export declare class FormoAnalytics implements IFormoAnalytics {
373
375
  */
374
376
  get solana(): SolanaManager;
375
377
  private getCurrentChainId;
378
+ /**
379
+ * Track an EIP-1193 provider the page constructed itself.
380
+ *
381
+ * Discovery covers EIP-6963 announcements and `window.ethereum`, which is
382
+ * every injected wallet and nothing else. WalletConnect and Ledger
383
+ * providers are built by the app (`EthereumProvider.init(...)`) and
384
+ * announce nothing, so their sessions were invisible: connects,
385
+ * signatures, and transactions all silently missing. Hand the provider
386
+ * here once it exists and it takes the exact pipeline a discovered
387
+ * provider takes - detect event, lifecycle listeners, request wrapper -
388
+ * and a session that is already live is adopted from the provider's
389
+ * synchronous state.
390
+ *
391
+ * Metadata resolution order: the caller's `info` overrides win; then a
392
+ * WalletConnect session's peer metadata, which names the REAL wallet on
393
+ * the far side of the transport (for example "Ledger Live"); then flag
394
+ * sniffing; then a generic fallback. One deliberate exception: a caller
395
+ * name of exactly "WalletConnect" is the generic transport name, so the
396
+ * live peer still replaces it on events - name the provider anything
397
+ * else to pin it verbatim.
398
+ *
399
+ * No-op outside the EIP-1193 path: in wagmi mode the connector system
400
+ * already tracks these sessions, and wrapping the same provider twice
401
+ * would double-report every event.
402
+ *
403
+ * With several live SDK instances (multi write-key pages) registering
404
+ * the SAME provider, request-derived events (signatures, transactions)
405
+ * go to the most recently registered live instance - the same
406
+ * single-observer semantics discovery has always had for the request
407
+ * wrapper. Lifecycle events (connect, chain, disconnect) reach every
408
+ * instance. Fanning request observations out to all instances is a
409
+ * separate feature.
410
+ *
411
+ * @returns true when the provider is (now) tracked, false when it was
412
+ * refused (wagmi mode, EVM disabled, or not a valid EIP-1193 provider).
413
+ *
414
+ * @example
415
+ * ```typescript
416
+ * const wcProvider = await EthereumProvider.init({ projectId, chains });
417
+ * formo.registerProvider(wcProvider);
418
+ * ```
419
+ */
420
+ /**
421
+ * INTERNAL. Install the request wrapper on a wagmi connector's provider.
422
+ *
423
+ * Wagmi mode watches the store and caches, which see hook-driven calls
424
+ * only; imperative viem calls (walletClient.sendTransaction,
425
+ * .signMessage, .writeContract, raw request) create no mutation and were
426
+ * silently lost. Every viem client in a wagmi app is built on the
427
+ * connector's EIP-1193 provider, so wrapping that provider closes the
428
+ * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
429
+ * request wrapper installs here. Double counting is prevented in the
430
+ * wrapper via `shouldSkipRequestCapture`.
431
+ */
432
+ _wrapWagmiProvider(provider: EIP1193Provider): void;
433
+ registerProvider(provider: EIP1193Provider, info?: {
434
+ name?: string;
435
+ rdns?: string;
436
+ icon?: `data:image/${string}`;
437
+ }): boolean;
438
+ /** Fallback uuid suffix for platforms without crypto.randomUUID. */
439
+ private static registeredProviderSeq;
376
440
  getTrackedProvidersCount(): number;
377
441
  /**
378
442
  * Get current provider state for debugging
@@ -57,6 +57,7 @@ import { validateAddress, validateAndChecksumAddress } from "./utils/address";
57
57
  import { TrackingPolicy, } from "./tracking/TrackingPolicy";
58
58
  import { WalletStateStore } from "./wallet/WalletStateStore";
59
59
  import { EvmProviderRegistry } from "./evm/EvmProviderRegistry";
60
+ import { detectInjectedProviderInfo, isValidProvider, readWalletConnectPeer, } from "./provider";
60
61
  import { EvmEventTracker } from "./evm/EvmEventTracker";
61
62
  import { EvmRequestTracker } from "./evm/EvmRequestTracker";
62
63
  import { parseChainId } from "./utils/chain";
@@ -90,6 +91,13 @@ var FormoAnalytics = /** @class */ (function () {
90
91
  this._currentUrl = "";
91
92
  this._pageHooksDisposed = false;
92
93
  this.currentUserId = "";
94
+ /**
95
+ * Clean up resources and event listeners
96
+ * Call this when destroying the analytics instance
97
+ * @returns {void}
98
+ */
99
+ /** Set by cleanup(); a torn-down instance refuses new registrations. */
100
+ this.isCleanedUp = false;
93
101
  this.config = {
94
102
  writeKey: writeKey,
95
103
  };
@@ -111,6 +119,7 @@ var FormoAnalytics = /** @class */ (function () {
111
119
  this.transaction = this.transaction.bind(this);
112
120
  this.detect = this.detect.bind(this);
113
121
  this.track = this.track.bind(this);
122
+ this.registerProvider = this.registerProvider.bind(this);
114
123
  this.page = this.page.bind(this);
115
124
  this.reset = this.reset.bind(this);
116
125
  this.cleanup = this.cleanup.bind(this);
@@ -168,6 +177,11 @@ var FormoAnalytics = /** @class */ (function () {
168
177
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
169
178
  signature: function (params, properties) { return _this.signature(params, properties); },
170
179
  transaction: function (params, properties) { return _this.transaction(params, properties); },
180
+ // Hybrid capture: in wagmi mode the wrapper skips a request that a
181
+ // PENDING wagmi mutation already covers - the mutation handler
182
+ // captures it with ABI enrichment - and captures everything else
183
+ // (imperative viem calls that create no mutation).
184
+ shouldSkipRequestCapture: function (method, params) { var _a, _b; return (_b = (_a = _this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.hasMatchingPendingMutation(method, params)) !== null && _b !== void 0 ? _b : false; },
171
185
  });
172
186
  this.evmEvents = new EvmEventTracker(this.wallet, this.evm, {
173
187
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
@@ -373,12 +387,8 @@ var FormoAnalytics = /** @class */ (function () {
373
387
  // re-attached to the next session's events.
374
388
  session().remove(SESSION_TRAFFIC_SOURCE_KEY);
375
389
  };
376
- /**
377
- * Clean up resources and event listeners
378
- * Call this when destroying the analytics instance
379
- * @returns {void}
380
- */
381
390
  FormoAnalytics.prototype.cleanup = function () {
391
+ this.isCleanedUp = true;
382
392
  logger.debug("FormoAnalytics: Cleaning up resources");
383
393
  // Close the queue, don't just empty it. clear() only drops what is
384
394
  // buffered at this instant; asynchronous work already in flight (event
@@ -1014,7 +1024,21 @@ var FormoAnalytics = /** @class */ (function () {
1014
1024
  * @returns {void}
1015
1025
  */
1016
1026
  FormoAnalytics.prototype.optInTracking = function () {
1027
+ var _this = this;
1017
1028
  var _a;
1029
+ // A provider registered while the visitor was opted out had its
1030
+ // session adoption refused; nothing else retries it. Guarded: a
1031
+ // cleanup() racing this timer must not drive the torn-down tracker.
1032
+ setTimeout(function () {
1033
+ if (_this.isCleanedUp)
1034
+ return;
1035
+ try {
1036
+ _this.evmEvents.retryExternalAdoptions();
1037
+ }
1038
+ catch (_a) {
1039
+ /* adoption retry must never break opt-in */
1040
+ }
1041
+ }, 0);
1018
1042
  logger.info("Opting back into tracking");
1019
1043
  // Remove opt-out flag
1020
1044
  removeConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY);
@@ -1105,6 +1129,17 @@ var FormoAnalytics = /** @class */ (function () {
1105
1129
  return __awaiter(this, void 0, void 0, function () {
1106
1130
  var _this = this;
1107
1131
  return __generator(this, function (_a) {
1132
+ // A route change can end path-based suppression; a provider registered
1133
+ // while suppressed gets its refused session adoption retried here.
1134
+ // Idempotent and cheap when nothing is pending.
1135
+ if (!this.isCleanedUp) {
1136
+ try {
1137
+ this.evmEvents.retryExternalAdoptions();
1138
+ }
1139
+ catch (_b) {
1140
+ /* never let the retry break a page hit */
1141
+ }
1142
+ }
1108
1143
  if (!this.shouldTrack()) {
1109
1144
  logger.info("Track page hit: Skipping event due to tracking configuration");
1110
1145
  return [2 /*return*/];
@@ -1272,6 +1307,119 @@ var FormoAnalytics = /** @class */ (function () {
1272
1307
  };
1273
1308
  // Explicitly untrack a provider: remove listeners, clear wrapper flag
1274
1309
  // and tracking
1310
+ /**
1311
+ * Track an EIP-1193 provider the page constructed itself.
1312
+ *
1313
+ * Discovery covers EIP-6963 announcements and `window.ethereum`, which is
1314
+ * every injected wallet and nothing else. WalletConnect and Ledger
1315
+ * providers are built by the app (`EthereumProvider.init(...)`) and
1316
+ * announce nothing, so their sessions were invisible: connects,
1317
+ * signatures, and transactions all silently missing. Hand the provider
1318
+ * here once it exists and it takes the exact pipeline a discovered
1319
+ * provider takes - detect event, lifecycle listeners, request wrapper -
1320
+ * and a session that is already live is adopted from the provider's
1321
+ * synchronous state.
1322
+ *
1323
+ * Metadata resolution order: the caller's `info` overrides win; then a
1324
+ * WalletConnect session's peer metadata, which names the REAL wallet on
1325
+ * the far side of the transport (for example "Ledger Live"); then flag
1326
+ * sniffing; then a generic fallback. One deliberate exception: a caller
1327
+ * name of exactly "WalletConnect" is the generic transport name, so the
1328
+ * live peer still replaces it on events - name the provider anything
1329
+ * else to pin it verbatim.
1330
+ *
1331
+ * No-op outside the EIP-1193 path: in wagmi mode the connector system
1332
+ * already tracks these sessions, and wrapping the same provider twice
1333
+ * would double-report every event.
1334
+ *
1335
+ * With several live SDK instances (multi write-key pages) registering
1336
+ * the SAME provider, request-derived events (signatures, transactions)
1337
+ * go to the most recently registered live instance - the same
1338
+ * single-observer semantics discovery has always had for the request
1339
+ * wrapper. Lifecycle events (connect, chain, disconnect) reach every
1340
+ * instance. Fanning request observations out to all instances is a
1341
+ * separate feature.
1342
+ *
1343
+ * @returns true when the provider is (now) tracked, false when it was
1344
+ * refused (wagmi mode, EVM disabled, or not a valid EIP-1193 provider).
1345
+ *
1346
+ * @example
1347
+ * ```typescript
1348
+ * const wcProvider = await EthereumProvider.init({ projectId, chains });
1349
+ * formo.registerProvider(wcProvider);
1350
+ * ```
1351
+ */
1352
+ /**
1353
+ * INTERNAL. Install the request wrapper on a wagmi connector's provider.
1354
+ *
1355
+ * Wagmi mode watches the store and caches, which see hook-driven calls
1356
+ * only; imperative viem calls (walletClient.sendTransaction,
1357
+ * .signMessage, .writeContract, raw request) create no mutation and were
1358
+ * silently lost. Every viem client in a wagmi app is built on the
1359
+ * connector's EIP-1193 provider, so wrapping that provider closes the
1360
+ * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
1361
+ * request wrapper installs here. Double counting is prevented in the
1362
+ * wrapper via `shouldSkipRequestCapture`.
1363
+ */
1364
+ FormoAnalytics.prototype._wrapWagmiProvider = function (provider) {
1365
+ if (this.isCleanedUp || !isValidProvider(provider))
1366
+ return;
1367
+ try {
1368
+ this.evmRequests.registerRequestListeners(provider);
1369
+ }
1370
+ catch (e) {
1371
+ logger.warn("Failed to wrap wagmi provider for hybrid capture", e);
1372
+ }
1373
+ };
1374
+ FormoAnalytics.prototype.registerProvider = function (provider, info) {
1375
+ var _a, _b, _c;
1376
+ if (this.isCleanedUp) {
1377
+ // Cleanup terminally closed the event queue; listeners attached now
1378
+ // would hold this instance forever and deliver nothing.
1379
+ logger.warn("registerProvider: instance is cleaned up; refusing");
1380
+ return false;
1381
+ }
1382
+ if (this.isEvmDisabled) {
1383
+ logger.warn("registerProvider: EVM tracking is disabled; refusing");
1384
+ return false;
1385
+ }
1386
+ if (this.isWagmiMode) {
1387
+ logger.warn("registerProvider: wagmi mode tracks connectors already; registering the provider here would double-report its events. Refusing.");
1388
+ return false;
1389
+ }
1390
+ if (!isValidProvider(provider)) {
1391
+ logger.warn("registerProvider: not a valid EIP-1193 provider; refusing");
1392
+ return false;
1393
+ }
1394
+ var detected = detectInjectedProviderInfo(provider);
1395
+ var peer = readWalletConnectPeer(provider);
1396
+ // A live peer identifies the session as WalletConnect even when the
1397
+ // provider carries no isWalletConnect flag (v2 providers often do not).
1398
+ var rdns = (_a = info === null || info === void 0 ? void 0 : info.rdns) !== null && _a !== void 0 ? _a : (peer && detected.rdns === "io.injected.provider"
1399
+ ? "com.walletconnect"
1400
+ : detected.rdns);
1401
+ // The peer name is deliberately NOT stored: sessions change wallets,
1402
+ // and metadata frozen at registration would misname every later one.
1403
+ // `infoFor` resolves the peer live on each read, over the generic
1404
+ // transport name; a caller's explicit name still wins everywhere.
1405
+ var name = (_b = info === null || info === void 0 ? void 0 : info.name) !== null && _b !== void 0 ? _b : (peer ? "WalletConnect" : detected.name);
1406
+ // Per-instance uuid: EIP-6963 consumers (mipd included) deduplicate on
1407
+ // it, so two registered instances sharing an rdns-derived uuid would
1408
+ // collapse into one. Random when the platform provides it; a
1409
+ // monotonic suffix otherwise.
1410
+ var uuid = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
1411
+ ? crypto.randomUUID()
1412
+ : "registered-".concat(rdns.replace(/[^a-zA-Z0-9]/g, "-"), "-").concat((FormoAnalytics.registeredProviderSeq += 1));
1413
+ return this.evmEvents.adoptExternalProvider({
1414
+ info: {
1415
+ name: name,
1416
+ rdns: rdns,
1417
+ uuid: uuid,
1418
+ icon: (_c = info === null || info === void 0 ? void 0 : info.icon) !== null && _c !== void 0 ? _c : detected.icon,
1419
+ },
1420
+ provider: provider,
1421
+ });
1422
+ };
1275
1423
  // Debug/monitoring helpers
1276
1424
  FormoAnalytics.prototype.getTrackedProvidersCount = function () {
1277
1425
  return this.evm.counts.trackedProviders;
@@ -1283,6 +1431,8 @@ var FormoAnalytics = /** @class */ (function () {
1283
1431
  FormoAnalytics.prototype.getProviderState = function () {
1284
1432
  return __assign(__assign({}, this.evm.counts), { activeProvider: !!this._provider });
1285
1433
  };
1434
+ /** Fallback uuid suffix for platforms without crypto.randomUUID. */
1435
+ FormoAnalytics.registeredProviderSeq = 0;
1286
1436
  return FormoAnalytics;
1287
1437
  }());
1288
1438
  export { FormoAnalytics };
@@ -55,6 +55,7 @@ var defaultContext = {
55
55
  page: function () { return Promise.resolve(); },
56
56
  reset: function () { },
57
57
  cleanup: function () { },
58
+ registerProvider: function () { return false; },
58
59
  detect: function () { return Promise.resolve(); },
59
60
  connect: function () { return Promise.resolve(); },
60
61
  disconnect: function () { return Promise.resolve(); },
@@ -43,20 +43,19 @@ export interface EvmEventTrackerDeps {
43
43
  */
44
44
  registerRequestListeners(provider: EIP1193Provider): boolean;
45
45
  }
46
- /**
47
- * The EIP-1193 side of wallet tracking: which providers to watch, and what
48
- * their events mean.
49
- *
50
- * Split out of `FormoAnalytics` (#336). It holds no wallet state and no
51
- * provider registry of its own - those have owners already - so what is left
52
- * here is the part that is genuinely about interpreting wallet events:
53
- * deciding when a connect has to be reported, when a switch is stale, and
54
- * when a provider has stopped being the one we follow.
55
- */
56
46
  export declare class EvmEventTracker {
57
47
  private readonly wallet;
58
48
  private readonly registry;
59
49
  private readonly deps;
50
+ /**
51
+ * Providers adopted through `registerProvider` rather than discovered.
52
+ * Announcement-driven cleanup must not touch them: they are never in an
53
+ * announcement list, so "missing from the announcement" is their normal
54
+ * state, not evidence of removal. A Set rather than a WeakSet because
55
+ * suppressed adoptions retry from it; the registry holds these providers
56
+ * strongly anyway, and untrack removes them.
57
+ */
58
+ private externallyRegistered;
60
59
  /**
61
60
  * The connect this SDK has already reported for a provider.
62
61
  *
@@ -91,6 +90,35 @@ export declare class EvmEventTracker {
91
90
  */
92
91
  trackEIP1193Provider(provider: EIP1193Provider): void;
93
92
  trackProviders(providers: readonly EIP6963ProviderDetail[]): void;
93
+ /**
94
+ * Adopt a provider the page constructed rather than announced.
95
+ *
96
+ * Discovery only ever sees EIP-6963 announcements and `window.ethereum`.
97
+ * A WalletConnect or Ledger provider is a constructed object that does
98
+ * neither, so without this entry point its whole session is invisible -
99
+ * the P-2403 gap. The pipeline from here on is the same one every
100
+ * discovered provider takes: registry, detect event, listeners, request
101
+ * wrapper.
102
+ *
103
+ * A session that already exists at registration is seeded from the
104
+ * provider's SYNCHRONOUS `accounts` state (WalletConnect exposes it), via
105
+ * the same accounts-arrival path a live `accountsChanged` takes. No RPC:
106
+ * nothing analytics-only may go on a wallet's transport, and
107
+ * WalletConnect's serialised relay socket is the very case that rule
108
+ * exists for.
109
+ */
110
+ adoptExternalProvider(detail: EIP6963ProviderDetail): boolean;
111
+ /**
112
+ * Re-run session adoption for every registered external provider.
113
+ *
114
+ * Registration while tracking was suppressed (opt-out, excluded route)
115
+ * reached the adoption path and was refused - and a provider whose
116
+ * session already exists may never emit another accountsChanged, so
117
+ * nothing would ever retry. Called when suppression can have ended
118
+ * (opt-in, page navigation). Idempotent: an already-adopted wallet is
119
+ * deduplicated by the same state and markers as any repeated signal.
120
+ */
121
+ retryExternalAdoptions(): void;
94
122
  private registerAccountsChangedListener;
95
123
  private onAccountsChanged;
96
124
  /**
@@ -1,3 +1,14 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
1
12
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
13
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
14
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -34,11 +45,20 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
34
45
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
46
  }
36
47
  };
48
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
49
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
50
+ if (ar || !(i in from)) {
51
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
52
+ ar[i] = from[i];
53
+ }
54
+ }
55
+ return to.concat(ar || Array.prototype.slice.call(from));
56
+ };
37
57
  import { createStore } from "mipd";
38
58
  import { logger } from "../logger";
39
59
  import { parseChainId } from "../utils/chain";
40
60
  import { validateAndChecksumAddress } from "../utils/address";
41
- import { detectInjectedProviderInfo, isValidProvider } from "../provider";
61
+ import { detectInjectedProviderInfo, isValidProvider, DEFAULT_PROVIDER_ICON } from "../provider";
42
62
  /** Why the tracker moved the active slot to another provider. Log copy. */
43
63
  var PROVIDER_SWITCH_REASONS = {
44
64
  ADDRESS_MISMATCH: "Address mismatch indicates wallet switch",
@@ -55,11 +75,71 @@ var PROVIDER_SWITCH_REASONS = {
55
75
  * deciding when a connect has to be reported, when a switch is stale, and
56
76
  * when a provider has stopped being the one we follow.
57
77
  */
78
+ /**
79
+ * A registered provider's current accounts, from its synchronous state.
80
+ *
81
+ * `provider.accounts` first - but a LIVE MetaMask Mobile session over
82
+ * WalletConnect has been observed with `accounts` EMPTY while the session's
83
+ * namespaces held the approved account ("eip155:11155111:0xabc..."), which
84
+ * silently defeated adoption. The namespaces are the session's ground
85
+ * truth, so they are the fallback. Still purely synchronous property
86
+ * reads; nothing goes on the wallet transport.
87
+ */
88
+ function readProviderAccounts(provider) {
89
+ var _a;
90
+ var direct = provider.accounts;
91
+ if (Array.isArray(direct) &&
92
+ direct.length > 0 &&
93
+ direct.every(function (a) { return typeof a === "string"; })) {
94
+ return direct;
95
+ }
96
+ var session = provider.session;
97
+ // eip155 ONLY: a session can also carry Solana or other namespaces, and
98
+ // feeding a non-EVM address into the EVM adoption path would make
99
+ // validation reject it and drop the whole adoption. A session can also
100
+ // authorize DIFFERENT accounts per chain, so entries for the provider's
101
+ // active chain come first - the adopted address should be the one this
102
+ // chain actually authorized.
103
+ var ns = (_a = session === null || session === void 0 ? void 0 : session.namespaces) === null || _a === void 0 ? void 0 : _a.eip155;
104
+ var chainId = provider.chainId;
105
+ var parsed = typeof chainId === "number"
106
+ ? chainId
107
+ : typeof chainId === "string"
108
+ ? parseChainId(chainId)
109
+ : undefined;
110
+ var activePrefix = parsed ? "eip155:".concat(parsed, ":") : undefined;
111
+ var forChain = [];
112
+ var others = [];
113
+ if (Array.isArray(ns === null || ns === void 0 ? void 0 : ns.accounts)) {
114
+ for (var _i = 0, _b = ns.accounts; _i < _b.length; _i++) {
115
+ var entry = _b[_i];
116
+ if (typeof entry !== "string" || !entry.startsWith("eip155:"))
117
+ continue;
118
+ var address = entry.split(":")[2];
119
+ if (!address)
120
+ continue;
121
+ var bucket = activePrefix && entry.startsWith(activePrefix) ? forChain : others;
122
+ if (!bucket.includes(address))
123
+ bucket.push(address);
124
+ }
125
+ }
126
+ var out = __spreadArray(__spreadArray([], forChain, true), others.filter(function (a) { return !forChain.includes(a); }), true);
127
+ return out;
128
+ }
58
129
  var EvmEventTracker = /** @class */ (function () {
59
130
  function EvmEventTracker(wallet, registry, deps) {
60
131
  this.wallet = wallet;
61
132
  this.registry = registry;
62
133
  this.deps = deps;
134
+ /**
135
+ * Providers adopted through `registerProvider` rather than discovered.
136
+ * Announcement-driven cleanup must not touch them: they are never in an
137
+ * announcement list, so "missing from the announcement" is their normal
138
+ * state, not evidence of removal. A Set rather than a WeakSet because
139
+ * suppressed adoptions retry from it; the registry holds these providers
140
+ * strongly anyway, and untrack removes them.
141
+ */
142
+ this.externallyRegistered = new Set();
63
143
  /**
64
144
  * The connect this SDK has already reported for a provider.
65
145
  *
@@ -187,6 +267,103 @@ var EvmEventTracker = /** @class */ (function () {
187
267
  logger.error("Failed to track EIP-6963 providers during initialization:", error);
188
268
  }
189
269
  };
270
+ /**
271
+ * Adopt a provider the page constructed rather than announced.
272
+ *
273
+ * Discovery only ever sees EIP-6963 announcements and `window.ethereum`.
274
+ * A WalletConnect or Ledger provider is a constructed object that does
275
+ * neither, so without this entry point its whole session is invisible -
276
+ * the P-2403 gap. The pipeline from here on is the same one every
277
+ * discovered provider takes: registry, detect event, listeners, request
278
+ * wrapper.
279
+ *
280
+ * A session that already exists at registration is seeded from the
281
+ * provider's SYNCHRONOUS `accounts` state (WalletConnect exposes it), via
282
+ * the same accounts-arrival path a live `accountsChanged` takes. No RPC:
283
+ * nothing analytics-only may go on a wallet's transport, and
284
+ * WalletConnect's serialised relay socket is the very case that rule
285
+ * exists for.
286
+ */
287
+ EvmEventTracker.prototype.adoptExternalProvider = function (detail) {
288
+ var provider = detail.provider;
289
+ // Exempt from announcement-driven cleanup BEFORE tracking: a registered
290
+ // provider is never in an EIP-6963 announcement, so without this the
291
+ // next wallet announcement would untrack it and its events would stop.
292
+ this.externallyRegistered.add(provider);
293
+ this.registry.add(detail);
294
+ this.trackProviders([detail]);
295
+ // Adoption and success both hinge on the wrapper actually installing:
296
+ // reporting success for a provider whose requests stay invisible would
297
+ // recreate the silent loss this API exists to close.
298
+ if (!this.registry.isTracked(provider)) {
299
+ this.externallyRegistered.delete(provider);
300
+ // Tracking got partway: lifecycle listeners may already be attached
301
+ // even though the request wrapper failed. Leaving them would leak
302
+ // callbacks that hold this instance for the life of the page.
303
+ this.untrackProvider(provider);
304
+ logger.warn("adoptExternalProvider: provider could not be tracked");
305
+ return false;
306
+ }
307
+ // A provider that was ALREADY tracked skips the pipeline above, and
308
+ // "tracked" means lifecycle listeners - it says nothing about the
309
+ // request wrapper, which a wallet can have replaced since. Re-verify
310
+ // it on every registration: the call reinstalls a displaced wrapper,
311
+ // rebinds ownership of an intact one, and refuses when it cannot -
312
+ // and success here must mean capture actually works.
313
+ if (!this.deps.registerRequestListeners(provider)) {
314
+ this.externallyRegistered.delete(provider);
315
+ this.untrackProvider(provider);
316
+ logger.warn("adoptExternalProvider: request wrapper could not be ensured");
317
+ return false;
318
+ }
319
+ // Detect with the LIVE name (peer-resolved when a session exists);
320
+ // the stored metadata stays generic so later sessions rename freely.
321
+ void this.detectWallets([
322
+ __assign(__assign({}, detail), { info: __assign(__assign({}, detail.info), this.registry.infoFor(provider)) }),
323
+ ]);
324
+ var accounts = readProviderAccounts(provider);
325
+ if (accounts.length > 0) {
326
+ void this.onAccountsChanged(provider, accounts);
327
+ }
328
+ return true;
329
+ };
330
+ /**
331
+ * Re-run session adoption for every registered external provider.
332
+ *
333
+ * Registration while tracking was suppressed (opt-out, excluded route)
334
+ * reached the adoption path and was refused - and a provider whose
335
+ * session already exists may never emit another accountsChanged, so
336
+ * nothing would ever retry. Called when suppression can have ended
337
+ * (opt-in, page navigation). Idempotent: an already-adopted wallet is
338
+ * deduplicated by the same state and markers as any repeated signal.
339
+ */
340
+ EvmEventTracker.prototype.retryExternalAdoptions = function () {
341
+ var _this = this;
342
+ this.externallyRegistered.forEach(function (provider) {
343
+ // A flagless provider registered BEFORE pairing detected as the
344
+ // generic injected identity; once the session's peer exists, the
345
+ // live identity differs and the corrected detect fires. The
346
+ // session-scoped rdns dedup keeps this from repeating.
347
+ var live = _this.registry.infoFor(provider);
348
+ if (live.rdns === "com.walletconnect") {
349
+ void _this.detectWallets([
350
+ {
351
+ info: {
352
+ name: live.name,
353
+ rdns: live.rdns,
354
+ uuid: "corrected-com-walletconnect",
355
+ icon: DEFAULT_PROVIDER_ICON,
356
+ },
357
+ provider: provider,
358
+ },
359
+ ]);
360
+ }
361
+ var accounts = readProviderAccounts(provider);
362
+ if (accounts.length > 0) {
363
+ void _this.onAccountsChanged(provider, accounts);
364
+ }
365
+ });
366
+ };
190
367
  EvmEventTracker.prototype.registerAccountsChangedListener = function (provider) {
191
368
  var _this = this;
192
369
  logger.info("registerAccountsChangedListener");
@@ -1001,6 +1178,11 @@ var EvmEventTracker = /** @class */ (function () {
1001
1178
  var currentProviderInstances = new Set(current.map(function (detail) { return detail.provider; }));
1002
1179
  for (var _i = 0, _a = this.registry.trackedProviders(); _i < _a.length; _i++) {
1003
1180
  var provider = _a[_i];
1181
+ // A registered external provider is never announced over EIP-6963;
1182
+ // its absence from an announcement list says nothing about it.
1183
+ if (this.externallyRegistered.has(provider)) {
1184
+ continue;
1185
+ }
1004
1186
  if (!currentProviderInstances.has(provider)) {
1005
1187
  logger.info("Cleaning up unavailable provider: ".concat(provider.constructor.name));
1006
1188
  this.untrackProvider(provider);
@@ -45,7 +45,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
45
45
  };
46
46
  import { logger } from "../logger";
47
47
  import { validateAndChecksumAddress } from "../utils/address";
48
- import { detectInjectedProviderInfo } from "../provider";
48
+ import { detectInjectedProviderInfo, readWalletConnectPeer } from "../provider";
49
49
  import { WRAPPED_REQUEST_SYMBOL, WRAPPED_REQUEST_REF_SYMBOL, } from "../types";
50
50
  /**
51
51
  * Which EVM wallets exist, and what is known about each.
@@ -140,11 +140,33 @@ var EvmProviderRegistry = /** @class */ (function () {
140
140
  */
141
141
  EvmProviderRegistry.prototype.infoFor = function (provider) {
142
142
  var announced = this.details.find(function (p) { return p.provider === provider; });
143
- if (announced) {
144
- return { name: announced.info.name, rdns: announced.info.rdns };
143
+ var info = announced
144
+ ? { name: announced.info.name, rdns: announced.info.rdns }
145
+ : (function () {
146
+ var injected = detectInjectedProviderInfo(provider);
147
+ return { name: injected.name, rdns: injected.rdns };
148
+ })();
149
+ // WalletConnect names the TRANSPORT; the session's peer names the
150
+ // wallet. Resolved live, per read: a session established after the
151
+ // provider was registered still gets its signer's name onto every
152
+ // event from then on. Only GENERIC names are replaced - a caller who
153
+ // registered an explicit display name keeps it. "Injected Provider"
154
+ // is included because a flagless WalletConnect-compatible provider
155
+ // registered BEFORE its session exists detects as nothing at all;
156
+ // the peer appearing later is itself the proof of what it was, so
157
+ // the rdns upgrades with it.
158
+ if (info.name === "WalletConnect" || info.name === "Injected Provider") {
159
+ var peer = readWalletConnectPeer(provider);
160
+ if (peer === null || peer === void 0 ? void 0 : peer.name) {
161
+ return {
162
+ name: peer.name,
163
+ rdns: info.rdns === "io.injected.provider"
164
+ ? "com.walletconnect"
165
+ : info.rdns,
166
+ };
167
+ }
145
168
  }
146
- var injected = detectInjectedProviderInfo(provider);
147
- return { name: injected.name, rdns: injected.rdns };
169
+ return info;
148
170
  };
149
171
  // ── listener bookkeeping ─────────────────────────────────────────────────
150
172
  EvmProviderRegistry.prototype.addListener = function (provider, event, listener) {