@meshconnect/uwc-core 1.9.0 → 1.10.0-snapshot.0112a96

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 (38) hide show
  1. package/dist/events.d.ts +107 -0
  2. package/dist/events.d.ts.map +1 -1
  3. package/dist/events.js +4 -0
  4. package/dist/events.js.map +1 -1
  5. package/dist/observability/instrument-handoff.d.ts +21 -3
  6. package/dist/observability/instrument-handoff.d.ts.map +1 -1
  7. package/dist/observability/instrument-handoff.js +38 -4
  8. package/dist/observability/instrument-handoff.js.map +1 -1
  9. package/dist/observability/telemetry.d.ts +39 -9
  10. package/dist/observability/telemetry.d.ts.map +1 -1
  11. package/dist/observability/telemetry.js +53 -5
  12. package/dist/observability/telemetry.js.map +1 -1
  13. package/dist/services/network-switch-service.d.ts.map +1 -1
  14. package/dist/services/network-switch-service.js +39 -1
  15. package/dist/services/network-switch-service.js.map +1 -1
  16. package/dist/services/signature-service.d.ts.map +1 -1
  17. package/dist/services/signature-service.js +10 -3
  18. package/dist/services/signature-service.js.map +1 -1
  19. package/dist/services/transaction-service.d.ts.map +1 -1
  20. package/dist/services/transaction-service.js +11 -3
  21. package/dist/services/transaction-service.js.map +1 -1
  22. package/dist/universal-wallet-connector.d.ts.map +1 -1
  23. package/dist/universal-wallet-connector.js +3 -0
  24. package/dist/universal-wallet-connector.js.map +1 -1
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.d.ts.map +1 -1
  27. package/dist/version.js +1 -1
  28. package/dist/version.js.map +1 -1
  29. package/package.json +5 -5
  30. package/src/events.ts +118 -0
  31. package/src/observability/instrument-handoff.ts +52 -5
  32. package/src/observability/telemetry.test.ts +111 -0
  33. package/src/observability/telemetry.ts +104 -14
  34. package/src/services/network-switch-service.ts +48 -1
  35. package/src/services/signature-service.ts +18 -3
  36. package/src/services/transaction-service.ts +19 -3
  37. package/src/universal-wallet-connector.ts +4 -0
  38. package/src/version.ts +1 -1
@@ -5,7 +5,7 @@ import type {
5
5
  Session
6
6
  } from '@meshconnect/uwc-types'
7
7
  import type { EventManager } from '../managers/event-manager'
8
- import type { UWCOperation } from '../events'
8
+ import type { ProviderKind, UWCOperation } from '../events'
9
9
  import { generateId } from '../utils/id'
10
10
  import {
11
11
  classifyError,
@@ -26,19 +26,56 @@ export interface HandoffContext {
26
26
  chainId?: NetworkId | undefined
27
27
  /** switchNetwork only: target chain already the active one (no-op switch). */
28
28
  targetAlreadyActive?: boolean | undefined
29
+ /** Provider surface used (namespace-parking risk is integratedBrowser-only). */
30
+ providerKind?: ProviderKind | undefined
31
+ /** Session-level: running inside the wallet's own in-app browser. */
32
+ usingIntegratedBrowser?: boolean | undefined
33
+ /** Active wallet's legacy `solana.injectedId` (e.g. coinbaseSolana), when set. */
34
+ injectedId?: string | undefined
35
+ /** Last namespace actually signed/sent before this op — the parking proxy. */
36
+ lastSignedNamespace?: Namespace | undefined
37
+ /** switchNetwork only: namespace active BEFORE the switch (cross-namespace detection). */
38
+ previousNamespace?: Namespace | undefined
29
39
  }
30
40
 
31
- /** Build handoff routing facets from the active session (no address). */
41
+ /** Which provider surface a namespace/mode maps to (parking risk = integratedBrowser). */
42
+ export function deriveProviderKind(
43
+ connectionMode: ConnectionMode,
44
+ usingIntegratedBrowser: boolean
45
+ ): ProviderKind {
46
+ if (connectionMode === 'walletConnect') return 'walletConnect'
47
+ if (connectionMode === 'tonConnect') return 'tonConnect'
48
+ return usingIntegratedBrowser ? 'integratedBrowser' : 'extension'
49
+ }
50
+
51
+ /**
52
+ * Build handoff routing facets from the active session (no address). Enriched
53
+ * with the namespace-parking triage facets — provider surface, in-app-browser,
54
+ * the wallet's legacy `solana.injectedId`, and the last-signed namespace — so a
55
+ * send/sign handoff self-describes the parking setup without a session join.
56
+ * `usingIntegratedBrowser` is connector-level state, so callers pass it in.
57
+ */
32
58
  export function sessionHandoffContext(
33
59
  operation: UWCOperation,
34
- session: Session
60
+ session: Session,
61
+ usingIntegratedBrowser = false
35
62
  ): HandoffContext {
63
+ const connectionMode = session.connectionMode ?? 'injected'
64
+ // `namespaceMetaData` (with the legacy `solana.injectedId`) lives on the
65
+ // injected provider, not the wallet — pick the surface in use.
66
+ const injectedProvider = usingIntegratedBrowser
67
+ ? session.activeWallet?.integratedBrowserInjectedProvider
68
+ : session.activeWallet?.extensionInjectedProvider
36
69
  return {
37
70
  operation,
38
- connectionMode: session.connectionMode ?? 'injected',
71
+ connectionMode,
39
72
  walletId: session.activeWallet?.id,
40
73
  namespace: session.activeNetwork?.namespace,
41
- chainId: session.activeNetwork?.id ?? undefined
74
+ chainId: session.activeNetwork?.id ?? undefined,
75
+ providerKind: deriveProviderKind(connectionMode, usingIntegratedBrowser),
76
+ usingIntegratedBrowser,
77
+ injectedId: injectedProvider?.namespaceMetaData?.solana?.injectedId,
78
+ lastSignedNamespace: session.lastSignedNamespace ?? undefined
42
79
  }
43
80
  }
44
81
 
@@ -134,6 +171,11 @@ export async function instrumentHandoff<T>(
134
171
  handoffId,
135
172
  walletFlowType: deriveWalletFlowType(context.connectionMode, platform),
136
173
  platform,
174
+ providerKind: context.providerKind,
175
+ usingIntegratedBrowser: context.usingIntegratedBrowser,
176
+ injectedId: context.injectedId,
177
+ lastSignedNamespace: context.lastSignedNamespace,
178
+ previousNamespace: context.previousNamespace,
137
179
  ...(context.targetAlreadyActive !== undefined && {
138
180
  targetAlreadyActive: context.targetAlreadyActive
139
181
  })
@@ -176,6 +218,11 @@ export async function instrumentHandoff<T>(
176
218
  walletId: context.walletId,
177
219
  namespace: context.namespace,
178
220
  chainId: context.chainId,
221
+ providerKind: context.providerKind,
222
+ usingIntegratedBrowser: context.usingIntegratedBrowser,
223
+ injectedId: context.injectedId,
224
+ lastSignedNamespace: context.lastSignedNamespace,
225
+ previousNamespace: context.previousNamespace,
179
226
  ...(context.targetAlreadyActive !== undefined && {
180
227
  targetAlreadyActive: context.targetAlreadyActive
181
228
  })
@@ -546,6 +546,41 @@ describe('normalizeEvent', () => {
546
546
  expect(capped.runtime).toBe('iframe')
547
547
  })
548
548
 
549
+ it('carries injectedId + gatherSource + runtime for secondarySolanaConnectGathered, length-capped (ONC-231)', () => {
550
+ const out = normalizeEvent(
551
+ 'secondarySolanaConnectGathered',
552
+ {
553
+ injectedId: 'coinbaseSolana',
554
+ source: 'connect',
555
+ legacyInjected: true,
556
+ framed: true
557
+ },
558
+ SDK_SESSION,
559
+ NOW
560
+ )
561
+ expect(out.injectedId).toBe('coinbaseSolana')
562
+ expect(out.legacyInjected).toBe(true)
563
+ expect(out.gatherSource).toBe('connect')
564
+ expect(out.runtime).toBe('iframe')
565
+ // never leaks a gateOutcome (that's the skip event's field)
566
+ expect(out.gateOutcome).toBeUndefined()
567
+
568
+ const capped = normalizeEvent(
569
+ 'secondarySolanaConnectGathered',
570
+ {
571
+ injectedId: 'y'.repeat(200),
572
+ source: 'existing',
573
+ legacyInjected: false,
574
+ framed: false
575
+ },
576
+ SDK_SESSION,
577
+ NOW
578
+ )
579
+ expect(capped.injectedId).toHaveLength(64)
580
+ expect(capped.gatherSource).toBe('existing')
581
+ expect(capped.runtime).toBe('direct')
582
+ })
583
+
549
584
  it('normalizes lifecycle events with routing only, no address (P2)', () => {
550
585
  const acct = normalizeEvent(
551
586
  'walletAccountChanged',
@@ -697,4 +732,80 @@ describe('normalizeEvent', () => {
697
732
  )
698
733
  expect('handoffCovered' in out).toBe(false)
699
734
  })
735
+
736
+ it('normalizes namespaceReassert(succeeded), mapping every field onto its reassert* facet', () => {
737
+ const out = normalizeEvent(
738
+ 'namespaceReassert',
739
+ {
740
+ phase: 'succeeded',
741
+ lever: 'eth_requestAccounts',
742
+ connectionMode: 'injected',
743
+ walletId: 'coinbase',
744
+ injectedId: 'coinbaseSolana',
745
+ fromNamespace: 'solana',
746
+ toNamespace: 'eip155',
747
+ durationMs: 42
748
+ },
749
+ SDK_SESSION,
750
+ NOW
751
+ )
752
+ expect(out.reassertPhase).toBe('succeeded')
753
+ expect(out.reassertLever).toBe('eth_requestAccounts')
754
+ expect(out.connectionMode).toBe('injected')
755
+ expect(out.walletId).toBe('coinbase')
756
+ expect(out.injectedId).toBe('coinbaseSolana')
757
+ expect(out.fromNamespace).toBe('solana')
758
+ expect(out.toNamespace).toBe('eip155')
759
+ expect(out.durationMs).toBe(42)
760
+ expect(out.error).toBeUndefined()
761
+ })
762
+
763
+ it('normalizes namespaceReassert(failed), capping injectedId and scrubbing the error message', () => {
764
+ const addr = '0x' + 'a'.repeat(40)
765
+ const out = normalizeEvent(
766
+ 'namespaceReassert',
767
+ {
768
+ phase: 'failed',
769
+ lever: 'eth_requestAccounts',
770
+ connectionMode: 'injected',
771
+ walletId: 'coinbase',
772
+ injectedId: 'x'.repeat(200),
773
+ fromNamespace: 'solana',
774
+ toNamespace: 'eip155',
775
+ durationMs: 12,
776
+ error: { type: 'unknown', message: `declined for ${addr}` }
777
+ },
778
+ SDK_SESSION,
779
+ NOW
780
+ )
781
+ expect(out.reassertPhase).toBe('failed')
782
+ expect(out.injectedId).toHaveLength(64)
783
+ expect(out.error?.message).not.toContain(addr)
784
+ expect(out.error?.message).toContain('[redacted-hex]')
785
+ })
786
+
787
+ it('carries parkingFacets (providerKind, usingIntegratedBrowser, injectedId capped, lastSignedNamespace, previousNamespace) on a handoff terminal', () => {
788
+ const out = normalizeEvent(
789
+ 'walletTimedOut',
790
+ {
791
+ handoffId: 'h-1',
792
+ operation: 'sendTransaction',
793
+ durationMs: 1000,
794
+ connectionMode: 'injected',
795
+ timeoutMs: 1000,
796
+ providerKind: 'integratedBrowser',
797
+ usingIntegratedBrowser: true,
798
+ injectedId: 'y'.repeat(200),
799
+ lastSignedNamespace: 'solana',
800
+ previousNamespace: 'solana'
801
+ },
802
+ SDK_SESSION,
803
+ NOW
804
+ )
805
+ expect(out.providerKind).toBe('integratedBrowser')
806
+ expect(out.usingIntegratedBrowser).toBe(true)
807
+ expect(out.injectedId).toHaveLength(64)
808
+ expect(out.lastSignedNamespace).toBe('solana')
809
+ expect(out.previousNamespace).toBe('solana')
810
+ })
700
811
  })
@@ -11,7 +11,12 @@ import type {
11
11
  LifecycleUnavailableReason,
12
12
  Session
13
13
  } from '@meshconnect/uwc-types'
14
- import type { UWCEventName, UWCEventMap, UWCOperation } from '../events'
14
+ import type {
15
+ UWCEventName,
16
+ UWCEventMap,
17
+ UWCOperation,
18
+ ProviderKind
19
+ } from '../events'
15
20
  import { UWC_CORE_VERSION } from '../version'
16
21
  import { scrubMessage } from './scrub'
17
22
 
@@ -50,6 +55,29 @@ function capNetworkMismatch(
50
55
  }
51
56
  }
52
57
 
58
+ /**
59
+ * Namespace-parking triage facets carried on every wallet-boundary handoff so a
60
+ * single event answers "which provider surface, in-app browser?, legacy Solana
61
+ * wallet?, and what namespace did we last sign on before this op?" — the inputs
62
+ * needed to spot the Coinbase/Base sequential Solana→EVM parking bug in RUM.
63
+ * `injectedId` is length-capped (catalog-metadata origin), the rest are enums/bools.
64
+ */
65
+ function parkingFacets(d: {
66
+ providerKind?: ProviderKind | undefined
67
+ usingIntegratedBrowser?: boolean | undefined
68
+ injectedId?: string | undefined
69
+ lastSignedNamespace?: Namespace | undefined
70
+ previousNamespace?: Namespace | undefined
71
+ }) {
72
+ return {
73
+ providerKind: d.providerKind,
74
+ usingIntegratedBrowser: d.usingIntegratedBrowser,
75
+ injectedId: d.injectedId?.slice(0, MAX_INJECTED_ID_LENGTH),
76
+ lastSignedNamespace: d.lastSignedNamespace,
77
+ previousNamespace: d.previousNamespace
78
+ }
79
+ }
80
+
53
81
  /**
54
82
  * A normalized, PII-safe telemetry record derived from a `UWCEventMap` event.
55
83
  *
@@ -193,17 +221,17 @@ export interface UWCTelemetryEvent {
193
221
  /** Handoff terminals, WalletConnect only: relay-socket drops seen while the handoff was pending. */
194
222
  relayDropCount?: number | undefined
195
223
  /**
196
- * `secondarySolanaConnectSkipped` only: the `solana.injectedId` metadata key
197
- * whose secondary gather the gate acted on (e.g. `coinbaseSolana`) — fires
198
- * framed or top-level in-app (`runtime` distinguishes). A consumer-configured
199
- * metadata key, never an address. Length-capped as defense-in-depth since
200
- * the value originates in catalog metadata.
224
+ * `secondarySolanaConnectSkipped` / `secondarySolanaConnectGathered`: the
225
+ * `solana.injectedId` metadata key the secondary gather acted on (e.g.
226
+ * `coinbaseSolana`). A consumer-configured metadata key, never an address.
227
+ * Length-capped as defense-in-depth since the value originates in catalog
228
+ * metadata.
201
229
  */
202
230
  injectedId?: string | undefined
203
231
  /**
204
- * `secondarySolanaConnectSkipped` only: true when the skipped wallet was
205
- * discovered via the legacy injected-Solana fallback (true "Legacy mode")
206
- * rather than the Wallet Standard registry.
232
+ * `secondarySolanaConnectSkipped` / `secondarySolanaConnectGathered`: true
233
+ * when the wallet was discovered via the legacy injected-Solana fallback
234
+ * (true "Legacy mode") rather than the Wallet Standard registry.
207
235
  */
208
236
  legacyInjected?: boolean | undefined
209
237
  /**
@@ -213,6 +241,36 @@ export interface UWCTelemetryEvent {
213
241
  * wallet-side state, not the gather.
214
242
  */
215
243
  gateOutcome?: 'connectSuppressed' | 'alreadyConnected' | undefined
244
+ /**
245
+ * `secondarySolanaConnectGathered` only (ONC-231): `existing` = the address
246
+ * surfaced via the passive/silent check; `connect` = a fresh connect was made.
247
+ */
248
+ gatherSource?: 'existing' | 'connect' | undefined
249
+ /**
250
+ * Handoff events: which provider surface the op used. Namespace parking
251
+ * (Coinbase/Base legacy Solana → EVM in-app) is observed only on
252
+ * `integratedBrowser`; riding it on every handoff separates that surface.
253
+ */
254
+ providerKind?: ProviderKind | undefined
255
+ /** Handoff events: the session runs inside the wallet's own in-app browser. */
256
+ usingIntegratedBrowser?: boolean | undefined
257
+ /**
258
+ * Handoff events: the last namespace actually signed/sent before this op — the
259
+ * parking proxy. A switch/send to `eip155` following `lastSignedNamespace:
260
+ * solana` on a legacy-injectedId wallet in an integrated browser is the
261
+ * fingerprint of the no-approval-sheet parking bug.
262
+ */
263
+ lastSignedNamespace?: Namespace | undefined
264
+ /** switchNetwork handoffs: the namespace active BEFORE the switch. */
265
+ previousNamespace?: Namespace | undefined
266
+ /** `namespaceReassert` only: lever attempt phase. */
267
+ reassertPhase?: 'fired' | 'succeeded' | 'failed' | undefined
268
+ /** `namespaceReassert` only: the wallet request used to re-assert the namespace. */
269
+ reassertLever?: 'eth_requestAccounts' | undefined
270
+ /** `namespaceReassert` only: the namespace the wallet was parked on (parking cause). */
271
+ fromNamespace?: Namespace | undefined
272
+ /** `namespaceReassert` only: the namespace being re-asserted (switch target). */
273
+ toNamespace?: Namespace | undefined
216
274
  }
217
275
 
218
276
  /**
@@ -405,7 +463,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
405
463
  handoffId: d.handoffId,
406
464
  walletFlowType: d.walletFlowType,
407
465
  platform: d.platform,
408
- targetAlreadyActive: d.targetAlreadyActive
466
+ targetAlreadyActive: d.targetAlreadyActive,
467
+ ...parkingFacets(d)
409
468
  }
410
469
  }
411
470
  case 'walletSucceeded': {
@@ -424,7 +483,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
424
483
  targetAlreadyActive: d.targetAlreadyActive,
425
484
  backgroundedMs: d.backgroundedMs,
426
485
  backgroundCount: d.backgroundCount,
427
- relayDropCount: d.relayDropCount
486
+ relayDropCount: d.relayDropCount,
487
+ ...parkingFacets(d)
428
488
  }
429
489
  }
430
490
  case 'walletRejected': {
@@ -449,7 +509,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
449
509
  : undefined,
450
510
  backgroundedMs: d.backgroundedMs,
451
511
  backgroundCount: d.backgroundCount,
452
- relayDropCount: d.relayDropCount
512
+ relayDropCount: d.relayDropCount,
513
+ ...parkingFacets(d)
453
514
  }
454
515
  }
455
516
  case 'walletTimedOut': {
@@ -467,7 +528,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
467
528
  targetAlreadyActive: d.targetAlreadyActive,
468
529
  backgroundedMs: d.backgroundedMs,
469
530
  backgroundCount: d.backgroundCount,
470
- relayDropCount: d.relayDropCount
531
+ relayDropCount: d.relayDropCount,
532
+ ...parkingFacets(d)
471
533
  }
472
534
  }
473
535
  case 'walletFailed': {
@@ -490,7 +552,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
490
552
  error: { type: d.error.type, message: scrubMessage(d.error.message) },
491
553
  backgroundedMs: d.backgroundedMs,
492
554
  backgroundCount: d.backgroundCount,
493
- relayDropCount: d.relayDropCount
555
+ relayDropCount: d.relayDropCount,
556
+ ...parkingFacets(d)
494
557
  }
495
558
  }
496
559
  case 'capabilitiesUpdated': {
@@ -539,6 +602,33 @@ function buildTelemetryEvent<K extends UWCEventName>(
539
602
  runtime: d.framed ? 'iframe' : 'direct'
540
603
  }
541
604
  }
605
+ case 'secondarySolanaConnectGathered': {
606
+ const d = data as UWCEventMap['secondarySolanaConnectGathered']
607
+ return {
608
+ ...base,
609
+ injectedId: d.injectedId.slice(0, MAX_INJECTED_ID_LENGTH),
610
+ legacyInjected: d.legacyInjected,
611
+ gatherSource: d.source,
612
+ runtime: d.framed ? 'iframe' : 'direct'
613
+ }
614
+ }
615
+ case 'namespaceReassert': {
616
+ const d = data as UWCEventMap['namespaceReassert']
617
+ return {
618
+ ...base,
619
+ reassertPhase: d.phase,
620
+ reassertLever: d.lever,
621
+ connectionMode: d.connectionMode,
622
+ walletId: d.walletId,
623
+ injectedId: d.injectedId?.slice(0, MAX_INJECTED_ID_LENGTH),
624
+ fromNamespace: d.fromNamespace,
625
+ toNamespace: d.toNamespace,
626
+ durationMs: d.durationMs,
627
+ error: d.error
628
+ ? { type: d.error.type, message: scrubMessage(d.error.message) }
629
+ : undefined
630
+ }
631
+ }
542
632
  case 'pageHiddenDuringHandoff':
543
633
  case 'pageVisibleDuringHandoff': {
544
634
  // Cast to the visible shape (superset). `hiddenMs` is absent on the hidden
@@ -11,6 +11,7 @@ import type {
11
11
  } from '@meshconnect/uwc-types'
12
12
  import type { SessionManager } from '../managers/session-manager'
13
13
  import type { EventManager } from '../managers/event-manager'
14
+ import type { ProviderKind } from '../events'
14
15
  import type { ServiceOptions } from './connection-service'
15
16
  import { instrumentHandoff } from '../observability/instrument-handoff'
16
17
 
@@ -97,6 +98,30 @@ export class NetworkSwitchService {
97
98
  )
98
99
  }
99
100
 
101
+ // Namespace-parking triage facets (rides the switch handoff). The parking bug
102
+ // (Coinbase/Base legacy Solana leaves the wallet's active namespace on Solana
103
+ // so the next EVM send shows no approval sheet) only occurs on the
104
+ // integratedBrowser surface for a wallet that declares a `solana.injectedId`.
105
+ const providerKind: ProviderKind =
106
+ session.connectionMode === 'walletConnect'
107
+ ? 'walletConnect'
108
+ : session.connectionMode === 'tonConnect'
109
+ ? 'tonConnect'
110
+ : this.usingIntegratedBrowser
111
+ ? 'integratedBrowser'
112
+ : 'extension'
113
+ const solanaInjectedId =
114
+ session.connectionMode === 'injected' ||
115
+ session.connectionMode === 'tonConnect'
116
+ ? (
117
+ provider as
118
+ | ExtensionInjectedProvider
119
+ | IntegratedBrowserInjectedProvider
120
+ ).namespaceMetaData?.solana?.injectedId
121
+ : undefined
122
+ // Captured BEFORE the switch mutates the session — the cross-namespace signal.
123
+ const previousNamespace = session.activeNetwork?.namespace
124
+
100
125
  // Get the active connector
101
126
  const activeConnector = this.connectors.get(session.connectionMode)
102
127
 
@@ -145,7 +170,11 @@ export class NetworkSwitchService {
145
170
  walletId: currentWallet.id,
146
171
  namespace: network.namespace,
147
172
  chainId: network.id,
148
- targetAlreadyActive
173
+ targetAlreadyActive,
174
+ providerKind,
175
+ usingIntegratedBrowser: this.usingIntegratedBrowser,
176
+ injectedId: solanaInjectedId,
177
+ previousNamespace
149
178
  },
150
179
  async () => {
151
180
  if (
@@ -172,6 +201,24 @@ export class NetworkSwitchService {
172
201
 
173
202
  options?.signal?.throwIfAborted()
174
203
 
204
+ // Surface the POC un-park lever (if the injected connector fired it) as a
205
+ // queryable telemetry event — the "did re-assert fix the parking?" signal.
206
+ if (result.reassert?.fired) {
207
+ this.eventManager.emit('namespaceReassert', {
208
+ phase: result.reassert.ok ? 'succeeded' : 'failed',
209
+ lever: result.reassert.lever,
210
+ connectionMode: session.connectionMode,
211
+ walletId: currentWallet.id,
212
+ injectedId: solanaInjectedId,
213
+ fromNamespace: previousNamespace,
214
+ toNamespace: network.namespace,
215
+ durationMs: result.reassert.durationMs,
216
+ ...(result.reassert.error && {
217
+ error: { type: 'unknown' as const, message: result.reassert.error }
218
+ })
219
+ })
220
+ }
221
+
175
222
  // Filter available addresses to only include those for configured networks
176
223
  const filteredAvailableAddresses = result.availableAddresses
177
224
  ? result.availableAddresses.filter(addr =>
@@ -71,13 +71,21 @@ export class SignatureService {
71
71
  timeoutMs: this.walletResponseTimeoutMs,
72
72
  signal: options?.signal
73
73
  },
74
- sessionHandoffContext('signMessage', session),
74
+ sessionHandoffContext(
75
+ 'signMessage',
76
+ session,
77
+ this.usingIntegratedBrowser
78
+ ),
75
79
  // tonConnect resolves undefined and keeps its message-only call shape.
76
80
  () =>
77
81
  provider === undefined
78
82
  ? connector.signMessage!(message)
79
83
  : connector.signMessage!(message, provider)
80
84
  )
85
+ // Record the namespace just signed — the parking proxy for the next op.
86
+ this.sessionManager.updateSession({
87
+ lastSignedNamespace: session.activeNetwork?.namespace ?? null
88
+ })
81
89
  // AFTER the funnel: the wallet settled, so its truthful terminal is already
82
90
  // recorded; an abort that raced completion still rejects the caller here.
83
91
  options?.signal?.throwIfAborted()
@@ -112,12 +120,16 @@ export class SignatureService {
112
120
  provider = resolveSessionProvider(session, this.usingIntegratedBrowser)
113
121
  }
114
122
 
115
- return await instrumentHandoff(
123
+ const signature = await instrumentHandoff(
116
124
  {
117
125
  eventManager: this.eventManager,
118
126
  timeoutMs: this.walletResponseTimeoutMs
119
127
  },
120
- sessionHandoffContext('signTypedData', session),
128
+ sessionHandoffContext(
129
+ 'signTypedData',
130
+ session,
131
+ this.usingIntegratedBrowser
132
+ ),
121
133
  () =>
122
134
  session.connectionMode === 'injected'
123
135
  ? connector.signTypedData!(
@@ -128,5 +140,8 @@ export class SignatureService {
128
140
  )
129
141
  : connector.signTypedData!(typedData)
130
142
  )
143
+ // signTypedData is EVM-only — records eip155 as the last-signed namespace.
144
+ this.sessionManager.updateSession({ lastSignedNamespace: 'eip155' })
145
+ return signature
131
146
  }
132
147
  }
@@ -90,13 +90,22 @@ export class TransactionService {
90
90
  timeoutMs: this.walletResponseTimeoutMs,
91
91
  signal: options?.signal
92
92
  },
93
- sessionHandoffContext('sendTransaction', session),
93
+ sessionHandoffContext(
94
+ 'sendTransaction',
95
+ session,
96
+ this.usingIntegratedBrowser
97
+ ),
94
98
  // tonConnect resolves undefined and keeps its request-only call shape.
95
99
  () =>
96
100
  provider === undefined
97
101
  ? connector.sendTransaction!(request)
98
102
  : connector.sendTransaction!(request, provider)
99
103
  )
104
+ // Record the namespace just signed — the parking proxy the NEXT op's handoff
105
+ // context reads (e.g. a following EVM switch/send after a Solana transfer).
106
+ this.sessionManager.updateSession({
107
+ lastSignedNamespace: session.activeNetwork?.namespace ?? null
108
+ })
100
109
  // AFTER the funnel: the tx already broadcast, so its truthful terminal is
101
110
  // recorded; an abort that raced completion still rejects the caller here.
102
111
  options?.signal?.throwIfAborted()
@@ -122,13 +131,20 @@ export class TransactionService {
122
131
  )
123
132
  }
124
133
 
125
- return await instrumentHandoff(
134
+ const signature = await instrumentHandoff(
126
135
  {
127
136
  eventManager: this.eventManager,
128
137
  timeoutMs: this.walletResponseTimeoutMs
129
138
  },
130
- sessionHandoffContext('signSolanaTransaction', session),
139
+ sessionHandoffContext(
140
+ 'signSolanaTransaction',
141
+ session,
142
+ this.usingIntegratedBrowser
143
+ ),
131
144
  () => connector.signSolanaTransactionBytes!(serializedTx)
132
145
  )
146
+ // Sign-only on Solana still parks the wallet's active namespace on Solana.
147
+ this.sessionManager.updateSession({ lastSignedNamespace: 'solana' })
148
+ return signature
133
149
  }
134
150
  }
@@ -317,6 +317,10 @@ export class UniversalWalletConnector {
317
317
  // Legacy-mode hang).
318
318
  onSecondarySolanaConnectSkipped: info =>
319
319
  this.eventManager.emit('secondarySolanaConnectSkipped', info),
320
+ // Positive counterpart (ONC-231): a legacy-`injectedId` wallet's secondary
321
+ // Solana address WAS gathered — confirms the framed gather works.
322
+ onSecondarySolanaConnectGathered: info =>
323
+ this.eventManager.emit('secondarySolanaConnectGathered', info),
320
324
  // The wallet-switch guard released a different wallet's live state —
321
325
  // surface it so the otherwise-silent release is observable in prod.
322
326
  onWalletConnectionsReleased: info =>
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs — do not edit.
2
- export const UWC_CORE_VERSION = '1.9.0'
2
+ export const UWC_CORE_VERSION = '1.10.0'