@blazium/ton-connect-mobile 1.2.0 → 1.2.3

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
@@ -13,6 +13,12 @@ import {
13
13
  SendTransactionRequest,
14
14
  StatusChangeCallback,
15
15
  PlatformAdapter,
16
+ Network,
17
+ TonConnectEventType,
18
+ TonConnectEventListener,
19
+ TransactionStatus,
20
+ TransactionStatusResponse,
21
+ BalanceResponse,
16
22
  } from './types';
17
23
  import {
18
24
  buildConnectionRequest,
@@ -81,14 +87,22 @@ export class UserRejectedError extends TonConnectError {
81
87
 
82
88
  export class ConnectionInProgressError extends TonConnectError {
83
89
  constructor() {
84
- super('Connection request already in progress', 'CONNECTION_IN_PROGRESS');
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
+ );
85
95
  this.name = 'ConnectionInProgressError';
86
96
  }
87
97
  }
88
98
 
89
99
  export class TransactionInProgressError extends TonConnectError {
90
100
  constructor() {
91
- super('Transaction request already in progress', 'TRANSACTION_IN_PROGRESS');
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
+ );
92
106
  this.name = 'TransactionInProgressError';
93
107
  }
94
108
  }
@@ -98,8 +112,13 @@ export class TransactionInProgressError extends TonConnectError {
98
112
  */
99
113
  export class TonConnectMobile {
100
114
  private adapter: PlatformAdapter;
101
- private config: Required<Omit<TonConnectMobileConfig, 'preferredWallet'>> & { preferredWallet?: string };
115
+ private config: Required<Omit<TonConnectMobileConfig, 'preferredWallet' | 'network' | 'tonApiEndpoint'>> & {
116
+ preferredWallet?: string;
117
+ network: Network;
118
+ tonApiEndpoint?: string;
119
+ };
102
120
  private statusChangeCallbacks: Set<StatusChangeCallback> = new Set();
121
+ private eventListeners: Map<TonConnectEventType, Set<TonConnectEventListener>> = new Map();
103
122
  private currentStatus: ConnectionStatus = { connected: false, wallet: null };
104
123
  private urlUnsubscribe: (() => void) | null = null;
105
124
  private currentWallet!: WalletDefinition;
@@ -128,14 +147,32 @@ export class TonConnectMobile {
128
147
  throw new TonConnectError('scheme is required');
129
148
  }
130
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
+
131
162
  this.config = {
132
163
  storageKeyPrefix: 'tonconnect_',
133
164
  connectionTimeout: 300000, // 5 minutes
134
165
  transactionTimeout: 300000, // 5 minutes
135
166
  skipCanOpenURLCheck: true, // Skip canOpenURL check by default (Android issue)
136
167
  preferredWallet: config.preferredWallet,
168
+ network,
169
+ tonApiEndpoint: config.tonApiEndpoint || defaultTonApiEndpoint,
137
170
  ...config,
138
- } as Required<Omit<TonConnectMobileConfig, 'preferredWallet'>> & { preferredWallet?: string };
171
+ } as Required<Omit<TonConnectMobileConfig, 'preferredWallet' | 'network' | 'tonApiEndpoint'>> & {
172
+ preferredWallet?: string;
173
+ network: Network;
174
+ tonApiEndpoint?: string;
175
+ };
139
176
 
140
177
  // Determine which wallet to use
141
178
  if (this.config.preferredWallet) {
@@ -154,6 +191,7 @@ export class TonConnectMobile {
154
191
  console.log('[TON Connect] Initializing SDK with config:', {
155
192
  manifestUrl: this.config.manifestUrl,
156
193
  scheme: this.config.scheme,
194
+ network: this.config.network,
157
195
  wallet: this.currentWallet.name,
158
196
  universalLink: this.currentWallet.universalLink,
159
197
  });
@@ -247,8 +285,9 @@ export class TonConnectMobile {
247
285
  console.log('[TON Connect] Parsed callback:', parsed.type, parsed.data ? 'has data' : 'no data');
248
286
 
249
287
  // CRITICAL FIX: Check for sign data response first (before other handlers)
250
- if (this.signDataPromise && !this.signDataPromise.timeout) {
251
- // Sign data request is pending
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
252
291
  if (parsed.type === 'error' && parsed.data) {
253
292
  const errorData = parsed.data as ErrorResponse;
254
293
  if (errorData?.error) {
@@ -338,13 +377,22 @@ export class TonConnectMobile {
338
377
  this.currentStatus = { connected: true, wallet };
339
378
  this.notifyStatusChange();
340
379
 
380
+ // Emit connect event
381
+ this.emit('connect', wallet);
382
+
341
383
  // Resolve connection promise
384
+ // CRITICAL: Only resolve if promise still exists and hasn't timed out
342
385
  if (this.connectionPromise) {
386
+ // Clear timeout if it exists
343
387
  if (this.connectionPromise.timeout !== null) {
344
388
  clearTimeout(this.connectionPromise.timeout);
345
389
  }
346
- this.connectionPromise.resolve(wallet);
390
+ // Store reference before clearing to prevent race conditions
391
+ const promise = this.connectionPromise;
392
+ // Clear promise first
347
393
  this.connectionPromise = null;
394
+ // Then resolve
395
+ promise.resolve(wallet);
348
396
  }
349
397
  }
350
398
 
@@ -357,16 +405,27 @@ export class TonConnectMobile {
357
405
  return;
358
406
  }
359
407
 
408
+ const transactionResult = {
409
+ boc: response.boc,
410
+ signature: response.signature,
411
+ };
412
+
413
+ // Emit transaction event
414
+ this.emit('transaction', transactionResult);
415
+
360
416
  // Resolve transaction promise
417
+ // CRITICAL: Only resolve if promise still exists and hasn't timed out
361
418
  if (this.transactionPromise) {
419
+ // Clear timeout if it exists
362
420
  if (this.transactionPromise.timeout !== null) {
363
421
  clearTimeout(this.transactionPromise.timeout);
364
422
  }
365
- this.transactionPromise.resolve({
366
- boc: response.boc,
367
- signature: response.signature,
368
- });
423
+ // Store reference before clearing
424
+ const promise = this.transactionPromise;
425
+ // Clear promise first to prevent race conditions
369
426
  this.transactionPromise = null;
427
+ // Then resolve
428
+ promise.resolve(transactionResult);
370
429
  }
371
430
  }
372
431
 
@@ -374,6 +433,9 @@ export class TonConnectMobile {
374
433
  * Reject current promise with error
375
434
  */
376
435
  private rejectWithError(error: Error): void {
436
+ // Emit error event
437
+ this.emit('error', error);
438
+
377
439
  if (this.connectionPromise) {
378
440
  if (this.connectionPromise.timeout !== null) {
379
441
  clearTimeout(this.connectionPromise.timeout);
@@ -388,6 +450,14 @@ export class TonConnectMobile {
388
450
  this.transactionPromise.reject(error);
389
451
  this.transactionPromise = null;
390
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
+ }
391
461
  }
392
462
 
393
463
  /**
@@ -743,6 +813,9 @@ export class TonConnectMobile {
743
813
  // Update status
744
814
  this.currentStatus = { connected: false, wallet: null };
745
815
  this.notifyStatusChange();
816
+
817
+ // Emit disconnect event
818
+ this.emit('disconnect', null);
746
819
  }
747
820
 
748
821
  /**
@@ -769,6 +842,8 @@ export class TonConnectMobile {
769
842
  /**
770
843
  * Check if a wallet is available on the current platform
771
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
772
847
  */
773
848
  async isWalletAvailable(walletName?: string): Promise<boolean> {
774
849
  const wallet = walletName ? getWalletByName(walletName) : this.currentWallet;
@@ -776,10 +851,14 @@ export class TonConnectMobile {
776
851
  return false;
777
852
  }
778
853
 
779
- // On web, check if wallet supports web platform
780
- // eslint-disable-next-line no-undef
781
- if (typeof globalThis !== 'undefined' && (globalThis as any).window) {
782
- return wallet.platforms.includes('web');
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;
783
862
  }
784
863
 
785
864
  // On mobile, we can't reliably check if wallet is installed
@@ -841,6 +920,63 @@ export class TonConnectMobile {
841
920
  // Ignore errors in callbacks
842
921
  }
843
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
+ }
844
980
  }
845
981
 
846
982
  /**
@@ -955,10 +1091,240 @@ export class TonConnectMobile {
955
1091
  }
956
1092
 
957
1093
  this.statusChangeCallbacks.clear();
1094
+ this.eventListeners.clear();
958
1095
  this.connectionPromise = null;
959
1096
  this.transactionPromise = null;
960
1097
  this.signDataPromise = null;
961
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
+ }
962
1328
  }
963
1329
 
964
1330
  // Export types