@meshconnect/uwc-core 1.0.3 → 1.1.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 (121) hide show
  1. package/README.md +247 -76
  2. package/dist/events.d.ts +241 -5
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +77 -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 +2 -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 +79 -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 +33 -0
  18. package/dist/observability/connect-observer.d.ts.map +1 -0
  19. package/dist/observability/connect-observer.js +115 -0
  20. package/dist/observability/connect-observer.js.map +1 -0
  21. package/dist/observability/focus-tracker.d.ts +42 -0
  22. package/dist/observability/focus-tracker.d.ts.map +1 -0
  23. package/dist/observability/focus-tracker.js +57 -0
  24. package/dist/observability/focus-tracker.js.map +1 -0
  25. package/dist/observability/instrument-handoff.d.ts +54 -0
  26. package/dist/observability/instrument-handoff.d.ts.map +1 -0
  27. package/dist/observability/instrument-handoff.js +173 -0
  28. package/dist/observability/instrument-handoff.js.map +1 -0
  29. package/dist/observability/lifecycle-bridge.d.ts +69 -0
  30. package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
  31. package/dist/observability/lifecycle-bridge.js +155 -0
  32. package/dist/observability/lifecycle-bridge.js.map +1 -0
  33. package/dist/observability/scrub.d.ts +31 -0
  34. package/dist/observability/scrub.d.ts.map +1 -0
  35. package/dist/observability/scrub.js +167 -0
  36. package/dist/observability/scrub.js.map +1 -0
  37. package/dist/observability/telemetry.d.ts +157 -0
  38. package/dist/observability/telemetry.d.ts.map +1 -0
  39. package/dist/observability/telemetry.js +373 -0
  40. package/dist/observability/telemetry.js.map +1 -0
  41. package/dist/services/connection-service.d.ts +23 -1
  42. package/dist/services/connection-service.d.ts.map +1 -1
  43. package/dist/services/connection-service.js +128 -19
  44. package/dist/services/connection-service.js.map +1 -1
  45. package/dist/services/network-switch-service.d.ts +2 -1
  46. package/dist/services/network-switch-service.d.ts.map +1 -1
  47. package/dist/services/network-switch-service.js +45 -16
  48. package/dist/services/network-switch-service.js.map +1 -1
  49. package/dist/services/signature-service.d.ts +14 -7
  50. package/dist/services/signature-service.d.ts.map +1 -1
  51. package/dist/services/signature-service.js +56 -58
  52. package/dist/services/signature-service.js.map +1 -1
  53. package/dist/services/transaction-service.d.ts +19 -5
  54. package/dist/services/transaction-service.d.ts.map +1 -1
  55. package/dist/services/transaction-service.js +55 -38
  56. package/dist/services/transaction-service.js.map +1 -1
  57. package/dist/universal-wallet-connector.d.ts +56 -1
  58. package/dist/universal-wallet-connector.d.ts.map +1 -1
  59. package/dist/universal-wallet-connector.js +207 -110
  60. package/dist/universal-wallet-connector.js.map +1 -1
  61. package/dist/utils/id.d.ts +2 -0
  62. package/dist/utils/id.d.ts.map +1 -0
  63. package/dist/utils/id.js +41 -0
  64. package/dist/utils/id.js.map +1 -0
  65. package/dist/utils/resolve-session-provider.d.ts +10 -0
  66. package/dist/utils/resolve-session-provider.d.ts.map +1 -0
  67. package/dist/utils/resolve-session-provider.js +21 -0
  68. package/dist/utils/resolve-session-provider.js.map +1 -0
  69. package/dist/utils/to-wallet-error.d.ts +35 -0
  70. package/dist/utils/to-wallet-error.d.ts.map +1 -0
  71. package/dist/utils/to-wallet-error.js +130 -0
  72. package/dist/utils/to-wallet-error.js.map +1 -0
  73. package/dist/utils/validate-wallet-session.d.ts +15 -0
  74. package/dist/utils/validate-wallet-session.d.ts.map +1 -0
  75. package/dist/utils/validate-wallet-session.js +29 -0
  76. package/dist/utils/validate-wallet-session.js.map +1 -0
  77. package/dist/version.d.ts +2 -0
  78. package/dist/version.d.ts.map +1 -0
  79. package/dist/version.js +3 -0
  80. package/dist/version.js.map +1 -0
  81. package/package.json +6 -6
  82. package/src/events.ts +326 -14
  83. package/src/index.ts +10 -0
  84. package/src/logger/create-logger.test.ts +126 -0
  85. package/src/logger/create-logger.ts +112 -0
  86. package/src/managers/event-manager.test.ts +55 -0
  87. package/src/managers/event-manager.ts +5 -1
  88. package/src/observability/connect-observer.test.ts +214 -0
  89. package/src/observability/connect-observer.ts +128 -0
  90. package/src/observability/focus-tracker.test.ts +191 -0
  91. package/src/observability/focus-tracker.ts +96 -0
  92. package/src/observability/instrument-handoff.test.ts +397 -0
  93. package/src/observability/instrument-handoff.ts +231 -0
  94. package/src/observability/lifecycle-bridge.test.ts +398 -0
  95. package/src/observability/lifecycle-bridge.ts +216 -0
  96. package/src/observability/scrub.test.ts +183 -0
  97. package/src/observability/scrub.ts +174 -0
  98. package/src/observability/telemetry.test.ts +549 -0
  99. package/src/observability/telemetry.ts +546 -0
  100. package/src/services/connection-service.test.ts +261 -1
  101. package/src/services/connection-service.ts +167 -38
  102. package/src/services/network-switch-service.test.ts +51 -1
  103. package/src/services/network-switch-service.ts +56 -24
  104. package/src/services/signature-service.test.ts +181 -76
  105. package/src/services/signature-service.ts +75 -85
  106. package/src/services/transaction-service.test.ts +270 -69
  107. package/src/services/transaction-service.ts +74 -51
  108. package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
  109. package/src/universal-wallet-connector.observability.test.ts +610 -0
  110. package/src/universal-wallet-connector.test.ts +27 -146
  111. package/src/universal-wallet-connector.ts +279 -142
  112. package/src/utils/id.test.ts +15 -0
  113. package/src/utils/id.ts +48 -0
  114. package/src/utils/resolve-session-provider.test.ts +54 -0
  115. package/src/utils/resolve-session-provider.ts +25 -0
  116. package/src/utils/to-wallet-error.test.ts +181 -0
  117. package/src/utils/to-wallet-error.ts +152 -0
  118. package/src/utils/validate-wallet-session.test.ts +77 -0
  119. package/src/utils/validate-wallet-session.ts +40 -0
  120. package/src/version.test.ts +11 -0
  121. package/src/version.ts +2 -0
@@ -1,89 +1,112 @@
1
1
  import type {
2
2
  ConnectionMode,
3
3
  Connector,
4
- ExtensionInjectedProvider,
5
- IntegratedBrowserInjectedProvider,
6
- WalletConnectProvider,
7
- SupportedProvider,
8
4
  TransactionRequest,
9
5
  TransactionResult
10
6
  } from '@meshconnect/uwc-types'
11
7
  import type { SessionManager } from '../managers/session-manager'
8
+ import type { EventManager } from '../managers/event-manager'
12
9
  import type { ServiceOptions } from './connection-service'
10
+ import {
11
+ instrumentHandoff,
12
+ sessionHandoffContext
13
+ } from '../observability/instrument-handoff'
14
+ import { resolveSessionProvider } from '../utils/resolve-session-provider'
15
+ import {
16
+ validateWalletSession,
17
+ getSessionConnector
18
+ } from '../utils/validate-wallet-session'
13
19
 
14
20
  /**
15
- * Service for handling transaction operations across different connector types
21
+ * Service for handling transaction operations across different connector types.
22
+ *
23
+ * Owns the WHOLE wallet-boundary op — validation (session guards, connector
24
+ * capability, provider resolution) BEFORE the funnel, then the instrumented
25
+ * connector call — the same layering as ConnectionService / NetworkSwitchService,
26
+ * so a pre-wallet failure never emits an `awaitingWallet`/terminal pair.
16
27
  */
17
28
  export class TransactionService {
18
29
  constructor(
19
30
  private sessionManager: SessionManager,
20
- private connectors: Map<ConnectionMode, Connector>
31
+ private connectors: Map<ConnectionMode, Connector>,
32
+ private usingIntegratedBrowser: boolean,
33
+ private eventManager: EventManager,
34
+ // Observational wallet-response deadline for the handoff timeout marker.
35
+ private walletResponseTimeoutMs: number
21
36
  ) {}
22
37
 
23
38
  /**
24
39
  * Send a transaction with the currently connected wallet
25
40
  * @param request The transaction request parameters
26
- * @param provider The wallet provider to use for sending (required for injected connector)
27
41
  * @param options Optional cancellation signal
28
42
  * @returns A promise that resolves to the transaction result
29
43
  */
30
44
  async sendTransaction(
31
45
  request: TransactionRequest,
32
- provider?: SupportedProvider,
33
46
  options?: ServiceOptions
34
47
  ): Promise<TransactionResult> {
35
48
  options?.signal?.throwIfAborted()
36
- const session = this.sessionManager.getSession()
49
+ const session = validateWalletSession(this.sessionManager, {
50
+ requireAddress: true
51
+ })
37
52
 
38
- if (!session.connectionMode) {
39
- throw new Error('No active connection')
53
+ const connector = getSessionConnector(this.connectors, session)
54
+ if (!connector.sendTransaction) {
55
+ throw new Error(`Connector does not support transactions`)
40
56
  }
41
57
 
42
- if (!session.activeAddress) {
43
- throw new Error('No active wallet address')
44
- }
58
+ // Validation, not wallet work — a missing provider must not enter the funnel.
59
+ // undefined only for tonConnect, whose connector resolves its own provider.
60
+ const provider = resolveSessionProvider(
61
+ session,
62
+ this.usingIntegratedBrowser
63
+ )
45
64
 
46
- // Get the appropriate connector
47
- const connector = this.connectors.get(session.connectionMode)
48
- if (!connector) {
49
- throw new Error(
50
- `No connector found for connection mode: ${session.connectionMode}`
51
- )
52
- }
65
+ const result = await instrumentHandoff(
66
+ {
67
+ eventManager: this.eventManager,
68
+ timeoutMs: this.walletResponseTimeoutMs,
69
+ signal: options?.signal
70
+ },
71
+ sessionHandoffContext('sendTransaction', session),
72
+ // tonConnect resolves undefined and keeps its request-only call shape.
73
+ () =>
74
+ provider === undefined
75
+ ? connector.sendTransaction!(request)
76
+ : connector.sendTransaction!(request, provider)
77
+ )
78
+ // AFTER the funnel: the tx already broadcast, so its truthful terminal is
79
+ // recorded; an abort that raced completion still rejects the caller here.
80
+ options?.signal?.throwIfAborted()
81
+ return result
82
+ }
53
83
 
54
- // Check if the connector supports transactions
55
- if (!connector.sendTransaction) {
56
- throw new Error(`Connector does not support transactions`)
57
- }
84
+ /**
85
+ * Sign a Solana tx without broadcasting (raw bytes in/out). Routes to the
86
+ * active connector's optional `signSolanaTransactionBytes`; the capability
87
+ * check is validation and stays OUTSIDE the funnel.
88
+ */
89
+ async signSolanaTransactionBytes(
90
+ serializedTx: Uint8Array
91
+ ): Promise<Uint8Array> {
92
+ const session = validateWalletSession(this.sessionManager, {
93
+ requireAddress: false
94
+ })
58
95
 
59
- // Call the connector's sendTransaction method with the appropriate provider
60
- let result: TransactionResult
61
- if (session.connectionMode === 'injected') {
62
- if (!provider) {
63
- throw new Error('Provider is required for injected connector')
64
- }
65
- result = await connector.sendTransaction(
66
- request,
67
- provider as
68
- | ExtensionInjectedProvider
69
- | IntegratedBrowserInjectedProvider
70
- )
71
- } else if (session.connectionMode === 'tonConnect') {
72
- result = await connector.sendTransaction(request)
73
- } else if (session.connectionMode === 'walletConnect') {
74
- if (!provider) {
75
- throw new Error('Provider is required for WalletConnect connector')
76
- }
77
- result = await connector.sendTransaction(
78
- request,
79
- provider as WalletConnectProvider
96
+ const connector = this.connectors.get(session.connectionMode!)
97
+ if (!connector?.signSolanaTransactionBytes) {
98
+ throw new Error(
99
+ `signSolanaTransaction is not supported for connection mode "${session.connectionMode}"`
80
100
  )
81
- } else {
82
- // For other connectors that might not need the provider
83
- result = await connector.sendTransaction(request)
84
101
  }
85
102
 
86
- options?.signal?.throwIfAborted()
87
- return result
103
+ return await instrumentHandoff(
104
+ {
105
+ eventManager: this.eventManager,
106
+ timeoutMs: this.walletResponseTimeoutMs
107
+ },
108
+ sessionHandoffContext('signSolanaTransaction', session),
109
+ () => connector.signSolanaTransactionBytes!(serializedTx)
110
+ )
88
111
  }
89
112
  }
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import type { Network, WalletMetadata, Session } from '@meshconnect/uwc-types'
3
+ import { UniversalWalletConnector } from './universal-wallet-connector'
4
+ import { UWC_OPERATIONS } from './events'
5
+ import type { UWCObserver, UWCTelemetryEvent } from './observability/telemetry'
6
+
7
+ // Guardrail: every wallet-boundary op reachable through the public API MUST go
8
+ // through `instrumentHandoff` — i.e. emit exactly one `awaitingWallet` and
9
+ // exactly one terminal. A new `UWCOperation` wired up without `instrumentHandoff`
10
+ // (the easy mistake when adding a sign/send method) fails this test.
11
+ //
12
+ // Every instrumented op lives INSIDE its service (validation first, then the
13
+ // funnel at the real wallet boundary) — one layering for connect, switch, sign
14
+ // and send. The sign/send ops below run END-TO-END through the REAL
15
+ // SignatureService / TransactionService against mocked connectors; connect and
16
+ // switchNetwork services are mocked here, with their handoff coverage in
17
+ // connection-service.test.ts / network-switch-service.test.ts.
18
+
19
+ const signMessageMock = vi.fn()
20
+ const signTypedDataMock = vi.fn()
21
+ const sendTransactionMock = vi.fn()
22
+ const signSolanaTransactionBytesMock = vi.fn()
23
+
24
+ const connectedSession: Session = {
25
+ connectionMode: 'injected',
26
+ activeNetwork: {
27
+ internalId: 1,
28
+ id: 'eip155:1',
29
+ namespace: 'eip155',
30
+ name: 'Ethereum',
31
+ logoUrl: '',
32
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
33
+ networkType: 'evm'
34
+ },
35
+ activeWalletCapabilities: null,
36
+ activeNetworkWalletCapabilities: null,
37
+ activeAddress: '0xabc',
38
+ publicKey: null,
39
+ availableNetworks: [],
40
+ activeWallet: {
41
+ id: 'metamask',
42
+ name: 'MetaMask',
43
+ extensionInjectedProvider: {}
44
+ } as unknown as WalletMetadata,
45
+ availableAddresses: []
46
+ }
47
+
48
+ vi.mock('@meshconnect/uwc-injected-connector', () => ({
49
+ InjectedConnector: vi.fn().mockImplementation(function () {
50
+ return {
51
+ getAvailableWallets: vi.fn().mockResolvedValue([]),
52
+ // The REAL services route the session's connectionMode ('injected') to
53
+ // this connector — these are the instrumented wallet boundaries.
54
+ signMessage: signMessageMock,
55
+ signTypedData: signTypedDataMock,
56
+ sendTransaction: sendTransactionMock,
57
+ signSolanaTransactionBytes: signSolanaTransactionBytesMock
58
+ }
59
+ })
60
+ }))
61
+ vi.mock('@meshconnect/uwc-wallet-connect-connector', () => ({
62
+ WalletConnectConnector: vi.fn().mockImplementation(function () {
63
+ return { getAvailableWallets: vi.fn().mockResolvedValue([]) }
64
+ })
65
+ }))
66
+ vi.mock('@meshconnect/uwc-ton-connector', () => ({
67
+ TonConnectConnector: vi.fn().mockImplementation(function () {
68
+ return { getAvailableWallets: vi.fn().mockResolvedValue([]) }
69
+ })
70
+ }))
71
+ vi.mock('./managers/session-manager', () => ({
72
+ SessionManager: vi.fn().mockImplementation(function () {
73
+ return { getSession: () => connectedSession, updateSession: vi.fn() }
74
+ })
75
+ }))
76
+ vi.mock('./services/connection-service', () => ({
77
+ ConnectionService: vi.fn().mockImplementation(function () {
78
+ return {
79
+ connect: vi.fn().mockResolvedValue(undefined),
80
+ disconnect: vi.fn().mockResolvedValue(undefined),
81
+ getWallets: vi.fn().mockReturnValue([]),
82
+ getNetworks: vi.fn().mockReturnValue([]),
83
+ getConnectionURI: vi.fn(),
84
+ updateWallets: vi.fn(),
85
+ teardownLifecycle: vi.fn()
86
+ }
87
+ })
88
+ }))
89
+ vi.mock('./services/network-switch-service', () => ({
90
+ NetworkSwitchService: vi.fn().mockImplementation(function () {
91
+ return { switchNetwork: vi.fn(), getLoadingState: vi.fn() }
92
+ })
93
+ }))
94
+ vi.mock('./services/wallet-capabilities-service', () => ({
95
+ WalletCapabilitiesService: vi.fn().mockImplementation(function () {
96
+ return { getWalletCapabilities: vi.fn() }
97
+ })
98
+ }))
99
+
100
+ const networks = [
101
+ {
102
+ internalId: 1,
103
+ id: 'eip155:1',
104
+ namespace: 'eip155',
105
+ name: 'Ethereum',
106
+ logoUrl: '',
107
+ nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
108
+ networkType: 'evm'
109
+ }
110
+ ] as unknown as Network[]
111
+
112
+ const wallets = [
113
+ { id: 'metamask', name: 'MetaMask' }
114
+ ] as unknown as WalletMetadata[]
115
+
116
+ const TERMINALS = ['walletSucceeded', 'walletRejected', 'walletFailed'] as const
117
+
118
+ type OpRunner = (uwc: UniversalWalletConnector) => Promise<unknown>
119
+
120
+ const OPS: Array<{ op: string; run: OpRunner }> = [
121
+ { op: 'signMessage', run: uwc => uwc.signMessage('hello') },
122
+ {
123
+ op: 'signTypedData',
124
+ run: uwc =>
125
+ uwc.signTypedData({
126
+ domain: {},
127
+ types: {},
128
+ primaryType: 'x',
129
+ message: {}
130
+ } as never)
131
+ },
132
+ {
133
+ op: 'sendTransaction',
134
+ run: uwc => uwc.sendTransaction({ to: '0xabc' } as never)
135
+ },
136
+ {
137
+ op: 'signSolanaTransaction',
138
+ run: uwc => uwc.signSolanaTransaction(new Uint8Array([1, 2, 3]))
139
+ }
140
+ ]
141
+
142
+ describe('UniversalWalletConnector — instrumentation guard', () => {
143
+ beforeEach(() => {
144
+ UniversalWalletConnector.resetInstance()
145
+ signMessageMock.mockReset().mockResolvedValue({
146
+ type: 'standard',
147
+ signature: '0xsig'
148
+ })
149
+ signTypedDataMock.mockReset().mockResolvedValue('0xsig')
150
+ sendTransactionMock.mockReset().mockResolvedValue({ hash: '0xtx' })
151
+ signSolanaTransactionBytesMock
152
+ .mockReset()
153
+ .mockResolvedValue(new Uint8Array([9]))
154
+ })
155
+
156
+ for (const { op, run } of OPS) {
157
+ it(`${op} emits awaitingWallet and exactly one terminal`, async () => {
158
+ const events: UWCTelemetryEvent[] = []
159
+ const observer: UWCObserver = { onEvent: e => events.push(e) }
160
+ const uwc = new UniversalWalletConnector({ networks, wallets, observer })
161
+
162
+ await run(uwc)
163
+
164
+ const awaiting = events.filter(e => e.name === 'awaitingWallet')
165
+ const terminals = events.filter(e =>
166
+ TERMINALS.includes(e.name as (typeof TERMINALS)[number])
167
+ )
168
+ expect(awaiting).toHaveLength(1)
169
+ expect(awaiting[0].operation).toBe(op)
170
+ expect(terminals).toHaveLength(1)
171
+ expect(terminals[0].handoffId).toBe(awaiting[0].handoffId)
172
+ })
173
+ }
174
+
175
+ // A new UWCOperation MUST be categorized here — instrumented inside a service
176
+ // (the OPS above run end-to-end through the real sign/send services; connect /
177
+ // switchNetwork are covered in their service tests) or deliberately
178
+ // uninstrumented (read-only / housekeeping). This partition fails on an
179
+ // UNcategorized addition, so a new sign/send op can't silently skip the
180
+ // handoff funnel just because the author forgot to extend OPS — the gap the
181
+ // hardcoded list alone couldn't catch.
182
+ it('every UWCOperation is categorized (none escapes the funnel decision)', () => {
183
+ const serviceInstrumented = [
184
+ ...OPS.map(o => o.op),
185
+ 'connect',
186
+ 'switchNetwork'
187
+ ]
188
+ const deliberatelyUninstrumented = [
189
+ 'disconnect',
190
+ 'initialize',
191
+ 'getWalletCapabilities'
192
+ ]
193
+ const categorized = [
194
+ ...serviceInstrumented,
195
+ ...deliberatelyUninstrumented
196
+ ].sort()
197
+ expect(categorized).toEqual([...UWC_OPERATIONS].sort())
198
+ })
199
+ })