walletpair-sdk 1.0.3 → 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 +1 -1
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +12 -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 +51 -41
package/src/wallet-session.ts
CHANGED
|
@@ -378,6 +378,7 @@ export class WalletSession extends Emitter<WalletSessionEvents> {
|
|
|
378
378
|
}
|
|
379
379
|
this.dappName = d.dappName ?? undefined
|
|
380
380
|
this.sessionStartTime = d.sessionStartTime ?? null
|
|
381
|
+
this.setPhase('connected')
|
|
381
382
|
return true
|
|
382
383
|
} catch {
|
|
383
384
|
return false
|
|
@@ -442,15 +443,14 @@ export class WalletSession extends Emitter<WalletSessionEvents> {
|
|
|
442
443
|
}
|
|
443
444
|
|
|
444
445
|
case 'req': {
|
|
446
|
+
if (this.phase !== 'connected') break
|
|
445
447
|
const reqBody = msg.body as { id?: string; sealed?: string }
|
|
446
448
|
if (this.remotePubKey && msg.from !== b64urlEncode(this.remotePubKey)) break
|
|
447
449
|
// All requests MUST be sealed — reject unsealed requests to prevent
|
|
448
450
|
// method injection by a malicious relay.
|
|
449
451
|
if (!reqBody.sealed || !reqBody.id || !this.recvKey) {
|
|
450
452
|
if (reqBody.id) {
|
|
451
|
-
this.observeSend(
|
|
452
|
-
this.reject(reqBody.id, 'protocol_error', 'Request must be encrypted'),
|
|
453
|
-
)
|
|
453
|
+
this.observeSend(this.reject(reqBody.id, 'protocol_error', 'Request must be encrypted'))
|
|
454
454
|
}
|
|
455
455
|
break
|
|
456
456
|
}
|
|
@@ -470,12 +470,10 @@ export class WalletSession extends Emitter<WalletSessionEvents> {
|
|
|
470
470
|
const afterPersist = () => this.processRequest(requestId, data, plaintext)
|
|
471
471
|
const persisted = this.persistSnapshot()
|
|
472
472
|
if (isPromiseLike(persisted)) {
|
|
473
|
-
void persisted
|
|
474
|
-
.
|
|
475
|
-
.
|
|
476
|
-
|
|
477
|
-
this.emit('error', this.persistenceError(e))
|
|
478
|
-
})
|
|
473
|
+
void persisted.then(afterPersist).catch((e) => {
|
|
474
|
+
this.recvSeq = prevRecvSeq // rollback on persist failure
|
|
475
|
+
this.emit('error', this.persistenceError(e))
|
|
476
|
+
})
|
|
479
477
|
} else {
|
|
480
478
|
afterPersist()
|
|
481
479
|
}
|
package/src/ws-transport.test.ts
CHANGED
|
@@ -1,231 +1,233 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { ProtocolMessage } from './types.js'
|
|
3
|
+
import { WebSocketTransport } from './ws-transport.js'
|
|
3
4
|
|
|
4
5
|
// Mock WebSocket for Node.js environment
|
|
5
6
|
class MockWebSocket {
|
|
6
|
-
static instances: MockWebSocket[] = []
|
|
7
|
+
static instances: MockWebSocket[] = []
|
|
7
8
|
|
|
8
|
-
url: string
|
|
9
|
-
protocols: string[]
|
|
10
|
-
readyState = 0
|
|
11
|
-
onopen: (() => void) | null = null
|
|
12
|
-
onmessage: ((event: { data: string }) => void) | null = null
|
|
13
|
-
onclose: (() => void) | null = null
|
|
14
|
-
onerror: (() => void) | null = null
|
|
15
|
-
sentMessages: string[] = []
|
|
9
|
+
url: string
|
|
10
|
+
protocols: string[]
|
|
11
|
+
readyState = 0 // CONNECTING
|
|
12
|
+
onopen: (() => void) | null = null
|
|
13
|
+
onmessage: ((event: { data: string }) => void) | null = null
|
|
14
|
+
onclose: (() => void) | null = null
|
|
15
|
+
onerror: (() => void) | null = null
|
|
16
|
+
sentMessages: string[] = []
|
|
16
17
|
|
|
17
18
|
constructor(url: string, protocols?: string[]) {
|
|
18
|
-
this.url = url
|
|
19
|
-
this.protocols = protocols ?? []
|
|
20
|
-
MockWebSocket.instances.push(this)
|
|
19
|
+
this.url = url
|
|
20
|
+
this.protocols = protocols ?? []
|
|
21
|
+
MockWebSocket.instances.push(this)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
send(data: string) {
|
|
24
|
-
this.sentMessages.push(data)
|
|
25
|
+
this.sentMessages.push(data)
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
close() {
|
|
28
|
-
this.readyState = 3
|
|
29
|
-
this.onclose?.()
|
|
29
|
+
this.readyState = 3 // CLOSED
|
|
30
|
+
this.onclose?.()
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// Test helpers
|
|
33
34
|
simulateOpen() {
|
|
34
|
-
this.readyState = 1
|
|
35
|
-
this.onopen?.()
|
|
35
|
+
this.readyState = 1 // OPEN
|
|
36
|
+
this.onopen?.()
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
simulateMessage(data: string) {
|
|
39
|
-
this.onmessage?.({ data })
|
|
40
|
+
this.onmessage?.({ data })
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
simulateClose() {
|
|
43
|
-
this.readyState = 3
|
|
44
|
-
this.onclose?.()
|
|
44
|
+
this.readyState = 3
|
|
45
|
+
this.onclose?.()
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
simulateError() {
|
|
48
|
-
this.onerror?.()
|
|
49
|
+
this.onerror?.()
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
describe('WebSocketTransport', () => {
|
|
53
|
-
let originalWebSocket: typeof globalThis.WebSocket
|
|
54
|
+
let originalWebSocket: typeof globalThis.WebSocket
|
|
54
55
|
|
|
55
56
|
beforeEach(() => {
|
|
56
|
-
MockWebSocket.instances = []
|
|
57
|
-
originalWebSocket = globalThis.WebSocket
|
|
58
|
-
(globalThis as
|
|
59
|
-
})
|
|
57
|
+
MockWebSocket.instances = []
|
|
58
|
+
originalWebSocket = globalThis.WebSocket
|
|
59
|
+
;(globalThis as unknown as Record<string, unknown>).WebSocket = MockWebSocket
|
|
60
|
+
})
|
|
60
61
|
|
|
61
62
|
afterEach(() => {
|
|
62
|
-
globalThis.WebSocket = originalWebSocket
|
|
63
|
-
})
|
|
63
|
+
globalThis.WebSocket = originalWebSocket
|
|
64
|
+
})
|
|
64
65
|
|
|
65
66
|
it('starts disconnected', () => {
|
|
66
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
67
|
-
expect(t.state).toBe('disconnected')
|
|
68
|
-
})
|
|
67
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
68
|
+
expect(t.state).toBe('disconnected')
|
|
69
|
+
})
|
|
69
70
|
|
|
70
71
|
it('accepts string URL constructor', () => {
|
|
71
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
72
|
-
expect(t.state).toBe('disconnected')
|
|
73
|
-
})
|
|
72
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
73
|
+
expect(t.state).toBe('disconnected')
|
|
74
|
+
})
|
|
74
75
|
|
|
75
76
|
it('accepts options object constructor', () => {
|
|
76
|
-
const t = new WebSocketTransport({ url: 'ws://localhost:8080/v1', protocols: ['custom'] })
|
|
77
|
-
expect(t.state).toBe('disconnected')
|
|
78
|
-
})
|
|
77
|
+
const t = new WebSocketTransport({ url: 'ws://localhost:8080/v1', protocols: ['custom'] })
|
|
78
|
+
expect(t.state).toBe('disconnected')
|
|
79
|
+
})
|
|
79
80
|
|
|
80
81
|
describe('connect', () => {
|
|
81
82
|
it('resolves on successful connection', async () => {
|
|
82
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
83
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
83
84
|
|
|
84
|
-
const connectPromise = t.connect()
|
|
85
|
+
const connectPromise = t.connect()
|
|
85
86
|
|
|
86
87
|
// Simulate WebSocket open
|
|
87
|
-
const ws = MockWebSocket.instances[0]
|
|
88
|
-
expect(ws
|
|
89
|
-
expect(ws
|
|
88
|
+
const ws = MockWebSocket.instances[0]
|
|
89
|
+
expect(ws).toBeDefined()
|
|
90
|
+
expect(ws?.url).toBe('ws://localhost:8080/v1')
|
|
91
|
+
expect(ws?.protocols).toEqual(['walletpair.v1'])
|
|
90
92
|
|
|
91
|
-
ws
|
|
93
|
+
ws?.simulateOpen()
|
|
92
94
|
|
|
93
|
-
await connectPromise
|
|
94
|
-
expect(t.state).toBe('connected')
|
|
95
|
-
})
|
|
95
|
+
await connectPromise
|
|
96
|
+
expect(t.state).toBe('connected')
|
|
97
|
+
})
|
|
96
98
|
|
|
97
99
|
it('rejects on connection failure', async () => {
|
|
98
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
100
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
99
101
|
|
|
100
|
-
const connectPromise = t.connect()
|
|
102
|
+
const connectPromise = t.connect()
|
|
101
103
|
|
|
102
|
-
const ws = MockWebSocket.instances[0]
|
|
103
|
-
ws
|
|
104
|
-
ws
|
|
104
|
+
const ws = MockWebSocket.instances[0]
|
|
105
|
+
ws?.simulateError()
|
|
106
|
+
ws?.simulateClose()
|
|
105
107
|
|
|
106
|
-
await expect(connectPromise).rejects.toThrow('WebSocket connection failed')
|
|
107
|
-
expect(t.state).toBe('disconnected')
|
|
108
|
-
})
|
|
108
|
+
await expect(connectPromise).rejects.toThrow('WebSocket connection failed')
|
|
109
|
+
expect(t.state).toBe('disconnected')
|
|
110
|
+
})
|
|
109
111
|
|
|
110
112
|
it('calls onOpen handler on successful connection', async () => {
|
|
111
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
112
|
-
const openHandler = vi.fn()
|
|
113
|
-
t.onOpen(openHandler)
|
|
113
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
114
|
+
const openHandler = vi.fn()
|
|
115
|
+
t.onOpen(openHandler)
|
|
114
116
|
|
|
115
|
-
const promise = t.connect()
|
|
116
|
-
MockWebSocket.instances[0]
|
|
117
|
-
await promise
|
|
117
|
+
const promise = t.connect()
|
|
118
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
119
|
+
await promise
|
|
118
120
|
|
|
119
|
-
expect(openHandler).toHaveBeenCalledTimes(1)
|
|
120
|
-
})
|
|
121
|
-
})
|
|
121
|
+
expect(openHandler).toHaveBeenCalledTimes(1)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
122
124
|
|
|
123
125
|
describe('send', () => {
|
|
124
126
|
it('sends JSON-serialized message', async () => {
|
|
125
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
126
|
-
const promise = t.connect()
|
|
127
|
-
MockWebSocket.instances[0]
|
|
128
|
-
await promise
|
|
127
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
128
|
+
const promise = t.connect()
|
|
129
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
130
|
+
await promise
|
|
129
131
|
|
|
130
|
-
const msg = { v: 1, t: 'ping', ch: 'abc', ts: 123 } as
|
|
131
|
-
t.send(msg)
|
|
132
|
+
const msg = { v: 1, t: 'ping', ch: 'abc', ts: 123 } as unknown as ProtocolMessage
|
|
133
|
+
t.send(msg)
|
|
132
134
|
|
|
133
|
-
const ws = MockWebSocket.instances[0]
|
|
134
|
-
expect(ws
|
|
135
|
-
expect(JSON.parse(ws
|
|
136
|
-
})
|
|
135
|
+
const ws = MockWebSocket.instances[0]
|
|
136
|
+
expect(ws?.sentMessages).toHaveLength(1)
|
|
137
|
+
expect(JSON.parse(ws?.sentMessages[0] ?? '')).toEqual(msg)
|
|
138
|
+
})
|
|
137
139
|
|
|
138
140
|
it('does nothing when not connected', () => {
|
|
139
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
141
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
140
142
|
// Not connected, should not throw
|
|
141
|
-
t.send({ v: 1, t: 'ping', ch: 'abc', ts: 123 } as
|
|
142
|
-
})
|
|
143
|
-
})
|
|
143
|
+
t.send({ v: 1, t: 'ping', ch: 'abc', ts: 123 } as unknown as ProtocolMessage)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
144
146
|
|
|
145
147
|
describe('receive', () => {
|
|
146
148
|
it('calls message handler on incoming messages', async () => {
|
|
147
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
148
|
-
const handler = vi.fn()
|
|
149
|
-
t.onMessage(handler)
|
|
149
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
150
|
+
const handler = vi.fn()
|
|
151
|
+
t.onMessage(handler)
|
|
150
152
|
|
|
151
|
-
const promise = t.connect()
|
|
152
|
-
MockWebSocket.instances[0]
|
|
153
|
-
await promise
|
|
153
|
+
const promise = t.connect()
|
|
154
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
155
|
+
await promise
|
|
154
156
|
|
|
155
|
-
const msg = { v: 1, t: 'ready', ch: 'abc', state: 'waiting' }
|
|
156
|
-
MockWebSocket.instances[0]
|
|
157
|
+
const msg = { v: 1, t: 'ready', ch: 'abc', state: 'waiting' }
|
|
158
|
+
MockWebSocket.instances[0]?.simulateMessage(JSON.stringify(msg))
|
|
157
159
|
|
|
158
|
-
expect(handler).toHaveBeenCalledWith(msg)
|
|
159
|
-
})
|
|
160
|
+
expect(handler).toHaveBeenCalledWith(msg)
|
|
161
|
+
})
|
|
160
162
|
|
|
161
163
|
it('ignores malformed JSON', async () => {
|
|
162
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
163
|
-
const handler = vi.fn()
|
|
164
|
-
t.onMessage(handler)
|
|
164
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
165
|
+
const handler = vi.fn()
|
|
166
|
+
t.onMessage(handler)
|
|
165
167
|
|
|
166
|
-
const promise = t.connect()
|
|
167
|
-
MockWebSocket.instances[0]
|
|
168
|
-
await promise
|
|
168
|
+
const promise = t.connect()
|
|
169
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
170
|
+
await promise
|
|
169
171
|
|
|
170
|
-
MockWebSocket.instances[0]
|
|
171
|
-
expect(handler).not.toHaveBeenCalled()
|
|
172
|
-
})
|
|
173
|
-
})
|
|
172
|
+
MockWebSocket.instances[0]?.simulateMessage('not json')
|
|
173
|
+
expect(handler).not.toHaveBeenCalled()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
174
176
|
|
|
175
177
|
describe('disconnect', () => {
|
|
176
178
|
it('transitions to disconnected', async () => {
|
|
177
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
178
|
-
const promise = t.connect()
|
|
179
|
-
MockWebSocket.instances[0]
|
|
180
|
-
await promise
|
|
179
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
180
|
+
const promise = t.connect()
|
|
181
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
182
|
+
await promise
|
|
181
183
|
|
|
182
|
-
t.disconnect()
|
|
183
|
-
expect(t.state).toBe('disconnected')
|
|
184
|
-
})
|
|
184
|
+
t.disconnect()
|
|
185
|
+
expect(t.state).toBe('disconnected')
|
|
186
|
+
})
|
|
185
187
|
|
|
186
188
|
it('does not call close handler on intentional disconnect', async () => {
|
|
187
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
188
|
-
const closeHandler = vi.fn()
|
|
189
|
-
t.onClose(closeHandler)
|
|
189
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
190
|
+
const closeHandler = vi.fn()
|
|
191
|
+
t.onClose(closeHandler)
|
|
190
192
|
|
|
191
|
-
const promise = t.connect()
|
|
192
|
-
MockWebSocket.instances[0]
|
|
193
|
-
await promise
|
|
193
|
+
const promise = t.connect()
|
|
194
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
195
|
+
await promise
|
|
194
196
|
|
|
195
|
-
t.disconnect()
|
|
196
|
-
expect(closeHandler).not.toHaveBeenCalled()
|
|
197
|
-
})
|
|
198
|
-
})
|
|
197
|
+
t.disconnect()
|
|
198
|
+
expect(closeHandler).not.toHaveBeenCalled()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
199
201
|
|
|
200
202
|
describe('unexpected close', () => {
|
|
201
203
|
it('calls close handler on unexpected transport close', async () => {
|
|
202
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
203
|
-
const closeHandler = vi.fn()
|
|
204
|
-
t.onClose(closeHandler)
|
|
204
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
205
|
+
const closeHandler = vi.fn()
|
|
206
|
+
t.onClose(closeHandler)
|
|
205
207
|
|
|
206
|
-
const promise = t.connect()
|
|
207
|
-
MockWebSocket.instances[0]
|
|
208
|
-
await promise
|
|
208
|
+
const promise = t.connect()
|
|
209
|
+
MockWebSocket.instances[0]?.simulateOpen()
|
|
210
|
+
await promise
|
|
209
211
|
|
|
210
212
|
// Simulate unexpected close (e.g., network drop)
|
|
211
|
-
MockWebSocket.instances[0]
|
|
213
|
+
MockWebSocket.instances[0]?.simulateClose()
|
|
212
214
|
|
|
213
|
-
expect(closeHandler).toHaveBeenCalledTimes(1)
|
|
214
|
-
expect(t.state).toBe('disconnected')
|
|
215
|
-
})
|
|
216
|
-
})
|
|
215
|
+
expect(closeHandler).toHaveBeenCalledTimes(1)
|
|
216
|
+
expect(t.state).toBe('disconnected')
|
|
217
|
+
})
|
|
218
|
+
})
|
|
217
219
|
|
|
218
220
|
describe('setUrl', () => {
|
|
219
221
|
it('updates the URL for next connection', async () => {
|
|
220
|
-
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
221
|
-
t.setUrl('ws://other:9090/v1')
|
|
222
|
-
|
|
223
|
-
const promise = t.connect()
|
|
224
|
-
const ws = MockWebSocket.instances[0]
|
|
225
|
-
expect(ws
|
|
226
|
-
|
|
227
|
-
ws
|
|
228
|
-
await promise
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
})
|
|
222
|
+
const t = new WebSocketTransport('ws://localhost:8080/v1')
|
|
223
|
+
t.setUrl('ws://other:9090/v1')
|
|
224
|
+
|
|
225
|
+
const promise = t.connect()
|
|
226
|
+
const ws = MockWebSocket.instances[0]
|
|
227
|
+
expect(ws?.url).toBe('ws://other:9090/v1')
|
|
228
|
+
|
|
229
|
+
ws?.simulateOpen()
|
|
230
|
+
await promise
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|
package/src/ws-transport.ts
CHANGED
|
@@ -4,90 +4,100 @@
|
|
|
4
4
|
* Works in browsers, Node.js 22+, Deno, Bun — anything with a global WebSocket.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Transport, TransportState
|
|
7
|
+
import type { ProtocolMessage, Transport, TransportState } from './types.js'
|
|
8
8
|
|
|
9
9
|
export interface WebSocketTransportOptions {
|
|
10
|
-
url: string
|
|
11
|
-
protocols?: string[]
|
|
10
|
+
url: string
|
|
11
|
+
protocols?: string[]
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export class WebSocketTransport implements Transport {
|
|
15
|
-
state: TransportState = 'disconnected'
|
|
15
|
+
state: TransportState = 'disconnected'
|
|
16
16
|
|
|
17
|
-
private ws: WebSocket | null = null
|
|
17
|
+
private ws: WebSocket | null = null
|
|
18
18
|
/** Current relay URL. Readable for channel hint injection. */
|
|
19
|
-
url: string
|
|
20
|
-
private protocols: string[]
|
|
19
|
+
url: string
|
|
20
|
+
private protocols: string[]
|
|
21
21
|
|
|
22
|
-
private messageHandler: ((msg: ProtocolMessage) => void) | null = null
|
|
23
|
-
private closeHandler: (() => void) | null = null
|
|
24
|
-
private openHandler: (() => void) | null = null
|
|
22
|
+
private messageHandler: ((msg: ProtocolMessage) => void) | null = null
|
|
23
|
+
private closeHandler: (() => void) | null = null
|
|
24
|
+
private openHandler: (() => void) | null = null
|
|
25
25
|
|
|
26
26
|
constructor(options: WebSocketTransportOptions | string) {
|
|
27
27
|
if (typeof options === 'string') {
|
|
28
|
-
this.url = options
|
|
29
|
-
this.protocols = ['walletpair.v1']
|
|
28
|
+
this.url = options
|
|
29
|
+
this.protocols = ['walletpair.v1']
|
|
30
30
|
} else {
|
|
31
|
-
this.url = options.url
|
|
32
|
-
this.protocols = options.protocols ?? ['walletpair.v1']
|
|
31
|
+
this.url = options.url
|
|
32
|
+
this.protocols = options.protocols ?? ['walletpair.v1']
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
onMessage(handler: (msg: ProtocolMessage) => void): void {
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
onMessage(handler: (msg: ProtocolMessage) => void): void {
|
|
37
|
+
this.messageHandler = handler
|
|
38
|
+
}
|
|
39
|
+
onClose(handler: () => void): void {
|
|
40
|
+
this.closeHandler = handler
|
|
41
|
+
}
|
|
42
|
+
onOpen(handler: () => void): void {
|
|
43
|
+
this.openHandler = handler
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
/** Update the relay URL (useful for reconnect to a different relay). */
|
|
41
47
|
setUrl(url: string): void {
|
|
42
|
-
this.url = url
|
|
48
|
+
this.url = url
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
connect(): Promise<void> {
|
|
46
52
|
return new Promise<void>((resolve, reject) => {
|
|
47
|
-
this.state = 'connecting'
|
|
48
|
-
const ws = new WebSocket(this.url, this.protocols)
|
|
53
|
+
this.state = 'connecting'
|
|
54
|
+
const ws = new WebSocket(this.url, this.protocols)
|
|
49
55
|
|
|
50
56
|
ws.onopen = () => {
|
|
51
|
-
this.state = 'connected'
|
|
52
|
-
this.ws = ws
|
|
53
|
-
this.openHandler?.()
|
|
54
|
-
resolve()
|
|
55
|
-
}
|
|
57
|
+
this.state = 'connected'
|
|
58
|
+
this.ws = ws
|
|
59
|
+
this.openHandler?.()
|
|
60
|
+
resolve()
|
|
61
|
+
}
|
|
56
62
|
|
|
57
63
|
ws.onmessage = (event: MessageEvent) => {
|
|
58
64
|
if (this.messageHandler) {
|
|
59
|
-
try {
|
|
65
|
+
try {
|
|
66
|
+
this.messageHandler(JSON.parse(event.data as string))
|
|
67
|
+
} catch {
|
|
68
|
+
/* bad json */
|
|
69
|
+
}
|
|
60
70
|
}
|
|
61
|
-
}
|
|
71
|
+
}
|
|
62
72
|
|
|
63
73
|
ws.onclose = () => {
|
|
64
|
-
const wasConnected = this.state === 'connected'
|
|
65
|
-
this.state = 'disconnected'
|
|
66
|
-
this.ws = null
|
|
74
|
+
const wasConnected = this.state === 'connected'
|
|
75
|
+
this.state = 'disconnected'
|
|
76
|
+
this.ws = null
|
|
67
77
|
if (wasConnected) {
|
|
68
|
-
this.closeHandler?.()
|
|
78
|
+
this.closeHandler?.()
|
|
69
79
|
} else {
|
|
70
|
-
reject(new Error('WebSocket connection failed'))
|
|
80
|
+
reject(new Error('WebSocket connection failed'))
|
|
71
81
|
}
|
|
72
|
-
}
|
|
82
|
+
}
|
|
73
83
|
|
|
74
84
|
ws.onerror = () => {
|
|
75
85
|
// onclose will fire after onerror, which handles the reject
|
|
76
|
-
}
|
|
77
|
-
})
|
|
86
|
+
}
|
|
87
|
+
})
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
send(msg: ProtocolMessage): void {
|
|
81
|
-
if (!this.ws || this.state !== 'connected') return
|
|
82
|
-
this.ws.send(JSON.stringify(msg))
|
|
91
|
+
if (!this.ws || this.state !== 'connected') return
|
|
92
|
+
this.ws.send(JSON.stringify(msg))
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
disconnect(): void {
|
|
86
96
|
if (this.ws) {
|
|
87
|
-
this.ws.onclose = null
|
|
88
|
-
this.ws.close()
|
|
89
|
-
this.ws = null
|
|
97
|
+
this.ws.onclose = null
|
|
98
|
+
this.ws.close()
|
|
99
|
+
this.ws = null
|
|
90
100
|
}
|
|
91
|
-
this.state = 'disconnected'
|
|
101
|
+
this.state = 'disconnected'
|
|
92
102
|
}
|
|
93
103
|
}
|