@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/src/index.ts CHANGED
@@ -1,1338 +1,811 @@
1
- /**
2
- * TON Connect Mobile SDK
3
- * Production-ready implementation for React Native and Expo
4
- */
5
-
6
- // Type declarations for runtime globals (imported from index.d.ts via reference)
7
- /// <reference path="./index.d.ts" />
8
-
9
- import {
10
- TonConnectMobileConfig,
11
- ConnectionStatus,
12
- WalletInfo,
13
- SendTransactionRequest,
14
- StatusChangeCallback,
15
- PlatformAdapter,
16
- Network,
17
- TonConnectEventType,
18
- TonConnectEventListener,
19
- TransactionStatus,
20
- TransactionStatusResponse,
21
- BalanceResponse,
22
- } from './types';
23
- import {
24
- buildConnectionRequest,
25
- buildTransactionRequest,
26
- parseCallbackURL,
27
- extractWalletInfo,
28
- validateConnectionResponse,
29
- validateTransactionRequest,
30
- validateTransactionResponse,
31
- decodeBase64URL,
32
- } from './core/protocol';
33
- import type {
34
- ConnectionResponsePayload,
35
- TransactionResponsePayload,
36
- ErrorResponse,
37
- ConnectionRequestPayload,
38
- } from './types';
39
- import { verifyConnectionProof, generateSessionId } from './core/crypto';
40
- import { ExpoAdapter } from './adapters/expo';
41
- import { ReactNativeAdapter } from './adapters/react-native';
42
- import { WebAdapter } from './adapters/web';
43
- import { getWalletByName, getDefaultWallet, SUPPORTED_WALLETS, type WalletDefinition } from './core/wallets';
44
-
45
- /**
46
- * Custom error classes
47
- */
48
- export class TonConnectError extends Error {
49
- constructor(message: string, public code?: string, public recoverySuggestion?: string) {
50
- super(message);
51
- this.name = 'TonConnectError';
52
- }
53
- }
54
-
55
- export class ConnectionTimeoutError extends TonConnectError {
56
- constructor() {
57
- super(
58
- 'Connection request timed out. The wallet did not respond in time.',
59
- 'CONNECTION_TIMEOUT',
60
- 'Please make sure the wallet app is installed and try again. If the issue persists, check your internet connection.'
61
- );
62
- this.name = 'ConnectionTimeoutError';
63
- }
64
- }
65
-
66
- export class TransactionTimeoutError extends TonConnectError {
67
- constructor() {
68
- super(
69
- 'Transaction request timed out. The wallet did not respond in time.',
70
- 'TRANSACTION_TIMEOUT',
71
- 'Please check the wallet app and try again. Make sure you approve or reject the transaction in the wallet.'
72
- );
73
- this.name = 'TransactionTimeoutError';
74
- }
75
- }
76
-
77
- export class UserRejectedError extends TonConnectError {
78
- constructor(message?: string) {
79
- super(
80
- message || 'User rejected the request',
81
- 'USER_REJECTED',
82
- 'The user cancelled the operation in the wallet app.'
83
- );
84
- this.name = 'UserRejectedError';
85
- }
86
- }
87
-
88
- export class ConnectionInProgressError extends TonConnectError {
89
- constructor() {
90
- super(
91
- 'Connection request already in progress',
92
- 'CONNECTION_IN_PROGRESS',
93
- 'Please wait for the current connection attempt to complete before trying again.'
94
- );
95
- this.name = 'ConnectionInProgressError';
96
- }
97
- }
98
-
99
- export class TransactionInProgressError extends TonConnectError {
100
- constructor() {
101
- super(
102
- 'Transaction request already in progress',
103
- 'TRANSACTION_IN_PROGRESS',
104
- 'Please wait for the current transaction to complete before sending another one.'
105
- );
106
- this.name = 'TransactionInProgressError';
107
- }
108
- }
109
-
110
- /**
111
- * Main TON Connect Mobile SDK class
112
- */
113
- export class TonConnectMobile {
114
- private adapter: PlatformAdapter;
115
- private config: Required<Omit<TonConnectMobileConfig, 'preferredWallet' | 'network' | 'tonApiEndpoint'>> & {
116
- preferredWallet?: string;
117
- network: Network;
118
- tonApiEndpoint?: string;
119
- };
120
- private statusChangeCallbacks: Set<StatusChangeCallback> = new Set();
121
- private eventListeners: Map<TonConnectEventType, Set<TonConnectEventListener>> = new Map();
122
- private currentStatus: ConnectionStatus = { connected: false, wallet: null };
123
- private urlUnsubscribe: (() => void) | null = null;
124
- private currentWallet!: WalletDefinition;
125
- private connectionPromise: {
126
- resolve: (wallet: WalletInfo) => void;
127
- reject: (error: Error) => void;
128
- timeout: number | null;
129
- } | null = null;
130
- private transactionPromise: {
131
- resolve: (response: { boc: string; signature: string }) => void;
132
- reject: (error: Error) => void;
133
- timeout: number | null;
134
- } | null = null;
135
- private signDataPromise: {
136
- resolve: (response: { signature: string; timestamp: number }) => void;
137
- reject: (error: Error) => void;
138
- timeout: number | null;
139
- } | null = null;
140
-
141
- constructor(config: TonConnectMobileConfig) {
142
- // Validate config
143
- if (!config.manifestUrl) {
144
- throw new TonConnectError('manifestUrl is required');
145
- }
146
- if (!config.scheme) {
147
- throw new TonConnectError('scheme is required');
148
- }
149
-
150
- // Validate network
151
- const network = config.network || 'mainnet';
152
- if (network !== 'mainnet' && network !== 'testnet') {
153
- throw new TonConnectError('Network must be either "mainnet" or "testnet"');
154
- }
155
-
156
- // Set default TON API endpoint based on network
157
- const defaultTonApiEndpoint =
158
- network === 'testnet'
159
- ? 'https://testnet.toncenter.com/api/v2'
160
- : 'https://toncenter.com/api/v2';
161
-
162
- this.config = {
163
- storageKeyPrefix: 'tonconnect_',
164
- connectionTimeout: 300000, // 5 minutes
165
- transactionTimeout: 300000, // 5 minutes
166
- skipCanOpenURLCheck: true, // Skip canOpenURL check by default (Android issue)
167
- preferredWallet: config.preferredWallet,
168
- network,
169
- tonApiEndpoint: config.tonApiEndpoint || defaultTonApiEndpoint,
170
- ...config,
171
- } as Required<Omit<TonConnectMobileConfig, 'preferredWallet' | 'network' | 'tonApiEndpoint'>> & {
172
- preferredWallet?: string;
173
- network: Network;
174
- tonApiEndpoint?: string;
175
- };
176
-
177
- // Determine which wallet to use
178
- if (this.config.preferredWallet) {
179
- const wallet = getWalletByName(this.config.preferredWallet);
180
- if (wallet) {
181
- this.currentWallet = wallet;
182
- console.log('[TON Connect] Using preferred wallet:', wallet.name);
183
- } else {
184
- console.warn('[TON Connect] Preferred wallet not found, using default');
185
- this.currentWallet = getDefaultWallet();
186
- }
187
- } else {
188
- this.currentWallet = getDefaultWallet();
189
- }
190
-
191
- console.log('[TON Connect] Initializing SDK with config:', {
192
- manifestUrl: this.config.manifestUrl,
193
- scheme: this.config.scheme,
194
- network: this.config.network,
195
- wallet: this.currentWallet.name,
196
- universalLink: this.currentWallet.universalLink,
197
- });
198
-
199
- // Initialize platform adapter
200
- this.adapter = this.createAdapter();
201
- console.log('[TON Connect] Adapter initialized:', this.adapter.constructor.name);
202
-
203
- // Set up URL listener
204
- this.setupURLListener();
205
-
206
- // Load persisted session
207
- this.loadSession();
208
- }
209
-
210
- /**
211
- * Create platform adapter based on available modules
212
- */
213
- private createAdapter(): PlatformAdapter {
214
- // Check if we're in a web environment
215
- // eslint-disable-next-line no-undef
216
- if (typeof globalThis !== 'undefined' && (globalThis as any).window && (globalThis as any).document) {
217
- // Web platform
218
- console.log('[TON Connect] Using WebAdapter');
219
- return new WebAdapter();
220
- }
221
-
222
- // Try to detect Expo environment
223
- try {
224
- // Check if expo-linking is available
225
- if (typeof require !== 'undefined') {
226
- const expoLinking = require('expo-linking');
227
- if (expoLinking) {
228
- console.log('[TON Connect] Using ExpoAdapter');
229
- return new ExpoAdapter();
230
- }
231
- }
232
- } catch (error) {
233
- console.log('[TON Connect] ExpoAdapter not available:', error);
234
- // expo-linking not available, continue to React Native adapter
235
- }
236
-
237
- // Fall back to React Native adapter
238
- // This will work for both React Native CLI and Expo (since Expo also has react-native)
239
- console.log('[TON Connect] Using ReactNativeAdapter');
240
- return new ReactNativeAdapter();
241
- }
242
-
243
- /**
244
- * Set up URL listener for wallet callbacks
245
- */
246
- private setupURLListener(): void {
247
- console.log('[TON Connect] Setting up URL listener...');
248
- this.urlUnsubscribe = this.adapter.addURLListener((url) => {
249
- console.log('[TON Connect] URL callback received:', url);
250
- this.handleCallback(url);
251
- });
252
-
253
- // Also check initial URL (when app was opened via deep link)
254
- this.adapter.getInitialURL().then((url) => {
255
- if (url) {
256
- console.log('[TON Connect] Initial URL found:', url);
257
- this.handleCallback(url);
258
- } else {
259
- console.log('[TON Connect] No initial URL');
260
- }
261
- });
262
- }
263
-
264
- /**
265
- * Handle callback from wallet
266
- */
267
- private handleCallback(url: string): void {
268
- console.log('[TON Connect] handleCallback called with URL:', url);
269
- console.log('[TON Connect] Expected scheme:', this.config.scheme);
270
- console.log('[TON Connect] URL starts with scheme?', url?.startsWith(`${this.config.scheme}://`));
271
-
272
- // CRITICAL FIX: Check if URL matches our scheme
273
- if (!url || typeof url !== 'string') {
274
- console.log('[TON Connect] Invalid URL, ignoring:', url);
275
- return;
276
- }
277
-
278
- if (!url.startsWith(`${this.config.scheme}://`)) {
279
- console.log('[TON Connect] Callback URL does not match scheme, ignoring:', url);
280
- console.log('[TON Connect] Expected prefix:', `${this.config.scheme}://`);
281
- return;
282
- }
283
-
284
- const parsed = parseCallbackURL(url, this.config.scheme);
285
- console.log('[TON Connect] Parsed callback:', parsed.type, parsed.data ? 'has data' : 'no data');
286
-
287
- // CRITICAL FIX: Check for sign data response first (before other handlers)
288
- // Note: We check if promise exists and hasn't timed out (timeout !== null means not timed out yet)
289
- if (this.signDataPromise && this.signDataPromise.timeout !== null) {
290
- // Sign data request is pending and hasn't timed out
291
- if (parsed.type === 'error' && parsed.data) {
292
- const errorData = parsed.data as ErrorResponse;
293
- if (errorData?.error) {
294
- const promise = this.signDataPromise;
295
- this.signDataPromise = null;
296
- if (errorData.error.code === 300) {
297
- promise.reject(new UserRejectedError());
298
- } else {
299
- promise.reject(new TonConnectError(errorData.error.message || 'Sign data failed'));
300
- }
301
- return;
302
- }
303
- }
304
-
305
- // Check for sign data response format
306
- // Note: TON Connect protocol may return sign data in different format
307
- // We check for signature field in the response
308
- if (parsed.data && typeof parsed.data === 'object') {
309
- const data = parsed.data as any;
310
- if (data.signature && typeof data.signature === 'string') {
311
- const promise = this.signDataPromise;
312
- this.signDataPromise = null;
313
- promise.resolve({
314
- signature: data.signature,
315
- timestamp: data.timestamp || Date.now(),
316
- });
317
- return;
318
- }
319
- }
320
- }
321
-
322
- // Handle connection responses
323
- if (parsed.type === 'connect' && parsed.data) {
324
- this.handleConnectionResponse(parsed.data as ConnectionResponsePayload);
325
- } else if (parsed.type === 'transaction' && parsed.data) {
326
- this.handleTransactionResponse(parsed.data as TransactionResponsePayload);
327
- } else if (parsed.type === 'error' && parsed.data) {
328
- const errorData = parsed.data as ErrorResponse;
329
- if (errorData?.error) {
330
- if (errorData.error.code === 300 || errorData.error.message?.toLowerCase().includes('reject')) {
331
- this.rejectWithError(new UserRejectedError());
332
- } else {
333
- this.rejectWithError(
334
- new TonConnectError(errorData.error.message || 'Unknown error', String(errorData.error.code))
335
- );
336
- }
337
- }
338
- }
339
- }
340
-
341
- /**
342
- * Handle connection response from wallet
343
- */
344
- private handleConnectionResponse(response: ConnectionResponsePayload): void {
345
- if (!validateConnectionResponse(response)) {
346
- this.rejectWithError(new TonConnectError('Invalid connection response'));
347
- return;
348
- }
349
-
350
- // Verify proof if present
351
- if (response.proof) {
352
- const isValid = verifyConnectionProof(response, this.config.manifestUrl);
353
- if (!isValid) {
354
- this.rejectWithError(new TonConnectError('Connection proof verification failed'));
355
- return;
356
- }
357
- } else {
358
- // Log warning if proof is missing (security consideration)
359
- console.warn('TON Connect: Connection proof missing - wallet may not support proof verification');
360
- }
361
-
362
- const wallet = extractWalletInfo(response);
363
-
364
- // Validate session ID before saving
365
- if (!this.validateSessionId(response.session)) {
366
- this.rejectWithError(new TonConnectError('Invalid session ID format'));
367
- return;
368
- }
369
-
370
- // Save session
371
- this.saveSession(response.session, wallet).catch((error) => {
372
- console.error('TON Connect: Failed to save session:', error);
373
- // Continue anyway - connection is still valid
374
- });
375
-
376
- // Update status
377
- this.currentStatus = { connected: true, wallet };
378
- this.notifyStatusChange();
379
-
380
- // Emit connect event
381
- this.emit('connect', wallet);
382
-
383
- // Resolve connection promise
384
- // CRITICAL: Only resolve if promise still exists and hasn't timed out
385
- if (this.connectionPromise) {
386
- // Clear timeout if it exists
387
- if (this.connectionPromise.timeout !== null) {
388
- clearTimeout(this.connectionPromise.timeout);
389
- }
390
- // Store reference before clearing to prevent race conditions
391
- const promise = this.connectionPromise;
392
- // Clear promise first
393
- this.connectionPromise = null;
394
- // Then resolve
395
- promise.resolve(wallet);
396
- }
397
- }
398
-
399
- /**
400
- * Handle transaction response from wallet
401
- */
402
- private handleTransactionResponse(response: TransactionResponsePayload): void {
403
- if (!validateTransactionResponse(response)) {
404
- this.rejectWithError(new TonConnectError('Invalid transaction response'));
405
- return;
406
- }
407
-
408
- const transactionResult = {
409
- boc: response.boc,
410
- signature: response.signature,
411
- };
412
-
413
- // Emit transaction event
414
- this.emit('transaction', transactionResult);
415
-
416
- // Resolve transaction promise
417
- // CRITICAL: Only resolve if promise still exists and hasn't timed out
418
- if (this.transactionPromise) {
419
- // Clear timeout if it exists
420
- if (this.transactionPromise.timeout !== null) {
421
- clearTimeout(this.transactionPromise.timeout);
422
- }
423
- // Store reference before clearing
424
- const promise = this.transactionPromise;
425
- // Clear promise first to prevent race conditions
426
- this.transactionPromise = null;
427
- // Then resolve
428
- promise.resolve(transactionResult);
429
- }
430
- }
431
-
432
- /**
433
- * Reject current promise with error
434
- */
435
- private rejectWithError(error: Error): void {
436
- // Emit error event
437
- this.emit('error', error);
438
-
439
- if (this.connectionPromise) {
440
- if (this.connectionPromise.timeout !== null) {
441
- clearTimeout(this.connectionPromise.timeout);
442
- }
443
- this.connectionPromise.reject(error);
444
- this.connectionPromise = null;
445
- }
446
- if (this.transactionPromise) {
447
- if (this.transactionPromise.timeout !== null) {
448
- clearTimeout(this.transactionPromise.timeout);
449
- }
450
- this.transactionPromise.reject(error);
451
- this.transactionPromise = null;
452
- }
453
- // CRITICAL FIX: Also clear signDataPromise to prevent memory leaks
454
- if (this.signDataPromise) {
455
- if (this.signDataPromise.timeout !== null) {
456
- clearTimeout(this.signDataPromise.timeout);
457
- }
458
- this.signDataPromise.reject(error);
459
- this.signDataPromise = null;
460
- }
461
- }
462
-
463
- /**
464
- * Connect to wallet
465
- */
466
- async connect(): Promise<WalletInfo> {
467
- console.log('[TON Connect] connect() called');
468
-
469
- // If already connected, return current wallet
470
- if (this.currentStatus.connected && this.currentStatus.wallet) {
471
- console.log('[TON Connect] Already connected, returning existing wallet');
472
- return this.currentStatus.wallet;
473
- }
474
-
475
- // CRITICAL FIX: Check if connection is already in progress
476
- if (this.connectionPromise) {
477
- console.log('[TON Connect] Connection already in progress');
478
- throw new ConnectionInProgressError();
479
- }
480
-
481
- // Build connection request URL (use wallet's universal link)
482
- console.log('[TON Connect] Building connection request URL for wallet:', this.currentWallet.name);
483
- console.log('[TON Connect] Using universal link:', this.currentWallet.universalLink);
484
- console.log('[TON Connect] Wallet return strategy:', this.currentWallet.preferredReturnStrategy || 'back');
485
- console.log('[TON Connect] Wallet requires returnScheme:', this.currentWallet.requiresReturnScheme !== false);
486
-
487
- const url = buildConnectionRequest(
488
- this.config.manifestUrl,
489
- this.config.scheme,
490
- this.currentWallet.universalLink,
491
- this.currentWallet.preferredReturnStrategy,
492
- this.currentWallet.requiresReturnScheme
493
- );
494
-
495
- // DEBUG: Decode and log the payload for debugging
496
- try {
497
- const urlParts = url.split('?');
498
- if (urlParts.length > 1) {
499
- const payload = urlParts[1];
500
- // CRITICAL FIX: Handle URL encoding - payload might have additional encoding
501
- const cleanPayload = decodeURIComponent(payload);
502
- const decoded = decodeBase64URL<ConnectionRequestPayload>(cleanPayload);
503
- console.log('[TON Connect] Connection request payload:', JSON.stringify(decoded, null, 2));
504
- }
505
- } catch (e: any) {
506
- // Log decode errors for debugging but don't fail
507
- console.log('[TON Connect] Could not decode payload for logging:', e?.message || e);
508
- // This is just for logging, the actual URL is correct
509
- }
510
-
511
- console.log('[TON Connect] Built URL:', url.substring(0, 100) + '...');
512
- console.log('[TON Connect] Full URL:', url);
513
- console.log('[TON Connect] Manifest URL:', this.config.manifestUrl);
514
- console.log('[TON Connect] Return scheme:', this.config.scheme);
515
- console.log('[TON Connect] Adapter type:', this.adapter.constructor.name);
516
-
517
- // Create promise for connection
518
- return new Promise<WalletInfo>((resolve, reject) => {
519
- let timeout: number | null = null;
520
-
521
- this.connectionPromise = {
522
- resolve: (wallet: WalletInfo) => {
523
- if (timeout !== null) {
524
- clearTimeout(timeout);
525
- }
526
- this.connectionPromise = null;
527
- resolve(wallet);
528
- },
529
- reject: (error: Error) => {
530
- if (timeout !== null) {
531
- clearTimeout(timeout);
532
- }
533
- this.connectionPromise = null;
534
- reject(error);
535
- },
536
- timeout: null,
537
- };
538
-
539
- // Set timeout
540
- timeout = setTimeout(() => {
541
- if (this.connectionPromise) {
542
- console.log('[TON Connect] Connection timeout after', this.config.connectionTimeout, 'ms');
543
- this.connectionPromise.reject(new ConnectionTimeoutError());
544
- }
545
- }, this.config.connectionTimeout) as unknown as number;
546
-
547
- this.connectionPromise.timeout = timeout;
548
-
549
- // Open wallet app
550
- console.log('[TON Connect] Attempting to open wallet app...');
551
- this.adapter.openURL(url, this.config.skipCanOpenURLCheck).then((success) => {
552
- console.log('[TON Connect] openURL result:', success);
553
- // URL opened successfully, wait for callback
554
- // If success is false, it should have thrown an error
555
- if (!success && this.connectionPromise) {
556
- console.log('[TON Connect] openURL returned false, rejecting promise');
557
- this.connectionPromise.reject(
558
- new TonConnectError('Failed to open wallet app. Please make sure a TON wallet is installed.')
559
- );
560
- } else {
561
- console.log('[TON Connect] URL opened successfully, waiting for wallet callback...');
562
- }
563
- }).catch((error) => {
564
- // Error opening URL - reject the promise
565
- console.error('[TON Connect] Error opening URL:', error);
566
- if (this.connectionPromise) {
567
- this.connectionPromise.reject(
568
- new TonConnectError(`Failed to open wallet: ${error?.message || String(error)}`)
569
- );
570
- }
571
- });
572
- });
573
- }
574
-
575
- /**
576
- * Send transaction
577
- */
578
- async sendTransaction(request: SendTransactionRequest): Promise<{ boc: string; signature: string }> {
579
- // Validate request
580
- const validation = validateTransactionRequest(request);
581
- if (!validation.valid) {
582
- throw new TonConnectError(validation.error || 'Invalid transaction request');
583
- }
584
-
585
- // Check if connected
586
- if (!this.currentStatus.connected || !this.currentStatus.wallet) {
587
- throw new TonConnectError('Not connected to wallet. Call connect() first.');
588
- }
589
-
590
- // CRITICAL FIX: Check if transaction is already in progress
591
- if (this.transactionPromise) {
592
- throw new TransactionInProgressError();
593
- }
594
-
595
- // Build transaction request URL (use universal link for Android compatibility)
596
- const url = buildTransactionRequest(
597
- this.config.manifestUrl,
598
- request,
599
- this.config.scheme,
600
- this.currentWallet.universalLink,
601
- this.currentWallet.preferredReturnStrategy,
602
- this.currentWallet.requiresReturnScheme
603
- );
604
-
605
- // Create promise for transaction
606
- return new Promise<{ boc: string; signature: string }>((resolve, reject) => {
607
- let timeout: number | null = null;
608
-
609
- this.transactionPromise = {
610
- resolve: (response: { boc: string; signature: string }) => {
611
- if (timeout !== null) {
612
- clearTimeout(timeout);
613
- }
614
- this.transactionPromise = null;
615
- resolve(response);
616
- },
617
- reject: (error: Error) => {
618
- if (timeout !== null) {
619
- clearTimeout(timeout);
620
- }
621
- this.transactionPromise = null;
622
- reject(error);
623
- },
624
- timeout: null,
625
- };
626
-
627
- // Set timeout
628
- timeout = setTimeout(() => {
629
- if (this.transactionPromise) {
630
- this.transactionPromise.reject(new TransactionTimeoutError());
631
- }
632
- }, this.config.transactionTimeout) as unknown as number;
633
-
634
- this.transactionPromise.timeout = timeout;
635
-
636
- // Open wallet app
637
- this.adapter.openURL(url, this.config.skipCanOpenURLCheck).then((success) => {
638
- // URL opened successfully, wait for callback
639
- // If success is false, it should have thrown an error
640
- if (!success && this.transactionPromise) {
641
- this.transactionPromise.reject(
642
- new TonConnectError('Failed to open wallet app. Please make sure a TON wallet is installed.')
643
- );
644
- }
645
- }).catch((error) => {
646
- // Error opening URL - reject the promise
647
- if (this.transactionPromise) {
648
- this.transactionPromise.reject(
649
- new TonConnectError(`Failed to open wallet: ${error?.message || String(error)}`)
650
- );
651
- }
652
- });
653
- });
654
- }
655
-
656
- /**
657
- * Sign data (for authentication, etc.)
658
- * Note: Not all wallets support signData. This is a TON Connect extension.
659
- */
660
- async signData(data: string | Uint8Array, version: string = '1.0'): Promise<{ signature: string; timestamp: number }> {
661
- // Check if connected
662
- if (!this.currentStatus.connected || !this.currentStatus.wallet) {
663
- throw new TonConnectError('Not connected to wallet. Call connect() first.');
664
- }
665
-
666
- // Helper function to encode bytes to base64
667
- const base64EncodeBytes = (bytes: Uint8Array): string => {
668
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
669
- let result = '';
670
- let i = 0;
671
-
672
- while (i < bytes.length) {
673
- const a = bytes[i++];
674
- const b = i < bytes.length ? bytes[i++] : 0;
675
- const c = i < bytes.length ? bytes[i++] : 0;
676
-
677
- const bitmap = (a << 16) | (b << 8) | c;
678
-
679
- result += chars.charAt((bitmap >> 18) & 63);
680
- result += chars.charAt((bitmap >> 12) & 63);
681
- result += i - 2 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
682
- result += i - 1 < bytes.length ? chars.charAt(bitmap & 63) : '=';
683
- }
684
-
685
- return result;
686
- };
687
-
688
- // Helper function to get TextEncoder
689
- const getTextEncoder = (): { encode(input: string): Uint8Array } => {
690
- // eslint-disable-next-line no-undef
691
- if (typeof globalThis !== 'undefined' && (globalThis as any).TextEncoder) {
692
- // eslint-disable-next-line no-undef
693
- return new (globalThis as any).TextEncoder();
694
- }
695
- // Fallback: manual encoding
696
- return {
697
- encode(input: string): Uint8Array {
698
- const bytes = new Uint8Array(input.length);
699
- for (let i = 0; i < input.length; i++) {
700
- bytes[i] = input.charCodeAt(i);
701
- }
702
- return bytes;
703
- },
704
- };
705
- };
706
-
707
- // Convert data to base64
708
- let dataBase64: string;
709
- if (typeof data === 'string') {
710
- // Check if it's already base64
711
- const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
712
- if (base64Regex.test(data) && data.length % 4 === 0) {
713
- // Likely base64, use as-is
714
- dataBase64 = data;
715
- } else {
716
- // Not base64, encode it
717
- const encoder = getTextEncoder();
718
- const bytes = encoder.encode(data);
719
- dataBase64 = base64EncodeBytes(bytes);
720
- }
721
- } else {
722
- // Uint8Array - convert to base64
723
- dataBase64 = base64EncodeBytes(data);
724
- }
725
-
726
- // Build sign data request
727
- const payload = {
728
- manifestUrl: this.config.manifestUrl,
729
- data: dataBase64,
730
- version,
731
- returnStrategy: this.currentWallet.preferredReturnStrategy || 'back',
732
- returnScheme: this.currentWallet.requiresReturnScheme !== false ? this.config.scheme : undefined,
733
- };
734
-
735
- // Encode payload
736
- const { encodeBase64URL } = require('./core/protocol');
737
- const encoded = encodeBase64URL(payload);
738
-
739
- // Build URL
740
- const baseUrl = this.currentWallet.universalLink.endsWith('/ton-connect')
741
- ? this.currentWallet.universalLink
742
- : `${this.currentWallet.universalLink}/ton-connect`;
743
- const url = `${baseUrl}/sign-data?${encoded}`;
744
-
745
- // Open wallet app and wait for response
746
- return new Promise<{ signature: string; timestamp: number }>((resolve, reject) => {
747
- let timeout: number | null = null;
748
- let resolved = false;
749
-
750
- // CRITICAL FIX: Check if sign data is already in progress
751
- if (this.signDataPromise) {
752
- throw new TonConnectError('Sign data request already in progress');
753
- }
754
-
755
- // Create promise for sign data
756
- const signDataPromise = {
757
- resolve: (response: { signature: string; timestamp: number }) => {
758
- if (timeout !== null) {
759
- clearTimeout(timeout);
760
- }
761
- resolved = true;
762
- if (this.signDataPromise === signDataPromise) {
763
- this.signDataPromise = null;
764
- }
765
- resolve(response);
766
- },
767
- reject: (error: Error) => {
768
- if (timeout !== null) {
769
- clearTimeout(timeout);
770
- }
771
- resolved = true;
772
- if (this.signDataPromise === signDataPromise) {
773
- this.signDataPromise = null;
774
- }
775
- reject(error);
776
- },
777
- timeout: null as number | null,
778
- };
779
-
780
- // Set timeout
781
- timeout = setTimeout(() => {
782
- if (!resolved && this.signDataPromise === signDataPromise) {
783
- this.signDataPromise = null;
784
- signDataPromise.reject(new TonConnectError('Sign data request timed out'));
785
- }
786
- }, this.config.transactionTimeout) as unknown as number;
787
-
788
- signDataPromise.timeout = timeout;
789
-
790
- // Store promise for callback handling
791
- // CRITICAL FIX: Don't mutate handleCallback method - use a separate tracking mechanism
792
- this.signDataPromise = signDataPromise;
793
-
794
- // Open URL
795
- this.adapter.openURL(url, this.config.skipCanOpenURLCheck).then(() => {
796
- // URL opened, wait for callback
797
- // Callback will be handled by handleCallback method checking signDataPromise
798
- }).catch((error: Error) => {
799
- // Clear promise on error
800
- this.signDataPromise = null;
801
- signDataPromise.reject(new TonConnectError(`Failed to open wallet: ${error?.message || String(error)}`));
802
- });
803
- });
804
- }
805
-
806
- /**
807
- * Disconnect from wallet
808
- */
809
- async disconnect(): Promise<void> {
810
- // Clear session
811
- await this.clearSession();
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';