@meshconnect/uwc-core 1.0.0 → 1.0.1-snapshot.c7e65ce

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 (76) hide show
  1. package/README.md +211 -66
  2. package/dist/events.d.ts +69 -2
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +40 -1
  5. package/dist/events.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger/create-logger.d.ts +32 -0
  11. package/dist/logger/create-logger.d.ts.map +1 -0
  12. package/dist/logger/create-logger.js +69 -0
  13. package/dist/logger/create-logger.js.map +1 -0
  14. package/dist/managers/event-manager.d.ts.map +1 -1
  15. package/dist/managers/event-manager.js +5 -1
  16. package/dist/managers/event-manager.js.map +1 -1
  17. package/dist/observability/connect-observer.d.ts +22 -0
  18. package/dist/observability/connect-observer.d.ts.map +1 -0
  19. package/dist/observability/connect-observer.js +60 -0
  20. package/dist/observability/connect-observer.js.map +1 -0
  21. package/dist/observability/instrument-handoff.d.ts +39 -0
  22. package/dist/observability/instrument-handoff.d.ts.map +1 -0
  23. package/dist/observability/instrument-handoff.js +86 -0
  24. package/dist/observability/instrument-handoff.js.map +1 -0
  25. package/dist/observability/scrub.d.ts +30 -0
  26. package/dist/observability/scrub.d.ts.map +1 -0
  27. package/dist/observability/scrub.js +135 -0
  28. package/dist/observability/scrub.js.map +1 -0
  29. package/dist/observability/telemetry.d.ts +77 -0
  30. package/dist/observability/telemetry.d.ts.map +1 -0
  31. package/dist/observability/telemetry.js +138 -0
  32. package/dist/observability/telemetry.js.map +1 -0
  33. package/dist/services/connection-service.d.ts +2 -1
  34. package/dist/services/connection-service.d.ts.map +1 -1
  35. package/dist/services/connection-service.js +35 -15
  36. package/dist/services/connection-service.js.map +1 -1
  37. package/dist/services/network-switch-service.d.ts +2 -1
  38. package/dist/services/network-switch-service.d.ts.map +1 -1
  39. package/dist/services/network-switch-service.js +33 -15
  40. package/dist/services/network-switch-service.js.map +1 -1
  41. package/dist/universal-wallet-connector.d.ts +51 -2
  42. package/dist/universal-wallet-connector.d.ts.map +1 -1
  43. package/dist/universal-wallet-connector.js +163 -61
  44. package/dist/universal-wallet-connector.js.map +1 -1
  45. package/dist/utils/id.d.ts +2 -0
  46. package/dist/utils/id.d.ts.map +1 -0
  47. package/dist/utils/id.js +41 -0
  48. package/dist/utils/id.js.map +1 -0
  49. package/dist/utils/to-wallet-error.d.ts +16 -0
  50. package/dist/utils/to-wallet-error.d.ts.map +1 -0
  51. package/dist/utils/to-wallet-error.js +33 -0
  52. package/dist/utils/to-wallet-error.js.map +1 -0
  53. package/package.json +5 -5
  54. package/src/events.ts +114 -1
  55. package/src/index.ts +6 -0
  56. package/src/logger/create-logger.test.ts +111 -0
  57. package/src/logger/create-logger.ts +101 -0
  58. package/src/managers/event-manager.test.ts +25 -0
  59. package/src/managers/event-manager.ts +5 -1
  60. package/src/observability/connect-observer.test.ts +100 -0
  61. package/src/observability/connect-observer.ts +71 -0
  62. package/src/observability/instrument-handoff.test.ts +150 -0
  63. package/src/observability/instrument-handoff.ts +122 -0
  64. package/src/observability/scrub.test.ts +109 -0
  65. package/src/observability/scrub.ts +142 -0
  66. package/src/observability/telemetry.test.ts +134 -0
  67. package/src/observability/telemetry.ts +211 -0
  68. package/src/services/connection-service.test.ts +66 -1
  69. package/src/services/connection-service.ts +54 -32
  70. package/src/services/network-switch-service.test.ts +51 -1
  71. package/src/services/network-switch-service.ts +43 -23
  72. package/src/universal-wallet-connector.observability.test.ts +265 -0
  73. package/src/universal-wallet-connector.ts +244 -71
  74. package/src/utils/id.test.ts +15 -0
  75. package/src/utils/id.ts +48 -0
  76. package/src/utils/to-wallet-error.ts +36 -0
@@ -18,9 +18,10 @@ import type {
18
18
  EVMCapabilities,
19
19
  SignatureType,
20
20
  EIP712TypedData,
21
- WalletError
21
+ LogLevel,
22
+ Logger,
23
+ Session
22
24
  } from '@meshconnect/uwc-types'
23
- import { WalletConnectorError } from '@meshconnect/uwc-types'
24
25
  import { InjectedConnector } from '@meshconnect/uwc-injected-connector'
25
26
  import { WalletConnectConnector } from '@meshconnect/uwc-wallet-connect-connector'
26
27
  import { TonConnectConnector } from '@meshconnect/uwc-ton-connector'
@@ -33,8 +34,20 @@ import { SignatureService } from './services/signature-service'
33
34
  import { TransactionService } from './services/transaction-service'
34
35
  import { WalletCapabilitiesService } from './services/wallet-capabilities-service'
35
36
  import { createNetworkRpcMap } from './utils/network-rpc-utils'
37
+ import { generateId } from './utils/id'
38
+ import { toWalletError, isAbortError } from './utils/to-wallet-error'
39
+ import { createLogger, type ManagedLogger } from './logger/create-logger'
40
+ import { connectObserver } from './observability/connect-observer'
41
+ import type { UWCObserver } from './observability/telemetry'
42
+ import {
43
+ instrumentHandoff,
44
+ type HandoffContext
45
+ } from './observability/instrument-handoff'
36
46
  import type { UWCEventName, UWCEventListener, UWCOperation } from './events'
37
47
 
48
+ /** Default observational wallet-response deadline (ms). Matches TON's 60s. */
49
+ const DEFAULT_WALLET_RESPONSE_TIMEOUT_MS = 60_000
50
+
38
51
  /**
39
52
  * Configuration object form of the UniversalWalletConnector constructor.
40
53
  * Prefer this over the positional constructor in new code.
@@ -49,6 +62,35 @@ export interface UWCConfig {
49
62
  walletConnectConfig?: WalletConnectConfig
50
63
  tonConnectConfig?: TonConnectConfig
51
64
  tronConnectorConfig?: TronConnectorConfig
65
+ // ---- Observability (all optional; defaults preserve current behaviour) -----
66
+ /**
67
+ * Vendor-agnostic telemetry sink. When omitted, UWC emits NOTHING externally —
68
+ * zero egress is the security-first default for a sign-only wallet layer. The
69
+ * consumer implements the sink and wires it to their backend (Datadog/…).
70
+ */
71
+ observer?: UWCObserver
72
+ /**
73
+ * Redirect UWC's internal logging into your own logger. Receives raw
74
+ * `(message, ...args)` at/above `logLevel`. When omitted, a console-backed
75
+ * logger is used.
76
+ */
77
+ logger?: Logger
78
+ /** Console threshold for the default logger. Default `'warn'`. */
79
+ logLevel?: LogLevel
80
+ /**
81
+ * Correlation id stamped on every telemetry record (`sdkSessionId`). Supply
82
+ * your own (e.g. the consumer's `link_sess_id`) so UWC events join your existing
83
+ * telemetry in Datadog/RUM without a separate mapping. Pass a getter if the id
84
+ * isn't known at construction or can change. Defaults to a generated per-instance
85
+ * id. Resolved per-event; a throwing/empty getter falls back to the default.
86
+ */
87
+ correlationId?: string | (() => string)
88
+ /**
89
+ * Observational wallet-response deadline (ms) for the `walletTimedOut` marker.
90
+ * `0` disables it. TON is always exempt (it bounds its own response). Default
91
+ * 60_000.
92
+ */
93
+ walletResponseTimeoutMs?: number
52
94
  }
53
95
 
54
96
  /** Common options for user-initiated async operations. */
@@ -75,6 +117,12 @@ export class UniversalWalletConnector {
75
117
  private usingIntegratedBrowser: boolean
76
118
  private detectionComplete = false
77
119
  private networkRpcMap: NetworkRpcMap
120
+ private logger: ManagedLogger
121
+ /** Per-instance correlation id stamped on every telemetry record. */
122
+ private readonly sdkSessionId: string = generateId()
123
+ private walletResponseTimeoutMs: number
124
+ /** Tears down the observer subscriptions; set only when an observer is wired. */
125
+ private observerTeardown: (() => void) | undefined
78
126
 
79
127
  /**
80
128
  * Recommended entry point: returns the shared instance, creating it on the
@@ -152,14 +200,49 @@ export class UniversalWalletConnector {
152
200
  tronConnectorConfig: configuredTronConnectorConfig
153
201
  } = config
154
202
 
203
+ // Observability seam — wire the event bus, logger, and (optional) observer
204
+ // FIRST so even construction-time logs/events are captured. `sdkSessionId` is
205
+ // already assigned as a field initializer.
206
+ this.eventManager = new EventManager()
207
+ this.walletResponseTimeoutMs =
208
+ config.walletResponseTimeoutMs ?? DEFAULT_WALLET_RESPONSE_TIMEOUT_MS
209
+ this.logger = createLogger({
210
+ level: config.logLevel ?? 'warn',
211
+ sink: config.logger,
212
+ // Pre-gate tap → typed `log` event → observer.onLog. Fires for all levels
213
+ // regardless of the console threshold.
214
+ onLog: (level, message, args) =>
215
+ this.eventManager.emit('log', { level, message, args })
216
+ })
217
+ if (config.observer) {
218
+ // Resolve the correlation id per-event. A consumer can align it to their
219
+ // own session key (e.g. link_sess_id); the generated `sdkSessionId` is the
220
+ // always-available fallback if none is supplied or a getter misbehaves.
221
+ const cid = config.correlationId
222
+ const getCorrelationId: () => string =
223
+ typeof cid === 'function'
224
+ ? () => {
225
+ try {
226
+ return cid() || this.sdkSessionId
227
+ } catch {
228
+ return this.sdkSessionId
229
+ }
230
+ }
231
+ : () => cid ?? this.sdkSessionId
232
+ this.observerTeardown = connectObserver(
233
+ this.eventManager,
234
+ config.observer,
235
+ getCorrelationId
236
+ )
237
+ }
238
+
155
239
  // Track instance creation and warn on duplicates
156
240
  UniversalWalletConnector.instanceCount++
157
241
  if (
158
242
  UniversalWalletConnector.instanceCount > 1 &&
159
243
  !UniversalWalletConnector.hasWarnedAboutMultipleInstances
160
244
  ) {
161
- // eslint-disable-next-line no-console
162
- console.error(
245
+ this.logger.error(
163
246
  `⚠️ WARNING: Multiple instances of UniversalWalletConnector detected (${UniversalWalletConnector.instanceCount} instances). ` +
164
247
  'This can lead to state inconsistencies and unexpected behavior. ' +
165
248
  'Please ensure you create only one instance and reuse it throughout your application. ' +
@@ -220,21 +303,22 @@ export class UniversalWalletConnector {
220
303
  }
221
304
 
222
305
  this.sessionManager = new SessionManager()
223
- this.eventManager = new EventManager()
224
306
  this.connectionService = new ConnectionService(
225
307
  networks,
226
308
  this.wallets,
227
309
  this.sessionManager,
228
310
  this.connectors,
229
311
  configuredUsingIntegratedBrowser,
230
- this.eventManager
312
+ this.eventManager,
313
+ this.walletResponseTimeoutMs
231
314
  )
232
315
  this.networkSwitchService = new NetworkSwitchService(
233
316
  networks,
234
317
  this.sessionManager,
235
318
  this.connectors,
236
319
  configuredUsingIntegratedBrowser,
237
- this.eventManager
320
+ this.eventManager,
321
+ this.walletResponseTimeoutMs
238
322
  )
239
323
  this.signatureService = new SignatureService(
240
324
  this.sessionManager,
@@ -518,6 +602,9 @@ export class UniversalWalletConnector {
518
602
  options?: OperationOptions
519
603
  ): Promise<void> {
520
604
  try {
605
+ // Hand-off instrumentation lives in ConnectionService.connect (emitted
606
+ // after validation, at the real wallet boundary) — not here, so pre-wallet
607
+ // validation failures don't pollute the funnel.
521
608
  await this.connectionService.connect(
522
609
  connectionMode,
523
610
  walletId,
@@ -548,6 +635,8 @@ export class UniversalWalletConnector {
548
635
  options?: OperationOptions
549
636
  ): Promise<void> {
550
637
  try {
638
+ // Hand-off instrumentation lives in NetworkSwitchService.switchNetwork
639
+ // (emitted after validation, at the real wallet boundary).
551
640
  await this.networkSwitchService.switchNetwork(networkId, options)
552
641
  } catch (error) {
553
642
  this.emitError(error, 'switchNetwork')
@@ -580,27 +669,36 @@ export class UniversalWalletConnector {
580
669
  }
581
670
 
582
671
  try {
583
- // Get the provider from the active wallet based on connection mode
584
- let provider
585
- if (session.connectionMode === 'injected') {
586
- provider = this.usingIntegratedBrowser
587
- ? session.activeWallet.integratedBrowserInjectedProvider
588
- : session.activeWallet.extensionInjectedProvider
589
- } else if (session.connectionMode === 'walletConnect') {
590
- provider = session.activeWallet.walletConnectProvider
591
- } else if (session.connectionMode === 'tonConnect') {
592
- return await this.signatureService.signMessage(
593
- message,
594
- undefined,
595
- options
596
- )
597
- }
672
+ return await this.instrument(
673
+ this.sessionHandoffContext('signMessage', session),
674
+ async () => {
675
+ // Get the provider from the active wallet based on connection mode
676
+ let provider
677
+ if (session.connectionMode === 'injected') {
678
+ provider = this.usingIntegratedBrowser
679
+ ? session.activeWallet!.integratedBrowserInjectedProvider
680
+ : session.activeWallet!.extensionInjectedProvider
681
+ } else if (session.connectionMode === 'walletConnect') {
682
+ provider = session.activeWallet!.walletConnectProvider
683
+ } else if (session.connectionMode === 'tonConnect') {
684
+ return await this.signatureService.signMessage(
685
+ message,
686
+ undefined,
687
+ options
688
+ )
689
+ }
598
690
 
599
- if (!provider) {
600
- throw new Error('No provider available for the active wallet')
601
- }
691
+ if (!provider) {
692
+ throw new Error('No provider available for the active wallet')
693
+ }
602
694
 
603
- return await this.signatureService.signMessage(message, provider, options)
695
+ return await this.signatureService.signMessage(
696
+ message,
697
+ provider,
698
+ options
699
+ )
700
+ }
701
+ )
604
702
  } catch (error) {
605
703
  this.emitError(error, 'signMessage')
606
704
  throw error
@@ -630,20 +728,30 @@ export class UniversalWalletConnector {
630
728
  throw new Error('signTypedData is not supported for TON wallets')
631
729
  }
632
730
 
633
- // Resolve the injected provider from the active wallet. WalletConnect
634
- // resolves its own provider inside the connector, so it stays undefined here.
635
- let provider
636
- if (session.connectionMode === 'injected') {
637
- provider = this.usingIntegratedBrowser
638
- ? session.activeWallet.integratedBrowserInjectedProvider
639
- : session.activeWallet.extensionInjectedProvider
731
+ try {
732
+ return await this.instrument(
733
+ this.sessionHandoffContext('signTypedData', session),
734
+ async () => {
735
+ // Resolve the injected provider from the active wallet. WalletConnect
736
+ // resolves its own provider inside the connector, so it stays undefined.
737
+ let provider
738
+ if (session.connectionMode === 'injected') {
739
+ provider = this.usingIntegratedBrowser
740
+ ? session.activeWallet!.integratedBrowserInjectedProvider
741
+ : session.activeWallet!.extensionInjectedProvider
742
+
743
+ if (!provider) {
744
+ throw new Error('No provider available for the active wallet')
745
+ }
746
+ }
640
747
 
641
- if (!provider) {
642
- throw new Error('No provider available for the active wallet')
643
- }
748
+ return await this.signatureService.signTypedData(typedData, provider)
749
+ }
750
+ )
751
+ } catch (error) {
752
+ this.emitError(error, 'signTypedData')
753
+ throw error
644
754
  }
645
-
646
- return await this.signatureService.signTypedData(typedData, provider)
647
755
  }
648
756
 
649
757
  /**
@@ -667,30 +775,35 @@ export class UniversalWalletConnector {
667
775
  }
668
776
 
669
777
  try {
670
- // Get the provider from the active wallet based on connection mode
671
- let provider
672
- if (session.connectionMode === 'injected') {
673
- provider = this.usingIntegratedBrowser
674
- ? session.activeWallet.integratedBrowserInjectedProvider
675
- : session.activeWallet.extensionInjectedProvider
676
- } else if (session.connectionMode === 'walletConnect') {
677
- provider = session.activeWallet.walletConnectProvider
678
- } else if (session.connectionMode === 'tonConnect') {
679
- return await this.transactionService.sendTransaction(
680
- request,
681
- undefined,
682
- options
683
- )
684
- }
778
+ return await this.instrument(
779
+ this.sessionHandoffContext('sendTransaction', session),
780
+ async () => {
781
+ // Get the provider from the active wallet based on connection mode
782
+ let provider
783
+ if (session.connectionMode === 'injected') {
784
+ provider = this.usingIntegratedBrowser
785
+ ? session.activeWallet!.integratedBrowserInjectedProvider
786
+ : session.activeWallet!.extensionInjectedProvider
787
+ } else if (session.connectionMode === 'walletConnect') {
788
+ provider = session.activeWallet!.walletConnectProvider
789
+ } else if (session.connectionMode === 'tonConnect') {
790
+ return await this.transactionService.sendTransaction(
791
+ request,
792
+ undefined,
793
+ options
794
+ )
795
+ }
685
796
 
686
- if (!provider) {
687
- throw new Error('No provider available for the active wallet')
688
- }
797
+ if (!provider) {
798
+ throw new Error('No provider available for the active wallet')
799
+ }
689
800
 
690
- return await this.transactionService.sendTransaction(
691
- request,
692
- provider,
693
- options
801
+ return await this.transactionService.sendTransaction(
802
+ request,
803
+ provider,
804
+ options
805
+ )
806
+ }
694
807
  )
695
808
  } catch (error) {
696
809
  this.emitError(error, 'sendTransaction')
@@ -722,7 +835,15 @@ export class UniversalWalletConnector {
722
835
  )
723
836
  }
724
837
 
725
- return await connector.signSolanaTransactionBytes(serializedTx)
838
+ try {
839
+ return await this.instrument(
840
+ this.sessionHandoffContext('signSolanaTransaction', session),
841
+ () => connector.signSolanaTransactionBytes!(serializedTx)
842
+ )
843
+ } catch (error) {
844
+ this.emitError(error, 'signSolanaTransaction')
845
+ throw error
846
+ }
726
847
  }
727
848
 
728
849
  /**
@@ -809,22 +930,74 @@ export class UniversalWalletConnector {
809
930
  return session.activeWalletCapabilities
810
931
  }
811
932
 
933
+ /**
934
+ * Set the console log threshold at runtime (e.g. from a support toggle). The
935
+ * global `window.UWC_DEBUG` override still takes precedence for ad-hoc triage.
936
+ */
937
+ setLogLevel(level: LogLevel): void {
938
+ this.logger.setLevel(level)
939
+ }
940
+
941
+ /**
942
+ * Release observer subscriptions and clear all event listeners. Call on full
943
+ * teardown / logout to avoid leaking listeners across connector lifecycles.
944
+ */
945
+ dispose(): void {
946
+ this.observerTeardown?.()
947
+ this.observerTeardown = undefined
948
+ this.eventManager.clearAllListeners()
949
+ // Full teardown: if this was the shared singleton, clear the static refs too
950
+ // so a later getInstance()/new starts clean — otherwise getInstance() would
951
+ // hand back this now-listener-less instance, and the instance-count warning
952
+ // would mis-fire on the next construction.
953
+ if (UniversalWalletConnector.instance === this) {
954
+ UniversalWalletConnector.resetInstance()
955
+ }
956
+ }
957
+
812
958
  // ---- private helpers ----
813
959
 
960
+ /** Instrument a wallet-boundary op (emits awaitingWallet + a terminal). */
961
+ private instrument<T>(
962
+ context: HandoffContext,
963
+ fn: () => Promise<T>
964
+ ): Promise<T> {
965
+ return instrumentHandoff(
966
+ {
967
+ eventManager: this.eventManager,
968
+ timeoutMs: this.walletResponseTimeoutMs
969
+ },
970
+ context,
971
+ fn
972
+ )
973
+ }
974
+
975
+ /** Build handoff routing facets from the active session (no address). */
976
+ private sessionHandoffContext(
977
+ operation: UWCOperation,
978
+ session: Session
979
+ ): HandoffContext {
980
+ return {
981
+ operation,
982
+ connectionMode: session.connectionMode ?? 'injected',
983
+ walletId: session.activeWallet?.id,
984
+ namespace: session.activeNetwork?.namespace,
985
+ chainId: session.activeNetwork?.id ?? undefined
986
+ }
987
+ }
988
+
814
989
  private emitError(error: unknown, operation: UWCOperation): void {
815
990
  // Intentional cancellations (AbortSignal.throwIfAborted, fetch abort,
816
991
  // DOMException) should NOT surface as wallet errors — the caller already
817
- // knows it asked to abort. Skip emission so error telemetry stays clean.
818
- if (error instanceof Error && error.name === 'AbortError') {
992
+ // knows it asked to abort. Uses the same name-based check as
993
+ // `instrumentHandoff` so a DOMException abort (not an `Error` subclass in
994
+ // every runtime) is skipped consistently on both paths.
995
+ if (isAbortError(error)) {
819
996
  return
820
997
  }
821
- const walletError: WalletError =
822
- error instanceof WalletConnectorError
823
- ? { type: error.type, message: error.message }
824
- : {
825
- type: 'unknown',
826
- message: error instanceof Error ? error.message : String(error)
827
- }
828
- this.eventManager.emit('error', { error: walletError, operation })
998
+ this.eventManager.emit('error', {
999
+ error: toWalletError(error),
1000
+ operation
1001
+ })
829
1002
  }
830
1003
  }
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { generateId } from './id'
3
+
4
+ describe('generateId', () => {
5
+ it('produces unique values', () => {
6
+ const ids = new Set(Array.from({ length: 1000 }, () => generateId()))
7
+ expect(ids.size).toBe(1000)
8
+ })
9
+
10
+ it('produces a non-empty string', () => {
11
+ const id = generateId()
12
+ expect(typeof id).toBe('string')
13
+ expect(id.length).toBeGreaterThan(0)
14
+ })
15
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Generate a correlation id (RFC-4122 v4 when available).
3
+ *
4
+ * Used for the per-instance `sdkSessionId` and per-handoff `handoffId` — both are
5
+ * opaque correlation tokens, never derived from wallet data (no address, no key).
6
+ * Prefers the native `crypto.randomUUID()` (browsers + Node ≥ 16); falls back to a
7
+ * `crypto.getRandomValues`-backed v4, and finally to a timestamp + monotonic
8
+ * counter only in environments that expose neither. Correlation ids need to be
9
+ * collision-resistant enough to pair a start event with its terminal — NOT
10
+ * cryptographically unpredictable — so the last-resort branch uses no PRNG.
11
+ */
12
+ // Monotonic counter for the no-crypto fallback: guarantees per-process uniqueness
13
+ // without any pseudo-random generator (correlation ids need uniqueness, not
14
+ // unpredictability — and a counter is strictly more collision-resistant here).
15
+ let fallbackCounter = 0
16
+
17
+ export function generateId(): string {
18
+ const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined
19
+
20
+ if (c?.randomUUID) {
21
+ return c.randomUUID()
22
+ }
23
+
24
+ if (c?.getRandomValues) {
25
+ const bytes = c.getRandomValues(new Uint8Array(16))
26
+ // Set the version (4) and variant (10xx) bits per RFC 4122 §4.4.
27
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40
28
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80
29
+ const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'))
30
+ return (
31
+ hex.slice(0, 4).join('') +
32
+ '-' +
33
+ hex.slice(4, 6).join('') +
34
+ '-' +
35
+ hex.slice(6, 8).join('') +
36
+ '-' +
37
+ hex.slice(8, 10).join('') +
38
+ '-' +
39
+ hex.slice(10, 16).join('')
40
+ )
41
+ }
42
+
43
+ // Last-resort fallback (no Web Crypto). Timestamp + monotonic counter keeps
44
+ // ids unique within the process without a PRNG. Sufficient for correlation.
45
+ return (
46
+ 'uwc-' + Date.now().toString(36) + '-' + (fallbackCounter++).toString(36)
47
+ )
48
+ }
@@ -0,0 +1,36 @@
1
+ import { WalletConnectorError, type WalletError } from '@meshconnect/uwc-types'
2
+
3
+ /** Normalize any thrown value into the PII-light `WalletError` shape. */
4
+ export function toWalletError(error: unknown): WalletError {
5
+ if (error instanceof WalletConnectorError) {
6
+ return { type: error.type, message: error.message }
7
+ }
8
+ return {
9
+ type: 'unknown',
10
+ message: error instanceof Error ? error.message : String(error)
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Whether a thrown value is an explicit user rejection. Connectors usually wrap
16
+ * these as `WalletConnectorError{type:'rejected'}`, but a raw EIP-1193 reject
17
+ * (code `4001`) or ethers' `ACTION_REJECTED` can also reach the façade.
18
+ */
19
+ export function isUserRejection(error: unknown): boolean {
20
+ if (error instanceof WalletConnectorError) return error.type === 'rejected'
21
+ const code = (error as { code?: unknown } | null | undefined)?.code
22
+ return code === 4001 || code === 'ACTION_REJECTED'
23
+ }
24
+
25
+ /**
26
+ * Whether a thrown value is an intentional cancellation (AbortSignal / fetch
27
+ * abort). Matched by `name` rather than `instanceof Error` because a real abort is
28
+ * a `DOMException`, which is not an `Error` subclass in every runtime.
29
+ */
30
+ export function isAbortError(error: unknown): boolean {
31
+ return (
32
+ typeof error === 'object' &&
33
+ error !== null &&
34
+ (error as { name?: unknown }).name === 'AbortError'
35
+ )
36
+ }