@blazium/ton-connect-mobile 1.1.5 → 1.2.1

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.
@@ -55,15 +55,15 @@ class ReactNativeAdapter {
55
55
  else {
56
56
  console.log('[ReactNativeAdapter] Skipping canOpenURL check (Android compatibility)');
57
57
  }
58
- // CRITICAL FIX: Android'de canOpenURL() tonconnect:// protokolünü tanımayabilir
59
- // Bu yüzden direkt openURL() çağırıyoruz. Eğer açılamazsa hata fırlatır.
58
+ // CRITICAL FIX: On Android, canOpenURL() may not recognize tonconnect:// protocol
59
+ // So we call openURL() directly. If it fails, it will throw an error.
60
60
  await Linking.openURL(url);
61
61
  console.log('[ReactNativeAdapter] URL opened successfully');
62
62
  return true;
63
63
  }
64
64
  catch (error) {
65
65
  console.error('[ReactNativeAdapter] Error in openURL:', error);
66
- // Android'de tonconnect:// protokolü tanınmıyorsa veya cüzdan yüklü değilse hata verir
66
+ // On Android, if tonconnect:// protocol is not recognized or wallet is not installed, it will throw an error
67
67
  const errorMessage = error?.message || String(error);
68
68
  if (errorMessage.includes('No Activity found') || errorMessage.includes('No app found') || errorMessage.includes('Cannot open URL')) {
69
69
  throw new Error('No TON wallet app found. Please install Tonkeeper or another TON Connect compatible wallet from Google Play Store.');
@@ -35,6 +35,7 @@ export declare function parseCallbackURL(url: string, scheme: string): {
35
35
  };
36
36
  /**
37
37
  * Extract wallet info from connection response
38
+ * CRITICAL: This function assumes response has been validated by validateConnectionResponse
38
39
  */
39
40
  export declare function extractWalletInfo(response: ConnectionResponsePayload): WalletInfo;
40
41
  /**
@@ -195,12 +195,21 @@ function parseCallbackURL(url, scheme) {
195
195
  return { type: 'unknown', data: null };
196
196
  }
197
197
  // Extract encoded payload
198
- const encoded = url.substring(expectedPrefix.length);
198
+ let encoded = url.substring(expectedPrefix.length);
199
+ // CRITICAL FIX: Decode URL encoding first (wallet may URL-encode the payload)
200
+ try {
201
+ encoded = decodeURIComponent(encoded);
202
+ }
203
+ catch (error) {
204
+ // If decodeURIComponent fails, try using the original encoded string
205
+ // Some wallets may not URL-encode the payload
206
+ console.log('[TON Connect] Payload not URL-encoded, using as-is');
207
+ }
199
208
  // CRITICAL FIX: Validate base64 payload size (prevent DoS)
200
209
  if (encoded.length === 0 || encoded.length > 5000) {
201
210
  return { type: 'unknown', data: null };
202
211
  }
203
- // CRITICAL FIX: Validate base64 characters only
212
+ // CRITICAL FIX: Validate base64 characters only (after URL decoding)
204
213
  if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
205
214
  return { type: 'unknown', data: null };
206
215
  }
@@ -241,12 +250,17 @@ function parseCallbackURL(url, scheme) {
241
250
  }
242
251
  /**
243
252
  * Extract wallet info from connection response
253
+ * CRITICAL: This function assumes response has been validated by validateConnectionResponse
244
254
  */
245
255
  function extractWalletInfo(response) {
256
+ // CRITICAL FIX: Add null checks to prevent runtime errors
257
+ if (!response || !response.name || !response.address || !response.publicKey) {
258
+ throw new Error('Invalid connection response: missing required fields');
259
+ }
246
260
  return {
247
261
  name: response.name,
248
- appName: response.appName,
249
- version: response.version,
262
+ appName: response.appName || response.name,
263
+ version: response.version || 'unknown',
250
264
  platform: response.platform || 'unknown',
251
265
  address: response.address,
252
266
  publicKey: response.publicKey,
@@ -280,13 +294,59 @@ function validateTransactionRequest(request) {
280
294
  if (!request.messages || request.messages.length === 0) {
281
295
  return { valid: false, error: 'Transaction must have at least one message' };
282
296
  }
283
- for (const msg of request.messages) {
284
- if (!msg.address) {
285
- return { valid: false, error: 'Message address is required' };
297
+ // CRITICAL: Validate each message
298
+ for (let i = 0; i < request.messages.length; i++) {
299
+ const msg = request.messages[i];
300
+ // Validate address
301
+ if (!msg.address || typeof msg.address !== 'string') {
302
+ return { valid: false, error: `Message ${i + 1}: Address is required and must be a string` };
303
+ }
304
+ // CRITICAL: Validate TON address format (EQ... or 0Q...)
305
+ if (!/^(EQ|0Q)[A-Za-z0-9_-]{46}$/.test(msg.address)) {
306
+ return { valid: false, error: `Message ${i + 1}: Invalid TON address format. Address must start with EQ or 0Q and be 48 characters long.` };
307
+ }
308
+ // Validate amount
309
+ if (!msg.amount || typeof msg.amount !== 'string') {
310
+ return { valid: false, error: `Message ${i + 1}: Amount is required and must be a string (nanotons)` };
311
+ }
312
+ // CRITICAL: Validate amount is a valid positive number (nanotons)
313
+ try {
314
+ const amount = BigInt(msg.amount);
315
+ if (amount <= 0n) {
316
+ return { valid: false, error: `Message ${i + 1}: Amount must be greater than 0` };
317
+ }
318
+ // Check for reasonable maximum (prevent overflow)
319
+ if (amount > BigInt('1000000000000000000')) { // 1 billion TON
320
+ return { valid: false, error: `Message ${i + 1}: Amount exceeds maximum allowed (1 billion TON)` };
321
+ }
286
322
  }
287
- if (!msg.amount || isNaN(Number(msg.amount))) {
288
- return { valid: false, error: 'Message amount must be a valid number' };
323
+ catch (error) {
324
+ return { valid: false, error: `Message ${i + 1}: Amount must be a valid number string (nanotons)` };
289
325
  }
326
+ // Validate payload if provided (must be base64)
327
+ if (msg.payload !== undefined && msg.payload !== null) {
328
+ if (typeof msg.payload !== 'string') {
329
+ return { valid: false, error: `Message ${i + 1}: Payload must be a base64 string` };
330
+ }
331
+ // Basic base64 validation
332
+ if (msg.payload.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.payload)) {
333
+ return { valid: false, error: `Message ${i + 1}: Payload must be valid base64 encoded` };
334
+ }
335
+ }
336
+ // Validate stateInit if provided (must be base64)
337
+ if (msg.stateInit !== undefined && msg.stateInit !== null) {
338
+ if (typeof msg.stateInit !== 'string') {
339
+ return { valid: false, error: `Message ${i + 1}: StateInit must be a base64 string` };
340
+ }
341
+ // Basic base64 validation
342
+ if (msg.stateInit.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.stateInit)) {
343
+ return { valid: false, error: `Message ${i + 1}: StateInit must be valid base64 encoded` };
344
+ }
345
+ }
346
+ }
347
+ // CRITICAL: Limit maximum number of messages (prevent DoS)
348
+ if (request.messages.length > 255) {
349
+ return { valid: false, error: 'Transaction cannot have more than 255 messages' };
290
350
  }
291
351
  return { valid: true };
292
352
  }
@@ -17,7 +17,7 @@ exports.SUPPORTED_WALLETS = [
17
17
  appName: 'Tonkeeper',
18
18
  universalLink: 'https://app.tonkeeper.com/ton-connect',
19
19
  deepLink: 'tonkeeper://',
20
- platforms: ['ios', 'android'],
20
+ platforms: ['ios', 'android', 'web'], // CRITICAL FIX: Tonkeeper Web is supported
21
21
  preferredReturnStrategy: 'post_redirect', // CRITICAL FIX: 'back' strategy may not send callback properly, use 'post_redirect'
22
22
  requiresReturnScheme: true, // CRITICAL FIX: Mobile apps need returnScheme for proper callback handling
23
23
  },
package/dist/index.d.ts CHANGED
@@ -9,7 +9,8 @@ import { type WalletDefinition } from './core/wallets';
9
9
  */
10
10
  export declare class TonConnectError extends Error {
11
11
  code?: string | undefined;
12
- constructor(message: string, code?: string | undefined);
12
+ recoverySuggestion?: string | undefined;
13
+ constructor(message: string, code?: string | undefined, recoverySuggestion?: string | undefined);
13
14
  }
14
15
  export declare class ConnectionTimeoutError extends TonConnectError {
15
16
  constructor();
@@ -18,7 +19,7 @@ export declare class TransactionTimeoutError extends TonConnectError {
18
19
  constructor();
19
20
  }
20
21
  export declare class UserRejectedError extends TonConnectError {
21
- constructor();
22
+ constructor(message?: string);
22
23
  }
23
24
  export declare class ConnectionInProgressError extends TonConnectError {
24
25
  constructor();
@@ -99,6 +100,13 @@ export declare class TonConnectMobile {
99
100
  * Get current wallet being used
100
101
  */
101
102
  getCurrentWallet(): WalletDefinition;
103
+ /**
104
+ * Check if a wallet is available on the current platform
105
+ * Note: This is a best-effort check and may not be 100% accurate
106
+ * CRITICAL FIX: On web, if wallet has universalLink, it's considered available
107
+ * because universal links can open in new tabs/windows
108
+ */
109
+ isWalletAvailable(walletName?: string): Promise<boolean>;
102
110
  /**
103
111
  * Set preferred wallet for connections
104
112
  */
@@ -135,3 +143,5 @@ export declare class TonConnectMobile {
135
143
  export * from './types';
136
144
  export type { WalletDefinition } from './core/wallets';
137
145
  export { SUPPORTED_WALLETS, getWalletByName, getDefaultWallet, getWalletsForPlatform } from './core/wallets';
146
+ export * from './utils/transactionBuilder';
147
+ export * from './utils/retry';
package/dist/index.js CHANGED
@@ -29,44 +29,45 @@ const wallets_1 = require("./core/wallets");
29
29
  * Custom error classes
30
30
  */
31
31
  class TonConnectError extends Error {
32
- constructor(message, code) {
32
+ constructor(message, code, recoverySuggestion) {
33
33
  super(message);
34
34
  this.code = code;
35
+ this.recoverySuggestion = recoverySuggestion;
35
36
  this.name = 'TonConnectError';
36
37
  }
37
38
  }
38
39
  exports.TonConnectError = TonConnectError;
39
40
  class ConnectionTimeoutError extends TonConnectError {
40
41
  constructor() {
41
- super('Connection request timed out', 'CONNECTION_TIMEOUT');
42
+ super('Connection request timed out. The wallet did not respond in time.', 'CONNECTION_TIMEOUT', 'Please make sure the wallet app is installed and try again. If the issue persists, check your internet connection.');
42
43
  this.name = 'ConnectionTimeoutError';
43
44
  }
44
45
  }
45
46
  exports.ConnectionTimeoutError = ConnectionTimeoutError;
46
47
  class TransactionTimeoutError extends TonConnectError {
47
48
  constructor() {
48
- super('Transaction request timed out', 'TRANSACTION_TIMEOUT');
49
+ super('Transaction request timed out. The wallet did not respond in time.', 'TRANSACTION_TIMEOUT', 'Please check the wallet app and try again. Make sure you approve or reject the transaction in the wallet.');
49
50
  this.name = 'TransactionTimeoutError';
50
51
  }
51
52
  }
52
53
  exports.TransactionTimeoutError = TransactionTimeoutError;
53
54
  class UserRejectedError extends TonConnectError {
54
- constructor() {
55
- super('User rejected the request', 'USER_REJECTED');
55
+ constructor(message) {
56
+ super(message || 'User rejected the request', 'USER_REJECTED', 'The user cancelled the operation in the wallet app.');
56
57
  this.name = 'UserRejectedError';
57
58
  }
58
59
  }
59
60
  exports.UserRejectedError = UserRejectedError;
60
61
  class ConnectionInProgressError extends TonConnectError {
61
62
  constructor() {
62
- super('Connection request already in progress', 'CONNECTION_IN_PROGRESS');
63
+ super('Connection request already in progress', 'CONNECTION_IN_PROGRESS', 'Please wait for the current connection attempt to complete before trying again.');
63
64
  this.name = 'ConnectionInProgressError';
64
65
  }
65
66
  }
66
67
  exports.ConnectionInProgressError = ConnectionInProgressError;
67
68
  class TransactionInProgressError extends TonConnectError {
68
69
  constructor() {
69
- super('Transaction request already in progress', 'TRANSACTION_IN_PROGRESS');
70
+ super('Transaction request already in progress', 'TRANSACTION_IN_PROGRESS', 'Please wait for the current transaction to complete before sending another one.');
70
71
  this.name = 'TransactionInProgressError';
71
72
  }
72
73
  }
@@ -197,8 +198,9 @@ class TonConnectMobile {
197
198
  const parsed = (0, protocol_1.parseCallbackURL)(url, this.config.scheme);
198
199
  console.log('[TON Connect] Parsed callback:', parsed.type, parsed.data ? 'has data' : 'no data');
199
200
  // CRITICAL FIX: Check for sign data response first (before other handlers)
200
- if (this.signDataPromise && !this.signDataPromise.timeout) {
201
- // Sign data request is pending
201
+ // Note: We check if promise exists and hasn't timed out (timeout !== null means not timed out yet)
202
+ if (this.signDataPromise && this.signDataPromise.timeout !== null) {
203
+ // Sign data request is pending and hasn't timed out
202
204
  if (parsed.type === 'error' && parsed.data) {
203
205
  const errorData = parsed.data;
204
206
  if (errorData?.error) {
@@ -283,12 +285,18 @@ class TonConnectMobile {
283
285
  this.currentStatus = { connected: true, wallet };
284
286
  this.notifyStatusChange();
285
287
  // Resolve connection promise
288
+ // CRITICAL: Only resolve if promise still exists and hasn't timed out
286
289
  if (this.connectionPromise) {
290
+ // Clear timeout if it exists
287
291
  if (this.connectionPromise.timeout !== null) {
288
292
  clearTimeout(this.connectionPromise.timeout);
289
293
  }
290
- this.connectionPromise.resolve(wallet);
294
+ // Store reference before clearing to prevent race conditions
295
+ const promise = this.connectionPromise;
296
+ // Clear promise first
291
297
  this.connectionPromise = null;
298
+ // Then resolve
299
+ promise.resolve(wallet);
292
300
  }
293
301
  }
294
302
  /**
@@ -300,15 +308,21 @@ class TonConnectMobile {
300
308
  return;
301
309
  }
302
310
  // Resolve transaction promise
311
+ // CRITICAL: Only resolve if promise still exists and hasn't timed out
303
312
  if (this.transactionPromise) {
313
+ // Clear timeout if it exists
304
314
  if (this.transactionPromise.timeout !== null) {
305
315
  clearTimeout(this.transactionPromise.timeout);
306
316
  }
307
- this.transactionPromise.resolve({
317
+ // Store reference before clearing
318
+ const promise = this.transactionPromise;
319
+ // Clear promise first to prevent race conditions
320
+ this.transactionPromise = null;
321
+ // Then resolve
322
+ promise.resolve({
308
323
  boc: response.boc,
309
324
  signature: response.signature,
310
325
  });
311
- this.transactionPromise = null;
312
326
  }
313
327
  }
314
328
  /**
@@ -329,6 +343,14 @@ class TonConnectMobile {
329
343
  this.transactionPromise.reject(error);
330
344
  this.transactionPromise = null;
331
345
  }
346
+ // CRITICAL FIX: Also clear signDataPromise to prevent memory leaks
347
+ if (this.signDataPromise) {
348
+ if (this.signDataPromise.timeout !== null) {
349
+ clearTimeout(this.signDataPromise.timeout);
350
+ }
351
+ this.signDataPromise.reject(error);
352
+ this.signDataPromise = null;
353
+ }
332
354
  }
333
355
  /**
334
356
  * Connect to wallet
@@ -645,6 +667,33 @@ class TonConnectMobile {
645
667
  getCurrentWallet() {
646
668
  return this.currentWallet;
647
669
  }
670
+ /**
671
+ * Check if a wallet is available on the current platform
672
+ * Note: This is a best-effort check and may not be 100% accurate
673
+ * CRITICAL FIX: On web, if wallet has universalLink, it's considered available
674
+ * because universal links can open in new tabs/windows
675
+ */
676
+ async isWalletAvailable(walletName) {
677
+ const wallet = walletName ? (0, wallets_1.getWalletByName)(walletName) : this.currentWallet;
678
+ if (!wallet) {
679
+ return false;
680
+ }
681
+ // CRITICAL FIX: Check adapter type to reliably detect web platform
682
+ // WebAdapter is only used on web, so this is the most reliable check
683
+ const isWeb = this.adapter.constructor.name === 'WebAdapter';
684
+ if (isWeb) {
685
+ // On web, if wallet has universalLink or supports web platform, it's available
686
+ // Universal links can open in a new tab on web
687
+ return wallet.platforms.includes('web') || !!wallet.universalLink;
688
+ }
689
+ // On mobile, we can't reliably check if wallet is installed
690
+ // Return true if wallet supports the current platform
691
+ // eslint-disable-next-line no-undef
692
+ const platform = typeof globalThis !== 'undefined' && globalThis.Platform
693
+ ? globalThis.Platform.OS === 'ios' ? 'ios' : 'android'
694
+ : 'android';
695
+ return wallet.platforms.includes(platform);
696
+ }
648
697
  /**
649
698
  * Set preferred wallet for connections
650
699
  */
@@ -805,3 +854,6 @@ Object.defineProperty(exports, "SUPPORTED_WALLETS", { enumerable: true, get: fun
805
854
  Object.defineProperty(exports, "getWalletByName", { enumerable: true, get: function () { return wallets_2.getWalletByName; } });
806
855
  Object.defineProperty(exports, "getDefaultWallet", { enumerable: true, get: function () { return wallets_2.getDefaultWallet; } });
807
856
  Object.defineProperty(exports, "getWalletsForPlatform", { enumerable: true, get: function () { return wallets_2.getWalletsForPlatform; } });
857
+ // Export utilities
858
+ __exportStar(require("./utils/transactionBuilder"), exports);
859
+ __exportStar(require("./utils/retry"), exports);
@@ -44,13 +44,26 @@ exports.useTonConnectModal = useTonConnectModal;
44
44
  exports.useTonConnectSDK = useTonConnectSDK;
45
45
  const react_1 = __importStar(require("react"));
46
46
  const index_1 = require("../index");
47
+ const WalletSelectionModal_1 = require("./WalletSelectionModal");
47
48
  const TonConnectUIContext = (0, react_1.createContext)(null);
48
49
  /**
49
50
  * TonConnectUIProvider - React context provider for TON Connect
50
51
  * Compatible with @tonconnect/ui-react API
51
52
  */
52
53
  function TonConnectUIProvider({ config, children, sdkInstance, }) {
53
- const [sdk] = (0, react_1.useState)(() => sdkInstance || new index_1.TonConnectMobile(config));
54
+ // CRITICAL: Initialize SDK only once
55
+ const [sdk] = (0, react_1.useState)(() => {
56
+ if (sdkInstance) {
57
+ return sdkInstance;
58
+ }
59
+ try {
60
+ return new index_1.TonConnectMobile(config);
61
+ }
62
+ catch (error) {
63
+ console.error('[TonConnectUIProvider] Failed to initialize SDK:', error);
64
+ throw error;
65
+ }
66
+ });
54
67
  const [walletState, setWalletState] = (0, react_1.useState)(null);
55
68
  const [modalOpen, setModalOpen] = (0, react_1.useState)(false);
56
69
  const [isConnecting, setIsConnecting] = (0, react_1.useState)(false);
@@ -98,8 +111,10 @@ function TonConnectUIProvider({ config, children, sdkInstance, }) {
98
111
  }, [sdk, updateWalletState]);
99
112
  // Open modal
100
113
  const openModal = (0, react_1.useCallback)(async () => {
101
- setModalOpen(true);
102
- }, []);
114
+ if (!walletState?.connected) {
115
+ setModalOpen(true);
116
+ }
117
+ }, [walletState?.connected]);
103
118
  // Close modal
104
119
  const closeModal = (0, react_1.useCallback)(() => {
105
120
  setModalOpen(false);
@@ -132,19 +147,42 @@ function TonConnectUIProvider({ config, children, sdkInstance, }) {
132
147
  }, [sdk]);
133
148
  // Send transaction
134
149
  const sendTransaction = (0, react_1.useCallback)(async (transaction) => {
135
- const response = await sdk.sendTransaction(transaction);
136
- return {
137
- boc: response.boc,
138
- signature: response.signature,
139
- };
150
+ try {
151
+ // Validate transaction before sending
152
+ if (!transaction || !transaction.messages || transaction.messages.length === 0) {
153
+ throw new Error('Invalid transaction: messages array is required and cannot be empty');
154
+ }
155
+ if (!transaction.validUntil || transaction.validUntil <= Date.now()) {
156
+ throw new Error('Invalid transaction: validUntil must be in the future');
157
+ }
158
+ const response = await sdk.sendTransaction(transaction);
159
+ return {
160
+ boc: response.boc,
161
+ signature: response.signature,
162
+ };
163
+ }
164
+ catch (error) {
165
+ console.error('[TonConnectUIProvider] Transaction error:', error);
166
+ throw error;
167
+ }
140
168
  }, [sdk]);
141
169
  // Sign data
142
170
  const signData = (0, react_1.useCallback)(async (request) => {
143
- const response = await sdk.signData(request.data, request.version);
144
- return {
145
- signature: response.signature,
146
- timestamp: response.timestamp,
147
- };
171
+ try {
172
+ // Validate request
173
+ if (!request || (!request.data && request.data !== '')) {
174
+ throw new Error('Invalid sign data request: data is required');
175
+ }
176
+ const response = await sdk.signData(request.data, request.version);
177
+ return {
178
+ signature: response.signature,
179
+ timestamp: response.timestamp,
180
+ };
181
+ }
182
+ catch (error) {
183
+ console.error('[TonConnectUIProvider] Sign data error:', error);
184
+ throw error;
185
+ }
148
186
  }, [sdk]);
149
187
  // Create TonConnectUI instance
150
188
  const tonConnectUI = {
@@ -164,7 +202,9 @@ function TonConnectUIProvider({ config, children, sdkInstance, }) {
164
202
  tonConnectUI,
165
203
  sdk,
166
204
  };
167
- return react_1.default.createElement(TonConnectUIContext.Provider, { value: contextValue }, children);
205
+ return (react_1.default.createElement(TonConnectUIContext.Provider, { value: contextValue },
206
+ children,
207
+ react_1.default.createElement(WalletSelectionModal_1.WalletSelectionModal, { visible: modalOpen && !walletState?.connected, onClose: closeModal })));
168
208
  }
169
209
  /**
170
210
  * Hook to access TonConnectUI instance
@@ -0,0 +1,20 @@
1
+ /**
2
+ * WalletSelectionModal component
3
+ * Provides a beautiful wallet selection UI compatible with @tonconnect/ui-react
4
+ */
5
+ import type { WalletDefinition } from '../index';
6
+ export interface WalletSelectionModalProps {
7
+ /** Whether the modal is visible */
8
+ visible: boolean;
9
+ /** Callback when modal should close */
10
+ onClose: () => void;
11
+ /** Custom wallet list (optional, uses SDK's supported wallets by default) */
12
+ wallets?: WalletDefinition[];
13
+ /** Custom styles */
14
+ style?: any;
15
+ }
16
+ /**
17
+ * WalletSelectionModal - Beautiful wallet selection modal
18
+ * Compatible with @tonconnect/ui-react modal behavior
19
+ */
20
+ export declare function WalletSelectionModal({ visible, onClose, wallets: customWallets, style, }: WalletSelectionModalProps): JSX.Element;