@meshconnect/uwc-core 2.5.16 → 2.7.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.
- package/README.md +11 -2
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/observability/instrument-handoff.d.ts +2 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +6 -0
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/telemetry.d.ts +2 -0
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +26 -10
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +5 -0
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +21 -4
- package/dist/services/connection-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +7 -0
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +3 -2
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/to-wallet-error.d.ts.map +1 -1
- package/dist/utils/to-wallet-error.js +13 -2
- package/dist/utils/to-wallet-error.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +10 -0
- package/src/observability/instrument-handoff.ts +8 -0
- package/src/observability/telemetry.test.ts +83 -0
- package/src/observability/telemetry.ts +29 -10
- package/src/services/connection-service.test.ts +113 -1
- package/src/services/connection-service.ts +30 -4
- package/src/services/transaction-service.test.ts +25 -0
- package/src/universal-wallet-connector.ts +10 -2
- package/src/utils/to-wallet-error.test.ts +29 -1
- package/src/utils/to-wallet-error.ts +12 -1
- package/src/version.ts +1 -1
|
@@ -12,7 +12,10 @@ import type {
|
|
|
12
12
|
ConnectionMode,
|
|
13
13
|
Connector
|
|
14
14
|
} from '@meshconnect/uwc-types'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
NoCommonNetworkError,
|
|
17
|
+
WalletNotAuthorizedError
|
|
18
|
+
} from '@meshconnect/uwc-types'
|
|
16
19
|
import type { WalletLifecycleEvent } from '@meshconnect/uwc-types'
|
|
17
20
|
|
|
18
21
|
describe('ConnectionService', () => {
|
|
@@ -211,6 +214,115 @@ describe('ConnectionService', () => {
|
|
|
211
214
|
expect(connectionService['activeConnector']).toBe(mockConnector)
|
|
212
215
|
})
|
|
213
216
|
|
|
217
|
+
// ONC-1975: silentOnly is a promise never to open wallet UI.
|
|
218
|
+
it('forwards silentOnly to the injected connector', async () => {
|
|
219
|
+
const provider = { supportedNetworkIds: ['eip155:1'] }
|
|
220
|
+
wallets[0].extensionInjectedProvider = provider as any
|
|
221
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
222
|
+
networkId: 'eip155:1',
|
|
223
|
+
address: '0x1234',
|
|
224
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1', {
|
|
228
|
+
silentOnly: true
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
expect(mockConnector.connect).toHaveBeenCalledWith(
|
|
232
|
+
networks[0],
|
|
233
|
+
provider,
|
|
234
|
+
{
|
|
235
|
+
silentOnly: true
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('tags the connect funnel with silentOnly, so a silent restore is not counted as a tap', async () => {
|
|
241
|
+
const provider = { supportedNetworkIds: ['eip155:1'] }
|
|
242
|
+
wallets[0].extensionInjectedProvider = provider as any
|
|
243
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
244
|
+
networkId: 'eip155:1',
|
|
245
|
+
address: '0x1234',
|
|
246
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
247
|
+
})
|
|
248
|
+
const awaiting = vi.fn()
|
|
249
|
+
const succeeded = vi.fn()
|
|
250
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
251
|
+
eventManager.on('walletSucceeded', succeeded)
|
|
252
|
+
|
|
253
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1', {
|
|
254
|
+
silentOnly: true
|
|
255
|
+
})
|
|
256
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1')
|
|
257
|
+
|
|
258
|
+
expect(awaiting.mock.calls.map(([e]) => e.silentOnly)).toEqual([
|
|
259
|
+
true,
|
|
260
|
+
false
|
|
261
|
+
])
|
|
262
|
+
expect(succeeded.mock.calls.map(([e]) => e.silentOnly)).toEqual([
|
|
263
|
+
true,
|
|
264
|
+
false
|
|
265
|
+
])
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('passes no connect options when neither silentOnly nor requiredAddresses is set', async () => {
|
|
269
|
+
const provider = { supportedNetworkIds: ['eip155:1'] }
|
|
270
|
+
wallets[0].extensionInjectedProvider = provider as any
|
|
271
|
+
mockConnector.connect = vi.fn().mockResolvedValue({
|
|
272
|
+
networkId: 'eip155:1',
|
|
273
|
+
address: '0x1234',
|
|
274
|
+
availableAddresses: [{ address: '0x1234', networkId: 'eip155:1' }]
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
await connectionService.connect('injected', 'metamask', 'eip155:1', {
|
|
278
|
+
silentOnly: false
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
expect(mockConnector.connect).toHaveBeenCalledWith(
|
|
282
|
+
networks[0],
|
|
283
|
+
provider,
|
|
284
|
+
undefined
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('rejects silentOnly for a non-injected mode before the attempt starts', async () => {
|
|
289
|
+
const walletConnectConnector: Connector = {
|
|
290
|
+
connect: vi.fn(),
|
|
291
|
+
disconnect: vi.fn(),
|
|
292
|
+
signMessage: vi.fn(),
|
|
293
|
+
sendTransaction: vi.fn(),
|
|
294
|
+
switchNetwork: vi.fn()
|
|
295
|
+
}
|
|
296
|
+
connectors.set('walletConnect', walletConnectConnector)
|
|
297
|
+
wallets[0].walletConnectProvider = {
|
|
298
|
+
supportedNetworkIds: ['eip155:1']
|
|
299
|
+
} as any
|
|
300
|
+
const connectingListener = vi.fn()
|
|
301
|
+
const awaiting = vi.fn()
|
|
302
|
+
eventManager.on('connecting', connectingListener)
|
|
303
|
+
eventManager.on('awaitingWallet', awaiting)
|
|
304
|
+
|
|
305
|
+
await expect(
|
|
306
|
+
connectionService.connect('walletConnect', 'metamask', 'eip155:1', {
|
|
307
|
+
silentOnly: true
|
|
308
|
+
})
|
|
309
|
+
).rejects.toThrow(
|
|
310
|
+
"silentOnly is not supported for connection mode 'walletConnect'"
|
|
311
|
+
)
|
|
312
|
+
// A caller bug, not the expected `not_authorized` miss.
|
|
313
|
+
await expect(
|
|
314
|
+
connectionService.connect('walletConnect', 'metamask', 'eip155:1', {
|
|
315
|
+
silentOnly: true
|
|
316
|
+
})
|
|
317
|
+
).rejects.not.toBeInstanceOf(WalletNotAuthorizedError)
|
|
318
|
+
|
|
319
|
+
// A validation failure: never reaches the wallet, never enters the funnel.
|
|
320
|
+
expect(walletConnectConnector.connect).not.toHaveBeenCalled()
|
|
321
|
+
expect(connectingListener).not.toHaveBeenCalled()
|
|
322
|
+
expect(awaiting).not.toHaveBeenCalled()
|
|
323
|
+
expect(connectionService['activeConnector']).toBeNull()
|
|
324
|
+
})
|
|
325
|
+
|
|
214
326
|
it('does not emit "connecting" when validation throws (unknown wallet)', async () => {
|
|
215
327
|
const connectingListener = vi.fn()
|
|
216
328
|
eventManager.on('connecting', connectingListener)
|
|
@@ -26,6 +26,11 @@ export interface ServiceOptions {
|
|
|
26
26
|
* Forwarded verbatim to the connector; see `ConnectorConnectOptions`.
|
|
27
27
|
*/
|
|
28
28
|
requiredAddresses?: readonly string[]
|
|
29
|
+
/**
|
|
30
|
+
* Connect only if the wallet already trusts this origin (connect only).
|
|
31
|
+
* Injected mode only; see `ConnectorConnectOptions.silentOnly`.
|
|
32
|
+
*/
|
|
33
|
+
silentOnly?: boolean
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/** Sibling-namespace live-account re-read poll: absorbs the lag while a wallet
|
|
@@ -101,6 +106,17 @@ export class ConnectionService {
|
|
|
101
106
|
const provider = this.getProviderForMode(wallet, connectionMode)
|
|
102
107
|
const network = this.validateAndDetermineNetwork(networkId, provider)
|
|
103
108
|
const connector = this.getConnector(connectionMode)
|
|
109
|
+
// A silent-only connect is a promise never to open wallet UI. Only the
|
|
110
|
+
// injected connector can keep it — WalletConnect / TON Connect always hand
|
|
111
|
+
// off to the wallet — so refuse the rest here, as a validation failure:
|
|
112
|
+
// before `connecting`, so the attempt never enters the hand-off funnel.
|
|
113
|
+
// A plain Error, not `WalletNotAuthorizedError`: that classifies as the
|
|
114
|
+
// expected `not_authorized` miss, which would hide this caller bug.
|
|
115
|
+
if (options?.silentOnly && connectionMode !== 'injected') {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`silentOnly is not supported for connection mode '${connectionMode}'`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
104
120
|
const connectAttemptId = generateId()
|
|
105
121
|
|
|
106
122
|
// Supersede any prior in-flight attempt: abort it so its URI poll and
|
|
@@ -157,7 +173,10 @@ export class ConnectionService {
|
|
|
157
173
|
walletId,
|
|
158
174
|
namespace: network.namespace,
|
|
159
175
|
chainId: network.id,
|
|
160
|
-
connectAttemptId
|
|
176
|
+
connectAttemptId,
|
|
177
|
+
// Lets telemetry tell a silent restore from a tapped connect: both
|
|
178
|
+
// succeed the same way, and only a miss carries a distinct failureKind.
|
|
179
|
+
silentOnly: options?.silentOnly === true
|
|
161
180
|
},
|
|
162
181
|
async handoffId => {
|
|
163
182
|
// Publish this attempt's routing for the connector's step sink.
|
|
@@ -170,7 +189,8 @@ export class ConnectionService {
|
|
|
170
189
|
provider,
|
|
171
190
|
signal,
|
|
172
191
|
connectAttemptId,
|
|
173
|
-
options?.requiredAddresses
|
|
192
|
+
options?.requiredAddresses,
|
|
193
|
+
options?.silentOnly
|
|
174
194
|
)
|
|
175
195
|
} finally {
|
|
176
196
|
// Retract ONLY if we are still the published attempt. An
|
|
@@ -261,7 +281,8 @@ export class ConnectionService {
|
|
|
261
281
|
provider: SupportedProvider,
|
|
262
282
|
signal: AbortSignal,
|
|
263
283
|
connectAttemptId: string,
|
|
264
|
-
requiredAddresses?: readonly string[]
|
|
284
|
+
requiredAddresses?: readonly string[],
|
|
285
|
+
silentOnly?: boolean
|
|
265
286
|
): Promise<ConnectorResult> {
|
|
266
287
|
// Race the connector call against the AbortSignal so a mid-flight abort
|
|
267
288
|
// (e.g. user cancels the WalletConnect QR while the prompt is open)
|
|
@@ -274,7 +295,12 @@ export class ConnectionService {
|
|
|
274
295
|
provider as
|
|
275
296
|
| ExtensionInjectedProvider
|
|
276
297
|
| IntegratedBrowserInjectedProvider,
|
|
277
|
-
requiredAddresses
|
|
298
|
+
requiredAddresses || silentOnly
|
|
299
|
+
? {
|
|
300
|
+
...(requiredAddresses ? { requiredAddresses } : {}),
|
|
301
|
+
...(silentOnly ? { silentOnly } : {})
|
|
302
|
+
}
|
|
303
|
+
: undefined
|
|
278
304
|
),
|
|
279
305
|
signal
|
|
280
306
|
)
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
|
2
2
|
import { TransactionService } from './transaction-service'
|
|
3
3
|
import { SessionManager } from '../managers/session-manager'
|
|
4
4
|
import { EventManager } from '../managers/event-manager'
|
|
5
|
+
import { WalletConnectorError } from '../../../types/src'
|
|
5
6
|
import type {
|
|
6
7
|
ConnectionMode,
|
|
7
8
|
Connector,
|
|
@@ -211,6 +212,30 @@ describe('TransactionService.sendTransaction', () => {
|
|
|
211
212
|
expect(events).toEqual(['awaitingWallet', 'walletRejected'])
|
|
212
213
|
})
|
|
213
214
|
|
|
215
|
+
it("carries the wallet's own reason onto walletFailed and the thrown error", async () => {
|
|
216
|
+
connectSession()
|
|
217
|
+
const failures: { error: { providerReason?: string } }[] = []
|
|
218
|
+
eventManager.on('walletFailed', d => failures.push(d))
|
|
219
|
+
vi.mocked(mockConnector.sendTransaction!).mockRejectedValueOnce(
|
|
220
|
+
new WalletConnectorError({
|
|
221
|
+
type: 'unknown',
|
|
222
|
+
message: 'An unknown RPC error occurred.',
|
|
223
|
+
providerReason: 'cancelled'
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
const error = await transactionService
|
|
228
|
+
.sendTransaction(request)
|
|
229
|
+
.catch((e: unknown) => e)
|
|
230
|
+
|
|
231
|
+
expect(error).toMatchObject({ providerReason: 'cancelled' })
|
|
232
|
+
expect(events).toEqual(['awaitingWallet', 'walletFailed'])
|
|
233
|
+
expect(failures[0]?.error).toMatchObject({
|
|
234
|
+
type: 'unknown',
|
|
235
|
+
providerReason: 'cancelled'
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
214
239
|
it('tonConnect enters the funnel with the connector resolving its own provider', async () => {
|
|
215
240
|
connectors.set('tonConnect', mockConnector)
|
|
216
241
|
connectSession('tonConnect', walletWithoutProviders)
|
|
@@ -130,6 +130,13 @@ export interface ConnectOptions extends OperationOptions {
|
|
|
130
130
|
* constraint.
|
|
131
131
|
*/
|
|
132
132
|
requiredAddresses?: readonly string[]
|
|
133
|
+
/**
|
|
134
|
+
* Connect only if the wallet already trusts this origin; never prompt.
|
|
135
|
+
* Forwarded to the connector as {@link ConnectorConnectOptions.silentOnly}
|
|
136
|
+
* — see there for the semantics. `'injected'` mode only: any other mode
|
|
137
|
+
* rejects with a plain `Error` before reaching the wallet.
|
|
138
|
+
*/
|
|
139
|
+
silentOnly?: boolean
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
export class UniversalWalletConnector {
|
|
@@ -473,8 +480,9 @@ export class UniversalWalletConnector {
|
|
|
473
480
|
UniversalWalletConnector.instance = this
|
|
474
481
|
}
|
|
475
482
|
|
|
476
|
-
// Initialize detected wallets after services are created
|
|
477
|
-
|
|
483
|
+
// Initialize detected wallets after services are created. Fire-and-forget on
|
|
484
|
+
// purpose (a constructor can't await); the method catches everything itself.
|
|
485
|
+
void this.initializeDetectedWallets()
|
|
478
486
|
}
|
|
479
487
|
|
|
480
488
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
WalletConnectorError,
|
|
4
|
-
NoCommonNetworkError
|
|
4
|
+
NoCommonNetworkError,
|
|
5
|
+
WalletNotAuthorizedError
|
|
5
6
|
} from '@meshconnect/uwc-types'
|
|
6
7
|
import {
|
|
7
8
|
classifyError,
|
|
@@ -38,6 +39,15 @@ describe('classifyError', () => {
|
|
|
38
39
|
})
|
|
39
40
|
})
|
|
40
41
|
|
|
42
|
+
it('classifies WalletNotAuthorizedError as not_authorized/connector (ONC-1975)', () => {
|
|
43
|
+
// A silent-only connect that found no authorization. Expected, not a
|
|
44
|
+
// fault, so it gets its own kind that failure rates can exclude.
|
|
45
|
+
expect(classifyError(new WalletNotAuthorizedError())).toEqual({
|
|
46
|
+
failureKind: 'not_authorized',
|
|
47
|
+
source: 'connector'
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
41
51
|
it('classifies -32002 (request already pending) as provider_error', () => {
|
|
42
52
|
// Delta B: the double-fire race. A provider-level error, not a user reject.
|
|
43
53
|
expect(classifyError({ code: -32002, message: 'already pending' })).toEqual(
|
|
@@ -203,6 +213,24 @@ describe('toWalletError', () => {
|
|
|
203
213
|
})
|
|
204
214
|
})
|
|
205
215
|
|
|
216
|
+
it('carries the provider reason of a connector error', () => {
|
|
217
|
+
const err = new WalletConnectorError({
|
|
218
|
+
type: 'unknown',
|
|
219
|
+
message: 'An unknown RPC error occurred.',
|
|
220
|
+
providerReason: 'cancelled'
|
|
221
|
+
})
|
|
222
|
+
expect(toWalletError(err)).toEqual({
|
|
223
|
+
type: 'unknown',
|
|
224
|
+
message: 'An unknown RPC error occurred.',
|
|
225
|
+
providerReason: 'cancelled'
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('omits providerReason when the connector error has none', () => {
|
|
230
|
+
const err = new WalletConnectorError({ type: 'unknown', message: 'x' })
|
|
231
|
+
expect(toWalletError(err)).not.toHaveProperty('providerReason')
|
|
232
|
+
})
|
|
233
|
+
|
|
206
234
|
it('bounds an over-long non-connector message (shared safeErrorMessage cap)', () => {
|
|
207
235
|
const err = new Error('x'.repeat(10_000))
|
|
208
236
|
expect(toWalletError(err).message).toHaveLength(500)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WalletConnectorError,
|
|
3
3
|
NoCommonNetworkError,
|
|
4
|
+
WalletNotAuthorizedError,
|
|
4
5
|
safeErrorMessage,
|
|
5
6
|
type WalletError,
|
|
6
7
|
type FailureKind,
|
|
@@ -26,7 +27,13 @@ const MAX_ERROR_CODE_LENGTH = 64
|
|
|
26
27
|
export function toWalletError(error: unknown): WalletError {
|
|
27
28
|
try {
|
|
28
29
|
if (error instanceof WalletConnectorError) {
|
|
29
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
type: error.type,
|
|
32
|
+
message: error.message,
|
|
33
|
+
...(error.providerReason
|
|
34
|
+
? { providerReason: error.providerReason }
|
|
35
|
+
: {})
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
return { type: 'unknown', message: safeErrorMessage(error) }
|
|
32
39
|
} catch {
|
|
@@ -101,6 +108,10 @@ function classifyErrorUnguarded(error: unknown): {
|
|
|
101
108
|
let failureKind: FailureKind = 'unknown'
|
|
102
109
|
if (isUserRejection(error)) {
|
|
103
110
|
failureKind = 'user_rejected'
|
|
111
|
+
} else if (error instanceof WalletNotAuthorizedError) {
|
|
112
|
+
// A silent-only connect stopped because the wallet hasn't authorized this
|
|
113
|
+
// origin. Nothing was shown to the user, so it is expected, not a fault.
|
|
114
|
+
failureKind = 'not_authorized'
|
|
104
115
|
} else if (error instanceof NoCommonNetworkError) {
|
|
105
116
|
// The wallet and configured networks don't overlap — the #1 prod connection
|
|
106
117
|
// failure. Carries its own CAIP-2 id lists; the caller forwards them.
|
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 = '2.
|
|
2
|
+
export const UWC_CORE_VERSION = '2.7.0'
|