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
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* conditions correctly per the specification.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// State machine model (pure functions, no SDK dependency)
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
type DAppState = 'idle' | 'waiting' | 'pending_accept' | 'connected' | 'disconnected' | 'closed'
|
|
16
|
-
type WalletState = 'idle' | 'waiting_accept' | 'connected' | 'disconnected' | 'closed'
|
|
15
|
+
type DAppState = 'idle' | 'waiting' | 'pending_accept' | 'connected' | 'disconnected' | 'closed'
|
|
16
|
+
type WalletState = 'idle' | 'waiting_accept' | 'connected' | 'disconnected' | 'closed'
|
|
17
17
|
|
|
18
18
|
type DAppEvent =
|
|
19
19
|
| 'send_create'
|
|
@@ -28,7 +28,7 @@ type DAppEvent =
|
|
|
28
28
|
| 'send_create_reconnect'
|
|
29
29
|
| 'receive_channel_exists'
|
|
30
30
|
| 'session_expired'
|
|
31
|
-
| 'give_up'
|
|
31
|
+
| 'give_up'
|
|
32
32
|
|
|
33
33
|
type WalletEvent =
|
|
34
34
|
| 'send_join'
|
|
@@ -40,38 +40,38 @@ type WalletEvent =
|
|
|
40
40
|
| 'send_join_reconnect'
|
|
41
41
|
| 'receive_channel_not_found'
|
|
42
42
|
| 'session_expired'
|
|
43
|
-
| 'give_up'
|
|
43
|
+
| 'give_up'
|
|
44
44
|
|
|
45
45
|
/** DApp state machine per Section 14. */
|
|
46
46
|
function dappTransition(state: DAppState, event: DAppEvent): DAppState | null {
|
|
47
47
|
switch (state) {
|
|
48
48
|
case 'idle':
|
|
49
|
-
if (event === 'send_create') return 'waiting'
|
|
50
|
-
return null
|
|
49
|
+
if (event === 'send_create') return 'waiting'
|
|
50
|
+
return null
|
|
51
51
|
case 'waiting':
|
|
52
|
-
if (event === 'receive_join') return 'pending_accept'
|
|
53
|
-
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
54
|
-
if (event === 'timeout') return 'closed'
|
|
55
|
-
return null
|
|
52
|
+
if (event === 'receive_join') return 'pending_accept'
|
|
53
|
+
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
54
|
+
if (event === 'timeout') return 'closed'
|
|
55
|
+
return null
|
|
56
56
|
case 'pending_accept':
|
|
57
|
-
if (event === 'sealed_join_verified_send_accept') return 'connected'
|
|
58
|
-
if (event === 'user_rejects_send_close') return 'closed'
|
|
59
|
-
if (event === 'receive_terminate') return 'closed'
|
|
60
|
-
if (event === 'timeout') return 'closed'
|
|
61
|
-
return null
|
|
57
|
+
if (event === 'sealed_join_verified_send_accept') return 'connected'
|
|
58
|
+
if (event === 'user_rejects_send_close') return 'closed'
|
|
59
|
+
if (event === 'receive_terminate') return 'closed'
|
|
60
|
+
if (event === 'timeout') return 'closed'
|
|
61
|
+
return null
|
|
62
62
|
case 'connected':
|
|
63
|
-
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
64
|
-
if (event === 'transport_disconnected') return 'disconnected'
|
|
65
|
-
if (event === 'session_expired') return 'closed'
|
|
66
|
-
return null
|
|
63
|
+
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
64
|
+
if (event === 'transport_disconnected') return 'disconnected'
|
|
65
|
+
if (event === 'session_expired') return 'closed'
|
|
66
|
+
return null
|
|
67
67
|
case 'disconnected':
|
|
68
|
-
if (event === 'send_create_reconnect') return 'waiting'
|
|
69
|
-
if (event === 'receive_channel_exists') return 'disconnected'
|
|
70
|
-
if (event === 'session_expired') return 'closed'
|
|
71
|
-
if (event === 'give_up') return 'closed'
|
|
72
|
-
return null
|
|
68
|
+
if (event === 'send_create_reconnect') return 'waiting'
|
|
69
|
+
if (event === 'receive_channel_exists') return 'disconnected'
|
|
70
|
+
if (event === 'session_expired') return 'closed'
|
|
71
|
+
if (event === 'give_up') return 'closed'
|
|
72
|
+
return null
|
|
73
73
|
case 'closed':
|
|
74
|
-
return null
|
|
74
|
+
return null // terminal state
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -79,26 +79,26 @@ function dappTransition(state: DAppState, event: DAppEvent): DAppState | null {
|
|
|
79
79
|
function walletTransition(state: WalletState, event: WalletEvent): WalletState | null {
|
|
80
80
|
switch (state) {
|
|
81
81
|
case 'idle':
|
|
82
|
-
if (event === 'send_join') return 'waiting_accept'
|
|
83
|
-
return null
|
|
82
|
+
if (event === 'send_join') return 'waiting_accept'
|
|
83
|
+
return null
|
|
84
84
|
case 'waiting_accept':
|
|
85
|
-
if (event === 'receive_ready_connected') return 'connected'
|
|
86
|
-
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
87
|
-
if (event === 'timeout') return 'closed'
|
|
88
|
-
return null
|
|
85
|
+
if (event === 'receive_ready_connected') return 'connected'
|
|
86
|
+
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
87
|
+
if (event === 'timeout') return 'closed'
|
|
88
|
+
return null
|
|
89
89
|
case 'connected':
|
|
90
|
-
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
91
|
-
if (event === 'transport_disconnected') return 'disconnected'
|
|
92
|
-
if (event === 'session_expired') return 'closed'
|
|
93
|
-
return null
|
|
90
|
+
if (event === 'receive_close' || event === 'receive_terminate') return 'closed'
|
|
91
|
+
if (event === 'transport_disconnected') return 'disconnected'
|
|
92
|
+
if (event === 'session_expired') return 'closed'
|
|
93
|
+
return null
|
|
94
94
|
case 'disconnected':
|
|
95
|
-
if (event === 'send_join_reconnect') return 'waiting_accept'
|
|
96
|
-
if (event === 'receive_channel_not_found') return 'disconnected'
|
|
97
|
-
if (event === 'session_expired') return 'closed'
|
|
98
|
-
if (event === 'give_up') return 'closed'
|
|
99
|
-
return null
|
|
95
|
+
if (event === 'send_join_reconnect') return 'waiting_accept'
|
|
96
|
+
if (event === 'receive_channel_not_found') return 'disconnected'
|
|
97
|
+
if (event === 'session_expired') return 'closed'
|
|
98
|
+
if (event === 'give_up') return 'closed'
|
|
99
|
+
return null
|
|
100
100
|
case 'closed':
|
|
101
|
-
return null
|
|
101
|
+
return null // terminal state
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -109,103 +109,108 @@ function walletTransition(state: WalletState, event: WalletEvent): WalletState |
|
|
|
109
109
|
describe('Section 14 — DApp state machine', () => {
|
|
110
110
|
describe('valid transitions: idle -> waiting -> pending_accept -> connected -> closed', () => {
|
|
111
111
|
it('full happy path', () => {
|
|
112
|
-
let state: DAppState = 'idle'
|
|
113
|
-
state = dappTransition(state, 'send_create')
|
|
114
|
-
expect(state).toBe('waiting')
|
|
115
|
-
state = dappTransition(state, 'receive_join')
|
|
116
|
-
expect(state).toBe('pending_accept')
|
|
117
|
-
state = dappTransition(state, 'sealed_join_verified_send_accept')
|
|
118
|
-
expect(state).toBe('connected')
|
|
119
|
-
})
|
|
112
|
+
let state: DAppState = 'idle'
|
|
113
|
+
state = dappTransition(state, 'send_create') ?? 'closed'
|
|
114
|
+
expect(state).toBe('waiting')
|
|
115
|
+
state = dappTransition(state, 'receive_join') ?? 'closed'
|
|
116
|
+
expect(state).toBe('pending_accept')
|
|
117
|
+
state = dappTransition(state, 'sealed_join_verified_send_accept') ?? 'closed'
|
|
118
|
+
expect(state).toBe('connected')
|
|
119
|
+
})
|
|
120
120
|
|
|
121
121
|
it('close from connected', () => {
|
|
122
|
-
expect(dappTransition('connected', 'receive_close')).toBe('closed')
|
|
123
|
-
})
|
|
122
|
+
expect(dappTransition('connected', 'receive_close')).toBe('closed')
|
|
123
|
+
})
|
|
124
124
|
|
|
125
125
|
it('terminate from connected', () => {
|
|
126
|
-
expect(dappTransition('connected', 'receive_terminate')).toBe('closed')
|
|
127
|
-
})
|
|
126
|
+
expect(dappTransition('connected', 'receive_terminate')).toBe('closed')
|
|
127
|
+
})
|
|
128
128
|
|
|
129
129
|
it('session expiry from connected', () => {
|
|
130
|
-
expect(dappTransition('connected', 'session_expired')).toBe('closed')
|
|
131
|
-
})
|
|
132
|
-
})
|
|
130
|
+
expect(dappTransition('connected', 'session_expired')).toBe('closed')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
133
|
|
|
134
134
|
describe('invalid transitions are rejected', () => {
|
|
135
135
|
it('cannot receive join in idle', () => {
|
|
136
|
-
expect(dappTransition('idle', 'receive_join')).toBeNull()
|
|
137
|
-
})
|
|
136
|
+
expect(dappTransition('idle', 'receive_join')).toBeNull()
|
|
137
|
+
})
|
|
138
138
|
|
|
139
139
|
it('cannot send create in waiting', () => {
|
|
140
|
-
expect(dappTransition('waiting', 'send_create')).toBeNull()
|
|
141
|
-
})
|
|
140
|
+
expect(dappTransition('waiting', 'send_create')).toBeNull()
|
|
141
|
+
})
|
|
142
142
|
|
|
143
143
|
it('cannot send create in pending_accept', () => {
|
|
144
|
-
expect(dappTransition('pending_accept', 'send_create')).toBeNull()
|
|
145
|
-
})
|
|
144
|
+
expect(dappTransition('pending_accept', 'send_create')).toBeNull()
|
|
145
|
+
})
|
|
146
146
|
|
|
147
147
|
it('cannot receive join in pending_accept', () => {
|
|
148
|
-
expect(dappTransition('pending_accept', 'receive_join')).toBeNull()
|
|
149
|
-
})
|
|
148
|
+
expect(dappTransition('pending_accept', 'receive_join')).toBeNull()
|
|
149
|
+
})
|
|
150
150
|
|
|
151
151
|
it('cannot send create in connected', () => {
|
|
152
|
-
expect(dappTransition('connected', 'send_create')).toBeNull()
|
|
153
|
-
})
|
|
152
|
+
expect(dappTransition('connected', 'send_create')).toBeNull()
|
|
153
|
+
})
|
|
154
154
|
|
|
155
155
|
it('closed is terminal — all events rejected', () => {
|
|
156
156
|
const events: DAppEvent[] = [
|
|
157
|
-
'send_create',
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
|
|
157
|
+
'send_create',
|
|
158
|
+
'receive_join',
|
|
159
|
+
'sealed_join_verified_send_accept',
|
|
160
|
+
'receive_close',
|
|
161
|
+
'receive_terminate',
|
|
162
|
+
'timeout',
|
|
163
|
+
'transport_disconnected',
|
|
164
|
+
'send_create_reconnect',
|
|
165
|
+
]
|
|
161
166
|
for (const event of events) {
|
|
162
|
-
expect(dappTransition('closed', event)).toBeNull()
|
|
167
|
+
expect(dappTransition('closed', event)).toBeNull()
|
|
163
168
|
}
|
|
164
|
-
})
|
|
165
|
-
})
|
|
169
|
+
})
|
|
170
|
+
})
|
|
166
171
|
|
|
167
172
|
describe('close and timeout paths', () => {
|
|
168
173
|
it('waiting -> close on receive_close', () => {
|
|
169
|
-
expect(dappTransition('waiting', 'receive_close')).toBe('closed')
|
|
170
|
-
})
|
|
174
|
+
expect(dappTransition('waiting', 'receive_close')).toBe('closed')
|
|
175
|
+
})
|
|
171
176
|
|
|
172
177
|
it('waiting -> closed on timeout', () => {
|
|
173
|
-
expect(dappTransition('waiting', 'timeout')).toBe('closed')
|
|
174
|
-
})
|
|
178
|
+
expect(dappTransition('waiting', 'timeout')).toBe('closed')
|
|
179
|
+
})
|
|
175
180
|
|
|
176
181
|
it('pending_accept -> closed on user_rejects_send_close', () => {
|
|
177
|
-
expect(dappTransition('pending_accept', 'user_rejects_send_close')).toBe('closed')
|
|
178
|
-
})
|
|
182
|
+
expect(dappTransition('pending_accept', 'user_rejects_send_close')).toBe('closed')
|
|
183
|
+
})
|
|
179
184
|
|
|
180
185
|
it('pending_accept -> closed on timeout', () => {
|
|
181
|
-
expect(dappTransition('pending_accept', 'timeout')).toBe('closed')
|
|
182
|
-
})
|
|
183
|
-
})
|
|
186
|
+
expect(dappTransition('pending_accept', 'timeout')).toBe('closed')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
184
189
|
|
|
185
190
|
describe('reconnect: disconnected -> waiting with same ch', () => {
|
|
186
191
|
it('transport disconnect moves to disconnected', () => {
|
|
187
|
-
expect(dappTransition('connected', 'transport_disconnected')).toBe('disconnected')
|
|
188
|
-
})
|
|
192
|
+
expect(dappTransition('connected', 'transport_disconnected')).toBe('disconnected')
|
|
193
|
+
})
|
|
189
194
|
|
|
190
195
|
it('send_create_reconnect from disconnected goes to waiting', () => {
|
|
191
|
-
expect(dappTransition('disconnected', 'send_create_reconnect')).toBe('waiting')
|
|
192
|
-
})
|
|
196
|
+
expect(dappTransition('disconnected', 'send_create_reconnect')).toBe('waiting')
|
|
197
|
+
})
|
|
193
198
|
|
|
194
199
|
it('session expiry from disconnected goes to closed', () => {
|
|
195
|
-
expect(dappTransition('disconnected', 'session_expired')).toBe('closed')
|
|
196
|
-
})
|
|
200
|
+
expect(dappTransition('disconnected', 'session_expired')).toBe('closed')
|
|
201
|
+
})
|
|
197
202
|
|
|
198
203
|
it('give up from disconnected goes to closed', () => {
|
|
199
|
-
expect(dappTransition('disconnected', 'give_up')).toBe('closed')
|
|
200
|
-
})
|
|
201
|
-
})
|
|
204
|
+
expect(dappTransition('disconnected', 'give_up')).toBe('closed')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
202
207
|
|
|
203
208
|
describe('race condition: channel_exists handling', () => {
|
|
204
209
|
it('receive_channel_exists stays in disconnected (transient failure)', () => {
|
|
205
|
-
expect(dappTransition('disconnected', 'receive_channel_exists')).toBe('disconnected')
|
|
206
|
-
})
|
|
207
|
-
})
|
|
208
|
-
})
|
|
210
|
+
expect(dappTransition('disconnected', 'receive_channel_exists')).toBe('disconnected')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
})
|
|
209
214
|
|
|
210
215
|
// ---------------------------------------------------------------------------
|
|
211
216
|
// Wallet state machine tests
|
|
@@ -214,81 +219,85 @@ describe('Section 14 — DApp state machine', () => {
|
|
|
214
219
|
describe('Section 14 — Wallet state machine', () => {
|
|
215
220
|
describe('valid transitions: idle -> waiting_accept -> connected -> closed', () => {
|
|
216
221
|
it('full happy path', () => {
|
|
217
|
-
let state: WalletState = 'idle'
|
|
218
|
-
state = walletTransition(state, 'send_join')
|
|
219
|
-
expect(state).toBe('waiting_accept')
|
|
220
|
-
state = walletTransition(state, 'receive_ready_connected')
|
|
221
|
-
expect(state).toBe('connected')
|
|
222
|
-
})
|
|
222
|
+
let state: WalletState = 'idle'
|
|
223
|
+
state = walletTransition(state, 'send_join') ?? 'closed'
|
|
224
|
+
expect(state).toBe('waiting_accept')
|
|
225
|
+
state = walletTransition(state, 'receive_ready_connected') ?? 'closed'
|
|
226
|
+
expect(state).toBe('connected')
|
|
227
|
+
})
|
|
223
228
|
|
|
224
229
|
it('close from connected', () => {
|
|
225
|
-
expect(walletTransition('connected', 'receive_close')).toBe('closed')
|
|
226
|
-
})
|
|
230
|
+
expect(walletTransition('connected', 'receive_close')).toBe('closed')
|
|
231
|
+
})
|
|
227
232
|
|
|
228
233
|
it('terminate from connected', () => {
|
|
229
|
-
expect(walletTransition('connected', 'receive_terminate')).toBe('closed')
|
|
230
|
-
})
|
|
231
|
-
})
|
|
234
|
+
expect(walletTransition('connected', 'receive_terminate')).toBe('closed')
|
|
235
|
+
})
|
|
236
|
+
})
|
|
232
237
|
|
|
233
238
|
describe('invalid transitions are rejected', () => {
|
|
234
239
|
it('cannot receive_ready_connected in idle', () => {
|
|
235
|
-
expect(walletTransition('idle', 'receive_ready_connected')).toBeNull()
|
|
236
|
-
})
|
|
240
|
+
expect(walletTransition('idle', 'receive_ready_connected')).toBeNull()
|
|
241
|
+
})
|
|
237
242
|
|
|
238
243
|
it('cannot send_join in waiting_accept', () => {
|
|
239
|
-
expect(walletTransition('waiting_accept', 'send_join')).toBeNull()
|
|
240
|
-
})
|
|
244
|
+
expect(walletTransition('waiting_accept', 'send_join')).toBeNull()
|
|
245
|
+
})
|
|
241
246
|
|
|
242
247
|
it('cannot send_join in connected', () => {
|
|
243
|
-
expect(walletTransition('connected', 'send_join')).toBeNull()
|
|
244
|
-
})
|
|
248
|
+
expect(walletTransition('connected', 'send_join')).toBeNull()
|
|
249
|
+
})
|
|
245
250
|
|
|
246
251
|
it('closed is terminal — all events rejected', () => {
|
|
247
252
|
const events: WalletEvent[] = [
|
|
248
|
-
'send_join',
|
|
249
|
-
'
|
|
253
|
+
'send_join',
|
|
254
|
+
'receive_ready_connected',
|
|
255
|
+
'receive_close',
|
|
256
|
+
'receive_terminate',
|
|
257
|
+
'timeout',
|
|
258
|
+
'transport_disconnected',
|
|
250
259
|
'send_join_reconnect',
|
|
251
|
-
]
|
|
260
|
+
]
|
|
252
261
|
for (const event of events) {
|
|
253
|
-
expect(walletTransition('closed', event)).toBeNull()
|
|
262
|
+
expect(walletTransition('closed', event)).toBeNull()
|
|
254
263
|
}
|
|
255
|
-
})
|
|
256
|
-
})
|
|
264
|
+
})
|
|
265
|
+
})
|
|
257
266
|
|
|
258
267
|
describe('close and timeout paths', () => {
|
|
259
268
|
it('waiting_accept -> closed on receive_close', () => {
|
|
260
|
-
expect(walletTransition('waiting_accept', 'receive_close')).toBe('closed')
|
|
261
|
-
})
|
|
269
|
+
expect(walletTransition('waiting_accept', 'receive_close')).toBe('closed')
|
|
270
|
+
})
|
|
262
271
|
|
|
263
272
|
it('waiting_accept -> closed on timeout', () => {
|
|
264
|
-
expect(walletTransition('waiting_accept', 'timeout')).toBe('closed')
|
|
265
|
-
})
|
|
266
|
-
})
|
|
273
|
+
expect(walletTransition('waiting_accept', 'timeout')).toBe('closed')
|
|
274
|
+
})
|
|
275
|
+
})
|
|
267
276
|
|
|
268
277
|
describe('reconnect: disconnected -> waiting_accept with same ch', () => {
|
|
269
278
|
it('transport disconnect moves to disconnected', () => {
|
|
270
|
-
expect(walletTransition('connected', 'transport_disconnected')).toBe('disconnected')
|
|
271
|
-
})
|
|
279
|
+
expect(walletTransition('connected', 'transport_disconnected')).toBe('disconnected')
|
|
280
|
+
})
|
|
272
281
|
|
|
273
282
|
it('send_join_reconnect from disconnected goes to waiting_accept', () => {
|
|
274
|
-
expect(walletTransition('disconnected', 'send_join_reconnect')).toBe('waiting_accept')
|
|
275
|
-
})
|
|
283
|
+
expect(walletTransition('disconnected', 'send_join_reconnect')).toBe('waiting_accept')
|
|
284
|
+
})
|
|
276
285
|
|
|
277
286
|
it('session expiry from disconnected goes to closed', () => {
|
|
278
|
-
expect(walletTransition('disconnected', 'session_expired')).toBe('closed')
|
|
279
|
-
})
|
|
287
|
+
expect(walletTransition('disconnected', 'session_expired')).toBe('closed')
|
|
288
|
+
})
|
|
280
289
|
|
|
281
290
|
it('give up from disconnected goes to closed', () => {
|
|
282
|
-
expect(walletTransition('disconnected', 'give_up')).toBe('closed')
|
|
283
|
-
})
|
|
284
|
-
})
|
|
291
|
+
expect(walletTransition('disconnected', 'give_up')).toBe('closed')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
285
294
|
|
|
286
295
|
describe('race condition: channel_not_found handling', () => {
|
|
287
296
|
it('receive_channel_not_found stays in disconnected (transient failure)', () => {
|
|
288
|
-
expect(walletTransition('disconnected', 'receive_channel_not_found')).toBe('disconnected')
|
|
289
|
-
})
|
|
290
|
-
})
|
|
291
|
-
})
|
|
297
|
+
expect(walletTransition('disconnected', 'receive_channel_not_found')).toBe('disconnected')
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
})
|
|
292
301
|
|
|
293
302
|
// ---------------------------------------------------------------------------
|
|
294
303
|
// Cross-cutting state machine properties
|
|
@@ -297,68 +306,82 @@ describe('Section 14 — Wallet state machine', () => {
|
|
|
297
306
|
describe('Section 14 — Cross-cutting properties', () => {
|
|
298
307
|
it('closed is terminal for dApp — no outgoing transitions', () => {
|
|
299
308
|
const allDappEvents: DAppEvent[] = [
|
|
300
|
-
'send_create',
|
|
301
|
-
'
|
|
302
|
-
'
|
|
303
|
-
'
|
|
309
|
+
'send_create',
|
|
310
|
+
'receive_join',
|
|
311
|
+
'sealed_join_verified_send_accept',
|
|
312
|
+
'user_rejects_send_close',
|
|
313
|
+
'receive_ready_connected',
|
|
314
|
+
'receive_close',
|
|
315
|
+
'receive_terminate',
|
|
316
|
+
'timeout',
|
|
317
|
+
'transport_disconnected',
|
|
318
|
+
'send_create_reconnect',
|
|
319
|
+
'receive_channel_exists',
|
|
320
|
+
'session_expired',
|
|
304
321
|
'give_up',
|
|
305
|
-
]
|
|
322
|
+
]
|
|
306
323
|
for (const event of allDappEvents) {
|
|
307
|
-
expect(dappTransition('closed', event)).toBeNull()
|
|
324
|
+
expect(dappTransition('closed', event)).toBeNull()
|
|
308
325
|
}
|
|
309
|
-
})
|
|
326
|
+
})
|
|
310
327
|
|
|
311
328
|
it('closed is terminal for wallet — no outgoing transitions', () => {
|
|
312
329
|
const allWalletEvents: WalletEvent[] = [
|
|
313
|
-
'send_join',
|
|
314
|
-
'
|
|
315
|
-
'
|
|
330
|
+
'send_join',
|
|
331
|
+
'receive_ready_connected',
|
|
332
|
+
'receive_close',
|
|
333
|
+
'receive_terminate',
|
|
334
|
+
'timeout',
|
|
335
|
+
'transport_disconnected',
|
|
336
|
+
'send_join_reconnect',
|
|
337
|
+
'receive_channel_not_found',
|
|
338
|
+
'session_expired',
|
|
316
339
|
'give_up',
|
|
317
|
-
]
|
|
340
|
+
]
|
|
318
341
|
for (const event of allWalletEvents) {
|
|
319
|
-
expect(walletTransition('closed', event)).toBeNull()
|
|
342
|
+
expect(walletTransition('closed', event)).toBeNull()
|
|
320
343
|
}
|
|
321
|
-
})
|
|
344
|
+
})
|
|
322
345
|
|
|
323
346
|
it('reconnect path reuses the full handshake flow (create/join/accept)', () => {
|
|
324
347
|
// DApp: disconnected -> waiting (via create) -> pending_accept (via join) -> connected
|
|
325
|
-
let dapp: DAppState = 'disconnected'
|
|
326
|
-
dapp = dappTransition(dapp, 'send_create_reconnect')
|
|
327
|
-
expect(dapp).toBe('waiting')
|
|
328
|
-
dapp = dappTransition(dapp, 'receive_join')
|
|
329
|
-
expect(dapp).toBe('pending_accept')
|
|
330
|
-
dapp = dappTransition(dapp, 'sealed_join_verified_send_accept')
|
|
331
|
-
expect(dapp).toBe('connected')
|
|
348
|
+
let dapp: DAppState = 'disconnected'
|
|
349
|
+
dapp = dappTransition(dapp, 'send_create_reconnect') ?? 'closed'
|
|
350
|
+
expect(dapp).toBe('waiting')
|
|
351
|
+
dapp = dappTransition(dapp, 'receive_join') ?? 'closed'
|
|
352
|
+
expect(dapp).toBe('pending_accept')
|
|
353
|
+
dapp = dappTransition(dapp, 'sealed_join_verified_send_accept') ?? 'closed'
|
|
354
|
+
expect(dapp).toBe('connected')
|
|
332
355
|
|
|
333
356
|
// Wallet: disconnected -> waiting_accept (via join) -> connected
|
|
334
|
-
let wallet: WalletState = 'disconnected'
|
|
335
|
-
wallet = walletTransition(wallet, 'send_join_reconnect')
|
|
336
|
-
expect(wallet).toBe('waiting_accept')
|
|
337
|
-
wallet = walletTransition(wallet, 'receive_ready_connected')
|
|
338
|
-
expect(wallet).toBe('connected')
|
|
339
|
-
})
|
|
357
|
+
let wallet: WalletState = 'disconnected'
|
|
358
|
+
wallet = walletTransition(wallet, 'send_join_reconnect') ?? 'closed'
|
|
359
|
+
expect(wallet).toBe('waiting_accept')
|
|
360
|
+
wallet = walletTransition(wallet, 'receive_ready_connected') ?? 'closed'
|
|
361
|
+
expect(wallet).toBe('connected')
|
|
362
|
+
})
|
|
340
363
|
|
|
341
364
|
it('race condition recovery: multiple channel_exists before successful reconnect', () => {
|
|
342
|
-
let state: DAppState = 'disconnected'
|
|
365
|
+
let state: DAppState = 'disconnected'
|
|
343
366
|
// Multiple transient failures
|
|
344
|
-
state = dappTransition(state, 'receive_channel_exists')
|
|
345
|
-
expect(state).toBe('disconnected')
|
|
346
|
-
state = dappTransition(state, 'receive_channel_exists')
|
|
347
|
-
expect(state).toBe('disconnected')
|
|
367
|
+
state = dappTransition(state, 'receive_channel_exists') ?? 'closed'
|
|
368
|
+
expect(state).toBe('disconnected')
|
|
369
|
+
state = dappTransition(state, 'receive_channel_exists') ?? 'closed'
|
|
370
|
+
expect(state).toBe('disconnected')
|
|
348
371
|
// Eventually succeeds
|
|
349
|
-
state = dappTransition(state, 'send_create_reconnect')
|
|
350
|
-
expect(state).toBe('waiting')
|
|
351
|
-
})
|
|
372
|
+
state = dappTransition(state, 'send_create_reconnect') ?? 'closed'
|
|
373
|
+
expect(state).toBe('waiting')
|
|
374
|
+
})
|
|
352
375
|
|
|
353
376
|
it('race condition recovery: multiple channel_not_found before wallet reconnects', () => {
|
|
354
|
-
let state: WalletState = 'disconnected'
|
|
377
|
+
let state: WalletState = 'disconnected'
|
|
355
378
|
// Multiple transient failures
|
|
356
|
-
state = walletTransition(state, 'receive_channel_not_found')
|
|
357
|
-
expect(state).toBe('disconnected')
|
|
358
|
-
state = walletTransition(state, 'receive_channel_not_found')
|
|
359
|
-
expect(state).toBe('disconnected')
|
|
379
|
+
state = walletTransition(state, 'receive_channel_not_found') ?? 'closed'
|
|
380
|
+
expect(state).toBe('disconnected')
|
|
381
|
+
state = walletTransition(state, 'receive_channel_not_found') ?? 'closed'
|
|
382
|
+
expect(state).toBe('disconnected')
|
|
360
383
|
// Eventually succeeds
|
|
361
|
-
state = walletTransition(state, 'send_join_reconnect')
|
|
362
|
-
expect(state).toBe('waiting_accept')
|
|
363
|
-
})
|
|
364
|
-
})
|
|
384
|
+
state = walletTransition(state, 'send_join_reconnect') ?? 'closed'
|
|
385
|
+
expect(state).toBe('waiting_accept')
|
|
386
|
+
})
|
|
387
|
+
})
|