walletpair-sdk 1.0.2 → 1.0.5
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 +13 -0
- package/dist/ble/framing.d.ts.map +1 -1
- package/dist/ble/framing.js +2 -2
- package/dist/ble/framing.js.map +1 -1
- package/dist/ble/index.d.ts +2 -2
- package/dist/ble/index.d.ts.map +1 -1
- package/dist/ble/index.js +2 -2
- package/dist/ble/index.js.map +1 -1
- package/dist/ble/web-ble-transport.d.ts +1 -1
- package/dist/ble/web-ble-transport.d.ts.map +1 -1
- package/dist/ble/web-ble-transport.js +23 -12
- package/dist/ble/web-ble-transport.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +29 -12
- package/dist/crypto.js.map +1 -1
- package/dist/dapp-session.d.ts.map +1 -1
- package/dist/dapp-session.js +15 -5
- package/dist/dapp-session.js.map +1 -1
- package/dist/emitter.d.ts +1 -3
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +4 -2
- package/dist/emitter.js.map +1 -1
- package/dist/evm/eip1193.d.ts +2 -2
- package/dist/evm/eip1193.d.ts.map +1 -1
- package/dist/evm/eip1193.js +32 -18
- package/dist/evm/eip1193.js.map +1 -1
- package/dist/evm/index.d.ts +2 -2
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js.map +1 -1
- package/dist/wallet-session.d.ts.map +1 -1
- package/dist/wallet-session.js +4 -3
- package/dist/wallet-session.js.map +1 -1
- package/dist/ws-transport.d.ts +3 -2
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +13 -4
- package/dist/ws-transport.js.map +1 -1
- package/package.json +20 -1
- package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
- package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
- package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
- package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
- package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
- package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
- package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
- package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
- package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
- package/src/ble/framing.test.ts +122 -114
- package/src/ble/framing.ts +48 -51
- package/src/ble/index.ts +7 -7
- package/src/ble/web-ble-transport.test.ts +93 -84
- package/src/ble/web-ble-transport.ts +70 -57
- package/src/ble/web-bluetooth.d.ts +19 -19
- package/src/canonical-json.test.ts +301 -285
- package/src/crypto-directional.test.ts +155 -129
- package/src/crypto-hardening.test.ts +292 -283
- package/src/crypto.test.ts +364 -346
- package/src/crypto.ts +185 -175
- package/src/dapp-session.test.ts +522 -385
- package/src/dapp-session.ts +17 -11
- package/src/emitter.test.ts +122 -122
- package/src/emitter.ts +20 -18
- package/src/evm/eip1193.test.ts +283 -205
- package/src/evm/eip1193.ts +162 -138
- package/src/evm/index.ts +5 -5
- package/src/evm/wagmi.test.ts +1 -1
- package/src/integration.test.ts +329 -201
- package/src/security.test.ts +331 -238
- package/src/sequence-validation.test.ts +6 -9
- package/src/test-helpers.ts +102 -78
- package/src/types.test.ts +45 -50
- package/src/wallet-session.test.ts +611 -383
- package/src/wallet-session.ts +7 -9
- package/src/ws-transport.test.ts +141 -139
- package/src/ws-transport.ts +52 -41
|
@@ -1,39 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { MockTransport } from './test-helpers.js';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { SessionCryptoContext } from './crypto.js'
|
|
4
3
|
import {
|
|
5
|
-
|
|
6
|
-
generateChannelId,
|
|
4
|
+
b64urlDecode,
|
|
7
5
|
buildPairingUri,
|
|
6
|
+
computeSessionFingerprint,
|
|
8
7
|
computeSharedSecret,
|
|
9
|
-
deriveSessionKey,
|
|
10
8
|
deriveDirectionalSessionKeys,
|
|
11
9
|
deriveJoinEncryptionKey,
|
|
12
|
-
|
|
10
|
+
deriveSessionKey,
|
|
11
|
+
generateChannelId,
|
|
12
|
+
generateX25519KeyPair,
|
|
13
13
|
sealPayload,
|
|
14
|
-
unsealPayload,
|
|
15
14
|
unsealJoin,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from './
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function flushMicrotasks(): Promise<void> {
|
|
25
|
-
return new Promise((r) => setTimeout(r, 10));
|
|
15
|
+
unsealPayload,
|
|
16
|
+
} from './crypto.js'
|
|
17
|
+
import { MockTransport } from './test-helpers.js'
|
|
18
|
+
import type { ProtocolMessage } from './types.js'
|
|
19
|
+
import { WalletSession } from './wallet-session.js'
|
|
20
|
+
|
|
21
|
+
function _flushMicrotasks(): Promise<void> {
|
|
22
|
+
return new Promise((r) => setTimeout(r, 10))
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
describe('WalletSession', () => {
|
|
29
|
-
let transport: MockTransport
|
|
30
|
-
let session: WalletSession
|
|
31
|
-
let dappKp: ReturnType<typeof generateX25519KeyPair
|
|
32
|
-
let channelId: string
|
|
33
|
-
let relayUrl: string
|
|
26
|
+
let transport: MockTransport
|
|
27
|
+
let session: WalletSession
|
|
28
|
+
let dappKp: ReturnType<typeof generateX25519KeyPair>
|
|
29
|
+
let channelId: string
|
|
30
|
+
let relayUrl: string
|
|
34
31
|
|
|
35
32
|
beforeEach(() => {
|
|
36
|
-
transport = new MockTransport()
|
|
33
|
+
transport = new MockTransport()
|
|
37
34
|
session = new WalletSession({
|
|
38
35
|
transport,
|
|
39
36
|
capabilities: {
|
|
@@ -41,12 +38,18 @@ describe('WalletSession', () => {
|
|
|
41
38
|
events: ['accountsChanged', 'chainChanged'],
|
|
42
39
|
chains: ['eip155:1'],
|
|
43
40
|
},
|
|
44
|
-
meta: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
meta: {
|
|
42
|
+
name: 'Test Wallet',
|
|
43
|
+
description: 'Test',
|
|
44
|
+
url: 'https://test.com',
|
|
45
|
+
icon: 'https://test.com/icon.png',
|
|
46
|
+
address: '0xtest',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
dappKp = generateX25519KeyPair()
|
|
50
|
+
channelId = generateChannelId()
|
|
51
|
+
relayUrl = 'ws://localhost:8080/v1'
|
|
52
|
+
})
|
|
50
53
|
|
|
51
54
|
function makePairingUri(): string {
|
|
52
55
|
return buildPairingUri({
|
|
@@ -56,99 +59,107 @@ describe('WalletSession', () => {
|
|
|
56
59
|
name: 'Test dApp',
|
|
57
60
|
url: 'https://test.com',
|
|
58
61
|
icon: 'https://test.com/icon.png',
|
|
59
|
-
})
|
|
62
|
+
})
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function receiveConnected(): void {
|
|
63
66
|
transport.receive({
|
|
64
|
-
v: 1,
|
|
65
|
-
|
|
67
|
+
v: 1,
|
|
68
|
+
t: 'ready',
|
|
69
|
+
ch: channelId,
|
|
70
|
+
ts: Date.now(),
|
|
71
|
+
from: '_adapter',
|
|
66
72
|
body: { state: 'connected', reconnect: false, remote: dappKp.publicKeyB64 },
|
|
67
|
-
} as ProtocolMessage)
|
|
73
|
+
} as ProtocolMessage)
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
describe('joinFromUri', () => {
|
|
71
77
|
it('starts in idle phase', () => {
|
|
72
|
-
expect(session.phase).toBe('idle')
|
|
73
|
-
})
|
|
78
|
+
expect(session.phase).toBe('idle')
|
|
79
|
+
})
|
|
74
80
|
|
|
75
81
|
it('parses URI and sends join message with sealed_join', async () => {
|
|
76
|
-
const uri = makePairingUri()
|
|
77
|
-
await session.joinFromUri(uri)
|
|
82
|
+
const uri = makePairingUri()
|
|
83
|
+
await session.joinFromUri(uri)
|
|
78
84
|
|
|
79
|
-
expect(session.channelId).toBe(channelId)
|
|
80
|
-
expect(session.phase).toBe('waiting_accept')
|
|
85
|
+
expect(session.channelId).toBe(channelId)
|
|
86
|
+
expect(session.phase).toBe('waiting_accept')
|
|
81
87
|
|
|
82
|
-
const joinMsg = transport.sent.find(m => m.t === 'join')
|
|
83
|
-
expect(joinMsg).toBeTruthy()
|
|
88
|
+
const joinMsg = transport.sent.find((m) => m.t === 'join')
|
|
89
|
+
expect(joinMsg).toBeTruthy()
|
|
84
90
|
// Capabilities are now inside sealed_join, not plaintext
|
|
85
|
-
expect((joinMsg as any).body.sealed_join).toBeTruthy()
|
|
86
|
-
})
|
|
91
|
+
expect((joinMsg as any).body.sealed_join).toBeTruthy()
|
|
92
|
+
})
|
|
87
93
|
|
|
88
94
|
it('computes and emits session fingerprint', async () => {
|
|
89
|
-
const handler = vi.fn()
|
|
90
|
-
session.on('sessionFingerprint', handler)
|
|
95
|
+
const handler = vi.fn()
|
|
96
|
+
session.on('sessionFingerprint', handler)
|
|
91
97
|
|
|
92
|
-
const uri = makePairingUri()
|
|
93
|
-
await session.joinFromUri(uri)
|
|
98
|
+
const uri = makePairingUri()
|
|
99
|
+
await session.joinFromUri(uri)
|
|
94
100
|
|
|
95
|
-
expect(handler).toHaveBeenCalled()
|
|
96
|
-
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
97
|
-
})
|
|
101
|
+
expect(handler).toHaveBeenCalled()
|
|
102
|
+
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
103
|
+
})
|
|
98
104
|
|
|
99
105
|
it('session fingerprint matches dApp side derivation', async () => {
|
|
100
|
-
const uri = makePairingUri()
|
|
101
|
-
await session.joinFromUri(uri)
|
|
106
|
+
const uri = makePairingUri()
|
|
107
|
+
await session.joinFromUri(uri)
|
|
102
108
|
|
|
103
109
|
// Both sides use computeSessionFingerprint(channelId, dappPubKeyB64)
|
|
104
|
-
const dappFingerprint = computeSessionFingerprint(channelId, dappKp.publicKeyB64)
|
|
110
|
+
const dappFingerprint = computeSessionFingerprint(channelId, dappKp.publicKeyB64)
|
|
105
111
|
|
|
106
|
-
expect(session.sessionFingerprint).toBe(dappFingerprint)
|
|
107
|
-
})
|
|
112
|
+
expect(session.sessionFingerprint).toBe(dappFingerprint)
|
|
113
|
+
})
|
|
108
114
|
|
|
109
115
|
it('transitions to connected on ready.connected', async () => {
|
|
110
|
-
const phases: string[] = []
|
|
111
|
-
session.on('phase', (p) => phases.push(p))
|
|
116
|
+
const phases: string[] = []
|
|
117
|
+
session.on('phase', (p) => phases.push(p))
|
|
112
118
|
|
|
113
|
-
await session.joinFromUri(makePairingUri())
|
|
119
|
+
await session.joinFromUri(makePairingUri())
|
|
114
120
|
|
|
115
|
-
receiveConnected()
|
|
121
|
+
receiveConnected()
|
|
116
122
|
|
|
117
|
-
expect(session.phase).toBe('connected')
|
|
118
|
-
expect(phases).toContain('waiting_accept')
|
|
119
|
-
expect(phases).toContain('connected')
|
|
120
|
-
})
|
|
123
|
+
expect(session.phase).toBe('connected')
|
|
124
|
+
expect(phases).toContain('waiting_accept')
|
|
125
|
+
expect(phases).toContain('connected')
|
|
126
|
+
})
|
|
121
127
|
|
|
122
128
|
it('rejects ready.connected with missing remote', async () => {
|
|
123
|
-
const errorHandler = vi.fn()
|
|
124
|
-
session.on('error', errorHandler)
|
|
129
|
+
const errorHandler = vi.fn()
|
|
130
|
+
session.on('error', errorHandler)
|
|
125
131
|
|
|
126
|
-
await session.joinFromUri(makePairingUri())
|
|
132
|
+
await session.joinFromUri(makePairingUri())
|
|
127
133
|
transport.receive({
|
|
128
|
-
v: 1,
|
|
129
|
-
|
|
134
|
+
v: 1,
|
|
135
|
+
t: 'ready',
|
|
136
|
+
ch: channelId,
|
|
137
|
+
ts: Date.now(),
|
|
138
|
+
from: '_adapter',
|
|
130
139
|
body: { state: 'connected', reconnect: false, remote: null },
|
|
131
|
-
} as ProtocolMessage)
|
|
140
|
+
} as ProtocolMessage)
|
|
132
141
|
|
|
133
|
-
expect(errorHandler).toHaveBeenCalledWith(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
143
|
+
expect.objectContaining({
|
|
144
|
+
message: expect.stringContaining('remote does not match'),
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
expect(session.phase).toBe('closed')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
139
150
|
|
|
140
151
|
describe('request handling', () => {
|
|
141
|
-
let dappToWalletKey: Uint8Array
|
|
142
|
-
let walletToDappKey: Uint8Array
|
|
152
|
+
let dappToWalletKey: Uint8Array
|
|
153
|
+
let walletToDappKey: Uint8Array
|
|
143
154
|
|
|
144
155
|
beforeEach(async () => {
|
|
145
|
-
await session.joinFromUri(makePairingUri())
|
|
156
|
+
await session.joinFromUri(makePairingUri())
|
|
146
157
|
|
|
147
158
|
// Derive directional keys from dApp side
|
|
148
|
-
const walletPubB64 = transport.sent.find(m => m.t === 'join')
|
|
149
|
-
const walletPub = b64urlDecode(walletPubB64)
|
|
150
|
-
const shared = computeSharedSecret(dappKp.privateKey, walletPub)
|
|
151
|
-
const rootKey = deriveSessionKey(shared, channelId)
|
|
159
|
+
const walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
|
|
160
|
+
const walletPub = b64urlDecode(walletPubB64)
|
|
161
|
+
const shared = computeSharedSecret(dappKp.privateKey, walletPub)
|
|
162
|
+
const rootKey = deriveSessionKey(shared, channelId)
|
|
152
163
|
const context: SessionCryptoContext = {
|
|
153
164
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
154
165
|
walletPubKeyB64: walletPubB64,
|
|
@@ -157,100 +168,145 @@ describe('WalletSession', () => {
|
|
|
157
168
|
events: ['accountsChanged', 'chainChanged'],
|
|
158
169
|
chains: ['eip155:1'],
|
|
159
170
|
},
|
|
160
|
-
walletMeta: {
|
|
171
|
+
walletMeta: {
|
|
172
|
+
name: 'Test Wallet',
|
|
173
|
+
description: 'Test',
|
|
174
|
+
url: 'https://test.com',
|
|
175
|
+
icon: 'https://test.com/icon.png',
|
|
176
|
+
address: '0xtest',
|
|
177
|
+
},
|
|
161
178
|
dappName: 'Test dApp',
|
|
162
|
-
}
|
|
163
|
-
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
164
|
-
dappToWalletKey = keys.dappToWalletKey
|
|
165
|
-
walletToDappKey = keys.walletToDappKey
|
|
179
|
+
}
|
|
180
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
181
|
+
dappToWalletKey = keys.dappToWalletKey
|
|
182
|
+
walletToDappKey = keys.walletToDappKey
|
|
166
183
|
|
|
167
184
|
// Connect
|
|
168
|
-
receiveConnected()
|
|
169
|
-
})
|
|
185
|
+
receiveConnected()
|
|
186
|
+
})
|
|
170
187
|
|
|
171
188
|
it('emits request event with decrypted params', () => {
|
|
172
|
-
const handler = vi.fn()
|
|
173
|
-
session.on('request', handler)
|
|
189
|
+
const handler = vi.fn()
|
|
190
|
+
session.on('request', handler)
|
|
174
191
|
|
|
175
192
|
// Real method inside sealed payload
|
|
176
|
-
const sealedParams = { _method: 'wallet_signMessage', message: 'Hello World' }
|
|
193
|
+
const sealedParams = { _method: 'wallet_signMessage', message: 'Hello World' }
|
|
177
194
|
transport.receive({
|
|
178
|
-
v: 1,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
v: 1,
|
|
196
|
+
t: 'req',
|
|
197
|
+
ch: channelId,
|
|
198
|
+
ts: Date.now(),
|
|
199
|
+
from: dappKp.publicKeyB64,
|
|
200
|
+
body: {
|
|
201
|
+
id: 'req-1',
|
|
202
|
+
sealed: sealPayload(dappToWalletKey, channelId, 0, sealedParams, {
|
|
203
|
+
type: 'req',
|
|
204
|
+
from: dappKp.publicKeyB64,
|
|
205
|
+
id: 'req-1',
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
} as ProtocolMessage)
|
|
182
209
|
|
|
183
210
|
expect(handler).toHaveBeenCalledWith({
|
|
184
211
|
id: 'req-1',
|
|
185
212
|
method: 'wallet_signMessage',
|
|
186
213
|
params: { message: 'Hello World' },
|
|
187
|
-
})
|
|
188
|
-
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
189
216
|
|
|
190
217
|
it('emits request with empty params for parameterless sealed request', () => {
|
|
191
|
-
const handler = vi.fn()
|
|
192
|
-
session.on('request', handler)
|
|
218
|
+
const handler = vi.fn()
|
|
219
|
+
session.on('request', handler)
|
|
193
220
|
|
|
194
|
-
const sealedParams = { _method: 'wallet_getAccounts' }
|
|
221
|
+
const sealedParams = { _method: 'wallet_getAccounts' }
|
|
195
222
|
transport.receive({
|
|
196
|
-
v: 1,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
v: 1,
|
|
224
|
+
t: 'req',
|
|
225
|
+
ch: channelId,
|
|
226
|
+
ts: Date.now(),
|
|
227
|
+
from: dappKp.publicKeyB64,
|
|
228
|
+
body: {
|
|
229
|
+
id: 'req-2',
|
|
230
|
+
sealed: sealPayload(dappToWalletKey, channelId, 1, sealedParams, {
|
|
231
|
+
type: 'req',
|
|
232
|
+
from: dappKp.publicKeyB64,
|
|
233
|
+
id: 'req-2',
|
|
234
|
+
}),
|
|
235
|
+
},
|
|
236
|
+
} as ProtocolMessage)
|
|
200
237
|
|
|
201
238
|
expect(handler).toHaveBeenCalledWith({
|
|
202
239
|
id: 'req-2',
|
|
203
240
|
method: 'wallet_getAccounts',
|
|
204
241
|
params: {},
|
|
205
|
-
})
|
|
206
|
-
})
|
|
242
|
+
})
|
|
243
|
+
})
|
|
207
244
|
|
|
208
245
|
it('rejects unsealed request with decryption_failed', () => {
|
|
209
|
-
const handler = vi.fn()
|
|
210
|
-
session.on('request', handler)
|
|
246
|
+
const handler = vi.fn()
|
|
247
|
+
session.on('request', handler)
|
|
211
248
|
|
|
212
249
|
transport.receive({
|
|
213
|
-
v: 1,
|
|
214
|
-
|
|
250
|
+
v: 1,
|
|
251
|
+
t: 'req',
|
|
252
|
+
ch: channelId,
|
|
253
|
+
ts: Date.now(),
|
|
254
|
+
from: dappKp.publicKeyB64,
|
|
215
255
|
body: { id: 'req-3' },
|
|
216
|
-
} as ProtocolMessage)
|
|
256
|
+
} as ProtocolMessage)
|
|
217
257
|
|
|
218
258
|
// Should NOT emit request - unsealed requests are rejected
|
|
219
|
-
expect(handler).not.toHaveBeenCalled()
|
|
259
|
+
expect(handler).not.toHaveBeenCalled()
|
|
220
260
|
// Should send a rejection response
|
|
221
|
-
const resMsg = transport.sent.find(m => m.t === 'res') as any
|
|
222
|
-
expect(resMsg).toBeTruthy()
|
|
261
|
+
const resMsg = transport.sent.find((m) => m.t === 'res') as any
|
|
262
|
+
expect(resMsg).toBeTruthy()
|
|
223
263
|
// ok no longer exists on wire body
|
|
224
|
-
})
|
|
264
|
+
})
|
|
225
265
|
|
|
226
266
|
it('rejects sealed request missing _method with invalid_params', () => {
|
|
227
|
-
const handler = vi.fn()
|
|
228
|
-
session.on('request', handler)
|
|
267
|
+
const handler = vi.fn()
|
|
268
|
+
session.on('request', handler)
|
|
229
269
|
|
|
230
270
|
transport.receive({
|
|
231
|
-
v: 1,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
271
|
+
v: 1,
|
|
272
|
+
t: 'req',
|
|
273
|
+
ch: channelId,
|
|
274
|
+
ts: Date.now(),
|
|
275
|
+
from: dappKp.publicKeyB64,
|
|
276
|
+
body: {
|
|
277
|
+
id: 'req-missing-method',
|
|
278
|
+
sealed: sealPayload(
|
|
279
|
+
dappToWalletKey,
|
|
280
|
+
channelId,
|
|
281
|
+
0,
|
|
282
|
+
{ message: 'no method' },
|
|
283
|
+
{ type: 'req', from: dappKp.publicKeyB64, id: 'req-missing-method' },
|
|
284
|
+
),
|
|
285
|
+
},
|
|
286
|
+
} as ProtocolMessage)
|
|
287
|
+
|
|
288
|
+
expect(handler).not.toHaveBeenCalled()
|
|
289
|
+
const resMsg = transport.sent.find((m) => m.t === 'res') as any
|
|
290
|
+
expect(resMsg).toBeTruthy()
|
|
291
|
+
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
292
|
+
type: 'res',
|
|
293
|
+
from: transport.sent.find((m) => m.t === 'join')?.from!,
|
|
294
|
+
id: 'req-missing-method',
|
|
295
|
+
})
|
|
296
|
+
expect(data).toMatchObject({ _ok: false, code: 'invalid_params' })
|
|
297
|
+
})
|
|
298
|
+
})
|
|
243
299
|
|
|
244
300
|
describe('approve/reject', () => {
|
|
245
|
-
let dappToWalletKey: Uint8Array
|
|
246
|
-
let walletToDappKey: Uint8Array
|
|
247
|
-
let walletPubB64: string
|
|
301
|
+
let dappToWalletKey: Uint8Array
|
|
302
|
+
let walletToDappKey: Uint8Array
|
|
303
|
+
let walletPubB64: string
|
|
248
304
|
|
|
249
305
|
beforeEach(async () => {
|
|
250
|
-
await session.joinFromUri(makePairingUri())
|
|
251
|
-
walletPubB64 = transport.sent.find(m => m.t === 'join')
|
|
252
|
-
const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
|
|
253
|
-
const rootKey = deriveSessionKey(shared, channelId)
|
|
306
|
+
await session.joinFromUri(makePairingUri())
|
|
307
|
+
walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
|
|
308
|
+
const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
|
|
309
|
+
const rootKey = deriveSessionKey(shared, channelId)
|
|
254
310
|
const context: SessionCryptoContext = {
|
|
255
311
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
256
312
|
walletPubKeyB64: walletPubB64,
|
|
@@ -259,130 +315,232 @@ describe('WalletSession', () => {
|
|
|
259
315
|
events: ['accountsChanged', 'chainChanged'],
|
|
260
316
|
chains: ['eip155:1'],
|
|
261
317
|
},
|
|
262
|
-
walletMeta: {
|
|
318
|
+
walletMeta: {
|
|
319
|
+
name: 'Test Wallet',
|
|
320
|
+
description: 'Test',
|
|
321
|
+
url: 'https://test.com',
|
|
322
|
+
icon: 'https://test.com/icon.png',
|
|
323
|
+
address: '0xtest',
|
|
324
|
+
},
|
|
263
325
|
dappName: 'Test dApp',
|
|
264
|
-
}
|
|
265
|
-
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
266
|
-
dappToWalletKey = keys.dappToWalletKey
|
|
267
|
-
walletToDappKey = keys.walletToDappKey
|
|
326
|
+
}
|
|
327
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
328
|
+
dappToWalletKey = keys.dappToWalletKey
|
|
329
|
+
walletToDappKey = keys.walletToDappKey
|
|
268
330
|
|
|
269
|
-
receiveConnected()
|
|
270
|
-
})
|
|
331
|
+
receiveConnected()
|
|
332
|
+
})
|
|
271
333
|
|
|
272
334
|
it('approve sends encrypted ok response', () => {
|
|
273
335
|
transport.receive({
|
|
274
|
-
v: 1,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
336
|
+
v: 1,
|
|
337
|
+
t: 'req',
|
|
338
|
+
ch: channelId,
|
|
339
|
+
ts: Date.now(),
|
|
340
|
+
from: dappKp.publicKeyB64,
|
|
341
|
+
body: {
|
|
342
|
+
id: 'req-1',
|
|
343
|
+
sealed: sealPayload(
|
|
344
|
+
dappToWalletKey,
|
|
345
|
+
channelId,
|
|
346
|
+
0,
|
|
347
|
+
{ _method: 'wallet_getAccounts' },
|
|
348
|
+
{ type: 'req', from: dappKp.publicKeyB64, id: 'req-1' },
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
} as ProtocolMessage)
|
|
278
352
|
|
|
279
|
-
session.approve('req-1', ['0xabc123'])
|
|
353
|
+
session.approve('req-1', ['0xabc123'])
|
|
280
354
|
|
|
281
|
-
const resMsg = transport.sent.find(m => m.t === 'res') as any
|
|
282
|
-
expect(resMsg).toBeTruthy()
|
|
283
|
-
expect(resMsg.body.id).toBe('req-1')
|
|
284
|
-
expect(resMsg.body.sealed).toBeTruthy()
|
|
355
|
+
const resMsg = transport.sent.find((m) => m.t === 'res') as any
|
|
356
|
+
expect(resMsg).toBeTruthy()
|
|
357
|
+
expect(resMsg.body.id).toBe('req-1')
|
|
358
|
+
expect(resMsg.body.sealed).toBeTruthy()
|
|
285
359
|
|
|
286
360
|
// Verify dApp can decrypt the response (wallet->dApp uses walletToDappKey)
|
|
287
|
-
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
288
|
-
|
|
289
|
-
|
|
361
|
+
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
362
|
+
type: 'res',
|
|
363
|
+
from: walletPubB64,
|
|
364
|
+
id: 'req-1',
|
|
365
|
+
})
|
|
366
|
+
expect(data).toEqual({ _ok: true, _result: ['0xabc123'] })
|
|
367
|
+
})
|
|
290
368
|
|
|
291
369
|
it('reject sends encrypted error response', () => {
|
|
292
370
|
transport.receive({
|
|
293
|
-
v: 1,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
371
|
+
v: 1,
|
|
372
|
+
t: 'req',
|
|
373
|
+
ch: channelId,
|
|
374
|
+
ts: Date.now(),
|
|
375
|
+
from: dappKp.publicKeyB64,
|
|
376
|
+
body: {
|
|
377
|
+
id: 'req-2',
|
|
378
|
+
sealed: sealPayload(
|
|
379
|
+
dappToWalletKey,
|
|
380
|
+
channelId,
|
|
381
|
+
0,
|
|
382
|
+
{ _method: 'wallet_signMessage', message: 'test' },
|
|
383
|
+
{ type: 'req', from: dappKp.publicKeyB64, id: 'req-2' },
|
|
384
|
+
),
|
|
385
|
+
},
|
|
386
|
+
} as ProtocolMessage)
|
|
297
387
|
|
|
298
|
-
session.reject('req-2', 'user_rejected', 'User said no')
|
|
388
|
+
session.reject('req-2', 'user_rejected', 'User said no')
|
|
299
389
|
|
|
300
|
-
const resMsg = transport.sent.find(m => m.t === 'res') as any
|
|
301
|
-
expect(resMsg).toBeTruthy()
|
|
302
|
-
expect(resMsg.body.id).toBe('req-2')
|
|
303
|
-
expect(resMsg.body.sealed).toBeTruthy()
|
|
390
|
+
const resMsg = transport.sent.find((m) => m.t === 'res') as any
|
|
391
|
+
expect(resMsg).toBeTruthy()
|
|
392
|
+
expect(resMsg.body.id).toBe('req-2')
|
|
393
|
+
expect(resMsg.body.sealed).toBeTruthy()
|
|
304
394
|
|
|
305
|
-
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
306
|
-
|
|
307
|
-
|
|
395
|
+
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
396
|
+
type: 'res',
|
|
397
|
+
from: walletPubB64,
|
|
398
|
+
id: 'req-2',
|
|
399
|
+
})
|
|
400
|
+
expect(data).toEqual({ _ok: false, code: 'user_rejected', message: 'User said no' })
|
|
401
|
+
})
|
|
308
402
|
|
|
309
403
|
it('approve increments send sequence', () => {
|
|
310
404
|
for (let i = 0; i < 3; i++) {
|
|
311
405
|
transport.receive({
|
|
312
|
-
v: 1,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
406
|
+
v: 1,
|
|
407
|
+
t: 'req',
|
|
408
|
+
ch: channelId,
|
|
409
|
+
ts: Date.now(),
|
|
410
|
+
from: dappKp.publicKeyB64,
|
|
411
|
+
body: {
|
|
412
|
+
id: `req-${i}`,
|
|
413
|
+
sealed: sealPayload(
|
|
414
|
+
dappToWalletKey,
|
|
415
|
+
channelId,
|
|
416
|
+
i,
|
|
417
|
+
{ _method: 'wallet_getAccounts' },
|
|
418
|
+
{ type: 'req', from: dappKp.publicKeyB64, id: `req-${i}` },
|
|
419
|
+
),
|
|
420
|
+
},
|
|
421
|
+
} as ProtocolMessage)
|
|
422
|
+
session.approve(`req-${i}`, ['0x123'])
|
|
317
423
|
}
|
|
318
424
|
|
|
319
425
|
// All 3 responses should have different sealed payloads (different seqs)
|
|
320
|
-
const responses = transport.sent.filter(m => m.t === 'res') as any[]
|
|
321
|
-
expect(responses).toHaveLength(3)
|
|
322
|
-
const sealedSet = new Set(responses.map((r: any) => r.body.sealed))
|
|
323
|
-
expect(sealedSet.size).toBe(3)
|
|
324
|
-
})
|
|
426
|
+
const responses = transport.sent.filter((m) => m.t === 'res') as any[]
|
|
427
|
+
expect(responses).toHaveLength(3)
|
|
428
|
+
const sealedSet = new Set(responses.map((r: any) => r.body.sealed))
|
|
429
|
+
expect(sealedSet.size).toBe(3)
|
|
430
|
+
})
|
|
325
431
|
|
|
326
432
|
it('re-encrypts cached response for duplicate request id with same params', () => {
|
|
327
|
-
const handler = vi.fn()
|
|
328
|
-
session.on('request', handler)
|
|
329
|
-
const requestPayload = { _method: 'wallet_getAccounts' }
|
|
433
|
+
const handler = vi.fn()
|
|
434
|
+
session.on('request', handler)
|
|
435
|
+
const requestPayload = { _method: 'wallet_getAccounts' }
|
|
330
436
|
|
|
331
437
|
transport.receive({
|
|
332
|
-
v: 1,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
438
|
+
v: 1,
|
|
439
|
+
t: 'req',
|
|
440
|
+
ch: channelId,
|
|
441
|
+
ts: Date.now(),
|
|
442
|
+
from: dappKp.publicKeyB64,
|
|
443
|
+
body: {
|
|
444
|
+
id: 'dup-1',
|
|
445
|
+
sealed: sealPayload(dappToWalletKey, channelId, 0, requestPayload, {
|
|
446
|
+
type: 'req',
|
|
447
|
+
from: dappKp.publicKeyB64,
|
|
448
|
+
id: 'dup-1',
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
} as ProtocolMessage)
|
|
452
|
+
session.approve('dup-1', ['0xabc123'])
|
|
337
453
|
|
|
338
454
|
transport.receive({
|
|
339
|
-
v: 1,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
455
|
+
v: 1,
|
|
456
|
+
t: 'req',
|
|
457
|
+
ch: channelId,
|
|
458
|
+
ts: Date.now(),
|
|
459
|
+
from: dappKp.publicKeyB64,
|
|
460
|
+
body: {
|
|
461
|
+
id: 'dup-1',
|
|
462
|
+
sealed: sealPayload(dappToWalletKey, channelId, 1, requestPayload, {
|
|
463
|
+
type: 'req',
|
|
464
|
+
from: dappKp.publicKeyB64,
|
|
465
|
+
id: 'dup-1',
|
|
466
|
+
}),
|
|
467
|
+
},
|
|
468
|
+
} as ProtocolMessage)
|
|
469
|
+
|
|
470
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
471
|
+
const responses = transport.sent.filter((m) => m.t === 'res') as any[]
|
|
472
|
+
expect(responses).toHaveLength(2)
|
|
473
|
+
expect(responses[0]?.body.sealed).not.toBe(responses[1]?.body.sealed)
|
|
474
|
+
const { data } = unsealPayload(walletToDappKey, channelId, responses[1]?.body.sealed, {
|
|
475
|
+
type: 'res',
|
|
476
|
+
from: walletPubB64,
|
|
477
|
+
id: 'dup-1',
|
|
478
|
+
})
|
|
479
|
+
expect(data).toEqual({ _ok: true, _result: ['0xabc123'] })
|
|
480
|
+
})
|
|
351
481
|
|
|
352
482
|
it('rejects duplicate request id with different params', () => {
|
|
353
|
-
const handler = vi.fn()
|
|
354
|
-
session.on('request', handler)
|
|
483
|
+
const handler = vi.fn()
|
|
484
|
+
session.on('request', handler)
|
|
355
485
|
|
|
356
486
|
transport.receive({
|
|
357
|
-
v: 1,
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
487
|
+
v: 1,
|
|
488
|
+
t: 'req',
|
|
489
|
+
ch: channelId,
|
|
490
|
+
ts: Date.now(),
|
|
491
|
+
from: dappKp.publicKeyB64,
|
|
492
|
+
body: {
|
|
493
|
+
id: 'dup-2',
|
|
494
|
+
sealed: sealPayload(
|
|
495
|
+
dappToWalletKey,
|
|
496
|
+
channelId,
|
|
497
|
+
0,
|
|
498
|
+
{ _method: 'wallet_getAccounts' },
|
|
499
|
+
{ type: 'req', from: dappKp.publicKeyB64, id: 'dup-2' },
|
|
500
|
+
),
|
|
501
|
+
},
|
|
502
|
+
} as ProtocolMessage)
|
|
503
|
+
session.approve('dup-2', ['0xabc123'])
|
|
362
504
|
|
|
363
505
|
transport.receive({
|
|
364
|
-
v: 1,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
506
|
+
v: 1,
|
|
507
|
+
t: 'req',
|
|
508
|
+
ch: channelId,
|
|
509
|
+
ts: Date.now(),
|
|
510
|
+
from: dappKp.publicKeyB64,
|
|
511
|
+
body: {
|
|
512
|
+
id: 'dup-2',
|
|
513
|
+
sealed: sealPayload(
|
|
514
|
+
dappToWalletKey,
|
|
515
|
+
channelId,
|
|
516
|
+
1,
|
|
517
|
+
{ _method: 'wallet_getAccounts', changed: true },
|
|
518
|
+
{ type: 'req', from: dappKp.publicKeyB64, id: 'dup-2' },
|
|
519
|
+
),
|
|
520
|
+
},
|
|
521
|
+
} as ProtocolMessage)
|
|
522
|
+
|
|
523
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
524
|
+
const responses = transport.sent.filter((m) => m.t === 'res') as any[]
|
|
525
|
+
const duplicateResponse = responses[1]!
|
|
526
|
+
const { data } = unsealPayload(walletToDappKey, channelId, duplicateResponse.body.sealed, {
|
|
527
|
+
type: 'res',
|
|
528
|
+
from: walletPubB64,
|
|
529
|
+
id: 'dup-2',
|
|
530
|
+
})
|
|
531
|
+
expect(data).toMatchObject({ _ok: false, code: 'invalid_params' })
|
|
532
|
+
})
|
|
533
|
+
})
|
|
376
534
|
|
|
377
535
|
describe('pushEvent', () => {
|
|
378
|
-
let walletToDappKey: Uint8Array
|
|
379
|
-
let walletPubB64: string
|
|
536
|
+
let walletToDappKey: Uint8Array
|
|
537
|
+
let walletPubB64: string
|
|
380
538
|
|
|
381
539
|
beforeEach(async () => {
|
|
382
|
-
await session.joinFromUri(makePairingUri())
|
|
383
|
-
walletPubB64 = transport.sent.find(m => m.t === 'join')
|
|
384
|
-
const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
|
|
385
|
-
const rootKey = deriveSessionKey(shared, channelId)
|
|
540
|
+
await session.joinFromUri(makePairingUri())
|
|
541
|
+
walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
|
|
542
|
+
const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
|
|
543
|
+
const rootKey = deriveSessionKey(shared, channelId)
|
|
386
544
|
const context: SessionCryptoContext = {
|
|
387
545
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
388
546
|
walletPubKeyB64: walletPubB64,
|
|
@@ -391,90 +549,113 @@ describe('WalletSession', () => {
|
|
|
391
549
|
events: ['accountsChanged', 'chainChanged'],
|
|
392
550
|
chains: ['eip155:1'],
|
|
393
551
|
},
|
|
394
|
-
walletMeta: {
|
|
552
|
+
walletMeta: {
|
|
553
|
+
name: 'Test Wallet',
|
|
554
|
+
description: 'Test',
|
|
555
|
+
url: 'https://test.com',
|
|
556
|
+
icon: 'https://test.com/icon.png',
|
|
557
|
+
address: '0xtest',
|
|
558
|
+
},
|
|
395
559
|
dappName: 'Test dApp',
|
|
396
|
-
}
|
|
397
|
-
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
398
|
-
walletToDappKey = keys.walletToDappKey
|
|
560
|
+
}
|
|
561
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
562
|
+
walletToDappKey = keys.walletToDappKey
|
|
399
563
|
|
|
400
|
-
receiveConnected()
|
|
401
|
-
})
|
|
564
|
+
receiveConnected()
|
|
565
|
+
})
|
|
402
566
|
|
|
403
567
|
it('sends encrypted event message', () => {
|
|
404
|
-
session.pushEvent('accountsChanged', { accounts: ['0xnew'] })
|
|
568
|
+
session.pushEvent('accountsChanged', { accounts: ['0xnew'] })
|
|
405
569
|
|
|
406
|
-
const evtMsg = transport.sent.find(m => m.t === 'evt') as any
|
|
407
|
-
expect(evtMsg).toBeTruthy()
|
|
408
|
-
expect(evtMsg.body.id).toBeTruthy()
|
|
409
|
-
expect(evtMsg.body.sealed).toBeTruthy()
|
|
570
|
+
const evtMsg = transport.sent.find((m) => m.t === 'evt') as any
|
|
571
|
+
expect(evtMsg).toBeTruthy()
|
|
572
|
+
expect(evtMsg.body.id).toBeTruthy()
|
|
573
|
+
expect(evtMsg.body.sealed).toBeTruthy()
|
|
410
574
|
|
|
411
575
|
// Verify dApp can decrypt (wallet->dApp uses walletToDappKey)
|
|
412
|
-
const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, {
|
|
576
|
+
const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, {
|
|
577
|
+
type: 'evt',
|
|
578
|
+
from: walletPubB64,
|
|
579
|
+
id: evtMsg.body.id,
|
|
580
|
+
})
|
|
413
581
|
// Real event name and data are inside sealed payload
|
|
414
|
-
expect(data).toEqual({ _event: 'accountsChanged', accounts: ['0xnew'] })
|
|
415
|
-
})
|
|
582
|
+
expect(data).toEqual({ _event: 'accountsChanged', accounts: ['0xnew'] })
|
|
583
|
+
})
|
|
416
584
|
|
|
417
585
|
it('does nothing when not connected', () => {
|
|
418
586
|
const idleSession = new WalletSession({
|
|
419
587
|
transport: new MockTransport(),
|
|
420
588
|
capabilities: { methods: [], events: [], chains: [] },
|
|
421
|
-
meta: {
|
|
422
|
-
|
|
423
|
-
|
|
589
|
+
meta: {
|
|
590
|
+
name: 'Test Wallet',
|
|
591
|
+
description: 'Test',
|
|
592
|
+
url: 'https://wallet.test',
|
|
593
|
+
icon: 'https://wallet.test/icon.png',
|
|
594
|
+
},
|
|
595
|
+
})
|
|
596
|
+
idleSession.pushEvent('test', {})
|
|
424
597
|
// Should not throw, just no-op
|
|
425
|
-
})
|
|
598
|
+
})
|
|
426
599
|
|
|
427
600
|
it('sends chainChanged event', () => {
|
|
428
|
-
session.pushEvent('chainChanged', { chainId: 'eip155:137' })
|
|
429
|
-
|
|
430
|
-
const evtMsg = transport.sent.find(m => m.t === 'evt') as any
|
|
431
|
-
const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
601
|
+
session.pushEvent('chainChanged', { chainId: 'eip155:137' })
|
|
602
|
+
|
|
603
|
+
const evtMsg = transport.sent.find((m) => m.t === 'evt') as any
|
|
604
|
+
const { data } = unsealPayload(walletToDappKey, channelId, evtMsg.body.sealed, {
|
|
605
|
+
type: 'evt',
|
|
606
|
+
from: walletPubB64,
|
|
607
|
+
id: evtMsg.body.id,
|
|
608
|
+
})
|
|
609
|
+
expect(data).toEqual({ _event: 'chainChanged', chainId: 'eip155:137' })
|
|
610
|
+
})
|
|
611
|
+
})
|
|
435
612
|
|
|
436
613
|
describe('ping/pong', () => {
|
|
437
614
|
beforeEach(async () => {
|
|
438
|
-
await session.joinFromUri(makePairingUri())
|
|
439
|
-
receiveConnected()
|
|
440
|
-
})
|
|
615
|
+
await session.joinFromUri(makePairingUri())
|
|
616
|
+
receiveConnected()
|
|
617
|
+
})
|
|
441
618
|
|
|
442
619
|
it('responds to ping with pong', () => {
|
|
443
620
|
transport.receive({
|
|
444
|
-
v: 1,
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
621
|
+
v: 1,
|
|
622
|
+
t: 'ping',
|
|
623
|
+
ch: channelId,
|
|
624
|
+
ts: 12345,
|
|
625
|
+
from: dappKp.publicKeyB64,
|
|
626
|
+
body: {},
|
|
627
|
+
} as ProtocolMessage)
|
|
628
|
+
|
|
629
|
+
const pong = transport.sent.find((m) => m.t === 'pong')
|
|
630
|
+
expect(pong).toBeTruthy()
|
|
631
|
+
})
|
|
451
632
|
|
|
452
633
|
it('sends ping', () => {
|
|
453
|
-
session.ping()
|
|
454
|
-
const ping = transport.sent.find(m => m.t === 'ping')
|
|
455
|
-
expect(ping).toBeTruthy()
|
|
456
|
-
})
|
|
457
|
-
})
|
|
634
|
+
session.ping()
|
|
635
|
+
const ping = transport.sent.find((m) => m.t === 'ping')
|
|
636
|
+
expect(ping).toBeTruthy()
|
|
637
|
+
})
|
|
638
|
+
})
|
|
458
639
|
|
|
459
640
|
describe('close', () => {
|
|
460
641
|
it('sends close and transitions to closed', async () => {
|
|
461
|
-
await session.joinFromUri(makePairingUri())
|
|
462
|
-
session.close()
|
|
642
|
+
await session.joinFromUri(makePairingUri())
|
|
643
|
+
session.close()
|
|
463
644
|
|
|
464
|
-
const closeMsg = transport.sent.find(m => m.t === 'close')
|
|
465
|
-
expect(closeMsg).toBeTruthy()
|
|
466
|
-
expect((closeMsg as any).body.reason).toBe('normal')
|
|
467
|
-
expect(session.phase).toBe('closed')
|
|
468
|
-
})
|
|
469
|
-
})
|
|
645
|
+
const closeMsg = transport.sent.find((m) => m.t === 'close')
|
|
646
|
+
expect(closeMsg).toBeTruthy()
|
|
647
|
+
expect((closeMsg as any).body.reason).toBe('normal')
|
|
648
|
+
expect(session.phase).toBe('closed')
|
|
649
|
+
})
|
|
650
|
+
})
|
|
470
651
|
|
|
471
652
|
describe('serialize/restore', () => {
|
|
472
653
|
it('round-trips session state', async () => {
|
|
473
|
-
await session.joinFromUri(makePairingUri())
|
|
654
|
+
await session.joinFromUri(makePairingUri())
|
|
474
655
|
|
|
475
|
-
receiveConnected()
|
|
656
|
+
receiveConnected()
|
|
476
657
|
|
|
477
|
-
const json = session.serialize()
|
|
658
|
+
const json = session.serialize()
|
|
478
659
|
const newSession = new WalletSession({
|
|
479
660
|
transport: new MockTransport(),
|
|
480
661
|
capabilities: {
|
|
@@ -482,115 +663,138 @@ describe('WalletSession', () => {
|
|
|
482
663
|
events: ['accountsChanged', 'chainChanged'],
|
|
483
664
|
chains: ['eip155:1'],
|
|
484
665
|
},
|
|
485
|
-
meta: {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
666
|
+
meta: {
|
|
667
|
+
name: 'Test Wallet',
|
|
668
|
+
description: 'Test',
|
|
669
|
+
url: 'https://test.com',
|
|
670
|
+
icon: 'https://test.com/icon.png',
|
|
671
|
+
address: '0xtest',
|
|
672
|
+
},
|
|
673
|
+
})
|
|
674
|
+
expect(newSession.restore(json)).toBe(true)
|
|
675
|
+
expect(newSession.channelId).toBe(channelId)
|
|
676
|
+
})
|
|
490
677
|
|
|
491
678
|
it('rejects restore when capabilities no longer match the transcript', async () => {
|
|
492
|
-
await session.joinFromUri(makePairingUri())
|
|
679
|
+
await session.joinFromUri(makePairingUri())
|
|
493
680
|
|
|
494
|
-
const json = session.serialize()
|
|
681
|
+
const json = session.serialize()
|
|
495
682
|
const newSession = new WalletSession({
|
|
496
683
|
transport: new MockTransport(),
|
|
497
684
|
capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
|
|
498
|
-
meta: {
|
|
499
|
-
|
|
685
|
+
meta: {
|
|
686
|
+
name: 'Test Wallet',
|
|
687
|
+
description: 'Test',
|
|
688
|
+
url: 'https://test.com',
|
|
689
|
+
icon: 'https://test.com/icon.png',
|
|
690
|
+
address: '0xtest',
|
|
691
|
+
},
|
|
692
|
+
})
|
|
500
693
|
|
|
501
|
-
expect(newSession.restore(json)).toBe(false)
|
|
502
|
-
})
|
|
694
|
+
expect(newSession.restore(json)).toBe(false)
|
|
695
|
+
})
|
|
503
696
|
|
|
504
697
|
it('returns false for invalid JSON', () => {
|
|
505
|
-
expect(session.restore('invalid')).toBe(false)
|
|
506
|
-
expect(session.restore('{}')).toBe(false)
|
|
507
|
-
})
|
|
508
|
-
})
|
|
698
|
+
expect(session.restore('invalid')).toBe(false)
|
|
699
|
+
expect(session.restore('{}')).toBe(false)
|
|
700
|
+
})
|
|
701
|
+
})
|
|
509
702
|
|
|
510
703
|
describe('close message handling', () => {
|
|
511
704
|
it('transitions to closed on close message from dApp', async () => {
|
|
512
|
-
await session.joinFromUri(makePairingUri())
|
|
513
|
-
receiveConnected()
|
|
705
|
+
await session.joinFromUri(makePairingUri())
|
|
706
|
+
receiveConnected()
|
|
514
707
|
|
|
515
708
|
transport.receive({
|
|
516
|
-
v: 1,
|
|
517
|
-
|
|
709
|
+
v: 1,
|
|
710
|
+
t: 'close',
|
|
711
|
+
ch: channelId,
|
|
712
|
+
ts: Date.now(),
|
|
713
|
+
from: dappKp.publicKeyB64,
|
|
518
714
|
body: { reason: 'normal' },
|
|
519
|
-
} as ProtocolMessage)
|
|
715
|
+
} as ProtocolMessage)
|
|
520
716
|
|
|
521
|
-
expect(session.phase).toBe('closed')
|
|
522
|
-
})
|
|
523
|
-
})
|
|
717
|
+
expect(session.phase).toBe('closed')
|
|
718
|
+
})
|
|
719
|
+
})
|
|
524
720
|
|
|
525
721
|
describe('destroy', () => {
|
|
526
722
|
it('closes and removes all listeners', async () => {
|
|
527
|
-
await session.joinFromUri(makePairingUri())
|
|
528
|
-
const handler = vi.fn()
|
|
529
|
-
session.on('phase', handler)
|
|
530
|
-
session.destroy()
|
|
531
|
-
expect(session.phase).toBe('closed')
|
|
532
|
-
})
|
|
533
|
-
})
|
|
723
|
+
await session.joinFromUri(makePairingUri())
|
|
724
|
+
const handler = vi.fn()
|
|
725
|
+
session.on('phase', handler)
|
|
726
|
+
session.destroy()
|
|
727
|
+
expect(session.phase).toBe('closed')
|
|
728
|
+
})
|
|
729
|
+
})
|
|
534
730
|
|
|
535
731
|
describe('session fingerprint after prepareJoin', () => {
|
|
536
732
|
it('sessionFingerprint is set and event was emitted after prepareJoin', () => {
|
|
537
|
-
const fpHandler = vi.fn()
|
|
538
|
-
session.on('sessionFingerprint', fpHandler)
|
|
733
|
+
const fpHandler = vi.fn()
|
|
734
|
+
session.on('sessionFingerprint', fpHandler)
|
|
539
735
|
|
|
540
|
-
const uri = makePairingUri()
|
|
541
|
-
const fingerprint = session.prepareJoin(uri)
|
|
736
|
+
const uri = makePairingUri()
|
|
737
|
+
const fingerprint = session.prepareJoin(uri)
|
|
542
738
|
|
|
543
|
-
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
544
|
-
expect(fingerprint).toBe(session.sessionFingerprint)
|
|
545
|
-
expect(fpHandler).toHaveBeenCalledTimes(1)
|
|
546
|
-
expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint)
|
|
547
|
-
})
|
|
548
|
-
})
|
|
739
|
+
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
740
|
+
expect(fingerprint).toBe(session.sessionFingerprint)
|
|
741
|
+
expect(fpHandler).toHaveBeenCalledTimes(1)
|
|
742
|
+
expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint)
|
|
743
|
+
})
|
|
744
|
+
})
|
|
549
745
|
|
|
550
746
|
describe('protocol compliance', () => {
|
|
551
747
|
it('rejects messages with from="_adapter" for peer types (§2)', async () => {
|
|
552
|
-
const errorHandler = vi.fn()
|
|
553
|
-
session.on('error', errorHandler)
|
|
748
|
+
const errorHandler = vi.fn()
|
|
749
|
+
session.on('error', errorHandler)
|
|
554
750
|
|
|
555
|
-
await session.joinFromUri(makePairingUri())
|
|
556
|
-
receiveConnected()
|
|
751
|
+
await session.joinFromUri(makePairingUri())
|
|
752
|
+
receiveConnected()
|
|
557
753
|
|
|
558
754
|
// Send a req message with from: '_adapter' — should be rejected
|
|
559
755
|
transport.receive({
|
|
560
|
-
v: 1,
|
|
561
|
-
|
|
756
|
+
v: 1,
|
|
757
|
+
t: 'req',
|
|
758
|
+
ch: channelId,
|
|
759
|
+
ts: Date.now(),
|
|
760
|
+
from: '_adapter',
|
|
562
761
|
body: { id: 'spoofed-1', sealed: 'fake' },
|
|
563
|
-
} as ProtocolMessage)
|
|
762
|
+
} as ProtocolMessage)
|
|
564
763
|
|
|
565
|
-
expect(errorHandler).toHaveBeenCalledWith(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
764
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
765
|
+
expect.objectContaining({
|
|
766
|
+
message: expect.stringContaining('_adapter'),
|
|
767
|
+
}),
|
|
768
|
+
)
|
|
769
|
+
})
|
|
569
770
|
|
|
570
771
|
it('rejects messages with unsupported version (§15 rule 12)', async () => {
|
|
571
|
-
await session.joinFromUri(makePairingUri())
|
|
572
|
-
receiveConnected()
|
|
772
|
+
await session.joinFromUri(makePairingUri())
|
|
773
|
+
receiveConnected()
|
|
573
774
|
|
|
574
775
|
// Send a message with v: 2 — should close with unsupported_version
|
|
575
776
|
transport.receive({
|
|
576
|
-
v: 2,
|
|
577
|
-
|
|
777
|
+
v: 2,
|
|
778
|
+
t: 'req',
|
|
779
|
+
ch: channelId,
|
|
780
|
+
ts: Date.now(),
|
|
781
|
+
from: dappKp.publicKeyB64,
|
|
578
782
|
body: { id: 'v2-req', sealed: 'fake' },
|
|
579
|
-
} as unknown as ProtocolMessage)
|
|
783
|
+
} as unknown as ProtocolMessage)
|
|
580
784
|
|
|
581
|
-
expect(session.phase).toBe('closed')
|
|
582
|
-
const closeMsg = transport.sent.find(m => m.t === 'close') as any
|
|
583
|
-
expect(closeMsg).toBeTruthy()
|
|
584
|
-
expect(closeMsg.body.reason).toBe('unsupported_version')
|
|
585
|
-
})
|
|
785
|
+
expect(session.phase).toBe('closed')
|
|
786
|
+
const closeMsg = transport.sent.find((m) => m.t === 'close') as any
|
|
787
|
+
expect(closeMsg).toBeTruthy()
|
|
788
|
+
expect(closeMsg.body.reason).toBe('unsupported_version')
|
|
789
|
+
})
|
|
586
790
|
|
|
587
791
|
it('rejects unsupported methods at runtime (§7.1)', async () => {
|
|
588
792
|
// Session has capabilities.methods = ['wallet_getAccounts', 'wallet_signMessage']
|
|
589
|
-
await session.joinFromUri(makePairingUri())
|
|
793
|
+
await session.joinFromUri(makePairingUri())
|
|
590
794
|
|
|
591
|
-
const walletPubB64 = transport.sent.find(m => m.t === 'join')
|
|
592
|
-
const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
|
|
593
|
-
const rootKey = deriveSessionKey(shared, channelId)
|
|
795
|
+
const walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
|
|
796
|
+
const shared = computeSharedSecret(dappKp.privateKey, b64urlDecode(walletPubB64))
|
|
797
|
+
const rootKey = deriveSessionKey(shared, channelId)
|
|
594
798
|
const context: SessionCryptoContext = {
|
|
595
799
|
dappPubKeyB64: dappKp.publicKeyB64,
|
|
596
800
|
walletPubKeyB64: walletPubB64,
|
|
@@ -599,40 +803,60 @@ describe('WalletSession', () => {
|
|
|
599
803
|
events: ['accountsChanged', 'chainChanged'],
|
|
600
804
|
chains: ['eip155:1'],
|
|
601
805
|
},
|
|
602
|
-
walletMeta: {
|
|
806
|
+
walletMeta: {
|
|
807
|
+
name: 'Test Wallet',
|
|
808
|
+
description: 'Test',
|
|
809
|
+
url: 'https://test.com',
|
|
810
|
+
icon: 'https://test.com/icon.png',
|
|
811
|
+
address: '0xtest',
|
|
812
|
+
},
|
|
603
813
|
dappName: 'Test dApp',
|
|
604
|
-
}
|
|
605
|
-
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
606
|
-
const dappToWalletKey = keys.dappToWalletKey
|
|
607
|
-
const walletToDappKey = keys.walletToDappKey
|
|
814
|
+
}
|
|
815
|
+
const keys = deriveDirectionalSessionKeys(rootKey, channelId, context)
|
|
816
|
+
const dappToWalletKey = keys.dappToWalletKey
|
|
817
|
+
const walletToDappKey = keys.walletToDappKey
|
|
608
818
|
|
|
609
|
-
receiveConnected()
|
|
819
|
+
receiveConnected()
|
|
610
820
|
|
|
611
|
-
const handler = vi.fn()
|
|
612
|
-
session.on('request', handler)
|
|
821
|
+
const handler = vi.fn()
|
|
822
|
+
session.on('request', handler)
|
|
613
823
|
|
|
614
824
|
// Send a request for a method NOT in capabilities
|
|
615
|
-
const sealedParams = { _method: 'wallet_signTransaction', data: '0x...' }
|
|
825
|
+
const sealedParams = { _method: 'wallet_signTransaction', data: '0x...' }
|
|
616
826
|
transport.receive({
|
|
617
|
-
v: 1,
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
827
|
+
v: 1,
|
|
828
|
+
t: 'req',
|
|
829
|
+
ch: channelId,
|
|
830
|
+
ts: Date.now(),
|
|
831
|
+
from: dappKp.publicKeyB64,
|
|
832
|
+
body: {
|
|
833
|
+
id: 'unsup-1',
|
|
834
|
+
sealed: sealPayload(dappToWalletKey, channelId, 0, sealedParams, {
|
|
835
|
+
type: 'req',
|
|
836
|
+
from: dappKp.publicKeyB64,
|
|
837
|
+
id: 'unsup-1',
|
|
838
|
+
}),
|
|
839
|
+
},
|
|
840
|
+
} as ProtocolMessage)
|
|
621
841
|
|
|
622
842
|
// Should NOT emit request
|
|
623
|
-
expect(handler).not.toHaveBeenCalled()
|
|
843
|
+
expect(handler).not.toHaveBeenCalled()
|
|
624
844
|
|
|
625
845
|
// Should send an error response with unsupported_method
|
|
626
|
-
const resMsg = transport.sent.find(m => m.t === 'res') as any
|
|
627
|
-
expect(resMsg).toBeTruthy()
|
|
628
|
-
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
846
|
+
const resMsg = transport.sent.find((m) => m.t === 'res') as any
|
|
847
|
+
expect(resMsg).toBeTruthy()
|
|
848
|
+
const { data } = unsealPayload(walletToDappKey, channelId, resMsg.body.sealed, {
|
|
849
|
+
type: 'res',
|
|
850
|
+
from: walletPubB64,
|
|
851
|
+
id: 'unsup-1',
|
|
852
|
+
})
|
|
853
|
+
expect(data).toMatchObject({ _ok: false, code: 'unsupported_method' })
|
|
854
|
+
})
|
|
855
|
+
})
|
|
632
856
|
|
|
633
857
|
describe('scope intersection (computeScopeIntersection)', () => {
|
|
634
858
|
it('intersects wallet capabilities with dApp-declared scope from URI', async () => {
|
|
635
|
-
const wideWalletTransport = new MockTransport()
|
|
859
|
+
const wideWalletTransport = new MockTransport()
|
|
636
860
|
const wideWalletSession = new WalletSession({
|
|
637
861
|
transport: wideWalletTransport,
|
|
638
862
|
capabilities: {
|
|
@@ -641,11 +865,11 @@ describe('WalletSession', () => {
|
|
|
641
865
|
chains: ['eip155:1', 'eip155:137'],
|
|
642
866
|
},
|
|
643
867
|
meta: { name: 'W', description: 'W', url: 'https://w.com', icon: 'https://w.com/i.png' },
|
|
644
|
-
})
|
|
868
|
+
})
|
|
645
869
|
|
|
646
870
|
// Build a URI that declares only methods=a,b and chains=eip155:1
|
|
647
|
-
const dappKpLocal = generateX25519KeyPair()
|
|
648
|
-
const chLocal = generateChannelId()
|
|
871
|
+
const dappKpLocal = generateX25519KeyPair()
|
|
872
|
+
const chLocal = generateChannelId()
|
|
649
873
|
const uri = buildPairingUri({
|
|
650
874
|
channelId: chLocal,
|
|
651
875
|
pubkeyB64: dappKpLocal.publicKeyB64,
|
|
@@ -655,29 +879,33 @@ describe('WalletSession', () => {
|
|
|
655
879
|
icon: 'https://test.com/icon.png',
|
|
656
880
|
methods: ['a', 'b'],
|
|
657
881
|
chains: ['eip155:1'],
|
|
658
|
-
})
|
|
882
|
+
})
|
|
659
883
|
|
|
660
|
-
await wideWalletSession.joinFromUri(uri)
|
|
884
|
+
await wideWalletSession.joinFromUri(uri)
|
|
661
885
|
|
|
662
886
|
// The join message should contain sealed_join with intersected capabilities
|
|
663
|
-
const joinMsg = wideWalletTransport.sent.find(m => m.t === 'join') as any
|
|
664
|
-
expect(joinMsg).toBeTruthy()
|
|
665
|
-
expect(joinMsg.body.sealed_join).toBeTruthy()
|
|
887
|
+
const joinMsg = wideWalletTransport.sent.find((m) => m.t === 'join') as any
|
|
888
|
+
expect(joinMsg).toBeTruthy()
|
|
889
|
+
expect(joinMsg.body.sealed_join).toBeTruthy()
|
|
666
890
|
|
|
667
891
|
// Unseal the join to verify effective capabilities
|
|
668
|
-
const walletPubB64 = joinMsg.from
|
|
669
|
-
const walletPub = b64urlDecode(walletPubB64)
|
|
670
|
-
const shared = computeSharedSecret(dappKpLocal.privateKey, walletPub)
|
|
671
|
-
const rootKey = deriveSessionKey(shared, chLocal)
|
|
672
|
-
const joinKey = deriveJoinEncryptionKey(rootKey, chLocal)
|
|
673
|
-
const unsealed = unsealJoin(joinKey, chLocal, joinMsg.body.sealed_join)
|
|
674
|
-
|
|
675
|
-
const caps = unsealed.capabilities as {
|
|
892
|
+
const walletPubB64 = joinMsg.from!
|
|
893
|
+
const walletPub = b64urlDecode(walletPubB64)
|
|
894
|
+
const shared = computeSharedSecret(dappKpLocal.privateKey, walletPub)
|
|
895
|
+
const rootKey = deriveSessionKey(shared, chLocal)
|
|
896
|
+
const joinKey = deriveJoinEncryptionKey(rootKey, chLocal)
|
|
897
|
+
const unsealed = unsealJoin(joinKey, chLocal, joinMsg.body.sealed_join)
|
|
898
|
+
|
|
899
|
+
const caps = unsealed.capabilities as {
|
|
900
|
+
methods: string[]
|
|
901
|
+
chains: string[]
|
|
902
|
+
events: string[]
|
|
903
|
+
}
|
|
676
904
|
// Wallet grants ALL its capabilities (not just the intersection)
|
|
677
905
|
// per §7.1: wallet MAY grant additional methods/chains beyond requested
|
|
678
|
-
expect(caps.methods).toEqual(['a', 'b', 'c'])
|
|
679
|
-
expect(caps.chains).toEqual(['eip155:1', 'eip155:137'])
|
|
680
|
-
expect(caps.events).toEqual(['accountsChanged'])
|
|
681
|
-
})
|
|
682
|
-
})
|
|
683
|
-
})
|
|
906
|
+
expect(caps.methods).toEqual(['a', 'b', 'c'])
|
|
907
|
+
expect(caps.chains).toEqual(['eip155:1', 'eip155:137'])
|
|
908
|
+
expect(caps.events).toEqual(['accountsChanged'])
|
|
909
|
+
})
|
|
910
|
+
})
|
|
911
|
+
})
|