@meshconnect/uwc-ton-connector 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/namespaced-storage.d.ts +2 -7
- package/dist/namespaced-storage.d.ts.map +1 -1
- package/dist/namespaced-storage.js.map +1 -1
- package/dist/ton-connect-connector.d.ts +36 -0
- package/dist/ton-connect-connector.d.ts.map +1 -0
- package/dist/ton-connect-connector.js +249 -0
- package/dist/ton-connect-connector.js.map +1 -0
- package/dist/ton-transaction-utils.d.ts +2 -14
- package/dist/ton-transaction-utils.d.ts.map +1 -1
- package/dist/ton-transaction-utils.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +2 -1
- package/src/namespaced-storage.ts +2 -7
- package/src/ton-connect-connector.test.ts +627 -0
- package/src/ton-connect-connector.ts +347 -0
- package/src/ton-transaction-utils.ts +3 -21
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import type { Network, TonConnectWalletProvider } from '@meshconnect/uwc-types'
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
mockSdkConnect,
|
|
6
|
+
mockDisconnect,
|
|
7
|
+
mockOnStatusChange,
|
|
8
|
+
mockSendTransaction,
|
|
9
|
+
mockSignData,
|
|
10
|
+
mockPauseConnection,
|
|
11
|
+
mockUnPauseConnection,
|
|
12
|
+
MockTonConnect,
|
|
13
|
+
MockUserRejectsError,
|
|
14
|
+
getLastSdkInstance,
|
|
15
|
+
setNextConnectedState
|
|
16
|
+
} = vi.hoisted(() => {
|
|
17
|
+
const mockSdkConnect = vi.fn()
|
|
18
|
+
const mockDisconnect = vi.fn()
|
|
19
|
+
const mockOnStatusChange = vi.fn()
|
|
20
|
+
const mockSendTransaction = vi.fn()
|
|
21
|
+
const mockSignData = vi.fn()
|
|
22
|
+
const mockPauseConnection = vi.fn()
|
|
23
|
+
const mockUnPauseConnection = vi.fn()
|
|
24
|
+
|
|
25
|
+
class MockUserRejectsError extends Error {
|
|
26
|
+
constructor(message?: string) {
|
|
27
|
+
super(message)
|
|
28
|
+
Object.setPrototypeOf(this, MockUserRejectsError.prototype)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sdkInstances: { current: MockTonConnect | null } = { current: null }
|
|
33
|
+
let nextConnectedState = false
|
|
34
|
+
|
|
35
|
+
class MockTonConnect {
|
|
36
|
+
connected = nextConnectedState
|
|
37
|
+
account: {
|
|
38
|
+
address: string
|
|
39
|
+
chain: string
|
|
40
|
+
walletStateInit: string
|
|
41
|
+
} | null = null
|
|
42
|
+
connect = mockSdkConnect
|
|
43
|
+
disconnect = mockDisconnect
|
|
44
|
+
onStatusChange = mockOnStatusChange
|
|
45
|
+
sendTransaction = mockSendTransaction
|
|
46
|
+
signData = mockSignData
|
|
47
|
+
getWallets = vi.fn().mockResolvedValue([
|
|
48
|
+
{
|
|
49
|
+
appName: 'tonkeeper',
|
|
50
|
+
universalLink: 'https://app.tonkeeper.com/ton-connect',
|
|
51
|
+
bridgeUrl: 'https://bridge.tonapi.io/bridge'
|
|
52
|
+
}
|
|
53
|
+
])
|
|
54
|
+
pauseConnection = mockPauseConnection
|
|
55
|
+
unPauseConnection = mockUnPauseConnection
|
|
56
|
+
constructor() {
|
|
57
|
+
this.connected = nextConnectedState
|
|
58
|
+
sdkInstances.current = this
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
mockSdkConnect,
|
|
64
|
+
mockDisconnect,
|
|
65
|
+
mockOnStatusChange,
|
|
66
|
+
mockSendTransaction,
|
|
67
|
+
mockSignData,
|
|
68
|
+
mockPauseConnection,
|
|
69
|
+
mockUnPauseConnection,
|
|
70
|
+
MockTonConnect,
|
|
71
|
+
MockUserRejectsError,
|
|
72
|
+
getLastSdkInstance: () => sdkInstances.current,
|
|
73
|
+
setNextConnectedState: (state: boolean) => {
|
|
74
|
+
nextConnectedState = state
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
vi.mock('@tonconnect/sdk', () => ({
|
|
80
|
+
TonConnect: MockTonConnect,
|
|
81
|
+
UserRejectsError: MockUserRejectsError,
|
|
82
|
+
toUserFriendlyAddress: (hex: string) =>
|
|
83
|
+
`UQ_converted_${hex.split(':')[1]?.slice(0, 8)}`
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
vi.mock('./ton-transaction-utils', async () => {
|
|
87
|
+
const actual = await vi.importActual<
|
|
88
|
+
typeof import('./ton-transaction-utils')
|
|
89
|
+
>('./ton-transaction-utils')
|
|
90
|
+
return {
|
|
91
|
+
...actual,
|
|
92
|
+
bocToHash: async (boc: string) => `hash_of_${boc}`
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
import { TonConnectConnector } from './ton-connect-connector'
|
|
97
|
+
|
|
98
|
+
const TON_NETWORK: Network = {
|
|
99
|
+
internalId: -239,
|
|
100
|
+
id: 'tvm:-239',
|
|
101
|
+
namespace: 'tvm',
|
|
102
|
+
name: 'TON',
|
|
103
|
+
logoUrl: 'https://example.com/ton.svg',
|
|
104
|
+
nativeCurrency: { name: 'Toncoin', symbol: 'TON', decimals: 9 },
|
|
105
|
+
networkType: 'tvm'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const PROVIDER: TonConnectWalletProvider = {
|
|
109
|
+
supportedNetworkIds: ['tvm:-239'],
|
|
110
|
+
walletListAppName: 'tonkeeper'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createConnector() {
|
|
114
|
+
return new TonConnectConnector({
|
|
115
|
+
manifestUrl: 'https://example.com/tonconnect-manifest.json'
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function simulateSuccessfulConnect() {
|
|
120
|
+
mockSdkConnect.mockReturnValue('tc://connect?...')
|
|
121
|
+
mockOnStatusChange.mockImplementation(callback => {
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
callback({ account: { address: '0:abcdef1234567890' } })
|
|
124
|
+
}, 10)
|
|
125
|
+
return vi.fn()
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('TonConnectConnector', () => {
|
|
130
|
+
let connector: TonConnectConnector
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
vi.clearAllMocks()
|
|
134
|
+
connector = createConnector()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
vi.useRealTimers()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('constructor', () => {
|
|
142
|
+
it('stores config and does not create SDK instance eagerly', () => {
|
|
143
|
+
expect(getLastSdkInstance()).toBeNull()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('accepts optional walletsListSource', () => {
|
|
147
|
+
const c = new TonConnectConnector({
|
|
148
|
+
manifestUrl: 'https://example.com/manifest.json',
|
|
149
|
+
walletsListSource: 'https://example.com/wallets.json'
|
|
150
|
+
})
|
|
151
|
+
expect(c).toBeInstanceOf(TonConnectConnector)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('connect', () => {
|
|
156
|
+
it('requires a TonConnectWalletProvider', async () => {
|
|
157
|
+
await expect(connector.connect(TON_NETWORK)).rejects.toThrow(
|
|
158
|
+
'TonConnectWalletProvider is required'
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('requires walletListAppName in provider', async () => {
|
|
163
|
+
const providerNoAppName = {
|
|
164
|
+
supportedNetworkIds: ['tvm:-239'],
|
|
165
|
+
walletListAppName: ''
|
|
166
|
+
} as TonConnectWalletProvider
|
|
167
|
+
await expect(
|
|
168
|
+
connector.connect(TON_NETWORK, providerNoAppName)
|
|
169
|
+
).rejects.toThrow('walletListAppName is required')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('throws when walletListAppName is not found in SDK wallet list', async () => {
|
|
173
|
+
simulateSuccessfulConnect()
|
|
174
|
+
const unknownProvider: TonConnectWalletProvider = {
|
|
175
|
+
supportedNetworkIds: ['tvm:-239'],
|
|
176
|
+
walletListAppName: 'unknownWallet'
|
|
177
|
+
}
|
|
178
|
+
await expect(
|
|
179
|
+
connector.connect(TON_NETWORK, unknownProvider)
|
|
180
|
+
).rejects.toThrow(
|
|
181
|
+
"Wallet 'unknownWallet' not found in TonConnect wallet list"
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('creates SDK with NamespacedStorage uwc-ton-remote', async () => {
|
|
186
|
+
simulateSuccessfulConnect()
|
|
187
|
+
|
|
188
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
189
|
+
|
|
190
|
+
const sdk = getLastSdkInstance()
|
|
191
|
+
expect(sdk).not.toBeNull()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('calls sdk.connect with universalLink and bridgeUrl', async () => {
|
|
195
|
+
simulateSuccessfulConnect()
|
|
196
|
+
|
|
197
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
198
|
+
|
|
199
|
+
expect(mockSdkConnect).toHaveBeenCalledWith({
|
|
200
|
+
universalLink: 'https://app.tonkeeper.com/ton-connect',
|
|
201
|
+
bridgeUrl: 'https://bridge.tonapi.io/bridge'
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('exposes the connection URI while approval is pending, clears it on success', async () => {
|
|
206
|
+
// URI is available during the pending window (sdk.connect sets it synchronously
|
|
207
|
+
// AFTER onStatusChange is registered). It is cleared after the wallet approves
|
|
208
|
+
// so the QR modal dismisses cleanly.
|
|
209
|
+
let capturedURI: string | undefined
|
|
210
|
+
|
|
211
|
+
mockOnStatusChange.mockImplementation(callback => {
|
|
212
|
+
// sdk.connect() is called after onStatusChange returns, so read the URI
|
|
213
|
+
// inside the scheduled callback (by which time sdk.connect has already run).
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
capturedURI = connector.getConnectionURI()
|
|
216
|
+
callback({ account: { address: '0:abc' } })
|
|
217
|
+
}, 0)
|
|
218
|
+
return vi.fn()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
222
|
+
|
|
223
|
+
expect(capturedURI).toBe('tc://connect?...')
|
|
224
|
+
expect(() => connector.getConnectionURI()).toThrow(
|
|
225
|
+
'No connection URI available'
|
|
226
|
+
)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('sets up onStatusChange BEFORE calling sdk.connect', async () => {
|
|
230
|
+
const callOrder: string[] = []
|
|
231
|
+
|
|
232
|
+
mockOnStatusChange.mockImplementation(callback => {
|
|
233
|
+
callOrder.push('onStatusChange')
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
callback({ account: { address: '0:abcdef1234567890' } })
|
|
236
|
+
}, 10)
|
|
237
|
+
return vi.fn()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
mockSdkConnect.mockImplementation(() => {
|
|
241
|
+
callOrder.push('connect')
|
|
242
|
+
return 'tc://uri'
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
246
|
+
|
|
247
|
+
expect(callOrder).toEqual(['onStatusChange', 'connect'])
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('converts raw address to user-friendly format', async () => {
|
|
251
|
+
simulateSuccessfulConnect()
|
|
252
|
+
|
|
253
|
+
const result = await connector.connect(TON_NETWORK, PROVIDER)
|
|
254
|
+
|
|
255
|
+
expect(result.address).toBe('UQ_converted_abcdef12')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('returns correct ConnectorResult shape', async () => {
|
|
259
|
+
simulateSuccessfulConnect()
|
|
260
|
+
|
|
261
|
+
const result = await connector.connect(TON_NETWORK, PROVIDER)
|
|
262
|
+
|
|
263
|
+
expect(result).toEqual({
|
|
264
|
+
networkId: 'tvm:-239',
|
|
265
|
+
address: 'UQ_converted_abcdef12',
|
|
266
|
+
availableAddresses: [
|
|
267
|
+
{ address: 'UQ_converted_abcdef12', networkId: 'tvm:-239' }
|
|
268
|
+
]
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('rejects when wallet returns null account and cleans up', async () => {
|
|
273
|
+
mockSdkConnect.mockReturnValue('tc://uri')
|
|
274
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
275
|
+
mockOnStatusChange.mockImplementation(callback => {
|
|
276
|
+
setTimeout(() => callback(null), 10)
|
|
277
|
+
return vi.fn()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
await expect(connector.connect(TON_NETWORK, PROVIDER)).rejects.toThrow(
|
|
281
|
+
'rejected'
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// URI cleared and SDK disconnected on rejection
|
|
285
|
+
expect(() => connector.getConnectionURI()).toThrow(
|
|
286
|
+
'No connection URI available'
|
|
287
|
+
)
|
|
288
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('rejects via error callback and cleans up', async () => {
|
|
292
|
+
mockSdkConnect.mockReturnValue('tc://uri')
|
|
293
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
294
|
+
mockOnStatusChange.mockImplementation((_callback, errorCallback) => {
|
|
295
|
+
setTimeout(
|
|
296
|
+
() =>
|
|
297
|
+
errorCallback({
|
|
298
|
+
payload: { code: 0, message: 'User rejected the request' }
|
|
299
|
+
}),
|
|
300
|
+
10
|
|
301
|
+
)
|
|
302
|
+
return vi.fn()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
await expect(connector.connect(TON_NETWORK, PROVIDER)).rejects.toThrow(
|
|
306
|
+
'User rejected the request'
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
// URI cleared and SDK disconnected on error callback
|
|
310
|
+
expect(() => connector.getConnectionURI()).toThrow(
|
|
311
|
+
'No connection URI available'
|
|
312
|
+
)
|
|
313
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('times out after 5 minutes and cleans up', async () => {
|
|
317
|
+
vi.useFakeTimers()
|
|
318
|
+
|
|
319
|
+
mockSdkConnect.mockReturnValue('tc://uri')
|
|
320
|
+
mockOnStatusChange.mockReturnValue(vi.fn())
|
|
321
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
322
|
+
|
|
323
|
+
const connectPromise = connector.connect(TON_NETWORK, PROVIDER)
|
|
324
|
+
const resultPromise = connectPromise.catch((e: Error) => e)
|
|
325
|
+
|
|
326
|
+
await vi.advanceTimersByTimeAsync(5 * 60 * 1000)
|
|
327
|
+
|
|
328
|
+
const error = await resultPromise
|
|
329
|
+
expect(error).toBeInstanceOf(Error)
|
|
330
|
+
expect(error.message).toContain('timed out')
|
|
331
|
+
|
|
332
|
+
// URI cleared and SDK disconnected on timeout
|
|
333
|
+
expect(() => connector.getConnectionURI()).toThrow(
|
|
334
|
+
'No connection URI available'
|
|
335
|
+
)
|
|
336
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('ignores late timeout after successful connection', async () => {
|
|
340
|
+
vi.useFakeTimers()
|
|
341
|
+
|
|
342
|
+
simulateSuccessfulConnect()
|
|
343
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
344
|
+
|
|
345
|
+
const connectPromise = connector.connect(TON_NETWORK, PROVIDER)
|
|
346
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
347
|
+
await connectPromise
|
|
348
|
+
|
|
349
|
+
mockDisconnect.mockClear()
|
|
350
|
+
await vi.advanceTimersByTimeAsync(5 * 60 * 1000)
|
|
351
|
+
expect(mockDisconnect).not.toHaveBeenCalled()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('cleans up previous SDK on reconnect', async () => {
|
|
355
|
+
simulateSuccessfulConnect()
|
|
356
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
357
|
+
|
|
358
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
359
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
360
|
+
|
|
361
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('rejects concurrent connect calls while one is in-flight', async () => {
|
|
365
|
+
mockSdkConnect.mockReturnValue('tc://uri')
|
|
366
|
+
mockOnStatusChange.mockImplementation(callback => {
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
callback({ account: { address: '0:abcdef1234567890' } })
|
|
369
|
+
}, 50)
|
|
370
|
+
return vi.fn()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const first = connector.connect(TON_NETWORK, PROVIDER)
|
|
374
|
+
await expect(connector.connect(TON_NETWORK, PROVIDER)).rejects.toThrow(
|
|
375
|
+
'already in progress'
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
await first
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('disconnects stale session from NamespacedStorage on fresh SDK', async () => {
|
|
382
|
+
setNextConnectedState(true)
|
|
383
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
384
|
+
simulateSuccessfulConnect()
|
|
385
|
+
|
|
386
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
387
|
+
|
|
388
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
389
|
+
setNextConnectedState(false)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('sets up visibility change listener', async () => {
|
|
393
|
+
const addEventSpy = vi.spyOn(document, 'addEventListener')
|
|
394
|
+
simulateSuccessfulConnect()
|
|
395
|
+
|
|
396
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
397
|
+
|
|
398
|
+
expect(addEventSpy).toHaveBeenCalledWith(
|
|
399
|
+
'visibilitychange',
|
|
400
|
+
expect.any(Function)
|
|
401
|
+
)
|
|
402
|
+
addEventSpy.mockRestore()
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
describe('getConnectionURI', () => {
|
|
407
|
+
it('throws before connect', () => {
|
|
408
|
+
expect(() => connector.getConnectionURI()).toThrow(
|
|
409
|
+
'No connection URI available'
|
|
410
|
+
)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('clears URI after successful connect', async () => {
|
|
414
|
+
simulateSuccessfulConnect()
|
|
415
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
416
|
+
// URI is consumed on success — getConnectionURI should throw until next connect
|
|
417
|
+
expect(() => connector.getConnectionURI()).toThrow(
|
|
418
|
+
'No connection URI available'
|
|
419
|
+
)
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
describe('signMessage', () => {
|
|
424
|
+
it('throws when not connected', async () => {
|
|
425
|
+
await expect(connector.signMessage!('hello')).rejects.toThrow(
|
|
426
|
+
'No active TON connection'
|
|
427
|
+
)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('delegates to executeSignData and returns result', async () => {
|
|
431
|
+
simulateSuccessfulConnect()
|
|
432
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
433
|
+
|
|
434
|
+
mockSignData.mockResolvedValue({
|
|
435
|
+
signature: 'sig123',
|
|
436
|
+
timestamp: 1700000000,
|
|
437
|
+
domain: 'example.com'
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const result = await connector.signMessage!('test message')
|
|
441
|
+
|
|
442
|
+
expect(mockSignData).toHaveBeenCalled()
|
|
443
|
+
expect(result).toEqual({
|
|
444
|
+
type: 'tvm',
|
|
445
|
+
signature: 'sig123',
|
|
446
|
+
timestamp: 1700000000,
|
|
447
|
+
domain: 'example.com',
|
|
448
|
+
payload: { type: 'text', text: 'test message' }
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('sendTransaction', () => {
|
|
454
|
+
it('throws when not connected', async () => {
|
|
455
|
+
await expect(
|
|
456
|
+
connector.sendTransaction!({
|
|
457
|
+
to: 'UQBxyz',
|
|
458
|
+
amount: '1000000000',
|
|
459
|
+
from: 'UQBabc'
|
|
460
|
+
})
|
|
461
|
+
).rejects.toThrow('No active TON connection')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('builds transaction, sends via SDK, and returns hash', async () => {
|
|
465
|
+
simulateSuccessfulConnect()
|
|
466
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
467
|
+
|
|
468
|
+
getLastSdkInstance().account = {
|
|
469
|
+
address: '0:abcdef1234567890',
|
|
470
|
+
chain: '-239'
|
|
471
|
+
}
|
|
472
|
+
mockSendTransaction.mockResolvedValue({ boc: 'te6cckEBAQEA...' })
|
|
473
|
+
|
|
474
|
+
const result = await connector.sendTransaction!({
|
|
475
|
+
to: 'UQBxyz',
|
|
476
|
+
amount: '1000000000',
|
|
477
|
+
from: 'UQBabc'
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
expect(mockSendTransaction).toHaveBeenCalled()
|
|
481
|
+
expect(result).toBe('hash_of_te6cckEBAQEA...')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('uses sdk.account.address as sender raw address', async () => {
|
|
485
|
+
simulateSuccessfulConnect()
|
|
486
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
487
|
+
|
|
488
|
+
getLastSdkInstance().account = {
|
|
489
|
+
address: '0:sender_raw',
|
|
490
|
+
chain: '-239'
|
|
491
|
+
}
|
|
492
|
+
mockSendTransaction.mockResolvedValue({ boc: 'boc123' })
|
|
493
|
+
|
|
494
|
+
await connector.sendTransaction!({
|
|
495
|
+
to: 'UQBxyz',
|
|
496
|
+
amount: '500000000',
|
|
497
|
+
from: 'UQBabc'
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const txArg = mockSendTransaction.mock.calls[0][0]
|
|
501
|
+
expect(txArg.from).toBe('0:sender_raw')
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('wraps UserRejectsError with clear message', async () => {
|
|
505
|
+
simulateSuccessfulConnect()
|
|
506
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
507
|
+
|
|
508
|
+
getLastSdkInstance().account = { address: '0:abc', chain: '-239' }
|
|
509
|
+
mockSendTransaction.mockRejectedValue(
|
|
510
|
+
new MockUserRejectsError('User rejected')
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
await expect(
|
|
514
|
+
connector.sendTransaction!({
|
|
515
|
+
to: 'UQBxyz',
|
|
516
|
+
amount: '1000000000',
|
|
517
|
+
from: 'UQBabc'
|
|
518
|
+
})
|
|
519
|
+
).rejects.toThrow('rejected')
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
describe('switchNetwork', () => {
|
|
524
|
+
it('is a no-op that returns the current address and networkId', async () => {
|
|
525
|
+
simulateSuccessfulConnect()
|
|
526
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
527
|
+
|
|
528
|
+
const result = await connector.switchNetwork(TON_NETWORK)
|
|
529
|
+
|
|
530
|
+
expect(result.networkId).toBe('tvm:-239')
|
|
531
|
+
expect(result.address).toBe('UQ_converted_abcdef12')
|
|
532
|
+
})
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
describe('disconnect', () => {
|
|
536
|
+
it('calls sdk.disconnect and clears state', async () => {
|
|
537
|
+
simulateSuccessfulConnect()
|
|
538
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
539
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
540
|
+
|
|
541
|
+
await connector.disconnect!()
|
|
542
|
+
|
|
543
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('does not throw if sdk.disconnect fails', async () => {
|
|
547
|
+
simulateSuccessfulConnect()
|
|
548
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
549
|
+
mockDisconnect.mockRejectedValue(new Error('already disconnected'))
|
|
550
|
+
|
|
551
|
+
await expect(connector.disconnect!()).resolves.toBeUndefined()
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('is safe to call when never connected', async () => {
|
|
555
|
+
await expect(connector.disconnect!()).resolves.toBeUndefined()
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('resets connectInFlight so subsequent connect works after disconnect', async () => {
|
|
559
|
+
simulateSuccessfulConnect()
|
|
560
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
561
|
+
|
|
562
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
563
|
+
|
|
564
|
+
// Manually set flag to simulate a stuck state
|
|
565
|
+
connector['connectInFlight'] = true
|
|
566
|
+
|
|
567
|
+
await connector.disconnect!()
|
|
568
|
+
|
|
569
|
+
// Should be able to connect again without "already in progress"
|
|
570
|
+
simulateSuccessfulConnect()
|
|
571
|
+
const result = await connector.connect(TON_NETWORK, PROVIDER)
|
|
572
|
+
expect(result.address).toBeDefined()
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
describe('destroy', () => {
|
|
577
|
+
it('disconnects SDK and removes visibility listener', async () => {
|
|
578
|
+
const removeSpy = vi.spyOn(document, 'removeEventListener')
|
|
579
|
+
simulateSuccessfulConnect()
|
|
580
|
+
mockDisconnect.mockResolvedValue(undefined)
|
|
581
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
582
|
+
|
|
583
|
+
await connector.destroy()
|
|
584
|
+
|
|
585
|
+
expect(mockDisconnect).toHaveBeenCalled()
|
|
586
|
+
expect(removeSpy).toHaveBeenCalledWith(
|
|
587
|
+
'visibilitychange',
|
|
588
|
+
expect.any(Function)
|
|
589
|
+
)
|
|
590
|
+
removeSpy.mockRestore()
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('is safe to call when never connected', async () => {
|
|
594
|
+
await expect(connector.destroy()).resolves.toBeUndefined()
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
describe('visibility handling', () => {
|
|
599
|
+
it('calls unPauseConnection when page becomes visible', async () => {
|
|
600
|
+
simulateSuccessfulConnect()
|
|
601
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
602
|
+
|
|
603
|
+
Object.defineProperty(document, 'visibilityState', {
|
|
604
|
+
value: 'visible',
|
|
605
|
+
writable: true,
|
|
606
|
+
configurable: true
|
|
607
|
+
})
|
|
608
|
+
document.dispatchEvent(new Event('visibilitychange'))
|
|
609
|
+
|
|
610
|
+
expect(mockUnPauseConnection).toHaveBeenCalled()
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('calls pauseConnection when page becomes hidden', async () => {
|
|
614
|
+
simulateSuccessfulConnect()
|
|
615
|
+
await connector.connect(TON_NETWORK, PROVIDER)
|
|
616
|
+
|
|
617
|
+
Object.defineProperty(document, 'visibilityState', {
|
|
618
|
+
value: 'hidden',
|
|
619
|
+
writable: true,
|
|
620
|
+
configurable: true
|
|
621
|
+
})
|
|
622
|
+
document.dispatchEvent(new Event('visibilitychange'))
|
|
623
|
+
|
|
624
|
+
expect(mockPauseConnection).toHaveBeenCalled()
|
|
625
|
+
})
|
|
626
|
+
})
|
|
627
|
+
})
|