@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/README.md +316 -18
- package/dist/adapters/expo.js +3 -3
- package/dist/adapters/react-native.js +3 -3
- package/dist/core/protocol.d.ts +1 -0
- package/dist/core/protocol.js +69 -9
- package/dist/core/wallets.js +5 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +309 -13
- package/dist/react/TonConnectUIProvider.d.ts +21 -2
- package/dist/react/TonConnectUIProvider.js +118 -14
- package/dist/react/WalletSelectionModal.d.ts +1 -0
- package/dist/react/WalletSelectionModal.js +153 -80
- package/dist/react/index.d.ts +1 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/utils/transactionBuilder.js +50 -7
- package/package.json +1 -1
- package/src/adapters/expo.ts +3 -3
- package/src/adapters/react-native.ts +3 -3
- package/src/core/protocol.ts +76 -9
- package/src/core/wallets.ts +5 -1
- package/src/index.ts +381 -15
- package/src/react/TonConnectUIProvider.tsx +154 -15
- package/src/react/WalletSelectionModal.tsx +180 -90
- package/src/react/index.ts +1 -0
- package/src/types/index.ts +52 -0
- package/src/utils/transactionBuilder.ts +56 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blazium/ton-connect-mobile",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Production-ready TON Connect Mobile SDK for React Native and Expo. Implements the real TonConnect protocol for mobile applications using deep links and callbacks.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/adapters/expo.ts
CHANGED
|
@@ -78,14 +78,14 @@ export class ExpoAdapter implements PlatformAdapter {
|
|
|
78
78
|
console.log('[ExpoAdapter] Skipping canOpenURL check (Android compatibility)');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// CRITICAL FIX: Android
|
|
82
|
-
//
|
|
81
|
+
// CRITICAL FIX: On Android, canOpenURL() may not recognize tonconnect:// protocol
|
|
82
|
+
// So we call openURL() directly. If it fails, it will throw an error.
|
|
83
83
|
await Linking.openURL(url);
|
|
84
84
|
console.log('[ExpoAdapter] URL opened successfully');
|
|
85
85
|
return true;
|
|
86
86
|
} catch (error: any) {
|
|
87
87
|
console.error('[ExpoAdapter] Error in openURL:', error);
|
|
88
|
-
// Android
|
|
88
|
+
// On Android, if tonconnect:// protocol is not recognized or wallet is not installed, it will throw an error
|
|
89
89
|
const errorMessage = error?.message || String(error);
|
|
90
90
|
if (errorMessage.includes('No Activity found') || errorMessage.includes('No app found') || errorMessage.includes('Cannot open URL')) {
|
|
91
91
|
throw new Error(
|
|
@@ -67,14 +67,14 @@ export class ReactNativeAdapter implements PlatformAdapter {
|
|
|
67
67
|
console.log('[ReactNativeAdapter] Skipping canOpenURL check (Android compatibility)');
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// CRITICAL FIX: Android
|
|
71
|
-
//
|
|
70
|
+
// CRITICAL FIX: On Android, canOpenURL() may not recognize tonconnect:// protocol
|
|
71
|
+
// So we call openURL() directly. If it fails, it will throw an error.
|
|
72
72
|
await Linking.openURL(url);
|
|
73
73
|
console.log('[ReactNativeAdapter] URL opened successfully');
|
|
74
74
|
return true;
|
|
75
75
|
} catch (error: any) {
|
|
76
76
|
console.error('[ReactNativeAdapter] Error in openURL:', error);
|
|
77
|
-
// Android
|
|
77
|
+
// On Android, if tonconnect:// protocol is not recognized or wallet is not installed, it will throw an error
|
|
78
78
|
const errorMessage = error?.message || String(error);
|
|
79
79
|
if (errorMessage.includes('No Activity found') || errorMessage.includes('No app found') || errorMessage.includes('Cannot open URL')) {
|
|
80
80
|
throw new Error(
|
package/src/core/protocol.ts
CHANGED
|
@@ -248,14 +248,23 @@ export function parseCallbackURL(url: string, scheme: string): {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Extract encoded payload
|
|
251
|
-
|
|
251
|
+
let encoded = url.substring(expectedPrefix.length);
|
|
252
|
+
|
|
253
|
+
// CRITICAL FIX: Decode URL encoding first (wallet may URL-encode the payload)
|
|
254
|
+
try {
|
|
255
|
+
encoded = decodeURIComponent(encoded);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// If decodeURIComponent fails, try using the original encoded string
|
|
258
|
+
// Some wallets may not URL-encode the payload
|
|
259
|
+
console.log('[TON Connect] Payload not URL-encoded, using as-is');
|
|
260
|
+
}
|
|
252
261
|
|
|
253
262
|
// CRITICAL FIX: Validate base64 payload size (prevent DoS)
|
|
254
263
|
if (encoded.length === 0 || encoded.length > 5000) {
|
|
255
264
|
return { type: 'unknown', data: null };
|
|
256
265
|
}
|
|
257
266
|
|
|
258
|
-
// CRITICAL FIX: Validate base64 characters only
|
|
267
|
+
// CRITICAL FIX: Validate base64 characters only (after URL decoding)
|
|
259
268
|
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
260
269
|
return { type: 'unknown', data: null };
|
|
261
270
|
}
|
|
@@ -306,14 +315,20 @@ export function parseCallbackURL(url: string, scheme: string): {
|
|
|
306
315
|
|
|
307
316
|
/**
|
|
308
317
|
* Extract wallet info from connection response
|
|
318
|
+
* CRITICAL: This function assumes response has been validated by validateConnectionResponse
|
|
309
319
|
*/
|
|
310
320
|
export function extractWalletInfo(
|
|
311
321
|
response: ConnectionResponsePayload
|
|
312
322
|
): WalletInfo {
|
|
323
|
+
// CRITICAL FIX: Add null checks to prevent runtime errors
|
|
324
|
+
if (!response || !response.name || !response.address || !response.publicKey) {
|
|
325
|
+
throw new Error('Invalid connection response: missing required fields');
|
|
326
|
+
}
|
|
327
|
+
|
|
313
328
|
return {
|
|
314
329
|
name: response.name,
|
|
315
|
-
appName: response.appName,
|
|
316
|
-
version: response.version,
|
|
330
|
+
appName: response.appName || response.name,
|
|
331
|
+
version: response.version || 'unknown',
|
|
317
332
|
platform: response.platform || 'unknown',
|
|
318
333
|
address: response.address,
|
|
319
334
|
publicKey: response.publicKey,
|
|
@@ -360,13 +375,65 @@ export function validateTransactionRequest(
|
|
|
360
375
|
return { valid: false, error: 'Transaction must have at least one message' };
|
|
361
376
|
}
|
|
362
377
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
378
|
+
// CRITICAL: Validate each message
|
|
379
|
+
for (let i = 0; i < request.messages.length; i++) {
|
|
380
|
+
const msg = request.messages[i];
|
|
381
|
+
|
|
382
|
+
// Validate address
|
|
383
|
+
if (!msg.address || typeof msg.address !== 'string') {
|
|
384
|
+
return { valid: false, error: `Message ${i + 1}: Address is required and must be a string` };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// CRITICAL: Validate TON address format (EQ... or 0Q...)
|
|
388
|
+
if (!/^(EQ|0Q)[A-Za-z0-9_-]{46}$/.test(msg.address)) {
|
|
389
|
+
return { valid: false, error: `Message ${i + 1}: Invalid TON address format. Address must start with EQ or 0Q and be 48 characters long.` };
|
|
366
390
|
}
|
|
367
|
-
|
|
368
|
-
|
|
391
|
+
|
|
392
|
+
// Validate amount
|
|
393
|
+
if (!msg.amount || typeof msg.amount !== 'string') {
|
|
394
|
+
return { valid: false, error: `Message ${i + 1}: Amount is required and must be a string (nanotons)` };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// CRITICAL: Validate amount is a valid positive number (nanotons)
|
|
398
|
+
try {
|
|
399
|
+
const amount = BigInt(msg.amount);
|
|
400
|
+
if (amount <= 0n) {
|
|
401
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be greater than 0` };
|
|
402
|
+
}
|
|
403
|
+
// Check for reasonable maximum (prevent overflow)
|
|
404
|
+
if (amount > BigInt('1000000000000000000')) { // 1 billion TON
|
|
405
|
+
return { valid: false, error: `Message ${i + 1}: Amount exceeds maximum allowed (1 billion TON)` };
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be a valid number string (nanotons)` };
|
|
369
409
|
}
|
|
410
|
+
|
|
411
|
+
// Validate payload if provided (must be base64)
|
|
412
|
+
if (msg.payload !== undefined && msg.payload !== null) {
|
|
413
|
+
if (typeof msg.payload !== 'string') {
|
|
414
|
+
return { valid: false, error: `Message ${i + 1}: Payload must be a base64 string` };
|
|
415
|
+
}
|
|
416
|
+
// Basic base64 validation
|
|
417
|
+
if (msg.payload.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.payload)) {
|
|
418
|
+
return { valid: false, error: `Message ${i + 1}: Payload must be valid base64 encoded` };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Validate stateInit if provided (must be base64)
|
|
423
|
+
if (msg.stateInit !== undefined && msg.stateInit !== null) {
|
|
424
|
+
if (typeof msg.stateInit !== 'string') {
|
|
425
|
+
return { valid: false, error: `Message ${i + 1}: StateInit must be a base64 string` };
|
|
426
|
+
}
|
|
427
|
+
// Basic base64 validation
|
|
428
|
+
if (msg.stateInit.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.stateInit)) {
|
|
429
|
+
return { valid: false, error: `Message ${i + 1}: StateInit must be valid base64 encoded` };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// CRITICAL: Limit maximum number of messages (prevent DoS)
|
|
435
|
+
if (request.messages.length > 255) {
|
|
436
|
+
return { valid: false, error: 'Transaction cannot have more than 255 messages' };
|
|
370
437
|
}
|
|
371
438
|
|
|
372
439
|
return { valid: true };
|
package/src/core/wallets.ts
CHANGED
|
@@ -31,7 +31,8 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
|
|
|
31
31
|
appName: 'Tonkeeper',
|
|
32
32
|
universalLink: 'https://app.tonkeeper.com/ton-connect',
|
|
33
33
|
deepLink: 'tonkeeper://',
|
|
34
|
-
|
|
34
|
+
iconUrl: 'https://tonkeeper.com/assets/tonconnect-icon.png',
|
|
35
|
+
platforms: ['ios', 'android', 'web'], // CRITICAL FIX: Tonkeeper Web is supported
|
|
35
36
|
preferredReturnStrategy: 'post_redirect', // CRITICAL FIX: 'back' strategy may not send callback properly, use 'post_redirect'
|
|
36
37
|
requiresReturnScheme: true, // CRITICAL FIX: Mobile apps need returnScheme for proper callback handling
|
|
37
38
|
},
|
|
@@ -40,6 +41,7 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
|
|
|
40
41
|
appName: 'MyTonWallet',
|
|
41
42
|
universalLink: 'https://connect.mytonwallet.org',
|
|
42
43
|
deepLink: 'mytonwallet://',
|
|
44
|
+
iconUrl: 'https://static.mytonwallet.io/icon-256.png',
|
|
43
45
|
platforms: ['ios', 'android', 'web'],
|
|
44
46
|
preferredReturnStrategy: 'post_redirect',
|
|
45
47
|
requiresReturnScheme: true, // MyTonWallet requires explicit returnScheme
|
|
@@ -49,6 +51,7 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
|
|
|
49
51
|
appName: 'Wallet',
|
|
50
52
|
universalLink: 'https://wallet.tonapi.io/ton-connect',
|
|
51
53
|
deepLink: 'tg://',
|
|
54
|
+
iconUrl: 'https://wallet.tonapi.io/icon.png',
|
|
52
55
|
platforms: ['ios', 'android'],
|
|
53
56
|
preferredReturnStrategy: 'post_redirect',
|
|
54
57
|
requiresReturnScheme: true, // Telegram Wallet requires explicit returnScheme
|
|
@@ -58,6 +61,7 @@ export const SUPPORTED_WALLETS: WalletDefinition[] = [
|
|
|
58
61
|
appName: 'Tonhub',
|
|
59
62
|
universalLink: 'https://tonhub.com/ton-connect',
|
|
60
63
|
deepLink: 'tonhub://',
|
|
64
|
+
iconUrl: 'https://tonhub.com/tonconnect_logo.png',
|
|
61
65
|
platforms: ['ios', 'android'],
|
|
62
66
|
preferredReturnStrategy: 'post_redirect',
|
|
63
67
|
requiresReturnScheme: true, // Tonhub requires explicit returnScheme for proper callback
|