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
package/src/dapp-session.test.ts
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { WalletSession } from './wallet-session.js';
|
|
4
|
-
import { makeJoinBody, MockTransport, MockRelay } from './test-helpers.js';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { AadHeader } from './crypto.js'
|
|
5
3
|
import {
|
|
6
|
-
|
|
4
|
+
b64urlDecode,
|
|
5
|
+
computeSessionFingerprint,
|
|
7
6
|
computeSharedSecret,
|
|
8
7
|
deriveSessionKey,
|
|
9
|
-
|
|
10
|
-
computeSessionFingerprint,
|
|
11
|
-
sealPayload,
|
|
12
|
-
b64urlEncode,
|
|
13
|
-
b64urlDecode,
|
|
8
|
+
generateX25519KeyPair,
|
|
14
9
|
parsePairingUri,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import
|
|
10
|
+
sealPayload,
|
|
11
|
+
} from './crypto.js'
|
|
12
|
+
import { DAppSession } from './dapp-session.js'
|
|
13
|
+
import { MockTransport, makeJoinBody } from './test-helpers.js'
|
|
14
|
+
import type { ProtocolMessage } from './types.js'
|
|
18
15
|
|
|
19
16
|
function flushMicrotasks(): Promise<void> {
|
|
20
|
-
return new Promise((r) => setTimeout(r, 10))
|
|
17
|
+
return new Promise((r) => setTimeout(r, 10))
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
function dappPubKeyFromCreate(transport: MockTransport): string {
|
|
24
|
-
return transport.sent.find(m => m.t === 'create')
|
|
21
|
+
return transport.sent.find((m) => m.t === 'create')?.from!
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
function receiveFreshJoin(
|
|
@@ -30,10 +27,13 @@ function receiveFreshJoin(
|
|
|
30
27
|
walletKp: ReturnType<typeof generateX25519KeyPair>,
|
|
31
28
|
): void {
|
|
32
29
|
transport.receive({
|
|
33
|
-
v: 1,
|
|
34
|
-
|
|
30
|
+
v: 1,
|
|
31
|
+
t: 'join',
|
|
32
|
+
ch: session.channelId,
|
|
33
|
+
ts: Date.now(),
|
|
34
|
+
from: walletKp.publicKeyB64,
|
|
35
35
|
body: makeJoinBody(session.channelId, dappPubKeyFromCreate(transport), walletKp),
|
|
36
|
-
} as ProtocolMessage)
|
|
36
|
+
} as ProtocolMessage)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function receiveConnected(
|
|
@@ -42,606 +42,743 @@ function receiveConnected(
|
|
|
42
42
|
walletPubKeyB64: string,
|
|
43
43
|
): void {
|
|
44
44
|
transport.receive({
|
|
45
|
-
v: 1,
|
|
46
|
-
|
|
45
|
+
v: 1,
|
|
46
|
+
t: 'ready',
|
|
47
|
+
ch: session.channelId,
|
|
48
|
+
ts: Date.now(),
|
|
49
|
+
from: '_adapter',
|
|
47
50
|
body: { state: 'connected', reconnect: false, remote: walletPubKeyB64 },
|
|
48
|
-
} as ProtocolMessage)
|
|
51
|
+
} as ProtocolMessage)
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
describe('DAppSession', () => {
|
|
52
|
-
let transport: MockTransport
|
|
53
|
-
let session: DAppSession
|
|
55
|
+
let transport: MockTransport
|
|
56
|
+
let session: DAppSession
|
|
54
57
|
|
|
55
58
|
beforeEach(() => {
|
|
56
|
-
transport = new MockTransport()
|
|
57
|
-
session = new DAppSession({
|
|
58
|
-
|
|
59
|
+
transport = new MockTransport()
|
|
60
|
+
session = new DAppSession({
|
|
61
|
+
transport,
|
|
62
|
+
meta: {
|
|
63
|
+
name: 'Test dApp',
|
|
64
|
+
description: 'Test',
|
|
65
|
+
url: 'https://test.com',
|
|
66
|
+
icon: 'https://test.com/icon.png',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
})
|
|
59
70
|
|
|
60
71
|
describe('createPairing', () => {
|
|
61
72
|
it('starts in idle phase', () => {
|
|
62
|
-
expect(session.phase).toBe('idle')
|
|
63
|
-
})
|
|
73
|
+
expect(session.phase).toBe('idle')
|
|
74
|
+
})
|
|
64
75
|
|
|
65
76
|
it('creates pairing and transitions to waiting', async () => {
|
|
66
|
-
const phases: string[] = []
|
|
67
|
-
session.on('phase', (p) => phases.push(p))
|
|
68
|
-
|
|
69
|
-
const uri = await session.createPairing()
|
|
70
|
-
expect(uri).toContain('walletpair:?ch=')
|
|
71
|
-
expect(uri).toContain('&pubkey=')
|
|
72
|
-
expect(session.phase).toBe('waiting')
|
|
73
|
-
expect(session.channelId).toHaveLength(64)
|
|
74
|
-
expect(session.pairingUri).toBe(uri)
|
|
75
|
-
expect(phases).toContain('waiting')
|
|
76
|
-
})
|
|
77
|
+
const phases: string[] = []
|
|
78
|
+
session.on('phase', (p) => phases.push(p))
|
|
79
|
+
|
|
80
|
+
const uri = await session.createPairing()
|
|
81
|
+
expect(uri).toContain('walletpair:?ch=')
|
|
82
|
+
expect(uri).toContain('&pubkey=')
|
|
83
|
+
expect(session.phase).toBe('waiting')
|
|
84
|
+
expect(session.channelId).toHaveLength(64)
|
|
85
|
+
expect(session.pairingUri).toBe(uri)
|
|
86
|
+
expect(phases).toContain('waiting')
|
|
87
|
+
})
|
|
77
88
|
|
|
78
89
|
it('emits pairingUri event', async () => {
|
|
79
|
-
const handler = vi.fn()
|
|
80
|
-
session.on('pairingUri', handler)
|
|
81
|
-
await session.createPairing()
|
|
82
|
-
expect(handler).toHaveBeenCalledWith(session.pairingUri)
|
|
83
|
-
})
|
|
90
|
+
const handler = vi.fn()
|
|
91
|
+
session.on('pairingUri', handler)
|
|
92
|
+
await session.createPairing()
|
|
93
|
+
expect(handler).toHaveBeenCalledWith(session.pairingUri)
|
|
94
|
+
})
|
|
84
95
|
|
|
85
96
|
it('sends create message to transport', async () => {
|
|
86
|
-
await session.createPairing()
|
|
87
|
-
expect(transport.sent).toHaveLength(1)
|
|
88
|
-
expect(transport.sent[0]
|
|
89
|
-
expect(transport.sent[0]
|
|
90
|
-
})
|
|
97
|
+
await session.createPairing()
|
|
98
|
+
expect(transport.sent).toHaveLength(1)
|
|
99
|
+
expect(transport.sent[0]?.t).toBe('create')
|
|
100
|
+
expect(transport.sent[0]?.from).toBeTruthy()
|
|
101
|
+
})
|
|
91
102
|
|
|
92
103
|
it('pairing URI is parseable', async () => {
|
|
93
|
-
await session.createPairing()
|
|
94
|
-
const parsed = parsePairingUri(session.pairingUri)
|
|
95
|
-
expect(parsed.ch).toBe(session.channelId)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
104
|
+
await session.createPairing()
|
|
105
|
+
const parsed = parsePairingUri(session.pairingUri)
|
|
106
|
+
expect(parsed.ch).toBe(session.channelId)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
98
109
|
|
|
99
110
|
describe('wallet join handling', () => {
|
|
100
|
-
let walletKp: ReturnType<typeof generateX25519KeyPair
|
|
111
|
+
let walletKp: ReturnType<typeof generateX25519KeyPair>
|
|
101
112
|
|
|
102
113
|
beforeEach(async () => {
|
|
103
|
-
await session.createPairing()
|
|
104
|
-
walletKp = generateX25519KeyPair()
|
|
105
|
-
})
|
|
114
|
+
await session.createPairing()
|
|
115
|
+
walletKp = generateX25519KeyPair()
|
|
116
|
+
})
|
|
106
117
|
|
|
107
118
|
it('auto-accepts and transitions to accepting on join', async () => {
|
|
108
|
-
const phases: string[] = []
|
|
109
|
-
session.on('phase', (p) => phases.push(p))
|
|
119
|
+
const phases: string[] = []
|
|
120
|
+
session.on('phase', (p) => phases.push(p))
|
|
110
121
|
|
|
111
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
122
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
112
123
|
|
|
113
124
|
// With auto-accept, the session should not stay in pending_accept
|
|
114
125
|
// It should proceed to accepting (waiting for ready.connected)
|
|
115
|
-
expect(session.phase).not.toBe('idle')
|
|
116
|
-
})
|
|
126
|
+
expect(session.phase).not.toBe('idle')
|
|
127
|
+
})
|
|
117
128
|
|
|
118
129
|
it('computes and emits session fingerprint on createPairing', async () => {
|
|
119
|
-
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
120
|
-
})
|
|
130
|
+
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
131
|
+
})
|
|
121
132
|
|
|
122
133
|
it('emits walletJoined with capabilities and meta from sealed_join', async () => {
|
|
123
|
-
const handler = vi.fn()
|
|
124
|
-
session.on('walletJoined', handler)
|
|
134
|
+
const handler = vi.fn()
|
|
135
|
+
session.on('walletJoined', handler)
|
|
125
136
|
|
|
126
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
137
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
127
138
|
|
|
128
139
|
expect(handler).toHaveBeenCalledWith({
|
|
129
140
|
capabilities: expect.objectContaining({ methods: expect.any(Array) }),
|
|
130
141
|
meta: expect.objectContaining({ name: 'Test Wallet' }),
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
134
145
|
|
|
135
146
|
describe('auto-accept on join', () => {
|
|
136
147
|
it('auto-accepts and transitions to connected on ready', async () => {
|
|
137
|
-
await session.createPairing()
|
|
138
|
-
const walletKp = generateX25519KeyPair()
|
|
148
|
+
await session.createPairing()
|
|
149
|
+
const walletKp = generateX25519KeyPair()
|
|
139
150
|
|
|
140
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
151
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
141
152
|
|
|
142
153
|
// Should have auto-sent accept (no manual acceptWallet needed)
|
|
143
|
-
const acceptMsg = transport.sent.find(m => m.t === 'accept')
|
|
144
|
-
expect(acceptMsg).toBeTruthy()
|
|
145
|
-
expect((acceptMsg as any).body.target).toBe(walletKp.publicKeyB64)
|
|
154
|
+
const acceptMsg = transport.sent.find((m) => m.t === 'accept')
|
|
155
|
+
expect(acceptMsg).toBeTruthy()
|
|
156
|
+
expect((acceptMsg as any).body.target).toBe(walletKp.publicKeyB64)
|
|
146
157
|
|
|
147
158
|
// Simulate relay responding with ready.connected
|
|
148
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
159
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
149
160
|
|
|
150
|
-
expect(session.phase).toBe('connected')
|
|
151
|
-
})
|
|
161
|
+
expect(session.phase).toBe('connected')
|
|
162
|
+
})
|
|
152
163
|
|
|
153
164
|
it('rejects ready.connected with missing remote', async () => {
|
|
154
|
-
await session.createPairing()
|
|
155
|
-
const walletKp = generateX25519KeyPair()
|
|
156
|
-
const errorHandler = vi.fn()
|
|
157
|
-
session.on('error', errorHandler)
|
|
165
|
+
await session.createPairing()
|
|
166
|
+
const walletKp = generateX25519KeyPair()
|
|
167
|
+
const errorHandler = vi.fn()
|
|
168
|
+
session.on('error', errorHandler)
|
|
158
169
|
|
|
159
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
170
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
160
171
|
|
|
161
172
|
transport.receive({
|
|
162
|
-
v: 1,
|
|
163
|
-
|
|
173
|
+
v: 1,
|
|
174
|
+
t: 'ready',
|
|
175
|
+
ch: session.channelId,
|
|
176
|
+
ts: Date.now(),
|
|
177
|
+
from: '_adapter',
|
|
164
178
|
body: { state: 'connected', reconnect: false, remote: null },
|
|
165
|
-
} as ProtocolMessage)
|
|
179
|
+
} as ProtocolMessage)
|
|
166
180
|
|
|
167
|
-
expect(errorHandler).toHaveBeenCalledWith(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
message: expect.stringContaining('remote does not match'),
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
expect(session.phase).toBe('closed')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
173
189
|
|
|
174
190
|
describe('rejectWallet', () => {
|
|
175
191
|
it('sends close with user_rejected and closes session (autoAccept disabled)', async () => {
|
|
176
192
|
const manualSession = new DAppSession({
|
|
177
|
-
transport,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
transport,
|
|
194
|
+
autoAccept: false,
|
|
195
|
+
meta: {
|
|
196
|
+
name: 'Test dApp',
|
|
197
|
+
description: 'Test',
|
|
198
|
+
url: 'https://test.com',
|
|
199
|
+
icon: 'https://test.com/icon.png',
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
await manualSession.createPairing()
|
|
203
|
+
const walletKp = generateX25519KeyPair()
|
|
204
|
+
|
|
205
|
+
receiveFreshJoin(transport, manualSession, walletKp)
|
|
206
|
+
|
|
207
|
+
manualSession.rejectWallet()
|
|
208
|
+
|
|
209
|
+
const closeMsg = transport.sent.find((m) => m.t === 'close')
|
|
210
|
+
expect(closeMsg).toBeTruthy()
|
|
211
|
+
expect((closeMsg as any).body.reason).toBe('user_rejected')
|
|
212
|
+
expect(manualSession.phase).toBe('closed')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
193
215
|
|
|
194
216
|
describe('request/response', () => {
|
|
195
|
-
let walletKp: ReturnType<typeof generateX25519KeyPair
|
|
196
|
-
let sessionKey: Uint8Array
|
|
197
|
-
let walletToDappKey: Uint8Array
|
|
217
|
+
let walletKp: ReturnType<typeof generateX25519KeyPair>
|
|
218
|
+
let sessionKey: Uint8Array
|
|
219
|
+
let walletToDappKey: Uint8Array
|
|
198
220
|
|
|
199
221
|
beforeEach(async () => {
|
|
200
|
-
await session.createPairing()
|
|
201
|
-
walletKp = generateX25519KeyPair()
|
|
222
|
+
await session.createPairing()
|
|
223
|
+
walletKp = generateX25519KeyPair()
|
|
202
224
|
|
|
203
225
|
// Simulate join
|
|
204
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
226
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
205
227
|
|
|
206
228
|
// Derive session key from wallet side
|
|
207
|
-
const dappPubB64 = transport.sent[0]
|
|
208
|
-
const dappPub = b64urlDecode(dappPubB64)
|
|
209
|
-
const shared = computeSharedSecret(walletKp.privateKey, dappPub)
|
|
210
|
-
sessionKey = deriveSessionKey(shared, session.channelId)
|
|
229
|
+
const dappPubB64 = transport.sent[0]?.from!
|
|
230
|
+
const dappPub = b64urlDecode(dappPubB64)
|
|
231
|
+
const shared = computeSharedSecret(walletKp.privateKey, dappPub)
|
|
232
|
+
sessionKey = deriveSessionKey(shared, session.channelId)
|
|
211
233
|
|
|
212
234
|
// Auto-accepted; simulate relay ready.connected
|
|
213
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
214
|
-
walletToDappKey = (session as any).recvKey
|
|
215
|
-
})
|
|
235
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
236
|
+
walletToDappKey = (session as any).recvKey
|
|
237
|
+
})
|
|
216
238
|
|
|
217
239
|
it('sends encrypted request', async () => {
|
|
218
|
-
const promise = session.request('wallet_getAccounts')
|
|
240
|
+
const promise = session.request('wallet_getAccounts')
|
|
219
241
|
|
|
220
|
-
await flushMicrotasks()
|
|
242
|
+
await flushMicrotasks()
|
|
221
243
|
|
|
222
|
-
const reqMsg = transport.sent.find(m => m.t === 'req')
|
|
223
|
-
expect(reqMsg).toBeTruthy()
|
|
224
|
-
const reqBody = (reqMsg as any).body
|
|
225
|
-
expect(reqBody.id).toMatch(/^req-/)
|
|
226
|
-
expect(reqBody.sealed).toBeTruthy()
|
|
244
|
+
const reqMsg = transport.sent.find((m) => m.t === 'req')
|
|
245
|
+
expect(reqMsg).toBeTruthy()
|
|
246
|
+
const reqBody = (reqMsg as any).body
|
|
247
|
+
expect(reqBody.id).toMatch(/^req-/)
|
|
248
|
+
expect(reqBody.sealed).toBeTruthy()
|
|
227
249
|
|
|
228
250
|
// Simulate wallet response
|
|
229
|
-
const resData = { _ok: true, _result: ['0xabc123'] }
|
|
230
|
-
const resHdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: reqBody.id }
|
|
251
|
+
const resData = { _ok: true, _result: ['0xabc123'] }
|
|
252
|
+
const resHdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: reqBody.id }
|
|
231
253
|
transport.receive({
|
|
232
|
-
v: 1,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
254
|
+
v: 1,
|
|
255
|
+
t: 'res',
|
|
256
|
+
ch: session.channelId,
|
|
257
|
+
ts: Date.now(),
|
|
258
|
+
from: walletKp.publicKeyB64,
|
|
259
|
+
body: {
|
|
260
|
+
id: reqBody.id,
|
|
261
|
+
sealed: sealPayload(walletToDappKey, session.channelId, 0, resData, resHdr),
|
|
262
|
+
},
|
|
263
|
+
} as ProtocolMessage)
|
|
264
|
+
|
|
265
|
+
const result = await promise
|
|
266
|
+
expect(result).toEqual(['0xabc123'])
|
|
267
|
+
})
|
|
240
268
|
|
|
241
269
|
it('sends request with encrypted params', async () => {
|
|
242
|
-
const promise = session.request('wallet_signMessage', { message: 'Hello' })
|
|
243
|
-
await flushMicrotasks()
|
|
270
|
+
const promise = session.request('wallet_signMessage', { message: 'Hello' })
|
|
271
|
+
await flushMicrotasks()
|
|
244
272
|
|
|
245
|
-
const reqMsg = transport.sent.find(m => m.t === 'req') as any
|
|
246
|
-
expect(reqMsg.body.sealed).toBeTruthy()
|
|
273
|
+
const reqMsg = transport.sent.find((m) => m.t === 'req') as any
|
|
274
|
+
expect(reqMsg.body.sealed).toBeTruthy() // params were sealed
|
|
247
275
|
|
|
248
276
|
// Respond
|
|
249
|
-
const reqId = reqMsg.body.id
|
|
277
|
+
const reqId = reqMsg.body.id
|
|
250
278
|
transport.receive({
|
|
251
|
-
v: 1,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
279
|
+
v: 1,
|
|
280
|
+
t: 'res',
|
|
281
|
+
ch: session.channelId,
|
|
282
|
+
ts: Date.now(),
|
|
283
|
+
from: walletKp.publicKeyB64,
|
|
284
|
+
body: {
|
|
285
|
+
id: reqId,
|
|
286
|
+
sealed: sealPayload(
|
|
287
|
+
walletToDappKey,
|
|
288
|
+
session.channelId,
|
|
289
|
+
0,
|
|
290
|
+
{ _ok: true, _result: { signature: '0x...' } },
|
|
291
|
+
{ type: 'res', from: walletKp.publicKeyB64, id: reqId },
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
} as ProtocolMessage)
|
|
295
|
+
|
|
296
|
+
const result = await promise
|
|
297
|
+
expect(result).toEqual({ signature: '0x...' })
|
|
298
|
+
})
|
|
259
299
|
|
|
260
300
|
it('rejects on error response', async () => {
|
|
261
|
-
const promise = session.request('wallet_signMessage', { message: 'Hi' })
|
|
262
|
-
await flushMicrotasks()
|
|
301
|
+
const promise = session.request('wallet_signMessage', { message: 'Hi' })
|
|
302
|
+
await flushMicrotasks()
|
|
263
303
|
|
|
264
|
-
const reqMsg = transport.sent.find(m => m.t === 'req') as any
|
|
265
|
-
const reqId = reqMsg.body.id
|
|
304
|
+
const reqMsg = transport.sent.find((m) => m.t === 'req') as any
|
|
305
|
+
const reqId = reqMsg.body.id
|
|
266
306
|
|
|
267
307
|
transport.receive({
|
|
268
|
-
v: 1,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
308
|
+
v: 1,
|
|
309
|
+
t: 'res',
|
|
310
|
+
ch: session.channelId,
|
|
311
|
+
ts: Date.now(),
|
|
312
|
+
from: walletKp.publicKeyB64,
|
|
313
|
+
body: {
|
|
314
|
+
id: reqId,
|
|
315
|
+
sealed: sealPayload(
|
|
316
|
+
walletToDappKey,
|
|
317
|
+
session.channelId,
|
|
318
|
+
0,
|
|
319
|
+
{ _ok: false, code: 'user_rejected', message: 'User rejected' },
|
|
320
|
+
{ type: 'res', from: walletKp.publicKeyB64, id: reqId },
|
|
321
|
+
),
|
|
322
|
+
},
|
|
323
|
+
} as ProtocolMessage)
|
|
324
|
+
|
|
325
|
+
await expect(promise).rejects.toThrow('User rejected')
|
|
326
|
+
})
|
|
275
327
|
|
|
276
328
|
it('rejects on timeout', async () => {
|
|
277
|
-
vi.useFakeTimers()
|
|
329
|
+
vi.useFakeTimers()
|
|
278
330
|
|
|
279
331
|
const shortTimeoutSession = new DAppSession({
|
|
280
|
-
transport,
|
|
281
|
-
|
|
332
|
+
transport,
|
|
333
|
+
meta: {
|
|
334
|
+
name: 'Test',
|
|
335
|
+
description: 'Test',
|
|
336
|
+
url: 'https://test.com',
|
|
337
|
+
icon: 'https://test.com/icon.png',
|
|
338
|
+
},
|
|
339
|
+
requestTimeout: 100,
|
|
340
|
+
})
|
|
282
341
|
// Manually set session state to connected
|
|
283
|
-
(shortTimeoutSession as any).phase = 'connected'
|
|
284
|
-
(shortTimeoutSession as any).sessionKey = sessionKey
|
|
285
|
-
(shortTimeoutSession as any).sendKey = new Uint8Array(32).fill(1)
|
|
286
|
-
(shortTimeoutSession as any).channelId = session.channelId
|
|
287
|
-
(shortTimeoutSession as any).pubKeyB64 = 'test'
|
|
342
|
+
;(shortTimeoutSession as any).phase = 'connected'
|
|
343
|
+
;(shortTimeoutSession as any).sessionKey = sessionKey
|
|
344
|
+
;(shortTimeoutSession as any).sendKey = new Uint8Array(32).fill(1)
|
|
345
|
+
;(shortTimeoutSession as any).channelId = session.channelId
|
|
346
|
+
;(shortTimeoutSession as any).pubKeyB64 = 'test'
|
|
288
347
|
|
|
289
|
-
const promise = shortTimeoutSession.request('wallet_getAccounts')
|
|
290
|
-
vi.advanceTimersByTime(200)
|
|
348
|
+
const promise = shortTimeoutSession.request('wallet_getAccounts')
|
|
349
|
+
vi.advanceTimersByTime(200)
|
|
291
350
|
|
|
292
|
-
await expect(promise).rejects.toThrow('timed out')
|
|
293
|
-
vi.useRealTimers()
|
|
294
|
-
})
|
|
351
|
+
await expect(promise).rejects.toThrow('timed out')
|
|
352
|
+
vi.useRealTimers()
|
|
353
|
+
})
|
|
295
354
|
|
|
296
355
|
it('emits response event', async () => {
|
|
297
|
-
const handler = vi.fn()
|
|
298
|
-
session.on('response', handler)
|
|
356
|
+
const handler = vi.fn()
|
|
357
|
+
session.on('response', handler)
|
|
299
358
|
|
|
300
|
-
const promise = session.request('wallet_getAccounts')
|
|
301
|
-
await flushMicrotasks()
|
|
359
|
+
const promise = session.request('wallet_getAccounts')
|
|
360
|
+
await flushMicrotasks()
|
|
302
361
|
|
|
303
|
-
const reqMsg = transport.sent.find(m => m.t === 'req') as any
|
|
304
|
-
const reqId = reqMsg.body.id
|
|
362
|
+
const reqMsg = transport.sent.find((m) => m.t === 'req') as any
|
|
363
|
+
const reqId = reqMsg.body.id
|
|
305
364
|
transport.receive({
|
|
306
|
-
v: 1,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
365
|
+
v: 1,
|
|
366
|
+
t: 'res',
|
|
367
|
+
ch: session.channelId,
|
|
368
|
+
ts: Date.now(),
|
|
369
|
+
from: walletKp.publicKeyB64,
|
|
370
|
+
body: {
|
|
371
|
+
id: reqId,
|
|
372
|
+
sealed: sealPayload(
|
|
373
|
+
walletToDappKey,
|
|
374
|
+
session.channelId,
|
|
375
|
+
0,
|
|
376
|
+
{ _ok: true, _result: ['0x123'] },
|
|
377
|
+
{ type: 'res', from: walletKp.publicKeyB64, id: reqId },
|
|
378
|
+
),
|
|
379
|
+
},
|
|
380
|
+
} as ProtocolMessage)
|
|
381
|
+
|
|
382
|
+
await promise
|
|
383
|
+
expect(handler).toHaveBeenCalledWith({ id: reqId, ok: true, result: ['0x123'] })
|
|
384
|
+
})
|
|
314
385
|
|
|
315
386
|
it('rejects request when not connected', async () => {
|
|
316
|
-
const idleSession = new DAppSession({
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
387
|
+
const idleSession = new DAppSession({
|
|
388
|
+
transport: new MockTransport(),
|
|
389
|
+
meta: {
|
|
390
|
+
name: 'Test',
|
|
391
|
+
description: 'Test',
|
|
392
|
+
url: 'https://test.com',
|
|
393
|
+
icon: 'https://test.com/icon.png',
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
await expect(idleSession.request('test')).rejects.toThrow('Not connected')
|
|
397
|
+
})
|
|
398
|
+
})
|
|
320
399
|
|
|
321
400
|
describe('event handling', () => {
|
|
322
401
|
it('emits event when wallet pushes evt', async () => {
|
|
323
|
-
await session.createPairing()
|
|
324
|
-
const walletKp = generateX25519KeyPair()
|
|
402
|
+
await session.createPairing()
|
|
403
|
+
const walletKp = generateX25519KeyPair()
|
|
325
404
|
|
|
326
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
405
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
327
406
|
|
|
328
|
-
const dappPub = b64urlDecode(transport.sent[0]
|
|
329
|
-
const shared = computeSharedSecret(walletKp.privateKey, dappPub)
|
|
330
|
-
deriveSessionKey(shared, session.channelId)
|
|
407
|
+
const dappPub = b64urlDecode(transport.sent[0]?.from!)
|
|
408
|
+
const shared = computeSharedSecret(walletKp.privateKey, dappPub)
|
|
409
|
+
deriveSessionKey(shared, session.channelId)
|
|
331
410
|
|
|
332
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
411
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
333
412
|
|
|
334
|
-
const handler = vi.fn()
|
|
335
|
-
session.on('event', handler)
|
|
413
|
+
const handler = vi.fn()
|
|
414
|
+
session.on('event', handler)
|
|
336
415
|
|
|
337
|
-
const evtId = 'evt-1'
|
|
416
|
+
const evtId = 'evt-1'
|
|
338
417
|
transport.receive({
|
|
339
|
-
v: 1,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
418
|
+
v: 1,
|
|
419
|
+
t: 'evt',
|
|
420
|
+
ch: session.channelId,
|
|
421
|
+
ts: Date.now(),
|
|
422
|
+
from: walletKp.publicKeyB64,
|
|
423
|
+
body: {
|
|
424
|
+
id: evtId,
|
|
425
|
+
sealed: sealPayload(
|
|
426
|
+
(session as any).recvKey,
|
|
427
|
+
session.channelId,
|
|
428
|
+
0,
|
|
429
|
+
{ _event: 'accountsChanged', accounts: ['0xabc'] },
|
|
430
|
+
{ type: 'evt', from: walletKp.publicKeyB64, id: evtId },
|
|
431
|
+
),
|
|
432
|
+
},
|
|
433
|
+
} as ProtocolMessage)
|
|
343
434
|
|
|
344
435
|
expect(handler).toHaveBeenCalledWith({
|
|
345
436
|
event: 'accountsChanged',
|
|
346
437
|
data: { accounts: ['0xabc'] },
|
|
347
|
-
})
|
|
348
|
-
})
|
|
349
|
-
})
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
})
|
|
350
441
|
|
|
351
442
|
describe('ping/pong', () => {
|
|
352
443
|
it('responds to ping with pong', async () => {
|
|
353
|
-
await session.createPairing()
|
|
354
|
-
const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
|
355
|
-
(session as any).remotePubKey = b64urlDecode(walletPubB64)
|
|
356
|
-
receiveConnected(transport, session, walletPubB64)
|
|
444
|
+
await session.createPairing()
|
|
445
|
+
const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
|
446
|
+
;(session as any).remotePubKey = b64urlDecode(walletPubB64)
|
|
447
|
+
receiveConnected(transport, session, walletPubB64)
|
|
357
448
|
|
|
358
449
|
transport.receive({
|
|
359
|
-
v: 1,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
450
|
+
v: 1,
|
|
451
|
+
t: 'ping',
|
|
452
|
+
ch: session.channelId,
|
|
453
|
+
ts: 1000,
|
|
454
|
+
from: walletPubB64,
|
|
455
|
+
body: {},
|
|
456
|
+
} as ProtocolMessage)
|
|
457
|
+
|
|
458
|
+
const pong = transport.sent.find((m) => m.t === 'pong')
|
|
459
|
+
expect(pong).toBeTruthy()
|
|
460
|
+
expect(pong?.ts).toBeTypeOf('number')
|
|
461
|
+
})
|
|
367
462
|
|
|
368
463
|
it('sends ping', async () => {
|
|
369
|
-
const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
|
370
|
-
await session.createPairing()
|
|
371
|
-
(session as any).remotePubKey = b64urlDecode(walletPubB64)
|
|
372
|
-
receiveConnected(transport, session, walletPubB64)
|
|
464
|
+
const walletPubB64 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
|
465
|
+
await session.createPairing()
|
|
466
|
+
;(session as any).remotePubKey = b64urlDecode(walletPubB64)
|
|
467
|
+
receiveConnected(transport, session, walletPubB64)
|
|
373
468
|
|
|
374
|
-
session.ping()
|
|
375
|
-
const ping = transport.sent.find(m => m.t === 'ping')
|
|
376
|
-
expect(ping).toBeTruthy()
|
|
377
|
-
})
|
|
378
|
-
})
|
|
469
|
+
session.ping()
|
|
470
|
+
const ping = transport.sent.find((m) => m.t === 'ping')
|
|
471
|
+
expect(ping).toBeTruthy()
|
|
472
|
+
})
|
|
473
|
+
})
|
|
379
474
|
|
|
380
475
|
describe('close', () => {
|
|
381
476
|
it('sends close message and transitions to closed', async () => {
|
|
382
|
-
await session.createPairing()
|
|
383
|
-
session.close()
|
|
477
|
+
await session.createPairing()
|
|
478
|
+
session.close()
|
|
384
479
|
|
|
385
|
-
const closeMsg = transport.sent.find(m => m.t === 'close')
|
|
386
|
-
expect(closeMsg).toBeTruthy()
|
|
387
|
-
expect((closeMsg as any).body.reason).toBe('normal')
|
|
388
|
-
expect(session.phase).toBe('closed')
|
|
389
|
-
})
|
|
480
|
+
const closeMsg = transport.sent.find((m) => m.t === 'close')
|
|
481
|
+
expect(closeMsg).toBeTruthy()
|
|
482
|
+
expect((closeMsg as any).body.reason).toBe('normal')
|
|
483
|
+
expect(session.phase).toBe('closed')
|
|
484
|
+
})
|
|
390
485
|
|
|
391
486
|
it('rejects all pending requests on close', async () => {
|
|
392
|
-
await session.createPairing()
|
|
393
|
-
const walletKp = generateX25519KeyPair()
|
|
487
|
+
await session.createPairing()
|
|
488
|
+
const walletKp = generateX25519KeyPair()
|
|
394
489
|
|
|
395
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
396
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
490
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
491
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
397
492
|
|
|
398
|
-
const promise = session.request('test')
|
|
399
|
-
session.close()
|
|
493
|
+
const promise = session.request('test')
|
|
494
|
+
session.close()
|
|
400
495
|
|
|
401
|
-
await expect(promise).rejects.toThrow('Session closed')
|
|
402
|
-
})
|
|
403
|
-
})
|
|
496
|
+
await expect(promise).rejects.toThrow('Session closed')
|
|
497
|
+
})
|
|
498
|
+
})
|
|
404
499
|
|
|
405
500
|
describe('serialize/restore', () => {
|
|
406
501
|
it('round-trips session state', async () => {
|
|
407
|
-
await session.createPairing()
|
|
408
|
-
const walletKp = generateX25519KeyPair()
|
|
409
|
-
|
|
410
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
411
|
-
|
|
412
|
-
const json = session.serialize()
|
|
413
|
-
expect(json).toBeTruthy()
|
|
414
|
-
|
|
415
|
-
const newTransport = new MockTransport()
|
|
416
|
-
const restored = new DAppSession({
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
502
|
+
await session.createPairing()
|
|
503
|
+
const walletKp = generateX25519KeyPair()
|
|
504
|
+
|
|
505
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
506
|
+
|
|
507
|
+
const json = session.serialize()
|
|
508
|
+
expect(json).toBeTruthy()
|
|
509
|
+
|
|
510
|
+
const newTransport = new MockTransport()
|
|
511
|
+
const restored = new DAppSession({
|
|
512
|
+
transport: newTransport,
|
|
513
|
+
meta: {
|
|
514
|
+
name: 'Test dApp',
|
|
515
|
+
description: 'Test',
|
|
516
|
+
url: 'https://test.com',
|
|
517
|
+
icon: 'https://test.com/icon.png',
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
expect(restored.restore(json)).toBe(true)
|
|
521
|
+
expect(restored.channelId).toBe(session.channelId)
|
|
522
|
+
})
|
|
420
523
|
|
|
421
524
|
it('returns false for invalid JSON', () => {
|
|
422
|
-
const s = new DAppSession({
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
525
|
+
const s = new DAppSession({
|
|
526
|
+
transport: new MockTransport(),
|
|
527
|
+
meta: {
|
|
528
|
+
name: 'Test dApp',
|
|
529
|
+
description: 'Test',
|
|
530
|
+
url: 'https://test.com',
|
|
531
|
+
icon: 'https://test.com/icon.png',
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
expect(s.restore('not json')).toBe(false)
|
|
535
|
+
expect(s.restore('{}')).toBe(false)
|
|
536
|
+
expect(s.restore('{"channelId":"abc"}')).toBe(false) // missing privKey
|
|
537
|
+
})
|
|
538
|
+
})
|
|
428
539
|
|
|
429
540
|
describe('auto-accept on rejoin', () => {
|
|
430
541
|
it('auto-accepts known wallet on rejoin (no sealed_join on reconnect)', async () => {
|
|
431
|
-
await session.createPairing()
|
|
432
|
-
const walletKp = generateX25519KeyPair()
|
|
542
|
+
await session.createPairing()
|
|
543
|
+
const walletKp = generateX25519KeyPair()
|
|
433
544
|
|
|
434
545
|
// First join carries sealed capabilities/meta (auto-accepted).
|
|
435
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
436
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
546
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
547
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
437
548
|
|
|
438
549
|
// Second join (rejoin) without sealed_join — should auto-accept (same wallet, same approved scope)
|
|
439
550
|
transport.receive({
|
|
440
|
-
v: 1,
|
|
441
|
-
|
|
551
|
+
v: 1,
|
|
552
|
+
t: 'join',
|
|
553
|
+
ch: session.channelId,
|
|
554
|
+
ts: Date.now(),
|
|
555
|
+
from: walletKp.publicKeyB64,
|
|
442
556
|
body: { sealed_join: null },
|
|
443
|
-
} as ProtocolMessage)
|
|
557
|
+
} as ProtocolMessage)
|
|
444
558
|
|
|
445
559
|
// First join auto-accepted + rejoin auto-accepted = 2 accept messages
|
|
446
|
-
const acceptMessages = transport.sent.filter(m => m.t === 'accept')
|
|
447
|
-
expect(acceptMessages).toHaveLength(2)
|
|
448
|
-
})
|
|
560
|
+
const acceptMessages = transport.sent.filter((m) => m.t === 'accept')
|
|
561
|
+
expect(acceptMessages).toHaveLength(2)
|
|
562
|
+
})
|
|
449
563
|
|
|
450
564
|
it('auto-accepts new wallet on rejoin (different pubkey)', async () => {
|
|
451
|
-
await session.createPairing()
|
|
452
|
-
const walletKp = generateX25519KeyPair()
|
|
453
|
-
const walletKp2 = generateX25519KeyPair()
|
|
565
|
+
await session.createPairing()
|
|
566
|
+
const walletKp = generateX25519KeyPair()
|
|
567
|
+
const walletKp2 = generateX25519KeyPair()
|
|
454
568
|
|
|
455
569
|
// First join (auto-accepted)
|
|
456
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
457
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
570
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
571
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
458
572
|
|
|
459
573
|
// Second join with different wallet — also auto-accepted (sealed_join decryption proves possession)
|
|
460
|
-
receiveFreshJoin(transport, session, walletKp2)
|
|
574
|
+
receiveFreshJoin(transport, session, walletKp2)
|
|
461
575
|
|
|
462
|
-
const acceptMessages = transport.sent.filter(m => m.t === 'accept')
|
|
463
|
-
expect(acceptMessages).toHaveLength(2)
|
|
464
|
-
})
|
|
465
|
-
})
|
|
576
|
+
const acceptMessages = transport.sent.filter((m) => m.t === 'accept')
|
|
577
|
+
expect(acceptMessages).toHaveLength(2)
|
|
578
|
+
})
|
|
579
|
+
})
|
|
466
580
|
|
|
467
581
|
describe('close message handling', () => {
|
|
468
582
|
it('transitions to closed on receiving close', async () => {
|
|
469
|
-
await session.createPairing()
|
|
470
|
-
const walletKp = generateX25519KeyPair()
|
|
583
|
+
await session.createPairing()
|
|
584
|
+
const walletKp = generateX25519KeyPair()
|
|
471
585
|
transport.receive({
|
|
472
|
-
v: 1,
|
|
473
|
-
|
|
586
|
+
v: 1,
|
|
587
|
+
t: 'close',
|
|
588
|
+
ch: session.channelId,
|
|
589
|
+
ts: Date.now(),
|
|
590
|
+
from: walletKp.publicKeyB64,
|
|
474
591
|
body: { reason: 'timeout' },
|
|
475
|
-
} as ProtocolMessage)
|
|
592
|
+
} as ProtocolMessage)
|
|
476
593
|
|
|
477
|
-
expect(session.phase).toBe('closed')
|
|
478
|
-
})
|
|
479
|
-
})
|
|
594
|
+
expect(session.phase).toBe('closed')
|
|
595
|
+
})
|
|
596
|
+
})
|
|
480
597
|
|
|
481
598
|
describe('destroy', () => {
|
|
482
599
|
it('closes and removes all listeners', async () => {
|
|
483
|
-
await session.createPairing()
|
|
484
|
-
const handler = vi.fn()
|
|
485
|
-
session.on('phase', handler)
|
|
486
|
-
session.destroy()
|
|
600
|
+
await session.createPairing()
|
|
601
|
+
const handler = vi.fn()
|
|
602
|
+
session.on('phase', handler)
|
|
603
|
+
session.destroy()
|
|
487
604
|
|
|
488
|
-
expect(session.phase).toBe('closed')
|
|
605
|
+
expect(session.phase).toBe('closed')
|
|
489
606
|
// After destroy, emitting should not call handler
|
|
490
607
|
// (removeAll was called)
|
|
491
|
-
})
|
|
492
|
-
})
|
|
608
|
+
})
|
|
609
|
+
})
|
|
493
610
|
|
|
494
611
|
describe('protocol compliance', () => {
|
|
495
612
|
it('rejects messages with from="_adapter" for peer types (§2)', async () => {
|
|
496
|
-
await session.createPairing()
|
|
497
|
-
const errorHandler = vi.fn()
|
|
498
|
-
session.on('error', errorHandler)
|
|
613
|
+
await session.createPairing()
|
|
614
|
+
const errorHandler = vi.fn()
|
|
615
|
+
session.on('error', errorHandler)
|
|
499
616
|
|
|
500
617
|
// Send a close message with from: '_adapter' — should be rejected
|
|
501
618
|
transport.receive({
|
|
502
|
-
v: 1,
|
|
503
|
-
|
|
619
|
+
v: 1,
|
|
620
|
+
t: 'close',
|
|
621
|
+
ch: session.channelId,
|
|
622
|
+
ts: Date.now(),
|
|
623
|
+
from: '_adapter',
|
|
504
624
|
body: { reason: 'normal' },
|
|
505
|
-
} as ProtocolMessage)
|
|
625
|
+
} as ProtocolMessage)
|
|
506
626
|
|
|
507
|
-
expect(errorHandler).toHaveBeenCalledWith(
|
|
508
|
-
|
|
509
|
-
|
|
627
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
628
|
+
expect.objectContaining({
|
|
629
|
+
message: expect.stringContaining('_adapter'),
|
|
630
|
+
}),
|
|
631
|
+
)
|
|
510
632
|
// Should NOT have processed it as a real close
|
|
511
|
-
expect(session.phase).not.toBe('closed')
|
|
512
|
-
})
|
|
633
|
+
expect(session.phase).not.toBe('closed')
|
|
634
|
+
})
|
|
513
635
|
|
|
514
636
|
it('rejects messages with unsupported version (§15 rule 12)', async () => {
|
|
515
|
-
await session.createPairing()
|
|
637
|
+
await session.createPairing()
|
|
516
638
|
|
|
517
639
|
// Send message with v: 2 — should close with unsupported_version
|
|
518
640
|
transport.receive({
|
|
519
|
-
v: 2,
|
|
520
|
-
|
|
641
|
+
v: 2,
|
|
642
|
+
t: 'close',
|
|
643
|
+
ch: session.channelId,
|
|
644
|
+
ts: Date.now(),
|
|
645
|
+
from: 'somepubkey',
|
|
521
646
|
body: { reason: 'normal' },
|
|
522
|
-
} as unknown as ProtocolMessage)
|
|
647
|
+
} as unknown as ProtocolMessage)
|
|
523
648
|
|
|
524
|
-
expect(session.phase).toBe('closed')
|
|
525
|
-
const closeMsg = transport.sent.find(m => m.t === 'close') as any
|
|
526
|
-
expect(closeMsg).toBeTruthy()
|
|
527
|
-
expect(closeMsg.body.reason).toBe('unsupported_version')
|
|
528
|
-
})
|
|
649
|
+
expect(session.phase).toBe('closed')
|
|
650
|
+
const closeMsg = transport.sent.find((m) => m.t === 'close') as any
|
|
651
|
+
expect(closeMsg).toBeTruthy()
|
|
652
|
+
expect(closeMsg.body.reason).toBe('unsupported_version')
|
|
653
|
+
})
|
|
529
654
|
|
|
530
655
|
it('uses null for missing walletMeta in session context', async () => {
|
|
531
|
-
await session.createPairing()
|
|
656
|
+
await session.createPairing()
|
|
532
657
|
|
|
533
658
|
// Before any wallet joins, walletMeta should be undefined
|
|
534
|
-
expect(session.walletMeta).toBeUndefined()
|
|
659
|
+
expect(session.walletMeta).toBeUndefined()
|
|
535
660
|
|
|
536
661
|
// Access the private sessionContext to verify it uses null (not {})
|
|
537
662
|
const context = (session as any).sessionContext(
|
|
538
663
|
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
539
664
|
undefined,
|
|
540
665
|
undefined,
|
|
541
|
-
)
|
|
542
|
-
expect(context.walletMeta).toBeNull()
|
|
543
|
-
})
|
|
544
|
-
})
|
|
666
|
+
)
|
|
667
|
+
expect(context.walletMeta).toBeNull()
|
|
668
|
+
})
|
|
669
|
+
})
|
|
545
670
|
|
|
546
671
|
describe('auto-accept flow (first join)', () => {
|
|
547
672
|
it('auto-accepts first wallet join with valid sealed_join, skipping pending_accept', async () => {
|
|
548
|
-
const phases: string[] = []
|
|
549
|
-
session.on('phase', (p) => phases.push(p))
|
|
673
|
+
const phases: string[] = []
|
|
674
|
+
session.on('phase', (p) => phases.push(p))
|
|
550
675
|
|
|
551
|
-
await session.createPairing()
|
|
552
|
-
const walletKp = generateX25519KeyPair()
|
|
676
|
+
await session.createPairing()
|
|
677
|
+
const walletKp = generateX25519KeyPair()
|
|
553
678
|
|
|
554
|
-
receiveFreshJoin(transport, session, walletKp)
|
|
679
|
+
receiveFreshJoin(transport, session, walletKp)
|
|
555
680
|
|
|
556
681
|
// Auto-accept should have sent an accept message without manual acceptWallet()
|
|
557
|
-
const acceptMsg = transport.sent.find(m => m.t === 'accept')
|
|
558
|
-
expect(acceptMsg).toBeTruthy()
|
|
682
|
+
const acceptMsg = transport.sent.find((m) => m.t === 'accept')
|
|
683
|
+
expect(acceptMsg).toBeTruthy()
|
|
559
684
|
|
|
560
685
|
// Simulate relay responding with ready.connected
|
|
561
|
-
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
686
|
+
receiveConnected(transport, session, walletKp.publicKeyB64)
|
|
562
687
|
|
|
563
|
-
expect(session.phase).toBe('connected')
|
|
688
|
+
expect(session.phase).toBe('connected')
|
|
564
689
|
// Phase goes waiting → pending_accept → (auto-accept) → accepting → connected
|
|
565
690
|
// pending_accept is emitted briefly before auto-accept kicks in
|
|
566
|
-
expect(phases).toContain('pending_accept')
|
|
567
|
-
})
|
|
568
|
-
})
|
|
691
|
+
expect(phases).toContain('pending_accept')
|
|
692
|
+
})
|
|
693
|
+
})
|
|
569
694
|
|
|
570
695
|
describe('session fingerprint after createPairing', () => {
|
|
571
696
|
it('sessionFingerprint is a 4-digit string and event was emitted', async () => {
|
|
572
|
-
const fpHandler = vi.fn()
|
|
573
|
-
session.on('sessionFingerprint', fpHandler)
|
|
697
|
+
const fpHandler = vi.fn()
|
|
698
|
+
session.on('sessionFingerprint', fpHandler)
|
|
574
699
|
|
|
575
|
-
await session.createPairing()
|
|
700
|
+
await session.createPairing()
|
|
576
701
|
|
|
577
|
-
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
578
|
-
expect(fpHandler).toHaveBeenCalledTimes(1)
|
|
579
|
-
expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint)
|
|
580
|
-
})
|
|
581
|
-
})
|
|
702
|
+
expect(session.sessionFingerprint).toMatch(/^\d{4}$/)
|
|
703
|
+
expect(fpHandler).toHaveBeenCalledTimes(1)
|
|
704
|
+
expect(fpHandler).toHaveBeenCalledWith(session.sessionFingerprint)
|
|
705
|
+
})
|
|
706
|
+
})
|
|
582
707
|
|
|
583
708
|
describe('session fingerprint matches wallet side', () => {
|
|
584
709
|
it('dApp and wallet compute the same fingerprint', async () => {
|
|
585
|
-
await session.createPairing()
|
|
586
|
-
const dappFingerprint = session.sessionFingerprint
|
|
710
|
+
await session.createPairing()
|
|
711
|
+
const dappFingerprint = session.sessionFingerprint
|
|
587
712
|
|
|
588
713
|
// The dApp computes fingerprint from its own pubkey + channelId
|
|
589
714
|
// The wallet computes it from (channelId, dappPubKeyB64) — same inputs
|
|
590
|
-
const dappPubB64 = dappPubKeyFromCreate(transport)
|
|
591
|
-
const walletSideFingerprint = computeSessionFingerprint(session.channelId, dappPubB64)
|
|
715
|
+
const dappPubB64 = dappPubKeyFromCreate(transport)
|
|
716
|
+
const walletSideFingerprint = computeSessionFingerprint(session.channelId, dappPubB64)
|
|
592
717
|
|
|
593
|
-
expect(dappFingerprint).toBe(walletSideFingerprint)
|
|
594
|
-
expect(dappFingerprint).toMatch(/^\d{4}$/)
|
|
595
|
-
})
|
|
596
|
-
})
|
|
718
|
+
expect(dappFingerprint).toBe(walletSideFingerprint)
|
|
719
|
+
expect(dappFingerprint).toMatch(/^\d{4}$/)
|
|
720
|
+
})
|
|
721
|
+
})
|
|
597
722
|
|
|
598
723
|
describe('session TTL enforcement', () => {
|
|
599
724
|
it('closes with reason timeout after TTL expires', async () => {
|
|
600
|
-
vi.useFakeTimers()
|
|
725
|
+
vi.useFakeTimers()
|
|
601
726
|
|
|
602
|
-
const shortTtlTransport = new MockTransport()
|
|
727
|
+
const shortTtlTransport = new MockTransport()
|
|
603
728
|
const shortTtlSession = new DAppSession({
|
|
604
729
|
transport: shortTtlTransport,
|
|
605
730
|
meta: { name: 'T', description: 'T', url: 'https://t.com', icon: 'https://t.com/i.png' },
|
|
606
731
|
sessionTtl: 100,
|
|
607
|
-
})
|
|
732
|
+
})
|
|
608
733
|
|
|
609
|
-
await shortTtlSession.createPairing()
|
|
610
|
-
const walletKp = generateX25519KeyPair()
|
|
734
|
+
await shortTtlSession.createPairing()
|
|
735
|
+
const walletKp = generateX25519KeyPair()
|
|
611
736
|
|
|
612
737
|
// Simulate wallet join
|
|
613
738
|
shortTtlTransport.receive({
|
|
614
|
-
v: 1,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
739
|
+
v: 1,
|
|
740
|
+
t: 'join',
|
|
741
|
+
ch: shortTtlSession.channelId,
|
|
742
|
+
ts: Date.now(),
|
|
743
|
+
from: walletKp.publicKeyB64,
|
|
744
|
+
body: makeJoinBody(
|
|
745
|
+
shortTtlSession.channelId,
|
|
746
|
+
shortTtlTransport.sent.find((m) => m.t === 'create')?.from!,
|
|
747
|
+
walletKp,
|
|
748
|
+
),
|
|
749
|
+
} as ProtocolMessage)
|
|
618
750
|
|
|
619
751
|
// Simulate ready.connected (this starts the TTL timer)
|
|
620
752
|
shortTtlTransport.receive({
|
|
621
|
-
v: 1,
|
|
622
|
-
|
|
753
|
+
v: 1,
|
|
754
|
+
t: 'ready',
|
|
755
|
+
ch: shortTtlSession.channelId,
|
|
756
|
+
ts: Date.now(),
|
|
757
|
+
from: '_adapter',
|
|
623
758
|
body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
|
|
624
|
-
} as ProtocolMessage)
|
|
759
|
+
} as ProtocolMessage)
|
|
625
760
|
|
|
626
|
-
expect(shortTtlSession.phase).toBe('connected')
|
|
761
|
+
expect(shortTtlSession.phase).toBe('connected')
|
|
627
762
|
|
|
628
|
-
const errorHandler = vi.fn()
|
|
629
|
-
shortTtlSession.on('error', errorHandler)
|
|
763
|
+
const errorHandler = vi.fn()
|
|
764
|
+
shortTtlSession.on('error', errorHandler)
|
|
630
765
|
|
|
631
766
|
// Advance time past the TTL
|
|
632
|
-
vi.advanceTimersByTime(150)
|
|
767
|
+
vi.advanceTimersByTime(150)
|
|
633
768
|
|
|
634
|
-
expect(shortTtlSession.phase).toBe('closed')
|
|
635
|
-
expect(errorHandler).toHaveBeenCalledWith(
|
|
636
|
-
|
|
637
|
-
|
|
769
|
+
expect(shortTtlSession.phase).toBe('closed')
|
|
770
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
771
|
+
expect.objectContaining({
|
|
772
|
+
message: expect.stringContaining('expired'),
|
|
773
|
+
}),
|
|
774
|
+
)
|
|
638
775
|
|
|
639
776
|
// Verify close message was sent with reason 'timeout'
|
|
640
|
-
const closeMsg = shortTtlTransport.sent.find(m => m.t === 'close')
|
|
641
|
-
expect(closeMsg).toBeTruthy()
|
|
642
|
-
expect((closeMsg as any).body.reason).toBe('timeout')
|
|
643
|
-
|
|
644
|
-
vi.useRealTimers()
|
|
645
|
-
})
|
|
646
|
-
})
|
|
647
|
-
})
|
|
777
|
+
const closeMsg = shortTtlTransport.sent.find((m) => m.t === 'close')
|
|
778
|
+
expect(closeMsg).toBeTruthy()
|
|
779
|
+
expect((closeMsg as any).body.reason).toBe('timeout')
|
|
780
|
+
|
|
781
|
+
vi.useRealTimers()
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
})
|