@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
|
@@ -3,86 +3,83 @@ import type {
|
|
|
3
3
|
Connector,
|
|
4
4
|
ExtensionInjectedProvider,
|
|
5
5
|
IntegratedBrowserInjectedProvider,
|
|
6
|
-
WalletConnectProvider,
|
|
7
6
|
SupportedProvider,
|
|
8
7
|
SignatureType,
|
|
9
8
|
EIP712TypedData
|
|
10
9
|
} from '@meshconnect/uwc-types'
|
|
11
10
|
import type { SessionManager } from '../managers/session-manager'
|
|
11
|
+
import type { EventManager } from '../managers/event-manager'
|
|
12
12
|
import type { ServiceOptions } from './connection-service'
|
|
13
|
+
import {
|
|
14
|
+
instrumentHandoff,
|
|
15
|
+
sessionHandoffContext
|
|
16
|
+
} from '../observability/instrument-handoff'
|
|
17
|
+
import { resolveSessionProvider } from '../utils/resolve-session-provider'
|
|
18
|
+
import {
|
|
19
|
+
validateWalletSession,
|
|
20
|
+
getSessionConnector
|
|
21
|
+
} from '../utils/validate-wallet-session'
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
|
-
* Service for handling signature operations across different connector types
|
|
24
|
+
* Service for handling signature operations across different connector types.
|
|
25
|
+
*
|
|
26
|
+
* Owns the WHOLE wallet-boundary op — validation (session guards, connector
|
|
27
|
+
* capability, provider resolution) BEFORE the funnel, then the instrumented
|
|
28
|
+
* connector call — the same layering as ConnectionService / NetworkSwitchService,
|
|
29
|
+
* so a pre-wallet failure never emits an `awaitingWallet`/terminal pair.
|
|
16
30
|
*/
|
|
17
31
|
export class SignatureService {
|
|
18
32
|
constructor(
|
|
19
33
|
private sessionManager: SessionManager,
|
|
20
|
-
private connectors: Map<ConnectionMode, Connector
|
|
34
|
+
private connectors: Map<ConnectionMode, Connector>,
|
|
35
|
+
private usingIntegratedBrowser: boolean,
|
|
36
|
+
private eventManager: EventManager,
|
|
37
|
+
// Observational wallet-response deadline for the handoff timeout marker.
|
|
38
|
+
private walletResponseTimeoutMs: number
|
|
21
39
|
) {}
|
|
22
40
|
|
|
23
41
|
/**
|
|
24
42
|
* Sign a message with the currently connected wallet
|
|
25
43
|
* @param message The message to sign
|
|
26
|
-
* @param provider The wallet provider to use for signing (required for injected connector)
|
|
27
44
|
* @param options Optional cancellation signal
|
|
28
45
|
* @returns A promise that resolves to the signature
|
|
29
46
|
*/
|
|
30
47
|
async signMessage(
|
|
31
48
|
message: string,
|
|
32
|
-
provider?: SupportedProvider,
|
|
33
49
|
options?: ServiceOptions
|
|
34
50
|
): Promise<SignatureType> {
|
|
35
51
|
options?.signal?.throwIfAborted()
|
|
36
|
-
const session = this.sessionManager
|
|
52
|
+
const session = validateWalletSession(this.sessionManager, {
|
|
53
|
+
requireAddress: true
|
|
54
|
+
})
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
throw new Error('No active connection')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!session.activeAddress) {
|
|
43
|
-
throw new Error('No active wallet address')
|
|
44
|
-
}
|
|
45
|
-
|
|
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
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check if the connector supports signing
|
|
56
|
+
const connector = getSessionConnector(this.connectors, session)
|
|
55
57
|
if (!connector.signMessage) {
|
|
56
58
|
throw new Error(`Connector does not support message signing`)
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
signature = await connector.signMessage(
|
|
66
|
-
message,
|
|
67
|
-
provider as
|
|
68
|
-
| ExtensionInjectedProvider
|
|
69
|
-
| IntegratedBrowserInjectedProvider
|
|
70
|
-
)
|
|
71
|
-
} else if (session.connectionMode === 'tonConnect') {
|
|
72
|
-
signature = await connector.signMessage(message)
|
|
73
|
-
} else if (session.connectionMode === 'walletConnect') {
|
|
74
|
-
if (!provider) {
|
|
75
|
-
throw new Error('Provider is required for WalletConnect connector')
|
|
76
|
-
}
|
|
77
|
-
signature = await connector.signMessage(
|
|
78
|
-
message,
|
|
79
|
-
provider as WalletConnectProvider
|
|
80
|
-
)
|
|
81
|
-
} else {
|
|
82
|
-
// For other connectors that might not need the provider
|
|
83
|
-
signature = await connector.signMessage(message)
|
|
84
|
-
}
|
|
61
|
+
// Validation, not wallet work — a missing provider must not enter the funnel.
|
|
62
|
+
// undefined only for tonConnect, whose connector resolves its own provider.
|
|
63
|
+
const provider = resolveSessionProvider(
|
|
64
|
+
session,
|
|
65
|
+
this.usingIntegratedBrowser
|
|
66
|
+
)
|
|
85
67
|
|
|
68
|
+
const signature = await instrumentHandoff(
|
|
69
|
+
{
|
|
70
|
+
eventManager: this.eventManager,
|
|
71
|
+
timeoutMs: this.walletResponseTimeoutMs,
|
|
72
|
+
signal: options?.signal
|
|
73
|
+
},
|
|
74
|
+
sessionHandoffContext('signMessage', session),
|
|
75
|
+
// tonConnect resolves undefined and keeps its message-only call shape.
|
|
76
|
+
() =>
|
|
77
|
+
provider === undefined
|
|
78
|
+
? connector.signMessage!(message)
|
|
79
|
+
: connector.signMessage!(message, provider)
|
|
80
|
+
)
|
|
81
|
+
// AFTER the funnel: the wallet settled, so its truthful terminal is already
|
|
82
|
+
// recorded; an abort that raced completion still rejects the caller here.
|
|
86
83
|
options?.signal?.throwIfAborted()
|
|
87
84
|
return signature
|
|
88
85
|
}
|
|
@@ -91,52 +88,45 @@ export class SignatureService {
|
|
|
91
88
|
* Sign EIP-712 typed structured data with the currently connected wallet.
|
|
92
89
|
* EVM-only (eip155); routes by connection mode. Throws for tonConnect.
|
|
93
90
|
* @param typedData The EIP-712 typed data to sign
|
|
94
|
-
* @param provider The wallet provider (required for the injected connector)
|
|
95
91
|
* @returns A promise that resolves to the 65-byte hex signature
|
|
96
92
|
*/
|
|
97
|
-
async signTypedData(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
async signTypedData(typedData: EIP712TypedData): Promise<string> {
|
|
94
|
+
const session = validateWalletSession(this.sessionManager, {
|
|
95
|
+
requireAddress: true
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const connector = getSessionConnector(this.connectors, session)
|
|
99
|
+
// TON first: TonConnectConnector has no signTypedData, so the capability
|
|
100
|
+
// guard would otherwise shadow this branch with the generic message.
|
|
101
|
+
if (session.connectionMode === 'tonConnect') {
|
|
102
|
+
throw new Error('signTypedData is not supported for TON wallets')
|
|
105
103
|
}
|
|
106
|
-
|
|
107
|
-
if (!session.activeAddress) {
|
|
108
|
-
throw new Error('No active wallet address')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const connector = this.connectors.get(session.connectionMode)
|
|
112
|
-
if (!connector) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`No connector found for connection mode: ${session.connectionMode}`
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
104
|
if (!connector.signTypedData) {
|
|
119
105
|
throw new Error(`Connector does not support typed data signing`)
|
|
120
106
|
}
|
|
121
107
|
|
|
108
|
+
// Only the injected path resolves (and validates) a provider — WalletConnect
|
|
109
|
+
// resolves its own inside the connector.
|
|
110
|
+
let provider: SupportedProvider | undefined
|
|
122
111
|
if (session.connectionMode === 'injected') {
|
|
123
|
-
|
|
124
|
-
throw new Error('Provider is required for injected connector')
|
|
125
|
-
}
|
|
126
|
-
return await connector.signTypedData(
|
|
127
|
-
typedData,
|
|
128
|
-
provider as
|
|
129
|
-
| ExtensionInjectedProvider
|
|
130
|
-
| IntegratedBrowserInjectedProvider
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (session.connectionMode === 'walletConnect') {
|
|
135
|
-
return await connector.signTypedData(typedData)
|
|
112
|
+
provider = resolveSessionProvider(session, this.usingIntegratedBrowser)
|
|
136
113
|
}
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
return await instrumentHandoff(
|
|
116
|
+
{
|
|
117
|
+
eventManager: this.eventManager,
|
|
118
|
+
timeoutMs: this.walletResponseTimeoutMs
|
|
119
|
+
},
|
|
120
|
+
sessionHandoffContext('signTypedData', session),
|
|
121
|
+
() =>
|
|
122
|
+
session.connectionMode === 'injected'
|
|
123
|
+
? connector.signTypedData!(
|
|
124
|
+
typedData,
|
|
125
|
+
provider as
|
|
126
|
+
| ExtensionInjectedProvider
|
|
127
|
+
| IntegratedBrowserInjectedProvider
|
|
128
|
+
)
|
|
129
|
+
: connector.signTypedData!(typedData)
|
|
140
130
|
)
|
|
141
131
|
}
|
|
142
132
|
}
|
|
@@ -1,20 +1,65 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
2
|
import { TransactionService } from './transaction-service'
|
|
3
3
|
import { SessionManager } from '../managers/session-manager'
|
|
4
|
+
import { EventManager } from '../managers/event-manager'
|
|
4
5
|
import type {
|
|
5
6
|
ConnectionMode,
|
|
6
7
|
Connector,
|
|
7
|
-
TransactionRequest
|
|
8
|
+
TransactionRequest,
|
|
9
|
+
WalletMetadata
|
|
8
10
|
} from '../../../types/src'
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
// Same layering contract as SignatureService: the service owns validation
|
|
13
|
+
// (session guards, connector capability, provider resolution) BEFORE the
|
|
14
|
+
// funnel, then the instrumented connector call.
|
|
15
|
+
|
|
16
|
+
const injectedWallet = {
|
|
17
|
+
id: 'metamask',
|
|
18
|
+
name: 'MetaMask',
|
|
19
|
+
extensionInjectedProvider: { supportedNetworkIds: ['eip155:1'] }
|
|
20
|
+
} as unknown as WalletMetadata
|
|
21
|
+
|
|
22
|
+
const walletWithoutProviders = {
|
|
23
|
+
id: 'metamask',
|
|
24
|
+
name: 'MetaMask'
|
|
25
|
+
} as unknown as WalletMetadata
|
|
26
|
+
|
|
27
|
+
const request: TransactionRequest = {
|
|
28
|
+
to: '0x456',
|
|
29
|
+
value: BigInt('1000000000000000000'),
|
|
30
|
+
from: '0x123'
|
|
31
|
+
} as unknown as TransactionRequest
|
|
32
|
+
|
|
33
|
+
describe('TransactionService.sendTransaction', () => {
|
|
11
34
|
let transactionService: TransactionService
|
|
12
35
|
let sessionManager: SessionManager
|
|
36
|
+
let eventManager: EventManager
|
|
13
37
|
let mockConnector: Connector
|
|
14
38
|
let connectors: Map<ConnectionMode, Connector>
|
|
39
|
+
let events: string[]
|
|
40
|
+
|
|
41
|
+
const connectSession = (
|
|
42
|
+
mode: ConnectionMode = 'injected',
|
|
43
|
+
wallet: WalletMetadata = injectedWallet
|
|
44
|
+
) =>
|
|
45
|
+
sessionManager.updateSession({
|
|
46
|
+
connectionMode: mode,
|
|
47
|
+
activeWallet: wallet,
|
|
48
|
+
activeAddress: '0x123'
|
|
49
|
+
})
|
|
15
50
|
|
|
16
51
|
beforeEach(() => {
|
|
17
52
|
sessionManager = new SessionManager()
|
|
53
|
+
eventManager = new EventManager()
|
|
54
|
+
events = []
|
|
55
|
+
for (const name of [
|
|
56
|
+
'awaitingWallet',
|
|
57
|
+
'walletSucceeded',
|
|
58
|
+
'walletRejected',
|
|
59
|
+
'walletFailed'
|
|
60
|
+
] as const) {
|
|
61
|
+
eventManager.on(name, () => events.push(name))
|
|
62
|
+
}
|
|
18
63
|
|
|
19
64
|
mockConnector = {
|
|
20
65
|
connect: vi.fn(),
|
|
@@ -32,59 +77,82 @@ describe('TransactionService', () => {
|
|
|
32
77
|
connectors = new Map()
|
|
33
78
|
connectors.set('injected', mockConnector)
|
|
34
79
|
|
|
35
|
-
transactionService = new TransactionService(
|
|
80
|
+
transactionService = new TransactionService(
|
|
81
|
+
sessionManager,
|
|
82
|
+
connectors,
|
|
83
|
+
false,
|
|
84
|
+
eventManager,
|
|
85
|
+
60_000
|
|
86
|
+
)
|
|
36
87
|
})
|
|
37
88
|
|
|
38
|
-
it('
|
|
39
|
-
expect(transactionService).
|
|
89
|
+
it('throws "No active wallet" first on an empty session (public façade message)', async () => {
|
|
90
|
+
await expect(transactionService.sendTransaction(request)).rejects.toThrow(
|
|
91
|
+
'No active wallet'
|
|
92
|
+
)
|
|
93
|
+
expect(events).toEqual([])
|
|
40
94
|
})
|
|
41
95
|
|
|
42
|
-
it('
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
96
|
+
it('records walletSucceeded when an abort races completion (the tx already broadcast)', async () => {
|
|
97
|
+
// The transaction went OUT — suppressing the terminal would under-count
|
|
98
|
+
// real sends; the abort still rejects the caller, after the terminal.
|
|
99
|
+
connectSession()
|
|
100
|
+
const controller = new AbortController()
|
|
101
|
+
vi.mocked(mockConnector.sendTransaction!).mockImplementation(async () => {
|
|
102
|
+
controller.abort()
|
|
103
|
+
return { hash: '0xtxhash' } as never
|
|
104
|
+
})
|
|
48
105
|
|
|
49
|
-
await expect(
|
|
50
|
-
|
|
51
|
-
|
|
106
|
+
await expect(
|
|
107
|
+
transactionService.sendTransaction(request, {
|
|
108
|
+
signal: controller.signal
|
|
109
|
+
})
|
|
110
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
111
|
+
expect(events).toEqual(['awaitingWallet', 'walletSucceeded'])
|
|
52
112
|
})
|
|
53
113
|
|
|
54
|
-
it('
|
|
55
|
-
|
|
56
|
-
|
|
114
|
+
it('rejects with a CUSTOM abort reason after broadcast — success terminal, no spurious failure', async () => {
|
|
115
|
+
connectSession()
|
|
116
|
+
const controller = new AbortController()
|
|
117
|
+
vi.mocked(mockConnector.sendTransaction!).mockImplementation(async () => {
|
|
118
|
+
controller.abort(new Error('nav away'))
|
|
119
|
+
return { hash: '0xtxhash' } as never
|
|
57
120
|
})
|
|
58
121
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
122
|
+
await expect(
|
|
123
|
+
transactionService.sendTransaction(request, {
|
|
124
|
+
signal: controller.signal
|
|
125
|
+
})
|
|
126
|
+
).rejects.toThrow('nav away')
|
|
127
|
+
expect(events).toEqual(['awaitingWallet', 'walletSucceeded'])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('throws when no active address', async () => {
|
|
131
|
+
sessionManager.updateSession({
|
|
132
|
+
connectionMode: 'injected',
|
|
133
|
+
activeWallet: injectedWallet
|
|
134
|
+
})
|
|
64
135
|
|
|
65
136
|
await expect(transactionService.sendTransaction(request)).rejects.toThrow(
|
|
66
137
|
'No active wallet address'
|
|
67
138
|
)
|
|
139
|
+
expect(events).toEqual([])
|
|
68
140
|
})
|
|
69
141
|
|
|
70
|
-
it('
|
|
142
|
+
it('throws when connector not found', async () => {
|
|
71
143
|
sessionManager.updateSession({
|
|
72
144
|
connectionMode: 'unknown' as ConnectionMode,
|
|
145
|
+
activeWallet: injectedWallet,
|
|
73
146
|
activeAddress: '0x123'
|
|
74
147
|
})
|
|
75
148
|
|
|
76
|
-
const request: TransactionRequest = {
|
|
77
|
-
to: '0x456',
|
|
78
|
-
value: BigInt('1000000000000000000'),
|
|
79
|
-
from: '0x123'
|
|
80
|
-
}
|
|
81
|
-
|
|
82
149
|
await expect(transactionService.sendTransaction(request)).rejects.toThrow(
|
|
83
150
|
'No connector found for connection mode: unknown'
|
|
84
151
|
)
|
|
152
|
+
expect(events).toEqual([])
|
|
85
153
|
})
|
|
86
154
|
|
|
87
|
-
it('
|
|
155
|
+
it('throws when connector does not support transactions', async () => {
|
|
88
156
|
const connectorWithoutTx = {
|
|
89
157
|
connect: vi.fn(),
|
|
90
158
|
disconnect: vi.fn(),
|
|
@@ -92,37 +160,29 @@ describe('TransactionService', () => {
|
|
|
92
160
|
switchNetwork: vi.fn()
|
|
93
161
|
}
|
|
94
162
|
connectors.set('injected', connectorWithoutTx)
|
|
163
|
+
connectSession()
|
|
95
164
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
165
|
+
await expect(transactionService.sendTransaction(request)).rejects.toThrow(
|
|
166
|
+
'Connector does not support transactions'
|
|
167
|
+
)
|
|
168
|
+
expect(events).toEqual([])
|
|
169
|
+
})
|
|
100
170
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
value: BigInt('1000000000000000000'),
|
|
104
|
-
from: '0x123'
|
|
105
|
-
}
|
|
171
|
+
it('a missing provider fails BEFORE the funnel (validation, not a wallet outcome)', async () => {
|
|
172
|
+
connectSession('injected', walletWithoutProviders)
|
|
106
173
|
|
|
107
174
|
await expect(transactionService.sendTransaction(request)).rejects.toThrow(
|
|
108
|
-
'
|
|
175
|
+
'No provider available for the active wallet'
|
|
109
176
|
)
|
|
177
|
+
expect(events).toEqual([])
|
|
178
|
+
expect(mockConnector.sendTransaction).not.toHaveBeenCalled()
|
|
110
179
|
})
|
|
111
180
|
|
|
112
|
-
it('
|
|
113
|
-
|
|
114
|
-
connectionMode: 'injected',
|
|
115
|
-
activeAddress: '0x123'
|
|
116
|
-
})
|
|
181
|
+
it('sends and emits awaitingWallet → walletSucceeded around the wallet call', async () => {
|
|
182
|
+
connectSession()
|
|
117
183
|
|
|
118
|
-
const
|
|
119
|
-
to: '0x456',
|
|
120
|
-
value: BigInt('1000000000000000000'),
|
|
121
|
-
from: '0x123'
|
|
122
|
-
}
|
|
184
|
+
const result = await transactionService.sendTransaction(request)
|
|
123
185
|
|
|
124
|
-
const provider = {} as any
|
|
125
|
-
const result = await transactionService.sendTransaction(request, provider)
|
|
126
186
|
expect(result).toEqual({
|
|
127
187
|
hash: '0xtxhash',
|
|
128
188
|
from: '0x123',
|
|
@@ -131,33 +191,174 @@ describe('TransactionService', () => {
|
|
|
131
191
|
})
|
|
132
192
|
expect(mockConnector.sendTransaction).toHaveBeenCalledWith(
|
|
133
193
|
request,
|
|
134
|
-
|
|
194
|
+
injectedWallet.extensionInjectedProvider
|
|
135
195
|
)
|
|
196
|
+
expect(events).toEqual(['awaitingWallet', 'walletSucceeded'])
|
|
136
197
|
})
|
|
137
198
|
|
|
138
|
-
it('
|
|
199
|
+
it('a user rejection closes the handoff as walletRejected', async () => {
|
|
200
|
+
connectSession()
|
|
201
|
+
vi.mocked(mockConnector.sendTransaction!).mockRejectedValueOnce({
|
|
202
|
+
code: 4001
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await expect(transactionService.sendTransaction(request)).rejects.toEqual({
|
|
206
|
+
code: 4001
|
|
207
|
+
})
|
|
208
|
+
expect(events).toEqual(['awaitingWallet', 'walletRejected'])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('tonConnect enters the funnel with the connector resolving its own provider', async () => {
|
|
212
|
+
connectors.set('tonConnect', mockConnector)
|
|
213
|
+
connectSession('tonConnect', walletWithoutProviders)
|
|
214
|
+
|
|
215
|
+
await transactionService.sendTransaction(request)
|
|
216
|
+
|
|
217
|
+
expect(mockConnector.sendTransaction).toHaveBeenCalledWith(request)
|
|
218
|
+
expect(events).toEqual(['awaitingWallet', 'walletSucceeded'])
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('TransactionService.signSolanaTransactionBytes', () => {
|
|
223
|
+
let transactionService: TransactionService
|
|
224
|
+
let sessionManager: SessionManager
|
|
225
|
+
let eventManager: EventManager
|
|
226
|
+
let mockConnector: Connector
|
|
227
|
+
let connectors: Map<ConnectionMode, Connector>
|
|
228
|
+
let events: string[]
|
|
229
|
+
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
sessionManager = new SessionManager()
|
|
232
|
+
eventManager = new EventManager()
|
|
233
|
+
events = []
|
|
234
|
+
for (const name of [
|
|
235
|
+
'awaitingWallet',
|
|
236
|
+
'walletSucceeded',
|
|
237
|
+
'walletFailed'
|
|
238
|
+
] as const) {
|
|
239
|
+
eventManager.on(name, () => events.push(name))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
mockConnector = {
|
|
243
|
+
connect: vi.fn(),
|
|
244
|
+
disconnect: vi.fn(),
|
|
245
|
+
signMessage: vi.fn(),
|
|
246
|
+
sendTransaction: vi.fn(),
|
|
247
|
+
switchNetwork: vi.fn(),
|
|
248
|
+
signSolanaTransactionBytes: vi.fn().mockResolvedValue(new Uint8Array([9]))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
connectors = new Map()
|
|
252
|
+
connectors.set('injected', mockConnector)
|
|
253
|
+
|
|
254
|
+
transactionService = new TransactionService(
|
|
255
|
+
sessionManager,
|
|
256
|
+
connectors,
|
|
257
|
+
false,
|
|
258
|
+
eventManager,
|
|
259
|
+
60_000
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('throws "No active wallet" on an empty session', async () => {
|
|
264
|
+
await expect(
|
|
265
|
+
transactionService.signSolanaTransactionBytes(new Uint8Array([1]))
|
|
266
|
+
).rejects.toThrow('No active wallet')
|
|
267
|
+
expect(events).toEqual([])
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('throws "No active connection" when the session has no connectionMode', async () => {
|
|
271
|
+
sessionManager.updateSession({ activeWallet: injectedWallet })
|
|
272
|
+
|
|
273
|
+
await expect(
|
|
274
|
+
transactionService.signSolanaTransactionBytes(new Uint8Array([1]))
|
|
275
|
+
).rejects.toThrow('No active connection')
|
|
276
|
+
expect(events).toEqual([])
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('throws "not supported" when connectionMode maps to no registered connector', async () => {
|
|
139
280
|
sessionManager.updateSession({
|
|
140
|
-
connectionMode: '
|
|
281
|
+
connectionMode: 'walletConnect',
|
|
282
|
+
activeWallet: injectedWallet,
|
|
141
283
|
activeAddress: '0x123'
|
|
142
284
|
})
|
|
143
285
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
286
|
+
await expect(
|
|
287
|
+
transactionService.signSolanaTransactionBytes(new Uint8Array([1]))
|
|
288
|
+
).rejects.toThrow(
|
|
289
|
+
'signSolanaTransaction is not supported for connection mode "walletConnect"'
|
|
290
|
+
)
|
|
291
|
+
expect(events).toEqual([])
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('routes serializedTx to the walletConnect connector when active', async () => {
|
|
295
|
+
const SIGNED_TX = new Uint8Array([6, 7, 8])
|
|
296
|
+
const wcConnector = {
|
|
297
|
+
connect: vi.fn(),
|
|
298
|
+
disconnect: vi.fn(),
|
|
299
|
+
switchNetwork: vi.fn(),
|
|
300
|
+
signSolanaTransactionBytes: vi.fn().mockResolvedValue(SIGNED_TX)
|
|
148
301
|
}
|
|
302
|
+
connectors.set('walletConnect', wcConnector)
|
|
303
|
+
sessionManager.updateSession({
|
|
304
|
+
connectionMode: 'walletConnect',
|
|
305
|
+
activeWallet: injectedWallet,
|
|
306
|
+
activeAddress: '0x123'
|
|
307
|
+
})
|
|
308
|
+
const bytes = new Uint8Array([1, 2, 3])
|
|
149
309
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
expect(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
310
|
+
const result = await transactionService.signSolanaTransactionBytes(bytes)
|
|
311
|
+
|
|
312
|
+
expect(wcConnector.signSolanaTransactionBytes).toHaveBeenCalledWith(bytes)
|
|
313
|
+
expect(result).toBe(SIGNED_TX)
|
|
314
|
+
expect(events).toEqual(['awaitingWallet', 'walletSucceeded'])
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('INVARIANT: connector.sendTransaction is never called on the sign-only path', async () => {
|
|
318
|
+
sessionManager.updateSession({
|
|
319
|
+
connectionMode: 'injected',
|
|
320
|
+
activeWallet: injectedWallet,
|
|
321
|
+
activeAddress: '0x123'
|
|
157
322
|
})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
323
|
+
|
|
324
|
+
await transactionService.signSolanaTransactionBytes(new Uint8Array([1]))
|
|
325
|
+
|
|
326
|
+
expect(mockConnector.sendTransaction).not.toHaveBeenCalled()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('throws the façade message when the connector lacks the capability, outside the funnel', async () => {
|
|
330
|
+
const connectorWithoutSolana = {
|
|
331
|
+
connect: vi.fn(),
|
|
332
|
+
disconnect: vi.fn(),
|
|
333
|
+
switchNetwork: vi.fn()
|
|
334
|
+
}
|
|
335
|
+
connectors.set('injected', connectorWithoutSolana)
|
|
336
|
+
sessionManager.updateSession({
|
|
337
|
+
connectionMode: 'injected',
|
|
338
|
+
activeWallet: injectedWallet,
|
|
339
|
+
activeAddress: '0x123'
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await expect(
|
|
343
|
+
transactionService.signSolanaTransactionBytes(new Uint8Array([1]))
|
|
344
|
+
).rejects.toThrow(
|
|
345
|
+
'signSolanaTransaction is not supported for connection mode "injected"'
|
|
161
346
|
)
|
|
347
|
+
expect(events).toEqual([])
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('signs (never broadcasts) inside the funnel', async () => {
|
|
351
|
+
sessionManager.updateSession({
|
|
352
|
+
connectionMode: 'injected',
|
|
353
|
+
activeWallet: injectedWallet,
|
|
354
|
+
activeAddress: '0x123'
|
|
355
|
+
})
|
|
356
|
+
const bytes = new Uint8Array([1, 2, 3])
|
|
357
|
+
|
|
358
|
+
const signed = await transactionService.signSolanaTransactionBytes(bytes)
|
|
359
|
+
|
|
360
|
+
expect(signed).toEqual(new Uint8Array([9]))
|
|
361
|
+
expect(mockConnector.signSolanaTransactionBytes).toHaveBeenCalledWith(bytes)
|
|
362
|
+
expect(events).toEqual(['awaitingWallet', 'walletSucceeded'])
|
|
162
363
|
})
|
|
163
364
|
})
|