@formo/analytics 1.38.1 → 1.39.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 (60) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +10 -1
  2. package/dist/cjs/src/FormoAnalytics.js +20 -5
  3. package/dist/cjs/src/FormoAnalyticsProvider.js +20 -0
  4. package/dist/cjs/src/evm/EvmEventTracker.d.ts +100 -9
  5. package/dist/cjs/src/evm/EvmEventTracker.js +357 -19
  6. package/dist/cjs/src/evm/EvmProviderRegistry.d.ts +34 -2
  7. package/dist/cjs/src/evm/EvmProviderRegistry.js +61 -4
  8. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +9 -4
  9. package/dist/cjs/src/evm/EvmRequestTracker.js +26 -16
  10. package/dist/cjs/src/solana/SolanaManager.d.ts +37 -9
  11. package/dist/cjs/src/solana/SolanaManager.js +135 -20
  12. package/dist/cjs/src/solana/SolanaStoreHandler.d.ts +6 -0
  13. package/dist/cjs/src/solana/SolanaStoreHandler.js +17 -8
  14. package/dist/cjs/src/solana/SolanaWalletStandardRegistry.d.ts +163 -0
  15. package/dist/cjs/src/solana/SolanaWalletStandardRegistry.js +447 -0
  16. package/dist/cjs/src/solana/index.d.ts +10 -5
  17. package/dist/cjs/src/solana/index.js +12 -6
  18. package/dist/cjs/src/solana/storeTypes.d.ts +20 -1
  19. package/dist/cjs/src/solana/types.d.ts +32 -9
  20. package/dist/cjs/src/solana/types.js +15 -0
  21. package/dist/cjs/src/solana/walletStandardTypes.d.ts +54 -0
  22. package/dist/cjs/src/solana/walletStandardTypes.js +21 -0
  23. package/dist/cjs/src/types/base.d.ts +15 -5
  24. package/dist/cjs/src/version.d.ts +1 -1
  25. package/dist/cjs/src/version.js +1 -1
  26. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +30 -1
  27. package/dist/cjs/src/wagmi/WagmiEventHandler.js +80 -1
  28. package/dist/cjs/src/wagmi/types.d.ts +6 -0
  29. package/dist/cjs/src/wagmi/utils.js +9 -1
  30. package/dist/esm/src/FormoAnalytics.d.ts +10 -1
  31. package/dist/esm/src/FormoAnalytics.js +20 -5
  32. package/dist/esm/src/FormoAnalyticsProvider.js +20 -0
  33. package/dist/esm/src/evm/EvmEventTracker.d.ts +100 -9
  34. package/dist/esm/src/evm/EvmEventTracker.js +357 -19
  35. package/dist/esm/src/evm/EvmProviderRegistry.d.ts +34 -2
  36. package/dist/esm/src/evm/EvmProviderRegistry.js +61 -4
  37. package/dist/esm/src/evm/EvmRequestTracker.d.ts +9 -4
  38. package/dist/esm/src/evm/EvmRequestTracker.js +26 -16
  39. package/dist/esm/src/solana/SolanaManager.d.ts +37 -9
  40. package/dist/esm/src/solana/SolanaManager.js +135 -20
  41. package/dist/esm/src/solana/SolanaStoreHandler.d.ts +6 -0
  42. package/dist/esm/src/solana/SolanaStoreHandler.js +18 -9
  43. package/dist/esm/src/solana/SolanaWalletStandardRegistry.d.ts +163 -0
  44. package/dist/esm/src/solana/SolanaWalletStandardRegistry.js +444 -0
  45. package/dist/esm/src/solana/index.d.ts +10 -5
  46. package/dist/esm/src/solana/index.js +10 -5
  47. package/dist/esm/src/solana/storeTypes.d.ts +20 -1
  48. package/dist/esm/src/solana/types.d.ts +32 -9
  49. package/dist/esm/src/solana/types.js +14 -0
  50. package/dist/esm/src/solana/walletStandardTypes.d.ts +54 -0
  51. package/dist/esm/src/solana/walletStandardTypes.js +18 -0
  52. package/dist/esm/src/types/base.d.ts +15 -5
  53. package/dist/esm/src/version.d.ts +1 -1
  54. package/dist/esm/src/version.js +1 -1
  55. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +30 -1
  56. package/dist/esm/src/wagmi/WagmiEventHandler.js +80 -1
  57. package/dist/esm/src/wagmi/types.d.ts +6 -0
  58. package/dist/esm/src/wagmi/utils.js +9 -1
  59. package/dist/index.umd.min.js +1 -1
  60. package/package.json +2 -2
@@ -45,6 +45,24 @@ export declare class EvmProviderRegistry {
45
45
  private listeners;
46
46
  /** The window-injected provider, once detected, so it is not re-wrapped. */
47
47
  private injectedDetail?;
48
+ /**
49
+ * Attribution supplied by the integration layer for a provider that was
50
+ * never announced: the wagmi fallback wrap names a connector's provider
51
+ * after the CONNECTOR, so request-derived events carry the same wallet
52
+ * name as the hook-driven events from the same connection. Without it
53
+ * the name came from flag sniffing, and a custom or embedded connector
54
+ * whose provider exposes no recognised flag split one wallet's activity
55
+ * between "Injected Provider" and the connector's name.
56
+ *
57
+ * A RESOLVER, read at each `infoFor`, not a value: the name is live for
58
+ * the connector the integration layer bound it to (a WalletConnect
59
+ * session that resolves or changes its peer renames without a re-wrap),
60
+ * and the integration layer names events on its hook path with the same
61
+ * function, so the two paths cannot disagree. Which connector a
62
+ * provider is bound to is the integration layer's decision; it is not
63
+ * necessarily the connector that is current when the request runs.
64
+ */
65
+ private attributions;
48
66
  constructor(deps: EvmProviderRegistryDeps);
49
67
  get all(): readonly EIP6963ProviderDetail[];
50
68
  isTracked(provider: EIP1193Provider): boolean;
@@ -62,11 +80,25 @@ export declare class EvmProviderRegistry {
62
80
  };
63
81
  /** Add a discovered provider once. Returns false if already present. */
64
82
  add(detail: EIP6963ProviderDetail): boolean;
83
+ /**
84
+ * Record (or, with `undefined`, forget) the integration layer's own
85
+ * attribution resolver for a provider. See `attributions`.
86
+ */
87
+ rememberAttribution(provider: EIP1193Provider | undefined, resolve: (() => {
88
+ name: string;
89
+ rdns?: string;
90
+ } | undefined) | undefined): void;
91
+ /** The supplied attribution for a provider, if any resolves right now. */
92
+ private suppliedAttributionFor;
65
93
  /**
66
94
  * A provider's display name and rdns.
67
95
  *
68
- * EIP-6963 metadata is authoritative when we have it; otherwise fall back to
69
- * sniffing the injected provider, which is all a pre-6963 wallet offers.
96
+ * EIP-6963 metadata is authoritative when we have it. A supplied
97
+ * attribution (the connector a wagmi app connected through) comes next:
98
+ * it describes the SESSION, which is what the hook-driven events report.
99
+ * Otherwise fall back to sniffing the injected provider, which is all a
100
+ * pre-6963 wallet offers; a supplied name without an rdns keeps the
101
+ * sniffed rdns.
70
102
  */
71
103
  infoFor(provider: EIP1193Provider): {
72
104
  name: string;
@@ -74,6 +74,24 @@ var EvmProviderRegistry = /** @class */ (function () {
74
74
  this.chainGenerations = new WeakMap();
75
75
  /** Listeners this SDK attached, so teardown can remove exactly those. */
76
76
  this.listeners = new Map();
77
+ /**
78
+ * Attribution supplied by the integration layer for a provider that was
79
+ * never announced: the wagmi fallback wrap names a connector's provider
80
+ * after the CONNECTOR, so request-derived events carry the same wallet
81
+ * name as the hook-driven events from the same connection. Without it
82
+ * the name came from flag sniffing, and a custom or embedded connector
83
+ * whose provider exposes no recognised flag split one wallet's activity
84
+ * between "Injected Provider" and the connector's name.
85
+ *
86
+ * A RESOLVER, read at each `infoFor`, not a value: the name is live for
87
+ * the connector the integration layer bound it to (a WalletConnect
88
+ * session that resolves or changes its peer renames without a re-wrap),
89
+ * and the integration layer names events on its hook path with the same
90
+ * function, so the two paths cannot disagree. Which connector a
91
+ * provider is bound to is the integration layer's decision; it is not
92
+ * necessarily the connector that is current when the request runs.
93
+ */
94
+ this.attributions = new WeakMap();
77
95
  }
78
96
  Object.defineProperty(EvmProviderRegistry.prototype, "all", {
79
97
  // ── the provider set ─────────────────────────────────────────────────────
@@ -132,19 +150,55 @@ var EvmProviderRegistry = /** @class */ (function () {
132
150
  this.seen.add(provider);
133
151
  return true;
134
152
  };
153
+ /**
154
+ * Record (or, with `undefined`, forget) the integration layer's own
155
+ * attribution resolver for a provider. See `attributions`.
156
+ */
157
+ EvmProviderRegistry.prototype.rememberAttribution = function (provider, resolve) {
158
+ if (!provider)
159
+ return;
160
+ if (!resolve) {
161
+ this.attributions.delete(provider);
162
+ return;
163
+ }
164
+ this.attributions.set(provider, resolve);
165
+ };
166
+ /** The supplied attribution for a provider, if any resolves right now. */
167
+ EvmProviderRegistry.prototype.suppliedAttributionFor = function (provider) {
168
+ var resolve = this.attributions.get(provider);
169
+ if (!resolve)
170
+ return undefined;
171
+ try {
172
+ var info = resolve();
173
+ return (info === null || info === void 0 ? void 0 : info.name) ? info : undefined;
174
+ }
175
+ catch (_a) {
176
+ return undefined;
177
+ }
178
+ };
135
179
  /**
136
180
  * A provider's display name and rdns.
137
181
  *
138
- * EIP-6963 metadata is authoritative when we have it; otherwise fall back to
139
- * sniffing the injected provider, which is all a pre-6963 wallet offers.
182
+ * EIP-6963 metadata is authoritative when we have it. A supplied
183
+ * attribution (the connector a wagmi app connected through) comes next:
184
+ * it describes the SESSION, which is what the hook-driven events report.
185
+ * Otherwise fall back to sniffing the injected provider, which is all a
186
+ * pre-6963 wallet offers; a supplied name without an rdns keeps the
187
+ * sniffed rdns.
140
188
  */
141
189
  EvmProviderRegistry.prototype.infoFor = function (provider) {
142
190
  var announced = this.details.find(function (p) { return p.provider === provider; });
191
+ var supplied = announced
192
+ ? undefined
193
+ : this.suppliedAttributionFor(provider);
143
194
  var info = announced
144
195
  ? { name: announced.info.name, rdns: announced.info.rdns }
145
196
  : (function () {
197
+ var _a;
146
198
  var injected = detectInjectedProviderInfo(provider);
147
- return { name: injected.name, rdns: injected.rdns };
199
+ return supplied
200
+ ? { name: supplied.name, rdns: (_a = supplied.rdns) !== null && _a !== void 0 ? _a : injected.rdns }
201
+ : { name: injected.name, rdns: injected.rdns };
148
202
  })();
149
203
  // WalletConnect names the TRANSPORT; the session's peer names the
150
204
  // wallet. Resolved live, per read: a session established after the
@@ -154,7 +208,10 @@ var EvmProviderRegistry = /** @class */ (function () {
154
208
  // is included because a flagless WalletConnect-compatible provider
155
209
  // registered BEFORE its session exists detects as nothing at all;
156
210
  // the peer appearing later is itself the proof of what it was, so
157
- // the rdns upgrades with it.
211
+ // the rdns upgrades with it. A supplied name gets the same treatment
212
+ // and no more: the integration layer resolves its own peer names
213
+ // where it wants them, and a branded connector must keep its name on
214
+ // both paths.
158
215
  if (info.name === "WalletConnect" || info.name === "Injected Provider") {
159
216
  var peer = readWalletConnectPeer(provider);
160
217
  if (peer === null || peer === void 0 ? void 0 : peer.name) {
@@ -83,10 +83,15 @@ export declare class EvmRequestTracker {
83
83
  /**
84
84
  * Wallet attribution for request-derived events.
85
85
  *
86
- * Live per read through the registry, so a WalletConnect session names
87
- * its actual signer ("MetaMask Wallet", "Ledger Live") - the live-test
88
- * rows had provider_name EMPTY on every signature and transaction, which
89
- * made per-wallet activity unanswerable in the warehouse.
86
+ * Resolved through the registry at the START of each request and held
87
+ * for that request's whole lifecycle, receipt included. Reading it per
88
+ * status let a connector switch (two connectors sharing one provider) or
89
+ * a session change made while a prompt was open split one operation
90
+ * between two wallet names. Still live across requests: a WalletConnect
91
+ * session names its actual signer ("MetaMask Wallet", "Ledger Live") -
92
+ * the live-test rows had provider_name EMPTY on every signature and
93
+ * transaction, which made per-wallet activity unanswerable in the
94
+ * warehouse.
90
95
  */
91
96
  private attributionFor;
92
97
  private buildSignatureEventPayload;
@@ -296,7 +296,7 @@ var EvmRequestTracker = /** @class */ (function () {
296
296
  */
297
297
  EvmRequestTracker.prototype.dispatchWrappedRequest = function (_a, provider_1, request_1) {
298
298
  return __awaiter(this, arguments, void 0, function (_b, provider, request) {
299
- var generation_1, responsePromise, capturedChainId_1, response_1, error_1, rpcError, txPromise, txChainId_1, transactionHash_1, error_2, rpcError;
299
+ var generation_1, responsePromise, capturedChainId_1, attribution_1, response_1, error_1, rpcError, txPromise, txChainId_1, attribution_2, transactionHash_1, error_2, rpcError;
300
300
  var _this = this;
301
301
  var _c, _d, _e, _f;
302
302
  var method = _b.method, params = _b.params;
@@ -339,6 +339,7 @@ var EvmRequestTracker = /** @class */ (function () {
339
339
  // below is never reported as unhandled. The real handling is there.
340
340
  responsePromise.catch(function () { return undefined; });
341
341
  capturedChainId_1 = this.registry.resolveChainId(provider);
342
+ attribution_1 = this.attributionFor(provider);
342
343
  // Fire-and-forget tracking
343
344
  (function () { return __awaiter(_this, void 0, void 0, function () {
344
345
  var e_1;
@@ -346,7 +347,7 @@ var EvmRequestTracker = /** @class */ (function () {
346
347
  switch (_a.label) {
347
348
  case 0:
348
349
  _a.trys.push([0, 2, , 3]);
349
- return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), this.attributionFor(provider))];
350
+ return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), attribution_1)];
350
351
  case 1:
351
352
  _a.sent();
352
353
  return [3 /*break*/, 3];
@@ -372,7 +373,7 @@ var EvmRequestTracker = /** @class */ (function () {
372
373
  switch (_a.label) {
373
374
  case 0:
374
375
  _a.trys.push([0, 2, , 3]);
375
- return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)), this.attributionFor(provider))];
376
+ return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)), attribution_1)];
376
377
  case 1:
377
378
  _a.sent();
378
379
  return [3 /*break*/, 3];
@@ -397,7 +398,7 @@ var EvmRequestTracker = /** @class */ (function () {
397
398
  switch (_a.label) {
398
399
  case 0:
399
400
  _a.trys.push([0, 2, , 3]);
400
- return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), this.attributionFor(provider))];
401
+ return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), attribution_1)];
401
402
  case 1:
402
403
  _a.sent();
403
404
  return [3 /*break*/, 3];
@@ -436,6 +437,7 @@ var EvmRequestTracker = /** @class */ (function () {
436
437
  txPromise = request({ method: method, params: params });
437
438
  txPromise.catch(function () { return undefined; });
438
439
  txChainId_1 = this.registry.resolveChainId(provider);
440
+ attribution_2 = this.attributionFor(provider);
439
441
  (function () { return __awaiter(_this, void 0, void 0, function () {
440
442
  var payload, e_4;
441
443
  return __generator(this, function (_a) {
@@ -445,7 +447,7 @@ var EvmRequestTracker = /** @class */ (function () {
445
447
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
446
448
  case 1:
447
449
  payload = _a.sent();
448
- return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.STARTED }, payload), this.attributionFor(provider))];
450
+ return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.STARTED }, payload), attribution_2)];
449
451
  case 2:
450
452
  _a.sent();
451
453
  return [3 /*break*/, 4];
@@ -472,11 +474,11 @@ var EvmRequestTracker = /** @class */ (function () {
472
474
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
473
475
  case 1:
474
476
  payload = _a.sent();
475
- return [4 /*yield*/, this.deps.transaction(__assign(__assign({ status: TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }), this.attributionFor(provider))];
477
+ return [4 /*yield*/, this.deps.transaction(__assign(__assign({ status: TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }), attribution_2)];
476
478
  case 2:
477
479
  _a.sent();
478
480
  // Start async polling for transaction receipt
479
- this.pollTransactionReceipt(provider, transactionHash_1, payload);
481
+ this.pollTransactionReceipt(provider, transactionHash_1, payload, attribution_2);
480
482
  return [3 /*break*/, 4];
481
483
  case 3:
482
484
  e_5 = _a.sent();
@@ -501,7 +503,7 @@ var EvmRequestTracker = /** @class */ (function () {
501
503
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
502
504
  case 1:
503
505
  payload = _a.sent();
504
- return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.REJECTED }, payload), this.attributionFor(provider))];
506
+ return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.REJECTED }, payload), attribution_2)];
505
507
  case 2:
506
508
  _a.sent();
507
509
  return [3 /*break*/, 4];
@@ -523,10 +525,15 @@ var EvmRequestTracker = /** @class */ (function () {
523
525
  /**
524
526
  * Wallet attribution for request-derived events.
525
527
  *
526
- * Live per read through the registry, so a WalletConnect session names
527
- * its actual signer ("MetaMask Wallet", "Ledger Live") - the live-test
528
- * rows had provider_name EMPTY on every signature and transaction, which
529
- * made per-wallet activity unanswerable in the warehouse.
528
+ * Resolved through the registry at the START of each request and held
529
+ * for that request's whole lifecycle, receipt included. Reading it per
530
+ * status let a connector switch (two connectors sharing one provider) or
531
+ * a session change made while a prompt was open split one operation
532
+ * between two wallet names. Still live across requests: a WalletConnect
533
+ * session names its actual signer ("MetaMask Wallet", "Ledger Live") -
534
+ * the live-test rows had provider_name EMPTY on every signature and
535
+ * transaction, which made per-wallet activity unanswerable in the
536
+ * warehouse.
530
537
  */
531
538
  EvmRequestTracker.prototype.attributionFor = function (provider) {
532
539
  var info = this.registry.infoFor(provider);
@@ -596,8 +603,11 @@ var EvmRequestTracker = /** @class */ (function () {
596
603
  /**
597
604
  * Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
598
605
  */
599
- EvmRequestTracker.prototype.pollTransactionReceipt = function (provider_1, transactionHash_2, payload_1) {
600
- return __awaiter(this, arguments, void 0, function (provider, transactionHash, payload, maxAttempts, intervalMs) {
606
+ EvmRequestTracker.prototype.pollTransactionReceipt = function (provider_1, transactionHash_2, payload_1, attribution_3) {
607
+ return __awaiter(this, arguments, void 0, function (provider, transactionHash, payload,
608
+ // Snapshot from the broadcast: a connector or session change during
609
+ // the poll window must not relabel the receipt.
610
+ attribution, maxAttempts, intervalMs) {
601
611
  var attempts, poll;
602
612
  var _this = this;
603
613
  if (maxAttempts === void 0) { maxAttempts = 10; }
@@ -626,7 +636,7 @@ var EvmRequestTracker = /** @class */ (function () {
626
636
  // status: 1 = success, 0 = reverted
627
637
  if (receipt.status === "0x1" || receipt.status === 1) {
628
638
  this.deps
629
- .transaction(__assign(__assign({ status: TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }), this.attributionFor(provider))
639
+ .transaction(__assign(__assign({ status: TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }), attribution)
630
640
  .catch(function (e) {
631
641
  return logger.error("Formo: Failed to track transaction confirmation", e);
632
642
  });
@@ -634,7 +644,7 @@ var EvmRequestTracker = /** @class */ (function () {
634
644
  }
635
645
  else if (receipt.status === "0x0" || receipt.status === 0) {
636
646
  this.deps
637
- .transaction(__assign(__assign({ status: TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }), this.attributionFor(provider))
647
+ .transaction(__assign(__assign({ status: TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }), attribution)
638
648
  .catch(function (e) {
639
649
  return logger.error("Formo: Failed to track transaction revert", e);
640
650
  });
@@ -1,14 +1,27 @@
1
1
  /**
2
2
  * SolanaManager
3
3
  *
4
- * Manages the lifecycle of the Solana store integration.
5
- * Subscribes to framework-kit's zustand store for automatic event capture
6
- * of wallet connect/disconnect and transaction lifecycle events.
4
+ * Owns the two ways the SDK learns about Solana wallets:
7
5
  *
8
- * For signMessage/signTransaction tracking (not captured by the store),
6
+ * 1. `SolanaWalletStandardRegistry`: discovers wallets through the Wallet
7
+ * Standard and reports detect / connect / disconnect. On by default, so
8
+ * compatible wallets registered by Solana Kit, wallet-adapter,
9
+ * framework-kit, or another host are covered with no configuration,
10
+ * exactly like EVM wallets through EIP-6963. `solana: false` turns it off.
11
+ * 2. `SolanaStoreHandler`: subscribes to framework-kit's zustand store for
12
+ * connect / disconnect / cluster changes AND the transaction lifecycle.
13
+ * Opt-in through `solana: { store }` or `formo.solana.setStore()`.
14
+ *
15
+ * Both observe the same Wallet Standard connection when a framework-kit app
16
+ * connects. A store supplied at initialization owns wallet events; a store
17
+ * attached later takes ownership when it observes its first connection and
18
+ * adopts any connect the registry already reported. One connect per
19
+ * connection, whichever path an app is on.
20
+ *
21
+ * For signMessage/signTransaction tracking (not captured by either path),
9
22
  * use formo.signature() directly with the address and chainId.
10
23
  *
11
- * For manual event tracking without the store, use the core API directly:
24
+ * For manual event tracking, use the core API directly:
12
25
  * formo.transaction(), formo.signature(), formo.connect(), formo.disconnect().
13
26
  */
14
27
  import { FormoAnalytics } from "../FormoAnalytics";
@@ -16,9 +29,19 @@ import { SolanaCluster, SolanaOptions } from "./types";
16
29
  import { SolanaClientStore } from "./storeTypes";
17
30
  export declare class SolanaManager {
18
31
  private formo;
32
+ private readonly enabled;
19
33
  private storeHandler?;
34
+ private registry?;
20
35
  private pendingCluster?;
21
- constructor(formo: FormoAnalytics, options?: SolanaOptions);
36
+ private storeOwnsWalletEvents;
37
+ /**
38
+ * @param formo - The SDK instance events are reported to.
39
+ * @param options - `options.solana` as passed to the SDK, if an object.
40
+ * @param enabled - Whether Solana tracking is enabled. False only when the
41
+ * host app passed `solana: false`; both discovery and stores then stay off.
42
+ */
43
+ constructor(formo: FormoAnalytics, options?: SolanaOptions, enabled?: boolean);
44
+ private attachStore;
22
45
  /**
23
46
  * Set the framework-kit zustand store for automatic event tracking.
24
47
  * This enables autocapture mode — connect/disconnect and transaction events
@@ -39,11 +62,16 @@ export declare class SolanaManager {
39
62
  cluster?: SolanaCluster;
40
63
  }): void;
41
64
  /**
42
- * Update the cluster/network. Only needed if the store endpoint doesn't
43
- * contain a recognizable cluster name (e.g. custom RPC URLs).
44
- * In most cases, the cluster is auto-detected from the store's endpoint.
65
+ * Update the cluster/network.
66
+ *
67
+ * With a framework-kit store, only needed if the store endpoint doesn't
68
+ * contain a recognizable cluster name (e.g. custom RPC URLs). Without one,
69
+ * this is how a non-mainnet app tells the SDK which cluster its Wallet
70
+ * Standard connections are on, since the standard itself cannot say.
45
71
  */
46
72
  setCluster(cluster: SolanaCluster): void;
73
+ /** Names of the Wallet Standard wallets discovered so far. */
74
+ get discoveredWallets(): string[];
47
75
  cleanup(): void;
48
76
  }
49
77
  //# sourceMappingURL=SolanaManager.d.ts.map
@@ -1,32 +1,124 @@
1
1
  /**
2
2
  * SolanaManager
3
3
  *
4
- * Manages the lifecycle of the Solana store integration.
5
- * Subscribes to framework-kit's zustand store for automatic event capture
6
- * of wallet connect/disconnect and transaction lifecycle events.
4
+ * Owns the two ways the SDK learns about Solana wallets:
7
5
  *
8
- * For signMessage/signTransaction tracking (not captured by the store),
6
+ * 1. `SolanaWalletStandardRegistry`: discovers wallets through the Wallet
7
+ * Standard and reports detect / connect / disconnect. On by default, so
8
+ * compatible wallets registered by Solana Kit, wallet-adapter,
9
+ * framework-kit, or another host are covered with no configuration,
10
+ * exactly like EVM wallets through EIP-6963. `solana: false` turns it off.
11
+ * 2. `SolanaStoreHandler`: subscribes to framework-kit's zustand store for
12
+ * connect / disconnect / cluster changes AND the transaction lifecycle.
13
+ * Opt-in through `solana: { store }` or `formo.solana.setStore()`.
14
+ *
15
+ * Both observe the same Wallet Standard connection when a framework-kit app
16
+ * connects. A store supplied at initialization owns wallet events; a store
17
+ * attached later takes ownership when it observes its first connection and
18
+ * adopts any connect the registry already reported. One connect per
19
+ * connection, whichever path an app is on.
20
+ *
21
+ * For signMessage/signTransaction tracking (not captured by either path),
9
22
  * use formo.signature() directly with the address and chainId.
10
23
  *
11
- * For manual event tracking without the store, use the core API directly:
24
+ * For manual event tracking, use the core API directly:
12
25
  * formo.transaction(), formo.signature(), formo.connect(), formo.disconnect().
13
26
  */
14
27
  import { logger } from "../logger";
15
28
  import { SolanaStoreHandler } from "./SolanaStoreHandler";
29
+ import { SolanaWalletStandardRegistry } from "./SolanaWalletStandardRegistry";
30
+ import { SOLANA_CLUSTERS_BY_ID, } from "./types";
16
31
  var SolanaManager = /** @class */ (function () {
17
- function SolanaManager(formo, options) {
32
+ /**
33
+ * @param formo - The SDK instance events are reported to.
34
+ * @param options - `options.solana` as passed to the SDK, if an object.
35
+ * @param enabled - Whether Solana tracking is enabled. False only when the
36
+ * host app passed `solana: false`; both discovery and stores then stay off.
37
+ */
38
+ function SolanaManager(formo, options, enabled) {
39
+ if (enabled === void 0) { enabled = true; }
40
+ var _this = this;
18
41
  this.formo = formo;
19
- if (options === null || options === void 0 ? void 0 : options.store) {
20
- logger.info("SolanaManager: Initializing store-based Solana tracking");
21
- this.storeHandler = new SolanaStoreHandler(formo, options.store, {
22
- cluster: options.cluster,
23
- });
24
- }
25
- else if (options === null || options === void 0 ? void 0 : options.cluster) {
42
+ this.enabled = enabled;
43
+ this.storeOwnsWalletEvents = false;
44
+ if (!enabled)
45
+ return;
46
+ if (options === null || options === void 0 ? void 0 : options.cluster) {
26
47
  // Store pending cluster for when setStore is called later
27
48
  this.pendingCluster = options.cluster;
28
49
  }
50
+ // A store supplied at initialization owns wallet events from the outset:
51
+ // unlike a store attached later, it has not missed any prior registry
52
+ // state and it knows the cluster more precisely.
53
+ this.storeOwnsWalletEvents = !!(options === null || options === void 0 ? void 0 : options.store);
54
+ this.registry = new SolanaWalletStandardRegistry({
55
+ isAutocaptureEnabled: function (t) { return _this.formo.isAutocaptureEnabled(t); },
56
+ willTrackEvent: function (chainId) { return _this.formo.willTrackEvent(chainId); },
57
+ detect: function (params) { return _this.formo.detect(params); },
58
+ connect: function (params, properties) {
59
+ return _this.formo.connect(params, properties);
60
+ },
61
+ disconnect: function (params) { return _this.formo.disconnect(params); },
62
+ chain: function (params) { return _this.formo.chain(params); },
63
+ syncWalletState: function (params) { return _this.formo.syncWalletState(params); },
64
+ currentAddress: function () { return _this.formo.currentAddress; },
65
+ ownsWalletEvents: function () { return !_this.storeOwnsWalletEvents; },
66
+ }, { cluster: options === null || options === void 0 ? void 0 : options.cluster });
67
+ if (options === null || options === void 0 ? void 0 : options.store) {
68
+ logger.info("SolanaManager: Initializing store-based Solana tracking");
69
+ this.attachStore(options.store, options.cluster);
70
+ }
29
71
  }
72
+ SolanaManager.prototype.attachStore = function (store, cluster) {
73
+ var _this = this;
74
+ var _a;
75
+ this.storeHandler = new SolanaStoreHandler(this.formo, store, {
76
+ cluster: cluster,
77
+ beforeWalletConnect: function (connection) {
78
+ var _a, _b;
79
+ // The store's cluster is authoritative even when chain autocapture is
80
+ // disabled. Keep central attribution correct without manufacturing a
81
+ // chain event in that mode.
82
+ _this.formo.syncWalletState({
83
+ address: connection.address,
84
+ chainId: connection.chainId,
85
+ });
86
+ if (_this.storeOwnsWalletEvents)
87
+ return true;
88
+ var reported = (_a = _this.registry) === null || _a === void 0 ? void 0 : _a.takeReportedConnection(connection.address, connection.rdns);
89
+ _this.storeOwnsWalletEvents = true;
90
+ // If Wallet Standard got there first, the store adopts that live
91
+ // connection instead of emitting it again. Correct its cluster if
92
+ // the store has more precise information.
93
+ if (!reported) {
94
+ // Nothing to adopt. Either the registry never reported this
95
+ // connection, or it reported it under another identity, in which
96
+ // case the store is about to emit a second connect for the same
97
+ // live connection. Both paths derive the rdns from the wallet's
98
+ // own name, so a mismatch means the store's connector is labelled
99
+ // differently from the registered wallet.
100
+ var held = (_b = _this.registry) === null || _b === void 0 ? void 0 : _b.reportedConnectionRdns(connection.address);
101
+ if (held) {
102
+ logger.warn("SolanaManager: Store connector does not match the discovered wallet; the connection is reported twice", { storeRdns: connection.rdns, walletRdns: held });
103
+ }
104
+ return true;
105
+ }
106
+ if (reported.chainId !== connection.chainId &&
107
+ _this.formo.isAutocaptureEnabled("chain")) {
108
+ _this.formo.chain(connection).catch(function (error) {
109
+ logger.error("SolanaManager: Error correcting cluster during store handoff", error);
110
+ });
111
+ }
112
+ return false;
113
+ },
114
+ });
115
+ // Keep the registry's snapshot on the store's detected cluster. This is
116
+ // silent once the store owns events; during a late handoff it corrects a
117
+ // registry-reported connection before the store adopts it.
118
+ var detectedCluster = SOLANA_CLUSTERS_BY_ID[this.storeHandler.getChainId()];
119
+ if (detectedCluster)
120
+ (_a = this.registry) === null || _a === void 0 ? void 0 : _a.setCluster(detectedCluster);
121
+ };
30
122
  /**
31
123
  * Set the framework-kit zustand store for automatic event tracking.
32
124
  * This enables autocapture mode — connect/disconnect and transaction events
@@ -45,29 +137,52 @@ var SolanaManager = /** @class */ (function () {
45
137
  */
46
138
  SolanaManager.prototype.setStore = function (store, options) {
47
139
  var _a;
140
+ if (!this.enabled) {
141
+ logger.warn("SolanaManager: Ignoring setStore. Solana tracking is off for this instance (solana: false, or the SDK was cleaned up)");
142
+ return;
143
+ }
48
144
  (_a = this.storeHandler) === null || _a === void 0 ? void 0 : _a.cleanup();
49
- this.storeHandler = new SolanaStoreHandler(this.formo, store, {
50
- cluster: (options === null || options === void 0 ? void 0 : options.cluster) || this.pendingCluster,
51
- });
145
+ this.storeHandler = undefined;
146
+ this.storeOwnsWalletEvents = false;
147
+ this.attachStore(store, (options === null || options === void 0 ? void 0 : options.cluster) || this.pendingCluster);
52
148
  this.pendingCluster = undefined;
53
149
  };
54
150
  /**
55
- * Update the cluster/network. Only needed if the store endpoint doesn't
56
- * contain a recognizable cluster name (e.g. custom RPC URLs).
57
- * In most cases, the cluster is auto-detected from the store's endpoint.
151
+ * Update the cluster/network.
152
+ *
153
+ * With a framework-kit store, only needed if the store endpoint doesn't
154
+ * contain a recognizable cluster name (e.g. custom RPC URLs). Without one,
155
+ * this is how a non-mainnet app tells the SDK which cluster its Wallet
156
+ * Standard connections are on, since the standard itself cannot say.
58
157
  */
59
158
  SolanaManager.prototype.setCluster = function (cluster) {
159
+ var _a;
160
+ if (!this.enabled)
161
+ return;
60
162
  if (this.storeHandler) {
61
163
  this.storeHandler.setCluster(cluster);
62
164
  }
63
165
  else {
64
166
  this.pendingCluster = cluster;
65
167
  }
168
+ (_a = this.registry) === null || _a === void 0 ? void 0 : _a.setCluster(cluster);
66
169
  };
170
+ Object.defineProperty(SolanaManager.prototype, "discoveredWallets", {
171
+ /** Names of the Wallet Standard wallets discovered so far. */
172
+ get: function () {
173
+ var _a, _b;
174
+ return (_b = (_a = this.registry) === null || _a === void 0 ? void 0 : _a.walletNames) !== null && _b !== void 0 ? _b : [];
175
+ },
176
+ enumerable: false,
177
+ configurable: true
178
+ });
67
179
  SolanaManager.prototype.cleanup = function () {
68
- var _a;
180
+ var _a, _b;
69
181
  (_a = this.storeHandler) === null || _a === void 0 ? void 0 : _a.cleanup();
70
182
  this.storeHandler = undefined;
183
+ this.storeOwnsWalletEvents = false;
184
+ (_b = this.registry) === null || _b === void 0 ? void 0 : _b.cleanup();
185
+ this.registry = undefined;
71
186
  };
72
187
  return SolanaManager;
73
188
  }());
@@ -48,8 +48,14 @@ export declare class SolanaStoreHandler {
48
48
  * When true, auto-detection from the store endpoint is disabled.
49
49
  */
50
50
  private explicitCluster;
51
+ private beforeWalletConnect?;
51
52
  constructor(formoAnalytics: FormoAnalytics, store: SolanaClientStore, options?: {
52
53
  cluster?: SolanaCluster;
54
+ beforeWalletConnect?: (connection: {
55
+ address: string;
56
+ chainId: number;
57
+ rdns: string;
58
+ }) => boolean;
53
59
  });
54
60
  /**
55
61
  * Update the cluster/network.