@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.
- package/README.md +247 -76
- package/dist/events.d.ts +241 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +77 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/create-logger.d.ts +32 -0
- package/dist/logger/create-logger.d.ts.map +1 -0
- package/dist/logger/create-logger.js +79 -0
- package/dist/logger/create-logger.js.map +1 -0
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +5 -1
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/observability/connect-observer.d.ts +33 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +115 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/focus-tracker.d.ts +42 -0
- package/dist/observability/focus-tracker.d.ts.map +1 -0
- package/dist/observability/focus-tracker.js +57 -0
- package/dist/observability/focus-tracker.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +54 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +173 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/lifecycle-bridge.d.ts +69 -0
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
- package/dist/observability/lifecycle-bridge.js +155 -0
- package/dist/observability/lifecycle-bridge.js.map +1 -0
- package/dist/observability/scrub.d.ts +31 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +167 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +157 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +373 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +23 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +128 -19
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +45 -16
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +14 -7
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +56 -58
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +19 -5
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +55 -38
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +207 -110
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/resolve-session-provider.d.ts +10 -0
- package/dist/utils/resolve-session-provider.d.ts.map +1 -0
- package/dist/utils/resolve-session-provider.js +21 -0
- package/dist/utils/resolve-session-provider.js.map +1 -0
- package/dist/utils/to-wallet-error.d.ts +35 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +130 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/dist/utils/validate-wallet-session.d.ts +15 -0
- package/dist/utils/validate-wallet-session.d.ts.map +1 -0
- package/dist/utils/validate-wallet-session.js +29 -0
- package/dist/utils/validate-wallet-session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/events.ts +326 -14
- package/src/index.ts +10 -0
- package/src/logger/create-logger.test.ts +126 -0
- package/src/logger/create-logger.ts +112 -0
- package/src/managers/event-manager.test.ts +55 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +214 -0
- package/src/observability/connect-observer.ts +128 -0
- package/src/observability/focus-tracker.test.ts +191 -0
- package/src/observability/focus-tracker.ts +96 -0
- package/src/observability/instrument-handoff.test.ts +397 -0
- package/src/observability/instrument-handoff.ts +231 -0
- package/src/observability/lifecycle-bridge.test.ts +398 -0
- package/src/observability/lifecycle-bridge.ts +216 -0
- package/src/observability/scrub.test.ts +183 -0
- package/src/observability/scrub.ts +174 -0
- package/src/observability/telemetry.test.ts +549 -0
- package/src/observability/telemetry.ts +546 -0
- package/src/services/connection-service.test.ts +261 -1
- package/src/services/connection-service.ts +167 -38
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +56 -24
- package/src/services/signature-service.test.ts +181 -76
- package/src/services/signature-service.ts +75 -85
- package/src/services/transaction-service.test.ts +270 -69
- package/src/services/transaction-service.ts +74 -51
- package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
- package/src/universal-wallet-connector.observability.test.ts +610 -0
- package/src/universal-wallet-connector.test.ts +27 -146
- package/src/universal-wallet-connector.ts +279 -142
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/resolve-session-provider.test.ts +54 -0
- package/src/utils/resolve-session-provider.ts +25 -0
- package/src/utils/to-wallet-error.test.ts +181 -0
- package/src/utils/to-wallet-error.ts +152 -0
- package/src/utils/validate-wallet-session.test.ts +77 -0
- package/src/utils/validate-wallet-session.ts +40 -0
- package/src/version.test.ts +11 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { Session, WalletMetadata } from '@meshconnect/uwc-types'
|
|
3
|
+
import { resolveSessionProvider } from './resolve-session-provider'
|
|
4
|
+
|
|
5
|
+
const extensionProvider = { kind: 'extension' }
|
|
6
|
+
const integratedProvider = { kind: 'integrated' }
|
|
7
|
+
const walletConnectProvider = { kind: 'walletConnect' }
|
|
8
|
+
|
|
9
|
+
const wallet = {
|
|
10
|
+
id: 'metamask',
|
|
11
|
+
name: 'MetaMask',
|
|
12
|
+
extensionInjectedProvider: extensionProvider,
|
|
13
|
+
integratedBrowserInjectedProvider: integratedProvider,
|
|
14
|
+
walletConnectProvider
|
|
15
|
+
} as unknown as WalletMetadata
|
|
16
|
+
|
|
17
|
+
const session = (
|
|
18
|
+
connectionMode: Session['connectionMode'],
|
|
19
|
+
activeWallet: WalletMetadata | null = wallet
|
|
20
|
+
) => ({ connectionMode, activeWallet }) as Session
|
|
21
|
+
|
|
22
|
+
describe('resolveSessionProvider', () => {
|
|
23
|
+
it('resolves the extension provider for injected outside the integrated browser', () => {
|
|
24
|
+
expect(resolveSessionProvider(session('injected'), false)).toBe(
|
|
25
|
+
extensionProvider
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('resolves the integrated-browser provider for injected inside it', () => {
|
|
30
|
+
expect(resolveSessionProvider(session('injected'), true)).toBe(
|
|
31
|
+
integratedProvider
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('resolves the walletConnect provider for walletConnect', () => {
|
|
36
|
+
expect(resolveSessionProvider(session('walletConnect'), false)).toBe(
|
|
37
|
+
walletConnectProvider
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('yields undefined for tonConnect — the connector resolves its own provider', () => {
|
|
42
|
+
expect(resolveSessionProvider(session('tonConnect'), false)).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('throws when the resolved provider is missing (configuration failure)', () => {
|
|
46
|
+
const bare = { id: 'metamask', name: 'MetaMask' } as WalletMetadata
|
|
47
|
+
expect(() =>
|
|
48
|
+
resolveSessionProvider(session('injected', bare), false)
|
|
49
|
+
).toThrow('No provider available for the active wallet')
|
|
50
|
+
expect(() =>
|
|
51
|
+
resolveSessionProvider(session('walletConnect', bare), false)
|
|
52
|
+
).toThrow('No provider available for the active wallet')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Session, SupportedProvider } from '@meshconnect/uwc-types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the active session's provider for a wallet-boundary op. This is
|
|
5
|
+
* validation, not wallet work — callers run it BEFORE `instrumentHandoff` so a
|
|
6
|
+
* missing provider (a configuration failure) never enters the handoff funnel.
|
|
7
|
+
* `tonConnect` resolves its provider inside the connector and legitimately
|
|
8
|
+
* yields undefined here.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveSessionProvider(
|
|
11
|
+
session: Session,
|
|
12
|
+
usingIntegratedBrowser: boolean
|
|
13
|
+
): SupportedProvider | undefined {
|
|
14
|
+
if (session.connectionMode === 'tonConnect') return undefined
|
|
15
|
+
const provider =
|
|
16
|
+
session.connectionMode === 'injected'
|
|
17
|
+
? usingIntegratedBrowser
|
|
18
|
+
? session.activeWallet?.integratedBrowserInjectedProvider
|
|
19
|
+
: session.activeWallet?.extensionInjectedProvider
|
|
20
|
+
: session.activeWallet?.walletConnectProvider
|
|
21
|
+
if (!provider) {
|
|
22
|
+
throw new Error('No provider available for the active wallet')
|
|
23
|
+
}
|
|
24
|
+
return provider
|
|
25
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
WalletConnectorError,
|
|
4
|
+
NoCommonNetworkError
|
|
5
|
+
} from '@meshconnect/uwc-types'
|
|
6
|
+
import {
|
|
7
|
+
classifyError,
|
|
8
|
+
isAbortError,
|
|
9
|
+
isUserRejection,
|
|
10
|
+
toWalletError
|
|
11
|
+
} from './to-wallet-error'
|
|
12
|
+
|
|
13
|
+
describe('classifyError', () => {
|
|
14
|
+
it('classifies an explicit user rejection (EIP-1193 4001)', () => {
|
|
15
|
+
expect(classifyError({ code: 4001, message: 'User rejected' })).toEqual({
|
|
16
|
+
errorCode: '4001',
|
|
17
|
+
failureKind: 'user_rejected',
|
|
18
|
+
source: 'provider'
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('classifies a WalletConnectorError{rejected} as user_rejected/connector', () => {
|
|
23
|
+
const err = new WalletConnectorError({ type: 'rejected', message: 'no' })
|
|
24
|
+
expect(classifyError(err)).toEqual({
|
|
25
|
+
failureKind: 'user_rejected',
|
|
26
|
+
source: 'connector'
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('classifies NoCommonNetworkError as no_common_network/connector', () => {
|
|
31
|
+
// Delta A: the #1 prod connection failure gets its own kind. It is a
|
|
32
|
+
// WalletConnectorError subclass, so source is 'connector'; it has no numeric
|
|
33
|
+
// code, so errorCode is omitted.
|
|
34
|
+
const err = new NoCommonNetworkError(['eip155:1'], ['solana:101'])
|
|
35
|
+
expect(classifyError(err)).toEqual({
|
|
36
|
+
failureKind: 'no_common_network',
|
|
37
|
+
source: 'connector'
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('classifies -32002 (request already pending) as provider_error', () => {
|
|
42
|
+
// Delta B: the double-fire race. A provider-level error, not a user reject.
|
|
43
|
+
expect(classifyError({ code: -32002, message: 'already pending' })).toEqual(
|
|
44
|
+
{
|
|
45
|
+
errorCode: '-32002',
|
|
46
|
+
failureKind: 'provider_error',
|
|
47
|
+
source: 'provider'
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('classifies an expired WalletConnectorError', () => {
|
|
53
|
+
const err = new WalletConnectorError({ type: 'expired', message: 'gone' })
|
|
54
|
+
expect(classifyError(err)).toEqual({
|
|
55
|
+
failureKind: 'expired',
|
|
56
|
+
source: 'connector'
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('maps known EIP-1193 codes to coarse kinds', () => {
|
|
61
|
+
expect(classifyError({ code: 4200 }).failureKind).toBe('unsupported_method')
|
|
62
|
+
expect(classifyError({ code: -32601 }).failureKind).toBe(
|
|
63
|
+
'unsupported_method'
|
|
64
|
+
)
|
|
65
|
+
expect(classifyError({ code: -32602 }).failureKind).toBe('invalid_params')
|
|
66
|
+
expect(classifyError({ code: 4900 }).failureKind).toBe('network_error')
|
|
67
|
+
expect(classifyError({ code: 4901 }).failureKind).toBe('network_error')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('falls back to unknown/unknown for a plain Error (no reliable signal)', () => {
|
|
71
|
+
// RELIABLE-ONLY: a plain Error carries no code and is not a typed connector
|
|
72
|
+
// error, so we never guess a kind from its message.
|
|
73
|
+
expect(classifyError(new Error('encoding overruns Uint8Array'))).toEqual({
|
|
74
|
+
failureKind: 'unknown',
|
|
75
|
+
source: 'unknown'
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('stringifies a string code without inventing a kind', () => {
|
|
80
|
+
expect(classifyError({ code: 'SERVER_ERROR' })).toEqual({
|
|
81
|
+
errorCode: 'SERVER_ERROR',
|
|
82
|
+
failureKind: 'unknown',
|
|
83
|
+
source: 'unknown'
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('scrubs a string error code (wallet-controlled free-form egress slot)', () => {
|
|
88
|
+
// A wallet can throw `{ code: '<anything>' }` — errorCode lands verbatim in
|
|
89
|
+
// walletRejected / walletFailed / error telemetry, so it must pass the same
|
|
90
|
+
// scrubber as error.message.
|
|
91
|
+
const addr = '0x' + 'a'.repeat(40)
|
|
92
|
+
const out = classifyError({ code: `denied for ${addr}` })
|
|
93
|
+
expect(out.errorCode).toBeDefined()
|
|
94
|
+
expect(out.errorCode).not.toContain(addr)
|
|
95
|
+
expect(out.errorCode).toContain('[redacted-hex]')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('caps an over-long string error code', () => {
|
|
99
|
+
// '#' matches no scrub pattern, so this exercises the length cap itself.
|
|
100
|
+
const out = classifyError({ code: '#'.repeat(300) })
|
|
101
|
+
expect(out.errorCode).toBe('#'.repeat(64))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('keeps known short string codes verbatim (ACTION_REJECTED)', () => {
|
|
105
|
+
expect(classifyError({ code: 'ACTION_REJECTED' })).toEqual({
|
|
106
|
+
errorCode: 'ACTION_REJECTED',
|
|
107
|
+
failureKind: 'user_rejected',
|
|
108
|
+
source: 'unknown'
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('isUserRejection', () => {
|
|
114
|
+
it('is true for 4001 and ACTION_REJECTED and WalletConnectorError{rejected}', () => {
|
|
115
|
+
expect(isUserRejection({ code: 4001 })).toBe(true)
|
|
116
|
+
expect(isUserRejection({ code: 'ACTION_REJECTED' })).toBe(true)
|
|
117
|
+
expect(
|
|
118
|
+
isUserRejection(
|
|
119
|
+
new WalletConnectorError({ type: 'rejected', message: '' })
|
|
120
|
+
)
|
|
121
|
+
).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('is false for a NoCommonNetworkError (not a user action)', () => {
|
|
125
|
+
expect(isUserRejection(new NoCommonNetworkError([], []))).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('toWalletError', () => {
|
|
130
|
+
it('preserves the type of a NoCommonNetworkError (unknown)', () => {
|
|
131
|
+
const err = new NoCommonNetworkError(['eip155:1'], ['solana:101'])
|
|
132
|
+
expect(toWalletError(err)).toEqual({
|
|
133
|
+
type: 'unknown',
|
|
134
|
+
message:
|
|
135
|
+
'No common supported network found between wallet and configured networks'
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('bounds an over-long non-connector message (shared safeErrorMessage cap)', () => {
|
|
140
|
+
const err = new Error('x'.repeat(10_000))
|
|
141
|
+
expect(toWalletError(err).message).toHaveLength(500)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('poisoned error objects (throwing getters)', () => {
|
|
146
|
+
// These helpers run inside telemetry catch blocks — a getter that throws
|
|
147
|
+
// must never replace the real rejection with its own error.
|
|
148
|
+
const poisoned = () => {
|
|
149
|
+
const evil = new Error('real message')
|
|
150
|
+
for (const prop of ['message', 'code', 'name'] as const) {
|
|
151
|
+
Object.defineProperty(evil, prop, {
|
|
152
|
+
get() {
|
|
153
|
+
throw new Error(`poisoned ${prop}`)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
return evil
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
it('toWalletError falls back instead of throwing', () => {
|
|
161
|
+
expect(toWalletError(poisoned())).toEqual({
|
|
162
|
+
type: 'unknown',
|
|
163
|
+
message: 'Unreadable error object'
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('isUserRejection returns false instead of throwing', () => {
|
|
168
|
+
expect(isUserRejection(poisoned())).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('classifyError returns unknown/unknown instead of throwing', () => {
|
|
172
|
+
expect(classifyError(poisoned())).toEqual({
|
|
173
|
+
failureKind: 'unknown',
|
|
174
|
+
source: 'unknown'
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('isAbortError returns false instead of throwing', () => {
|
|
179
|
+
expect(isAbortError(poisoned())).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WalletConnectorError,
|
|
3
|
+
NoCommonNetworkError,
|
|
4
|
+
safeErrorMessage,
|
|
5
|
+
type WalletError,
|
|
6
|
+
type FailureKind,
|
|
7
|
+
type ErrorSource
|
|
8
|
+
} from '@meshconnect/uwc-types'
|
|
9
|
+
import { scrubMessage } from '../observability/scrub'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cap for STRING error codes. A wallet controls `error.code`, so an unbounded
|
|
13
|
+
* string here is a free-form slot on the telemetry egress path. The longest
|
|
14
|
+
* legitimate enum-like code in the wild (ethers') is ~23 chars — 64 keeps every
|
|
15
|
+
* real code intact while bounding abuse.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_ERROR_CODE_LENGTH = 64
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize any thrown value into the PII-light `WalletError` shape.
|
|
21
|
+
*
|
|
22
|
+
* Every helper in this file guards its property reads: they run inside the
|
|
23
|
+
* telemetry catch blocks, where a poisoned getter (`code`/`message`/`toString`
|
|
24
|
+
* that throws) would otherwise REPLACE the real rejection with its own error.
|
|
25
|
+
*/
|
|
26
|
+
export function toWalletError(error: unknown): WalletError {
|
|
27
|
+
try {
|
|
28
|
+
if (error instanceof WalletConnectorError) {
|
|
29
|
+
return { type: error.type, message: error.message }
|
|
30
|
+
}
|
|
31
|
+
return { type: 'unknown', message: safeErrorMessage(error) }
|
|
32
|
+
} catch {
|
|
33
|
+
return { type: 'unknown', message: 'Unreadable error object' }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a thrown value is an explicit user rejection. Connectors usually wrap
|
|
39
|
+
* these as `WalletConnectorError{type:'rejected'}`, but a raw EIP-1193 reject
|
|
40
|
+
* (code `4001`) or ethers' `ACTION_REJECTED` can also reach the façade.
|
|
41
|
+
*/
|
|
42
|
+
export function isUserRejection(error: unknown): boolean {
|
|
43
|
+
try {
|
|
44
|
+
if (error instanceof WalletConnectorError) return error.type === 'rejected'
|
|
45
|
+
const code = (error as { code?: unknown } | null | undefined)?.code
|
|
46
|
+
return code === 4001 || code === 'ACTION_REJECTED'
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Classify a thrown wallet error into stable telemetry facets. RELIABLE-ONLY:
|
|
54
|
+
* every branch keys off a structural signal (a typed `WalletConnectorError`, an
|
|
55
|
+
* explicit user rejection, or a known EIP-1193 numeric code). Anything we can't
|
|
56
|
+
* place from that evidence stays `unknown` — we never infer a kind/source from a
|
|
57
|
+
* free-form message, which would be a guess and would also risk leaking content
|
|
58
|
+
* into a structured field.
|
|
59
|
+
*/
|
|
60
|
+
export function classifyError(error: unknown): {
|
|
61
|
+
errorCode?: string
|
|
62
|
+
failureKind: FailureKind
|
|
63
|
+
source: ErrorSource
|
|
64
|
+
} {
|
|
65
|
+
try {
|
|
66
|
+
return classifyErrorUnguarded(error)
|
|
67
|
+
} catch {
|
|
68
|
+
return { failureKind: 'unknown', source: 'unknown' }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function classifyErrorUnguarded(error: unknown): {
|
|
73
|
+
errorCode?: string
|
|
74
|
+
failureKind: FailureKind
|
|
75
|
+
source: ErrorSource
|
|
76
|
+
} {
|
|
77
|
+
const code = (error as { code?: unknown } | null | undefined)?.code
|
|
78
|
+
const hasNumericCode = typeof code === 'number'
|
|
79
|
+
|
|
80
|
+
// Numeric codes pass through; STRING codes are wallet-controlled free-form
|
|
81
|
+
// data headed for structured telemetry fields, so they get the same scrubber
|
|
82
|
+
// as error.message plus a length cap. Known enum-likes ('ACTION_REJECTED')
|
|
83
|
+
// are short and pattern-free — they pass unchanged.
|
|
84
|
+
const errorCode =
|
|
85
|
+
typeof code === 'number'
|
|
86
|
+
? String(code)
|
|
87
|
+
: typeof code === 'string'
|
|
88
|
+
? scrubMessage(code).slice(0, MAX_ERROR_CODE_LENGTH)
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
const source: ErrorSource =
|
|
92
|
+
error instanceof WalletConnectorError
|
|
93
|
+
? 'connector'
|
|
94
|
+
: hasNumericCode
|
|
95
|
+
? 'provider'
|
|
96
|
+
: 'unknown'
|
|
97
|
+
|
|
98
|
+
let failureKind: FailureKind = 'unknown'
|
|
99
|
+
if (isUserRejection(error)) {
|
|
100
|
+
failureKind = 'user_rejected'
|
|
101
|
+
} else if (error instanceof NoCommonNetworkError) {
|
|
102
|
+
// The wallet and configured networks don't overlap — the #1 prod connection
|
|
103
|
+
// failure. Carries its own CAIP-2 id lists; the caller forwards them.
|
|
104
|
+
failureKind = 'no_common_network'
|
|
105
|
+
} else if (
|
|
106
|
+
error instanceof WalletConnectorError &&
|
|
107
|
+
error.type === 'expired'
|
|
108
|
+
) {
|
|
109
|
+
failureKind = 'expired'
|
|
110
|
+
} else if (hasNumericCode) {
|
|
111
|
+
switch (code) {
|
|
112
|
+
case 4200:
|
|
113
|
+
case -32601:
|
|
114
|
+
failureKind = 'unsupported_method'
|
|
115
|
+
break
|
|
116
|
+
case -32602:
|
|
117
|
+
failureKind = 'invalid_params'
|
|
118
|
+
break
|
|
119
|
+
case 4900:
|
|
120
|
+
case 4901:
|
|
121
|
+
failureKind = 'network_error'
|
|
122
|
+
break
|
|
123
|
+
// -32002 = "resource unavailable": a connect/permission request is already
|
|
124
|
+
// pending for this origin. The double-fire race seen in prod (a second
|
|
125
|
+
// connect while one is open) — a provider-level error, not a user reject.
|
|
126
|
+
case -32002:
|
|
127
|
+
failureKind = 'provider_error'
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return errorCode === undefined
|
|
133
|
+
? { failureKind, source }
|
|
134
|
+
: { errorCode, failureKind, source }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether a thrown value is an intentional cancellation (AbortSignal / fetch
|
|
139
|
+
* abort). Matched by `name` rather than `instanceof Error` because a real abort is
|
|
140
|
+
* a `DOMException`, which is not an `Error` subclass in every runtime.
|
|
141
|
+
*/
|
|
142
|
+
export function isAbortError(error: unknown): boolean {
|
|
143
|
+
try {
|
|
144
|
+
return (
|
|
145
|
+
typeof error === 'object' &&
|
|
146
|
+
error !== null &&
|
|
147
|
+
(error as { name?: unknown }).name === 'AbortError'
|
|
148
|
+
)
|
|
149
|
+
} catch {
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import type {
|
|
3
|
+
ConnectionMode,
|
|
4
|
+
Connector,
|
|
5
|
+
Session,
|
|
6
|
+
WalletMetadata
|
|
7
|
+
} from '@meshconnect/uwc-types'
|
|
8
|
+
import { SessionManager } from '../managers/session-manager'
|
|
9
|
+
import {
|
|
10
|
+
validateWalletSession,
|
|
11
|
+
getSessionConnector
|
|
12
|
+
} from './validate-wallet-session'
|
|
13
|
+
|
|
14
|
+
const wallet = { id: 'metamask', name: 'MetaMask' } as WalletMetadata
|
|
15
|
+
|
|
16
|
+
const managerWith = (session: Partial<Session>) => {
|
|
17
|
+
const manager = new SessionManager()
|
|
18
|
+
manager.updateSession(session)
|
|
19
|
+
return manager
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('validateWalletSession', () => {
|
|
23
|
+
it('returns the session when all guards pass', () => {
|
|
24
|
+
const manager = managerWith({
|
|
25
|
+
connectionMode: 'injected',
|
|
26
|
+
activeWallet: wallet,
|
|
27
|
+
activeAddress: '0x123'
|
|
28
|
+
})
|
|
29
|
+
const session = validateWalletSession(manager, { requireAddress: true })
|
|
30
|
+
expect(session.activeWallet).toBe(wallet)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('throws the public-API messages in guard order', () => {
|
|
34
|
+
expect(() =>
|
|
35
|
+
validateWalletSession(new SessionManager(), { requireAddress: true })
|
|
36
|
+
).toThrow('No active wallet')
|
|
37
|
+
expect(() =>
|
|
38
|
+
validateWalletSession(managerWith({ activeWallet: wallet }), {
|
|
39
|
+
requireAddress: true
|
|
40
|
+
})
|
|
41
|
+
).toThrow('No active connection')
|
|
42
|
+
expect(() =>
|
|
43
|
+
validateWalletSession(
|
|
44
|
+
managerWith({ activeWallet: wallet, connectionMode: 'injected' }),
|
|
45
|
+
{ requireAddress: true }
|
|
46
|
+
)
|
|
47
|
+
).toThrow('No active wallet address')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('skips the address guard when requireAddress is false (Solana bytes path)', () => {
|
|
51
|
+
const manager = managerWith({
|
|
52
|
+
connectionMode: 'injected',
|
|
53
|
+
activeWallet: wallet
|
|
54
|
+
})
|
|
55
|
+
expect(
|
|
56
|
+
validateWalletSession(manager, { requireAddress: false }).connectionMode
|
|
57
|
+
).toBe('injected')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('getSessionConnector', () => {
|
|
62
|
+
const session = { connectionMode: 'injected' } as Session
|
|
63
|
+
|
|
64
|
+
it('returns the connector registered for the session mode', () => {
|
|
65
|
+
const connector = { connect: vi.fn() } as unknown as Connector
|
|
66
|
+
const connectors = new Map<ConnectionMode, Connector>([
|
|
67
|
+
['injected', connector]
|
|
68
|
+
])
|
|
69
|
+
expect(getSessionConnector(connectors, session)).toBe(connector)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('throws the public-API message when no connector is registered', () => {
|
|
73
|
+
expect(() => getSessionConnector(new Map(), session)).toThrow(
|
|
74
|
+
'No connector found for connection mode: injected'
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ConnectionMode, Connector, Session } from '@meshconnect/uwc-types'
|
|
2
|
+
import type { SessionManager } from '../managers/session-manager'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session guards shared by the signature/transaction services — the thrown
|
|
6
|
+
* messages are part of the public API and must stay stable. Runs BEFORE
|
|
7
|
+
* `instrumentHandoff` so a pre-wallet failure never enters the handoff funnel.
|
|
8
|
+
* The Solana bytes path deliberately skips the address guard
|
|
9
|
+
* (`requireAddress: false`) — it signs caller-supplied bytes.
|
|
10
|
+
*/
|
|
11
|
+
export function validateWalletSession(
|
|
12
|
+
sessionManager: SessionManager,
|
|
13
|
+
options: { requireAddress: boolean }
|
|
14
|
+
): Session {
|
|
15
|
+
const session = sessionManager.getSession()
|
|
16
|
+
if (!session.activeWallet) {
|
|
17
|
+
throw new Error('No active wallet')
|
|
18
|
+
}
|
|
19
|
+
if (!session.connectionMode) {
|
|
20
|
+
throw new Error('No active connection')
|
|
21
|
+
}
|
|
22
|
+
if (options.requireAddress && !session.activeAddress) {
|
|
23
|
+
throw new Error('No active wallet address')
|
|
24
|
+
}
|
|
25
|
+
return session
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolve the active session's connector — validation, same funnel rule. */
|
|
29
|
+
export function getSessionConnector(
|
|
30
|
+
connectors: Map<ConnectionMode, Connector>,
|
|
31
|
+
session: Session
|
|
32
|
+
): Connector {
|
|
33
|
+
const connector = connectors.get(session.connectionMode!)
|
|
34
|
+
if (!connector) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`No connector found for connection mode: ${session.connectionMode}`
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return connector
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { UWC_CORE_VERSION } from './version'
|
|
3
|
+
import pkg from '../package.json'
|
|
4
|
+
|
|
5
|
+
describe('UWC_CORE_VERSION', () => {
|
|
6
|
+
it('matches package.json (stale? run: node scripts/generate-version.mjs)', () => {
|
|
7
|
+
// The constant is generated from package.json at build / version-packages
|
|
8
|
+
// time; this guard catches a manual bump that skipped regeneration.
|
|
9
|
+
expect(UWC_CORE_VERSION).toBe(pkg.version)
|
|
10
|
+
})
|
|
11
|
+
})
|
package/src/version.ts
ADDED