@blazium/ton-connect-mobile 1.2.4 → 1.2.6
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 +7 -20
- package/dist/adapters/react-native.js +8 -1
- package/dist/core/bridge.d.ts +61 -0
- package/dist/core/bridge.js +237 -0
- package/dist/core/crypto.d.ts +8 -19
- package/dist/core/crypto.js +15 -141
- package/dist/core/index.d.ts +5 -3
- package/dist/core/index.js +20 -17
- package/dist/core/protocol.d.ts +35 -32
- package/dist/core/protocol.js +109 -285
- package/dist/core/session.d.ts +65 -0
- package/dist/core/session.js +235 -0
- package/dist/core/wallets.d.ts +6 -6
- package/dist/core/wallets.js +17 -18
- package/dist/index.d.ts +33 -72
- package/dist/index.js +322 -769
- package/dist/react/TonConnectUIProvider.d.ts +4 -52
- package/dist/react/TonConnectUIProvider.js +18 -122
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.js +0 -1
- package/dist/types/index.d.ts +84 -139
- package/dist/types/index.js +1 -1
- package/package.json +2 -3
- package/src/adapters/react-native.ts +7 -1
- package/src/core/bridge.ts +307 -0
- package/src/core/crypto.ts +62 -238
- package/src/core/index.ts +17 -7
- package/src/core/protocol.ts +217 -441
- package/src/core/session.ts +247 -0
- package/src/core/wallets.ts +90 -93
- package/src/index.ts +811 -1338
- package/src/react/TonConnectUIProvider.tsx +272 -441
- package/src/react/index.ts +23 -27
- package/src/types/index.ts +217 -272
package/src/index.ts
CHANGED
|
@@ -1,1338 +1,811 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TON Connect Mobile SDK
|
|
3
|
-
* Production-ready implementation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from './core/
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
*
|
|
245
|
-
*/
|
|
246
|
-
private
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
//
|
|
437
|
-
this.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
this.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
// Update status
|
|
814
|
-
this.currentStatus = { connected: false, wallet: null };
|
|
815
|
-
this.notifyStatusChange();
|
|
816
|
-
|
|
817
|
-
// Emit disconnect event
|
|
818
|
-
this.emit('disconnect', null);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Get current connection status
|
|
823
|
-
*/
|
|
824
|
-
getStatus(): ConnectionStatus {
|
|
825
|
-
return { ...this.currentStatus };
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Get list of supported wallets
|
|
830
|
-
*/
|
|
831
|
-
getSupportedWallets(): WalletDefinition[] {
|
|
832
|
-
return SUPPORTED_WALLETS;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Get current wallet being used
|
|
837
|
-
*/
|
|
838
|
-
getCurrentWallet(): WalletDefinition {
|
|
839
|
-
return this.currentWallet;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* Check if a wallet is available on the current platform
|
|
844
|
-
* Note: This is a best-effort check and may not be 100% accurate
|
|
845
|
-
* CRITICAL FIX: On web, if wallet has universalLink, it's considered available
|
|
846
|
-
* because universal links can open in new tabs/windows
|
|
847
|
-
*/
|
|
848
|
-
async isWalletAvailable(walletName?: string): Promise<boolean> {
|
|
849
|
-
const wallet = walletName ? getWalletByName(walletName) : this.currentWallet;
|
|
850
|
-
if (!wallet) {
|
|
851
|
-
return false;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// CRITICAL FIX: Check adapter type to reliably detect web platform
|
|
855
|
-
// WebAdapter is only used on web, so this is the most reliable check
|
|
856
|
-
const isWeb = this.adapter.constructor.name === 'WebAdapter';
|
|
857
|
-
|
|
858
|
-
if (isWeb) {
|
|
859
|
-
// On web, if wallet has universalLink or supports web platform, it's available
|
|
860
|
-
// Universal links can open in a new tab on web
|
|
861
|
-
return wallet.platforms.includes('web') || !!wallet.universalLink;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// On mobile, we can't reliably check if wallet is installed
|
|
865
|
-
// Return true if wallet supports the current platform
|
|
866
|
-
// eslint-disable-next-line no-undef
|
|
867
|
-
const platform = typeof globalThis !== 'undefined' && (globalThis as any).Platform
|
|
868
|
-
? (globalThis as any).Platform.OS === 'ios' ? 'ios' : 'android'
|
|
869
|
-
: 'android';
|
|
870
|
-
|
|
871
|
-
return wallet.platforms.includes(platform);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Set preferred wallet for connections
|
|
876
|
-
*/
|
|
877
|
-
setPreferredWallet(walletName: string): void {
|
|
878
|
-
const wallet = getWalletByName(walletName);
|
|
879
|
-
if (!wallet) {
|
|
880
|
-
throw new TonConnectError(`Wallet "${walletName}" not found. Available wallets: ${SUPPORTED_WALLETS.map(w => w.name).join(', ')}`);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// CRITICAL FIX: Clear any pending connection when wallet changes
|
|
884
|
-
if (this.connectionPromise) {
|
|
885
|
-
console.log('[TON Connect] Clearing pending connection due to wallet change');
|
|
886
|
-
if (this.connectionPromise.timeout !== null) {
|
|
887
|
-
clearTimeout(this.connectionPromise.timeout);
|
|
888
|
-
}
|
|
889
|
-
this.connectionPromise = null;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
this.currentWallet = wallet;
|
|
893
|
-
console.log('[TON Connect] Preferred wallet changed to:', wallet.name);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Subscribe to status changes
|
|
898
|
-
*/
|
|
899
|
-
onStatusChange(callback: StatusChangeCallback): () => void {
|
|
900
|
-
this.statusChangeCallbacks.add(callback);
|
|
901
|
-
|
|
902
|
-
// Immediately call with current status
|
|
903
|
-
callback(this.getStatus());
|
|
904
|
-
|
|
905
|
-
// Return unsubscribe function
|
|
906
|
-
return () => {
|
|
907
|
-
this.statusChangeCallbacks.delete(callback);
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/**
|
|
912
|
-
* Notify all status change callbacks
|
|
913
|
-
*/
|
|
914
|
-
private notifyStatusChange(): void {
|
|
915
|
-
const status = this.getStatus();
|
|
916
|
-
this.statusChangeCallbacks.forEach((callback) => {
|
|
917
|
-
try {
|
|
918
|
-
callback(status);
|
|
919
|
-
} catch (error) {
|
|
920
|
-
// Ignore errors in callbacks
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
// Emit statusChange event
|
|
924
|
-
this.emit('statusChange', status);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Emit event to all listeners
|
|
929
|
-
*/
|
|
930
|
-
private emit<T>(event: TonConnectEventType, data: T): void {
|
|
931
|
-
const listeners = this.eventListeners.get(event);
|
|
932
|
-
if (listeners) {
|
|
933
|
-
listeners.forEach((listener) => {
|
|
934
|
-
try {
|
|
935
|
-
listener(data);
|
|
936
|
-
} catch (error) {
|
|
937
|
-
console.error(`[TON Connect] Error in event listener for ${event}:`, error);
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* Add event listener
|
|
945
|
-
*/
|
|
946
|
-
on<T = any>(event: TonConnectEventType, listener: TonConnectEventListener<T>): () => void {
|
|
947
|
-
if (!this.eventListeners.has(event)) {
|
|
948
|
-
this.eventListeners.set(event, new Set());
|
|
949
|
-
}
|
|
950
|
-
this.eventListeners.get(event)!.add(listener);
|
|
951
|
-
|
|
952
|
-
// Return unsubscribe function
|
|
953
|
-
return () => {
|
|
954
|
-
const listeners = this.eventListeners.get(event);
|
|
955
|
-
if (listeners) {
|
|
956
|
-
listeners.delete(listener);
|
|
957
|
-
}
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/**
|
|
962
|
-
* Remove event listener
|
|
963
|
-
*/
|
|
964
|
-
off<T = any>(event: TonConnectEventType, listener: TonConnectEventListener<T>): void {
|
|
965
|
-
const listeners = this.eventListeners.get(event);
|
|
966
|
-
if (listeners) {
|
|
967
|
-
listeners.delete(listener);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Remove all listeners for an event
|
|
973
|
-
*/
|
|
974
|
-
removeAllListeners(event?: TonConnectEventType): void {
|
|
975
|
-
if (event) {
|
|
976
|
-
this.eventListeners.delete(event);
|
|
977
|
-
} else {
|
|
978
|
-
this.eventListeners.clear();
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* Validate session ID format
|
|
984
|
-
*/
|
|
985
|
-
private validateSessionId(sessionId: string): boolean {
|
|
986
|
-
if (!sessionId || typeof sessionId !== 'string') {
|
|
987
|
-
return false;
|
|
988
|
-
}
|
|
989
|
-
// Session ID should be reasonable length (1-200 characters)
|
|
990
|
-
if (sessionId.length === 0 || sessionId.length > 200) {
|
|
991
|
-
return false;
|
|
992
|
-
}
|
|
993
|
-
// Basic validation: should not contain control characters
|
|
994
|
-
if (/[\x00-\x1F\x7F]/.test(sessionId)) {
|
|
995
|
-
return false;
|
|
996
|
-
}
|
|
997
|
-
return true;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* Save session to storage
|
|
1002
|
-
*/
|
|
1003
|
-
private async saveSession(sessionId: string, wallet: WalletInfo): Promise<void> {
|
|
1004
|
-
// Validate inputs
|
|
1005
|
-
if (!this.validateSessionId(sessionId)) {
|
|
1006
|
-
throw new TonConnectError('Invalid session ID format');
|
|
1007
|
-
}
|
|
1008
|
-
if (!wallet || !wallet.address || !wallet.publicKey) {
|
|
1009
|
-
throw new TonConnectError('Invalid wallet data');
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
try {
|
|
1013
|
-
const sessionKey = `${this.config.storageKeyPrefix}session`;
|
|
1014
|
-
const walletKey = `${this.config.storageKeyPrefix}wallet`;
|
|
1015
|
-
|
|
1016
|
-
await this.adapter.setItem(sessionKey, sessionId);
|
|
1017
|
-
await this.adapter.setItem(walletKey, JSON.stringify(wallet));
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
// Log error but don't throw - connection is still valid
|
|
1020
|
-
console.error('TON Connect: Failed to save session to storage:', error);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Load session from storage
|
|
1026
|
-
*/
|
|
1027
|
-
private async loadSession(): Promise<void> {
|
|
1028
|
-
try {
|
|
1029
|
-
const sessionKey = `${this.config.storageKeyPrefix}session`;
|
|
1030
|
-
const walletKey = `${this.config.storageKeyPrefix}wallet`;
|
|
1031
|
-
|
|
1032
|
-
const sessionId = await this.adapter.getItem(sessionKey);
|
|
1033
|
-
const walletJson = await this.adapter.getItem(walletKey);
|
|
1034
|
-
|
|
1035
|
-
if (sessionId && walletJson) {
|
|
1036
|
-
try {
|
|
1037
|
-
// Validate session ID
|
|
1038
|
-
if (!this.validateSessionId(sessionId)) {
|
|
1039
|
-
await this.clearSession();
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
const wallet = JSON.parse(walletJson) as WalletInfo;
|
|
1044
|
-
|
|
1045
|
-
// Validate wallet data
|
|
1046
|
-
if (!wallet || !wallet.address || !wallet.publicKey) {
|
|
1047
|
-
await this.clearSession();
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
this.currentStatus = { connected: true, wallet };
|
|
1052
|
-
this.notifyStatusChange();
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
// Invalid wallet data, clear it
|
|
1055
|
-
console.error('TON Connect: Invalid session data, clearing:', error);
|
|
1056
|
-
await this.clearSession();
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
} catch (error) {
|
|
1060
|
-
// Log storage errors for debugging
|
|
1061
|
-
console.error('TON Connect: Failed to load session from storage:', error);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* Clear session from storage
|
|
1067
|
-
*/
|
|
1068
|
-
private async clearSession(): Promise<void> {
|
|
1069
|
-
try {
|
|
1070
|
-
const sessionKey = `${this.config.storageKeyPrefix}session`;
|
|
1071
|
-
const walletKey = `${this.config.storageKeyPrefix}wallet`;
|
|
1072
|
-
|
|
1073
|
-
await this.adapter.removeItem(sessionKey);
|
|
1074
|
-
await this.adapter.removeItem(walletKey);
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
// Ignore storage errors
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* Cleanup resources
|
|
1082
|
-
*/
|
|
1083
|
-
destroy(): void {
|
|
1084
|
-
if (this.urlUnsubscribe) {
|
|
1085
|
-
this.urlUnsubscribe();
|
|
1086
|
-
this.urlUnsubscribe = null;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
if ('destroy' in this.adapter && typeof (this.adapter as { destroy?: () => void }).destroy === 'function') {
|
|
1090
|
-
(this.adapter as { destroy: () => void }).destroy();
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
this.statusChangeCallbacks.clear();
|
|
1094
|
-
this.eventListeners.clear();
|
|
1095
|
-
this.connectionPromise = null;
|
|
1096
|
-
this.transactionPromise = null;
|
|
1097
|
-
this.signDataPromise = null;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Get current network
|
|
1102
|
-
*/
|
|
1103
|
-
getNetwork(): Network {
|
|
1104
|
-
return this.config.network;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Set network (mainnet/testnet)
|
|
1109
|
-
*/
|
|
1110
|
-
setNetwork(network: Network): void {
|
|
1111
|
-
if (network !== 'mainnet' && network !== 'testnet') {
|
|
1112
|
-
throw new TonConnectError('Network must be either "mainnet" or "testnet"');
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const oldNetwork = this.config.network;
|
|
1116
|
-
|
|
1117
|
-
// Warn if switching network while connected (wallet connection is network-specific)
|
|
1118
|
-
if (this.currentStatus.connected && oldNetwork !== network) {
|
|
1119
|
-
console.warn(
|
|
1120
|
-
'[TON Connect] Network changed while wallet is connected. ' +
|
|
1121
|
-
'The wallet connection may be invalid for the new network. ' +
|
|
1122
|
-
'Consider disconnecting and reconnecting after network change.'
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
this.config.network = network;
|
|
1127
|
-
|
|
1128
|
-
// Update TON API endpoint if not explicitly set
|
|
1129
|
-
if (!this.config.tonApiEndpoint || this.config.tonApiEndpoint.includes(oldNetwork)) {
|
|
1130
|
-
this.config.tonApiEndpoint =
|
|
1131
|
-
network === 'testnet'
|
|
1132
|
-
? 'https://testnet.toncenter.com/api/v2'
|
|
1133
|
-
: 'https://toncenter.com/api/v2';
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
console.log('[TON Connect] Network changed to:', network);
|
|
1137
|
-
|
|
1138
|
-
// Notify status change to update chain ID in React components
|
|
1139
|
-
this.notifyStatusChange();
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Get wallet balance
|
|
1144
|
-
*/
|
|
1145
|
-
async getBalance(address?: string): Promise<BalanceResponse> {
|
|
1146
|
-
const targetAddress = address || this.currentStatus.wallet?.address;
|
|
1147
|
-
if (!targetAddress) {
|
|
1148
|
-
throw new TonConnectError('Address is required. Either connect a wallet or provide an address.');
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// Validate address format
|
|
1152
|
-
if (!/^[0-9A-Za-z_-]{48}$/.test(targetAddress)) {
|
|
1153
|
-
throw new TonConnectError('Invalid TON address format');
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
try {
|
|
1157
|
-
const apiEndpoint = this.config.tonApiEndpoint ||
|
|
1158
|
-
(this.config.network === 'testnet'
|
|
1159
|
-
? 'https://testnet.toncenter.com/api/v2'
|
|
1160
|
-
: 'https://toncenter.com/api/v2');
|
|
1161
|
-
|
|
1162
|
-
const url = `${apiEndpoint}/getAddressInformation?address=${encodeURIComponent(targetAddress)}`;
|
|
1163
|
-
|
|
1164
|
-
const response = await fetch(url, {
|
|
1165
|
-
method: 'GET',
|
|
1166
|
-
headers: {
|
|
1167
|
-
'Accept': 'application/json',
|
|
1168
|
-
},
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
if (!response.ok) {
|
|
1172
|
-
throw new TonConnectError(`Failed to fetch balance: ${response.status} ${response.statusText}`);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
const data = await response.json();
|
|
1176
|
-
|
|
1177
|
-
if (data.ok === false) {
|
|
1178
|
-
throw new TonConnectError(data.error || 'Failed to fetch balance');
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// TON Center API returns balance in nanotons
|
|
1182
|
-
const balance = data.result?.balance || '0';
|
|
1183
|
-
const balanceTon = (BigInt(balance) / BigInt(1000000000)).toString() + '.' +
|
|
1184
|
-
(BigInt(balance) % BigInt(1000000000)).toString().padStart(9, '0').replace(/0+$/, '');
|
|
1185
|
-
|
|
1186
|
-
return {
|
|
1187
|
-
balance,
|
|
1188
|
-
balanceTon: balanceTon === '0.' ? '0' : balanceTon,
|
|
1189
|
-
network: this.config.network,
|
|
1190
|
-
};
|
|
1191
|
-
} catch (error: any) {
|
|
1192
|
-
if (error instanceof TonConnectError) {
|
|
1193
|
-
throw error;
|
|
1194
|
-
}
|
|
1195
|
-
throw new TonConnectError(`Failed to get balance: ${error?.message || String(error)}`);
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
/**
|
|
1200
|
-
* Get transaction status
|
|
1201
|
-
*/
|
|
1202
|
-
async getTransactionStatus(boc: string, maxAttempts: number = 10, intervalMs: number = 2000): Promise<TransactionStatusResponse> {
|
|
1203
|
-
if (!boc || typeof boc !== 'string' || boc.length === 0) {
|
|
1204
|
-
throw new TonConnectError('Transaction BOC is required');
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// Extract transaction hash from BOC (simplified - in production, you'd parse the BOC properly)
|
|
1208
|
-
// For now, we'll use a polling approach with TON Center API
|
|
1209
|
-
try {
|
|
1210
|
-
const apiEndpoint = this.config.tonApiEndpoint ||
|
|
1211
|
-
(this.config.network === 'testnet'
|
|
1212
|
-
? 'https://testnet.toncenter.com/api/v2'
|
|
1213
|
-
: 'https://toncenter.com/api/v2');
|
|
1214
|
-
|
|
1215
|
-
// Try to get transaction info
|
|
1216
|
-
// Note: This is a simplified implementation. In production, you'd need to:
|
|
1217
|
-
// 1. Parse the BOC to extract transaction hash
|
|
1218
|
-
// 2. Query the blockchain for transaction status
|
|
1219
|
-
// 3. Handle different confirmation states
|
|
1220
|
-
|
|
1221
|
-
// For now, we'll return a basic status
|
|
1222
|
-
// In a real implementation, you'd query the blockchain API
|
|
1223
|
-
let attempts = 0;
|
|
1224
|
-
let lastError: Error | null = null;
|
|
1225
|
-
|
|
1226
|
-
while (attempts < maxAttempts) {
|
|
1227
|
-
try {
|
|
1228
|
-
// This is a placeholder - you'd need to implement actual transaction lookup
|
|
1229
|
-
// For now, we'll simulate checking
|
|
1230
|
-
await new Promise<void>((resolve) => setTimeout(() => resolve(), intervalMs));
|
|
1231
|
-
|
|
1232
|
-
// In production, you would:
|
|
1233
|
-
// 1. Parse BOC to get transaction hash
|
|
1234
|
-
// 2. Query TON API: GET /getTransactions?address=...&limit=1
|
|
1235
|
-
// 3. Check if transaction exists and is confirmed
|
|
1236
|
-
|
|
1237
|
-
// For now, return unknown status (as we can't parse BOC without additional libraries)
|
|
1238
|
-
return {
|
|
1239
|
-
status: 'unknown',
|
|
1240
|
-
error: 'Transaction status checking requires BOC parsing. Please use a TON library to parse the BOC and extract the transaction hash.',
|
|
1241
|
-
};
|
|
1242
|
-
} catch (error: any) {
|
|
1243
|
-
lastError = error;
|
|
1244
|
-
attempts++;
|
|
1245
|
-
if (attempts < maxAttempts) {
|
|
1246
|
-
await new Promise<void>((resolve) => setTimeout(() => resolve(), intervalMs));
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
return {
|
|
1252
|
-
status: 'failed',
|
|
1253
|
-
error: lastError?.message || 'Failed to check transaction status',
|
|
1254
|
-
};
|
|
1255
|
-
} catch (error: any) {
|
|
1256
|
-
throw new TonConnectError(`Failed to get transaction status: ${error?.message || String(error)}`);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Get transaction status by hash (more reliable than BOC)
|
|
1262
|
-
*/
|
|
1263
|
-
async getTransactionStatusByHash(txHash: string, address: string): Promise<TransactionStatusResponse> {
|
|
1264
|
-
if (!txHash || typeof txHash !== 'string' || txHash.length === 0) {
|
|
1265
|
-
throw new TonConnectError('Transaction hash is required');
|
|
1266
|
-
}
|
|
1267
|
-
if (!address || typeof address !== 'string' || address.length === 0) {
|
|
1268
|
-
throw new TonConnectError('Address is required');
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
try {
|
|
1272
|
-
const apiEndpoint = this.config.tonApiEndpoint ||
|
|
1273
|
-
(this.config.network === 'testnet'
|
|
1274
|
-
? 'https://testnet.toncenter.com/api/v2'
|
|
1275
|
-
: 'https://toncenter.com/api/v2');
|
|
1276
|
-
|
|
1277
|
-
// Query transactions for the address
|
|
1278
|
-
const url = `${apiEndpoint}/getTransactions?address=${encodeURIComponent(address)}&limit=100`;
|
|
1279
|
-
|
|
1280
|
-
const response = await fetch(url, {
|
|
1281
|
-
method: 'GET',
|
|
1282
|
-
headers: {
|
|
1283
|
-
'Accept': 'application/json',
|
|
1284
|
-
},
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
if (!response.ok) {
|
|
1288
|
-
throw new TonConnectError(`Failed to fetch transactions: ${response.status} ${response.statusText}`);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
const data = await response.json();
|
|
1292
|
-
|
|
1293
|
-
if (data.ok === false) {
|
|
1294
|
-
throw new TonConnectError(data.error || 'Failed to fetch transactions');
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// Search for transaction with matching hash
|
|
1298
|
-
const transactions = data.result || [];
|
|
1299
|
-
const transaction = transactions.find((tx: any) =>
|
|
1300
|
-
tx.transaction_id?.hash === txHash ||
|
|
1301
|
-
tx.transaction_id?.lt === txHash ||
|
|
1302
|
-
JSON.stringify(tx.transaction_id).includes(txHash)
|
|
1303
|
-
);
|
|
1304
|
-
|
|
1305
|
-
if (transaction) {
|
|
1306
|
-
return {
|
|
1307
|
-
status: 'confirmed',
|
|
1308
|
-
hash: transaction.transaction_id?.hash || txHash,
|
|
1309
|
-
blockNumber: transaction.transaction_id?.lt,
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// Transaction not found - could be pending or failed
|
|
1314
|
-
return {
|
|
1315
|
-
status: 'pending',
|
|
1316
|
-
hash: txHash,
|
|
1317
|
-
};
|
|
1318
|
-
} catch (error: any) {
|
|
1319
|
-
if (error instanceof TonConnectError) {
|
|
1320
|
-
throw error;
|
|
1321
|
-
}
|
|
1322
|
-
return {
|
|
1323
|
-
status: 'failed',
|
|
1324
|
-
error: error?.message || 'Failed to check transaction status',
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// Export types
|
|
1331
|
-
export * from './types';
|
|
1332
|
-
export type { WalletDefinition } from './core/wallets';
|
|
1333
|
-
export { SUPPORTED_WALLETS, getWalletByName, getDefaultWallet, getWalletsForPlatform } from './core/wallets';
|
|
1334
|
-
|
|
1335
|
-
// Export utilities
|
|
1336
|
-
export * from './utils/transactionBuilder';
|
|
1337
|
-
export * from './utils/retry';
|
|
1338
|
-
|
|
1
|
+
/**
|
|
2
|
+
* TON Connect Mobile SDK
|
|
3
|
+
* Production-ready implementation using TON Connect v2 bridge protocol
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/// <reference path="./index.d.ts" />
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
TonConnectMobileConfig,
|
|
10
|
+
ConnectionStatus,
|
|
11
|
+
WalletInfo,
|
|
12
|
+
SendTransactionRequest,
|
|
13
|
+
StatusChangeCallback,
|
|
14
|
+
PlatformAdapter,
|
|
15
|
+
Network,
|
|
16
|
+
TonConnectEventType,
|
|
17
|
+
TonConnectEventListener,
|
|
18
|
+
TransactionStatusResponse,
|
|
19
|
+
BalanceResponse,
|
|
20
|
+
PersistedSession,
|
|
21
|
+
} from './types';
|
|
22
|
+
import {
|
|
23
|
+
buildConnectUniversalLink,
|
|
24
|
+
buildReturnUniversalLink,
|
|
25
|
+
buildSendTransactionRpcRequest,
|
|
26
|
+
buildDisconnectRpcRequest,
|
|
27
|
+
parseConnectResponse,
|
|
28
|
+
parseRpcResponse,
|
|
29
|
+
extractWalletInfoFromEvent,
|
|
30
|
+
validateTransactionRequest,
|
|
31
|
+
} from './core/protocol';
|
|
32
|
+
import { SessionCrypto, hexToBytes, base64ToBytes, bytesToHex } from './core/session';
|
|
33
|
+
import { BridgeGateway, BridgeIncomingMessage } from './core/bridge';
|
|
34
|
+
import { ExpoAdapter } from './adapters/expo';
|
|
35
|
+
import { ReactNativeAdapter } from './adapters/react-native';
|
|
36
|
+
import { WebAdapter } from './adapters/web';
|
|
37
|
+
import { getWalletByName, getDefaultWallet, SUPPORTED_WALLETS, type WalletDefinition } from './core/wallets';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Custom error classes
|
|
41
|
+
*/
|
|
42
|
+
export class TonConnectError extends Error {
|
|
43
|
+
constructor(message: string, public code?: string, public recoverySuggestion?: string) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'TonConnectError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ConnectionTimeoutError extends TonConnectError {
|
|
50
|
+
constructor() {
|
|
51
|
+
super(
|
|
52
|
+
'Connection request timed out. The wallet did not respond in time.',
|
|
53
|
+
'CONNECTION_TIMEOUT',
|
|
54
|
+
'Make sure the wallet app is installed and try again.'
|
|
55
|
+
);
|
|
56
|
+
this.name = 'ConnectionTimeoutError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class TransactionTimeoutError extends TonConnectError {
|
|
61
|
+
constructor() {
|
|
62
|
+
super(
|
|
63
|
+
'Transaction request timed out.',
|
|
64
|
+
'TRANSACTION_TIMEOUT',
|
|
65
|
+
'Check the wallet app and try again.'
|
|
66
|
+
);
|
|
67
|
+
this.name = 'TransactionTimeoutError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class UserRejectedError extends TonConnectError {
|
|
72
|
+
constructor(message?: string) {
|
|
73
|
+
super(message || 'User rejected the request', 'USER_REJECTED');
|
|
74
|
+
this.name = 'UserRejectedError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ConnectionInProgressError extends TonConnectError {
|
|
79
|
+
constructor() {
|
|
80
|
+
super('Connection request already in progress', 'CONNECTION_IN_PROGRESS');
|
|
81
|
+
this.name = 'ConnectionInProgressError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class TransactionInProgressError extends TonConnectError {
|
|
86
|
+
constructor() {
|
|
87
|
+
super('Transaction request already in progress', 'TRANSACTION_IN_PROGRESS');
|
|
88
|
+
this.name = 'TransactionInProgressError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Main TON Connect Mobile SDK class
|
|
94
|
+
* Implements the real TON Connect v2 bridge protocol
|
|
95
|
+
*/
|
|
96
|
+
export class TonConnectMobile {
|
|
97
|
+
private adapter: PlatformAdapter;
|
|
98
|
+
private config: Required<Omit<TonConnectMobileConfig, 'preferredWallet' | 'network' | 'tonApiEndpoint'>> & {
|
|
99
|
+
preferredWallet?: string;
|
|
100
|
+
network: Network;
|
|
101
|
+
tonApiEndpoint?: string;
|
|
102
|
+
};
|
|
103
|
+
private statusChangeCallbacks: Set<StatusChangeCallback> = new Set();
|
|
104
|
+
private eventListeners: Map<TonConnectEventType, Set<TonConnectEventListener>> = new Map();
|
|
105
|
+
private currentStatus: ConnectionStatus = { connected: false, wallet: null };
|
|
106
|
+
private currentWallet!: WalletDefinition;
|
|
107
|
+
|
|
108
|
+
// TON Connect v2 protocol state
|
|
109
|
+
private session: SessionCrypto | null = null;
|
|
110
|
+
private bridge: BridgeGateway = new BridgeGateway();
|
|
111
|
+
private walletBridgePublicKey: string | null = null; // hex wallet public key from bridge
|
|
112
|
+
|
|
113
|
+
// Pending promises
|
|
114
|
+
private connectionPromise: {
|
|
115
|
+
resolve: (wallet: WalletInfo) => void;
|
|
116
|
+
reject: (error: Error) => void;
|
|
117
|
+
timeout: ReturnType<typeof setTimeout> | null;
|
|
118
|
+
} | null = null;
|
|
119
|
+
|
|
120
|
+
private pendingRpcRequests: Map<number, {
|
|
121
|
+
resolve: (result: string) => void;
|
|
122
|
+
reject: (error: Error) => void;
|
|
123
|
+
timeout: ReturnType<typeof setTimeout> | null;
|
|
124
|
+
}> = new Map();
|
|
125
|
+
|
|
126
|
+
private rpcIdCounter: number = 1;
|
|
127
|
+
|
|
128
|
+
constructor(config: TonConnectMobileConfig) {
|
|
129
|
+
if (!config.manifestUrl) {
|
|
130
|
+
throw new TonConnectError('manifestUrl is required');
|
|
131
|
+
}
|
|
132
|
+
if (!config.scheme) {
|
|
133
|
+
throw new TonConnectError('scheme is required');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const network = config.network || 'mainnet';
|
|
137
|
+
const defaultTonApiEndpoint =
|
|
138
|
+
network === 'testnet'
|
|
139
|
+
? 'https://testnet.toncenter.com/api/v2'
|
|
140
|
+
: 'https://toncenter.com/api/v2';
|
|
141
|
+
|
|
142
|
+
this.config = {
|
|
143
|
+
storageKeyPrefix: 'tonconnect_',
|
|
144
|
+
connectionTimeout: 300000,
|
|
145
|
+
transactionTimeout: 300000,
|
|
146
|
+
skipCanOpenURLCheck: true,
|
|
147
|
+
preferredWallet: config.preferredWallet,
|
|
148
|
+
network,
|
|
149
|
+
tonApiEndpoint: config.tonApiEndpoint || defaultTonApiEndpoint,
|
|
150
|
+
...config,
|
|
151
|
+
} as any;
|
|
152
|
+
|
|
153
|
+
// Determine wallet
|
|
154
|
+
if (this.config.preferredWallet) {
|
|
155
|
+
const wallet = getWalletByName(this.config.preferredWallet);
|
|
156
|
+
this.currentWallet = wallet || getDefaultWallet();
|
|
157
|
+
} else {
|
|
158
|
+
this.currentWallet = getDefaultWallet();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log('[TON Connect] Initializing SDK v2 with wallet:', this.currentWallet.name);
|
|
162
|
+
console.log('[TON Connect] Bridge URL:', this.currentWallet.bridgeUrl);
|
|
163
|
+
|
|
164
|
+
// Initialize platform adapter
|
|
165
|
+
this.adapter = this.createAdapter();
|
|
166
|
+
|
|
167
|
+
// Load persisted session
|
|
168
|
+
this.loadSession();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create platform adapter
|
|
173
|
+
*/
|
|
174
|
+
private createAdapter(): PlatformAdapter {
|
|
175
|
+
// eslint-disable-next-line no-undef
|
|
176
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).window && (globalThis as any).document) {
|
|
177
|
+
return new WebAdapter();
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
if (typeof require !== 'undefined') {
|
|
181
|
+
const expoLinking = require('expo-linking');
|
|
182
|
+
if (expoLinking) {
|
|
183
|
+
return new ExpoAdapter();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Not Expo
|
|
188
|
+
}
|
|
189
|
+
return new ReactNativeAdapter();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handle incoming bridge message from wallet
|
|
194
|
+
*/
|
|
195
|
+
private handleBridgeMessage(msg: BridgeIncomingMessage): void {
|
|
196
|
+
if (!this.session) {
|
|
197
|
+
console.error('[TON Connect] Received bridge message but no session');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Decode base64 encrypted message
|
|
203
|
+
const encryptedBytes = base64ToBytes(msg.message);
|
|
204
|
+
const senderPublicKey = hexToBytes(msg.from);
|
|
205
|
+
|
|
206
|
+
// Decrypt
|
|
207
|
+
const decrypted = this.session.decrypt(encryptedBytes, senderPublicKey);
|
|
208
|
+
console.log('[TON Connect] Decrypted bridge message');
|
|
209
|
+
|
|
210
|
+
// Store wallet's bridge public key for future communication
|
|
211
|
+
this.walletBridgePublicKey = msg.from;
|
|
212
|
+
|
|
213
|
+
// Try to parse as connect response first
|
|
214
|
+
const connectResult = parseConnectResponse(decrypted);
|
|
215
|
+
if (connectResult) {
|
|
216
|
+
if (connectResult.type === 'connect') {
|
|
217
|
+
this.handleConnectSuccess(connectResult.data);
|
|
218
|
+
} else if (connectResult.type === 'error') {
|
|
219
|
+
this.handleConnectError(connectResult.data);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Try to parse as RPC response
|
|
225
|
+
const rpcResult = parseRpcResponse(decrypted);
|
|
226
|
+
if (rpcResult) {
|
|
227
|
+
if (rpcResult.type === 'event' && rpcResult.event === 'disconnect') {
|
|
228
|
+
this.handleRemoteDisconnect();
|
|
229
|
+
} else if (rpcResult.type === 'result') {
|
|
230
|
+
this.handleRpcResult(rpcResult.data);
|
|
231
|
+
} else if (rpcResult.type === 'error') {
|
|
232
|
+
this.handleRpcError(rpcResult.data);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.warn('[TON Connect] Unknown bridge message format');
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('[TON Connect] Error handling bridge message:', error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle successful wallet connection
|
|
245
|
+
*/
|
|
246
|
+
private handleConnectSuccess(event: any): void {
|
|
247
|
+
try {
|
|
248
|
+
const wallet = extractWalletInfoFromEvent(event);
|
|
249
|
+
|
|
250
|
+
// Save session for persistence
|
|
251
|
+
this.saveSession(wallet).catch((err) => {
|
|
252
|
+
console.error('[TON Connect] Failed to save session:', err);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Update status
|
|
256
|
+
this.currentStatus = { connected: true, wallet };
|
|
257
|
+
this.notifyStatusChange();
|
|
258
|
+
this.emit('connect', wallet);
|
|
259
|
+
|
|
260
|
+
// Resolve pending connection promise
|
|
261
|
+
if (this.connectionPromise) {
|
|
262
|
+
if (this.connectionPromise.timeout) {
|
|
263
|
+
clearTimeout(this.connectionPromise.timeout);
|
|
264
|
+
}
|
|
265
|
+
const promise = this.connectionPromise;
|
|
266
|
+
this.connectionPromise = null;
|
|
267
|
+
promise.resolve(wallet);
|
|
268
|
+
}
|
|
269
|
+
} catch (error: any) {
|
|
270
|
+
console.error('[TON Connect] Error processing connect response:', error);
|
|
271
|
+
this.rejectConnection(new TonConnectError(error?.message || 'Invalid connect response'));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle connect error from wallet
|
|
277
|
+
*/
|
|
278
|
+
private handleConnectError(event: any): void {
|
|
279
|
+
const code = event.payload?.code;
|
|
280
|
+
const message = event.payload?.message || 'Connection rejected';
|
|
281
|
+
|
|
282
|
+
let error: Error;
|
|
283
|
+
if (code === 300) {
|
|
284
|
+
error = new UserRejectedError(message);
|
|
285
|
+
} else {
|
|
286
|
+
error = new TonConnectError(message, String(code));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.rejectConnection(error);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle remote disconnect from wallet
|
|
294
|
+
*/
|
|
295
|
+
private handleRemoteDisconnect(): void {
|
|
296
|
+
console.log('[TON Connect] Wallet disconnected remotely');
|
|
297
|
+
this.currentStatus = { connected: false, wallet: null };
|
|
298
|
+
this.clearSession().catch(() => {});
|
|
299
|
+
this.notifyStatusChange();
|
|
300
|
+
this.emit('disconnect', null);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Handle RPC success result
|
|
305
|
+
*/
|
|
306
|
+
private handleRpcResult(response: { result: string; id: number }): void {
|
|
307
|
+
const pending = this.pendingRpcRequests.get(response.id);
|
|
308
|
+
if (pending) {
|
|
309
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
310
|
+
this.pendingRpcRequests.delete(response.id);
|
|
311
|
+
pending.resolve(response.result);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle RPC error result
|
|
317
|
+
*/
|
|
318
|
+
private handleRpcError(response: { error: { code: number; message: string }; id: number }): void {
|
|
319
|
+
const pending = this.pendingRpcRequests.get(response.id);
|
|
320
|
+
if (pending) {
|
|
321
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
322
|
+
this.pendingRpcRequests.delete(response.id);
|
|
323
|
+
|
|
324
|
+
let error: Error;
|
|
325
|
+
if (response.error.code === 300) {
|
|
326
|
+
error = new UserRejectedError(response.error.message);
|
|
327
|
+
} else {
|
|
328
|
+
error = new TonConnectError(response.error.message, String(response.error.code));
|
|
329
|
+
}
|
|
330
|
+
pending.reject(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Reject pending connection promise
|
|
336
|
+
*/
|
|
337
|
+
private rejectConnection(error: Error): void {
|
|
338
|
+
this.emit('error', error);
|
|
339
|
+
if (this.connectionPromise) {
|
|
340
|
+
if (this.connectionPromise.timeout) {
|
|
341
|
+
clearTimeout(this.connectionPromise.timeout);
|
|
342
|
+
}
|
|
343
|
+
const promise = this.connectionPromise;
|
|
344
|
+
this.connectionPromise = null;
|
|
345
|
+
promise.reject(error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Connect to wallet using TON Connect v2 bridge protocol
|
|
351
|
+
*/
|
|
352
|
+
async connect(): Promise<WalletInfo> {
|
|
353
|
+
// If already connected, return current wallet
|
|
354
|
+
if (this.currentStatus.connected && this.currentStatus.wallet) {
|
|
355
|
+
return this.currentStatus.wallet;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (this.connectionPromise) {
|
|
359
|
+
throw new ConnectionInProgressError();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log('[TON Connect] Starting connection to', this.currentWallet.name);
|
|
363
|
+
|
|
364
|
+
// 1. Create new session (X25519 keypair)
|
|
365
|
+
this.session = new SessionCrypto();
|
|
366
|
+
console.log('[TON Connect] Session ID:', this.session.sessionId.substring(0, 16) + '...');
|
|
367
|
+
|
|
368
|
+
// 2. Connect to bridge SSE
|
|
369
|
+
this.bridge.close(); // Close any existing connection
|
|
370
|
+
this.bridge.connect(
|
|
371
|
+
this.currentWallet.bridgeUrl,
|
|
372
|
+
this.session.sessionId,
|
|
373
|
+
(msg) => this.handleBridgeMessage(msg),
|
|
374
|
+
(error) => console.error('[TON Connect] Bridge error:', error)
|
|
375
|
+
);
|
|
376
|
+
console.log('[TON Connect] Bridge SSE connection initiated');
|
|
377
|
+
|
|
378
|
+
// 3. Build universal link
|
|
379
|
+
const universalLink = buildConnectUniversalLink(
|
|
380
|
+
this.currentWallet.universalLink,
|
|
381
|
+
this.session.sessionId,
|
|
382
|
+
this.config.manifestUrl,
|
|
383
|
+
'back'
|
|
384
|
+
);
|
|
385
|
+
console.log('[TON Connect] Universal link built');
|
|
386
|
+
|
|
387
|
+
// 4. Return promise that resolves when wallet responds via bridge
|
|
388
|
+
return new Promise<WalletInfo>((resolve, reject) => {
|
|
389
|
+
const timeout = setTimeout(() => {
|
|
390
|
+
if (this.connectionPromise) {
|
|
391
|
+
this.connectionPromise = null;
|
|
392
|
+
this.bridge.close();
|
|
393
|
+
reject(new ConnectionTimeoutError());
|
|
394
|
+
}
|
|
395
|
+
}, this.config.connectionTimeout);
|
|
396
|
+
|
|
397
|
+
this.connectionPromise = { resolve, reject, timeout };
|
|
398
|
+
|
|
399
|
+
// 5. Open wallet app
|
|
400
|
+
console.log('[TON Connect] Opening wallet app...');
|
|
401
|
+
this.adapter.openURL(universalLink, this.config.skipCanOpenURLCheck).then((success) => {
|
|
402
|
+
if (!success && this.connectionPromise) {
|
|
403
|
+
this.connectionPromise = null;
|
|
404
|
+
this.bridge.close();
|
|
405
|
+
reject(new TonConnectError('Failed to open wallet app'));
|
|
406
|
+
}
|
|
407
|
+
console.log('[TON Connect] Wallet app opened, waiting for bridge response...');
|
|
408
|
+
}).catch((error) => {
|
|
409
|
+
if (this.connectionPromise) {
|
|
410
|
+
if (this.connectionPromise.timeout) clearTimeout(this.connectionPromise.timeout);
|
|
411
|
+
this.connectionPromise = null;
|
|
412
|
+
this.bridge.close();
|
|
413
|
+
reject(new TonConnectError(`Failed to open wallet: ${error?.message || String(error)}`));
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Send transaction via TON Connect v2 bridge protocol
|
|
421
|
+
*/
|
|
422
|
+
async sendTransaction(request: SendTransactionRequest): Promise<{ boc: string }> {
|
|
423
|
+
const validation = validateTransactionRequest(request);
|
|
424
|
+
if (!validation.valid) {
|
|
425
|
+
throw new TonConnectError(validation.error || 'Invalid transaction request');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!this.currentStatus.connected || !this.currentStatus.wallet) {
|
|
429
|
+
throw new TonConnectError('Not connected to wallet. Call connect() first.');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!this.session || !this.walletBridgePublicKey) {
|
|
433
|
+
throw new TonConnectError('Session not established. Please reconnect.');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Build JSON-RPC request
|
|
437
|
+
const rpcId = this.rpcIdCounter++;
|
|
438
|
+
const rpcRequest = buildSendTransactionRpcRequest(request, rpcId);
|
|
439
|
+
|
|
440
|
+
// Encrypt and send via bridge
|
|
441
|
+
const walletPubKeyBytes = hexToBytes(this.walletBridgePublicKey);
|
|
442
|
+
const encrypted = this.session.encrypt(rpcRequest, walletPubKeyBytes);
|
|
443
|
+
|
|
444
|
+
await this.bridge.send(
|
|
445
|
+
this.currentWallet.bridgeUrl,
|
|
446
|
+
this.session.sessionId,
|
|
447
|
+
this.walletBridgePublicKey,
|
|
448
|
+
encrypted
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Open wallet to foreground
|
|
452
|
+
const returnLink = buildReturnUniversalLink(this.currentWallet.universalLink, 'back');
|
|
453
|
+
this.adapter.openURL(returnLink, this.config.skipCanOpenURLCheck).catch(() => {
|
|
454
|
+
// Non-critical — wallet may already be in foreground
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Wait for response via bridge
|
|
458
|
+
return new Promise<{ boc: string }>((resolve, reject) => {
|
|
459
|
+
const timeout = setTimeout(() => {
|
|
460
|
+
this.pendingRpcRequests.delete(rpcId);
|
|
461
|
+
reject(new TransactionTimeoutError());
|
|
462
|
+
}, this.config.transactionTimeout);
|
|
463
|
+
|
|
464
|
+
this.pendingRpcRequests.set(rpcId, {
|
|
465
|
+
resolve: (result: string) => {
|
|
466
|
+
this.emit('transaction', { boc: result });
|
|
467
|
+
resolve({ boc: result });
|
|
468
|
+
},
|
|
469
|
+
reject: (error: Error) => {
|
|
470
|
+
this.emit('error', error);
|
|
471
|
+
reject(error);
|
|
472
|
+
},
|
|
473
|
+
timeout,
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Disconnect from wallet
|
|
480
|
+
*/
|
|
481
|
+
async disconnect(): Promise<void> {
|
|
482
|
+
// Send disconnect event via bridge if connected
|
|
483
|
+
if (this.session && this.walletBridgePublicKey) {
|
|
484
|
+
try {
|
|
485
|
+
const rpcId = this.rpcIdCounter++;
|
|
486
|
+
const disconnectRequest = buildDisconnectRpcRequest(rpcId);
|
|
487
|
+
const walletPubKeyBytes = hexToBytes(this.walletBridgePublicKey);
|
|
488
|
+
const encrypted = this.session.encrypt(disconnectRequest, walletPubKeyBytes);
|
|
489
|
+
await this.bridge.send(
|
|
490
|
+
this.currentWallet.bridgeUrl,
|
|
491
|
+
this.session.sessionId,
|
|
492
|
+
this.walletBridgePublicKey,
|
|
493
|
+
encrypted
|
|
494
|
+
);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.warn('[TON Connect] Failed to send disconnect to wallet:', error);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Close bridge
|
|
501
|
+
this.bridge.close();
|
|
502
|
+
|
|
503
|
+
// Clear session
|
|
504
|
+
this.session = null;
|
|
505
|
+
this.walletBridgePublicKey = null;
|
|
506
|
+
await this.clearSession();
|
|
507
|
+
|
|
508
|
+
// Update status
|
|
509
|
+
this.currentStatus = { connected: false, wallet: null };
|
|
510
|
+
this.notifyStatusChange();
|
|
511
|
+
this.emit('disconnect', null);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Get current connection status
|
|
516
|
+
*/
|
|
517
|
+
getStatus(): ConnectionStatus {
|
|
518
|
+
return { ...this.currentStatus };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get list of supported wallets
|
|
523
|
+
*/
|
|
524
|
+
getSupportedWallets(): WalletDefinition[] {
|
|
525
|
+
return SUPPORTED_WALLETS;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get current wallet being used
|
|
530
|
+
*/
|
|
531
|
+
getCurrentWallet(): WalletDefinition {
|
|
532
|
+
return this.currentWallet;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Check if a wallet is available on the current platform
|
|
537
|
+
*/
|
|
538
|
+
async isWalletAvailable(walletName?: string): Promise<boolean> {
|
|
539
|
+
const wallet = walletName ? getWalletByName(walletName) : this.currentWallet;
|
|
540
|
+
if (!wallet) return false;
|
|
541
|
+
|
|
542
|
+
const isWeb = this.adapter.constructor.name === 'WebAdapter';
|
|
543
|
+
if (isWeb) {
|
|
544
|
+
return wallet.platforms.includes('web') || !!wallet.universalLink;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// eslint-disable-next-line no-undef
|
|
548
|
+
const platform = typeof globalThis !== 'undefined' && (globalThis as any).Platform
|
|
549
|
+
? (globalThis as any).Platform.OS === 'ios' ? 'ios' : 'android'
|
|
550
|
+
: 'android';
|
|
551
|
+
|
|
552
|
+
return wallet.platforms.includes(platform);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Set preferred wallet
|
|
557
|
+
*/
|
|
558
|
+
setPreferredWallet(walletName: string): void {
|
|
559
|
+
const wallet = getWalletByName(walletName);
|
|
560
|
+
if (!wallet) {
|
|
561
|
+
throw new TonConnectError(
|
|
562
|
+
`Wallet "${walletName}" not found. Available: ${SUPPORTED_WALLETS.map((w) => w.name).join(', ')}`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
this.currentWallet = wallet;
|
|
566
|
+
console.log('[TON Connect] Preferred wallet changed to:', wallet.name);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Subscribe to status changes
|
|
571
|
+
*/
|
|
572
|
+
onStatusChange(callback: StatusChangeCallback): () => void {
|
|
573
|
+
this.statusChangeCallbacks.add(callback);
|
|
574
|
+
callback(this.getStatus());
|
|
575
|
+
return () => {
|
|
576
|
+
this.statusChangeCallbacks.delete(callback);
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private notifyStatusChange(): void {
|
|
581
|
+
const status = this.getStatus();
|
|
582
|
+
this.statusChangeCallbacks.forEach((cb) => {
|
|
583
|
+
try { cb(status); } catch { /* ignore */ }
|
|
584
|
+
});
|
|
585
|
+
this.emit('statusChange', status);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private emit<T>(event: TonConnectEventType, data: T): void {
|
|
589
|
+
const listeners = this.eventListeners.get(event);
|
|
590
|
+
if (listeners) {
|
|
591
|
+
listeners.forEach((listener) => {
|
|
592
|
+
try { listener(data); } catch { /* ignore */ }
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
on<T = any>(event: TonConnectEventType, listener: TonConnectEventListener<T>): () => void {
|
|
598
|
+
if (!this.eventListeners.has(event)) {
|
|
599
|
+
this.eventListeners.set(event, new Set());
|
|
600
|
+
}
|
|
601
|
+
this.eventListeners.get(event)!.add(listener);
|
|
602
|
+
return () => {
|
|
603
|
+
const listeners = this.eventListeners.get(event);
|
|
604
|
+
if (listeners) listeners.delete(listener);
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
off<T = any>(event: TonConnectEventType, listener: TonConnectEventListener<T>): void {
|
|
609
|
+
const listeners = this.eventListeners.get(event);
|
|
610
|
+
if (listeners) listeners.delete(listener);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
removeAllListeners(event?: TonConnectEventType): void {
|
|
614
|
+
if (event) {
|
|
615
|
+
this.eventListeners.delete(event);
|
|
616
|
+
} else {
|
|
617
|
+
this.eventListeners.clear();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Cleanup resources
|
|
623
|
+
*/
|
|
624
|
+
destroy(): void {
|
|
625
|
+
this.bridge.close();
|
|
626
|
+
this.statusChangeCallbacks.clear();
|
|
627
|
+
this.eventListeners.clear();
|
|
628
|
+
this.connectionPromise = null;
|
|
629
|
+
this.pendingRpcRequests.forEach((req) => {
|
|
630
|
+
if (req.timeout) clearTimeout(req.timeout);
|
|
631
|
+
});
|
|
632
|
+
this.pendingRpcRequests.clear();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
getNetwork(): Network {
|
|
636
|
+
return this.config.network;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
setNetwork(network: Network): void {
|
|
640
|
+
if (network !== 'mainnet' && network !== 'testnet') {
|
|
641
|
+
throw new TonConnectError('Network must be "mainnet" or "testnet"');
|
|
642
|
+
}
|
|
643
|
+
const oldNetwork = this.config.network;
|
|
644
|
+
this.config.network = network;
|
|
645
|
+
|
|
646
|
+
if (!this.config.tonApiEndpoint || this.config.tonApiEndpoint.includes(oldNetwork)) {
|
|
647
|
+
this.config.tonApiEndpoint =
|
|
648
|
+
network === 'testnet'
|
|
649
|
+
? 'https://testnet.toncenter.com/api/v2'
|
|
650
|
+
: 'https://toncenter.com/api/v2';
|
|
651
|
+
}
|
|
652
|
+
this.notifyStatusChange();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Get wallet balance
|
|
657
|
+
*/
|
|
658
|
+
async getBalance(address?: string): Promise<BalanceResponse> {
|
|
659
|
+
const targetAddress = address || this.currentStatus.wallet?.address;
|
|
660
|
+
if (!targetAddress) {
|
|
661
|
+
throw new TonConnectError('Address required. Connect a wallet or provide an address.');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const apiEndpoint = this.config.tonApiEndpoint ||
|
|
665
|
+
(this.config.network === 'testnet'
|
|
666
|
+
? 'https://testnet.toncenter.com/api/v2'
|
|
667
|
+
: 'https://toncenter.com/api/v2');
|
|
668
|
+
|
|
669
|
+
const url = `${apiEndpoint}/getAddressInformation?address=${encodeURIComponent(targetAddress)}`;
|
|
670
|
+
|
|
671
|
+
const response = await fetch(url, {
|
|
672
|
+
method: 'GET',
|
|
673
|
+
headers: { 'Accept': 'application/json' },
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
if (!response.ok) {
|
|
677
|
+
throw new TonConnectError(`Failed to fetch balance: ${response.status}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const data = await response.json();
|
|
681
|
+
if (data.ok === false) {
|
|
682
|
+
throw new TonConnectError(data.error || 'Failed to fetch balance');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const balance = data.result?.balance || '0';
|
|
686
|
+
const balanceTon =
|
|
687
|
+
(BigInt(balance) / BigInt(1000000000)).toString() +
|
|
688
|
+
'.' +
|
|
689
|
+
(BigInt(balance) % BigInt(1000000000)).toString().padStart(9, '0').replace(/0+$/, '');
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
balance,
|
|
693
|
+
balanceTon: balanceTon === '0.' ? '0' : balanceTon,
|
|
694
|
+
network: this.config.network,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Get transaction status by hash
|
|
700
|
+
*/
|
|
701
|
+
async getTransactionStatusByHash(txHash: string, address: string): Promise<TransactionStatusResponse> {
|
|
702
|
+
if (!txHash) throw new TonConnectError('Transaction hash is required');
|
|
703
|
+
if (!address) throw new TonConnectError('Address is required');
|
|
704
|
+
|
|
705
|
+
const apiEndpoint = this.config.tonApiEndpoint ||
|
|
706
|
+
(this.config.network === 'testnet'
|
|
707
|
+
? 'https://testnet.toncenter.com/api/v2'
|
|
708
|
+
: 'https://toncenter.com/api/v2');
|
|
709
|
+
|
|
710
|
+
const url = `${apiEndpoint}/getTransactions?address=${encodeURIComponent(address)}&limit=100`;
|
|
711
|
+
|
|
712
|
+
const response = await fetch(url, {
|
|
713
|
+
method: 'GET',
|
|
714
|
+
headers: { 'Accept': 'application/json' },
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
if (!response.ok) {
|
|
718
|
+
throw new TonConnectError(`Failed to fetch transactions: ${response.status}`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const data = await response.json();
|
|
722
|
+
if (data.ok === false) {
|
|
723
|
+
throw new TonConnectError(data.error || 'Failed to fetch transactions');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const transactions = data.result || [];
|
|
727
|
+
const transaction = transactions.find((tx: any) =>
|
|
728
|
+
tx.transaction_id?.hash === txHash ||
|
|
729
|
+
tx.transaction_id?.lt === txHash
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
if (transaction) {
|
|
733
|
+
return {
|
|
734
|
+
status: 'confirmed',
|
|
735
|
+
hash: transaction.transaction_id?.hash || txHash,
|
|
736
|
+
blockNumber: transaction.transaction_id?.lt,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return { status: 'pending', hash: txHash };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ─── Session Persistence ───
|
|
744
|
+
|
|
745
|
+
private async saveSession(wallet: WalletInfo): Promise<void> {
|
|
746
|
+
if (!this.session || !this.walletBridgePublicKey) return;
|
|
747
|
+
|
|
748
|
+
const sessionData: PersistedSession = {
|
|
749
|
+
sessionSecretKey: bytesToHex(this.session.secretKey),
|
|
750
|
+
walletPublicKey: this.walletBridgePublicKey,
|
|
751
|
+
bridgeUrl: this.currentWallet.bridgeUrl,
|
|
752
|
+
wallet,
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const key = `${this.config.storageKeyPrefix}session_v2`;
|
|
756
|
+
await this.adapter.setItem(key, JSON.stringify(sessionData));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private async loadSession(): Promise<void> {
|
|
760
|
+
try {
|
|
761
|
+
const key = `${this.config.storageKeyPrefix}session_v2`;
|
|
762
|
+
const json = await this.adapter.getItem(key);
|
|
763
|
+
if (!json) return;
|
|
764
|
+
|
|
765
|
+
const data = JSON.parse(json) as PersistedSession;
|
|
766
|
+
if (!data.sessionSecretKey || !data.walletPublicKey || !data.wallet) {
|
|
767
|
+
await this.clearSession();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Restore session
|
|
772
|
+
this.session = SessionCrypto.fromState({ secretKey: data.sessionSecretKey });
|
|
773
|
+
this.walletBridgePublicKey = data.walletPublicKey;
|
|
774
|
+
|
|
775
|
+
// Reconnect to bridge
|
|
776
|
+
this.bridge.connect(
|
|
777
|
+
data.bridgeUrl,
|
|
778
|
+
this.session.sessionId,
|
|
779
|
+
(msg) => this.handleBridgeMessage(msg),
|
|
780
|
+
(error) => console.error('[TON Connect] Bridge error:', error)
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// Restore status
|
|
784
|
+
this.currentStatus = { connected: true, wallet: data.wallet };
|
|
785
|
+
this.notifyStatusChange();
|
|
786
|
+
|
|
787
|
+
console.log('[TON Connect] Session restored for wallet:', data.wallet.name);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
console.error('[TON Connect] Failed to load session:', error);
|
|
790
|
+
await this.clearSession();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private async clearSession(): Promise<void> {
|
|
795
|
+
try {
|
|
796
|
+
const key = `${this.config.storageKeyPrefix}session_v2`;
|
|
797
|
+
await this.adapter.removeItem(key);
|
|
798
|
+
} catch {
|
|
799
|
+
// Ignore
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Export types
|
|
805
|
+
export * from './types';
|
|
806
|
+
export type { WalletDefinition } from './core/wallets';
|
|
807
|
+
export { SUPPORTED_WALLETS, getWalletByName, getDefaultWallet, getWalletsForPlatform } from './core/wallets';
|
|
808
|
+
|
|
809
|
+
// Export utilities
|
|
810
|
+
export * from './utils/transactionBuilder';
|
|
811
|
+
export * from './utils/retry';
|