@blazium/ton-connect-mobile 1.2.5 → 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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Session crypto for TON Connect v2 protocol
3
+ * Uses X25519 (NaCl box) for key exchange and message encryption
4
+ */
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ const nacl = require('tweetnacl');
8
+
9
+ /**
10
+ * Convert Uint8Array to hex string
11
+ */
12
+ export function bytesToHex(bytes: Uint8Array): string {
13
+ return Array.from(bytes)
14
+ .map((b) => b.toString(16).padStart(2, '0'))
15
+ .join('');
16
+ }
17
+
18
+ /**
19
+ * Convert hex string to Uint8Array
20
+ */
21
+ export function hexToBytes(hex: string): Uint8Array {
22
+ const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
23
+ const padded = clean.length % 2 === 0 ? clean : '0' + clean;
24
+ const bytes = new Uint8Array(padded.length / 2);
25
+ for (let i = 0; i < padded.length; i += 2) {
26
+ bytes[i / 2] = parseInt(padded.substring(i, i + 2), 16);
27
+ }
28
+ return bytes;
29
+ }
30
+
31
+ /**
32
+ * Convert Uint8Array to base64
33
+ */
34
+ export function bytesToBase64(bytes: Uint8Array): string {
35
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
36
+ let result = '';
37
+ const len = bytes.length;
38
+ for (let i = 0; i < len; i += 3) {
39
+ const a = bytes[i];
40
+ const b = i + 1 < len ? bytes[i + 1] : 0;
41
+ const c = i + 2 < len ? bytes[i + 2] : 0;
42
+ const bitmap = (a << 16) | (b << 8) | c;
43
+ result += chars.charAt((bitmap >> 18) & 63);
44
+ result += chars.charAt((bitmap >> 12) & 63);
45
+ result += i + 1 < len ? chars.charAt((bitmap >> 6) & 63) : '=';
46
+ result += i + 2 < len ? chars.charAt(bitmap & 63) : '=';
47
+ }
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Convert base64 to Uint8Array
53
+ */
54
+ export function base64ToBytes(base64: string): Uint8Array {
55
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
56
+ const bytes: number[] = [];
57
+ let buffer = 0;
58
+ let bitsCollected = 0;
59
+ for (let i = 0; i < base64.length; i++) {
60
+ const ch = base64[i];
61
+ if (ch === '=') break;
62
+ // Also handle URL-safe base64
63
+ let index: number;
64
+ if (ch === '-') {
65
+ index = 62;
66
+ } else if (ch === '_') {
67
+ index = 63;
68
+ } else {
69
+ index = chars.indexOf(ch);
70
+ }
71
+ if (index === -1) continue;
72
+ buffer = (buffer << 6) | index;
73
+ bitsCollected += 6;
74
+ if (bitsCollected >= 8) {
75
+ bitsCollected -= 8;
76
+ bytes.push((buffer >> bitsCollected) & 0xff);
77
+ buffer &= (1 << bitsCollected) - 1;
78
+ }
79
+ }
80
+ return new Uint8Array(bytes);
81
+ }
82
+
83
+ /**
84
+ * Serialized session state for persistence
85
+ */
86
+ export interface SessionState {
87
+ secretKey: string; // hex-encoded
88
+ walletPublicKey?: string; // hex-encoded, set after connection
89
+ }
90
+
91
+ /**
92
+ * Session crypto for TON Connect v2
93
+ * Handles X25519 key exchange and NaCl box encryption/decryption
94
+ */
95
+ export class SessionCrypto {
96
+ private keypair: { publicKey: Uint8Array; secretKey: Uint8Array };
97
+
98
+ constructor(existingSecretKey?: Uint8Array) {
99
+ if (existingSecretKey) {
100
+ this.keypair = nacl.box.keyPair.fromSecretKey(existingSecretKey);
101
+ } else {
102
+ this.keypair = nacl.box.keyPair();
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Session ID = hex-encoded public key (used as client_id in bridge)
108
+ */
109
+ get sessionId(): string {
110
+ return bytesToHex(this.keypair.publicKey);
111
+ }
112
+
113
+ /**
114
+ * Public key bytes
115
+ */
116
+ get publicKey(): Uint8Array {
117
+ return this.keypair.publicKey;
118
+ }
119
+
120
+ /**
121
+ * Secret key bytes (for persistence)
122
+ */
123
+ get secretKey(): Uint8Array {
124
+ return this.keypair.secretKey;
125
+ }
126
+
127
+ /**
128
+ * Encrypt a message for a recipient
129
+ * Format: nonce (24 bytes) + ciphertext
130
+ */
131
+ encrypt(message: string, receiverPublicKey: Uint8Array): Uint8Array {
132
+ const msgBytes = encodeUTF8(message);
133
+ const nonce = nacl.randomBytes(nacl.box.nonceLength); // 24 bytes
134
+ const encrypted = nacl.box(msgBytes, nonce, receiverPublicKey, this.keypair.secretKey);
135
+ if (!encrypted) {
136
+ throw new Error('Encryption failed');
137
+ }
138
+ // Prepend nonce to ciphertext
139
+ const result = new Uint8Array(nonce.length + encrypted.length);
140
+ result.set(nonce);
141
+ result.set(encrypted, nonce.length);
142
+ return result;
143
+ }
144
+
145
+ /**
146
+ * Decrypt a message from a sender
147
+ * Input format: nonce (24 bytes) + ciphertext
148
+ */
149
+ decrypt(encryptedMessage: Uint8Array, senderPublicKey: Uint8Array): string {
150
+ if (encryptedMessage.length < nacl.box.nonceLength) {
151
+ throw new Error('Encrypted message too short');
152
+ }
153
+ const nonce = encryptedMessage.slice(0, nacl.box.nonceLength);
154
+ const ciphertext = encryptedMessage.slice(nacl.box.nonceLength);
155
+ const decrypted = nacl.box.open(ciphertext, nonce, senderPublicKey, this.keypair.secretKey);
156
+ if (!decrypted) {
157
+ throw new Error('Decryption failed — invalid key or corrupted message');
158
+ }
159
+ return decodeUTF8(decrypted);
160
+ }
161
+
162
+ /**
163
+ * Serialize session for persistence
164
+ */
165
+ serialize(): SessionState {
166
+ return {
167
+ secretKey: bytesToHex(this.keypair.secretKey),
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Restore session from persisted state
173
+ */
174
+ static fromState(state: SessionState): SessionCrypto {
175
+ return new SessionCrypto(hexToBytes(state.secretKey));
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Encode string to UTF-8 Uint8Array
181
+ */
182
+ function encodeUTF8(str: string): Uint8Array {
183
+ // eslint-disable-next-line no-undef
184
+ if (typeof globalThis !== 'undefined' && (globalThis as any).TextEncoder) {
185
+ // eslint-disable-next-line no-undef
186
+ return new (globalThis as any).TextEncoder().encode(str);
187
+ }
188
+ // Fallback for older environments
189
+ const bytes: number[] = [];
190
+ for (let i = 0; i < str.length; i++) {
191
+ let charCode = str.charCodeAt(i);
192
+ if (charCode < 0x80) {
193
+ bytes.push(charCode);
194
+ } else if (charCode < 0x800) {
195
+ bytes.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f));
196
+ } else if (charCode >= 0xd800 && charCode < 0xdc00) {
197
+ // Surrogate pair
198
+ i++;
199
+ const low = str.charCodeAt(i);
200
+ charCode = ((charCode - 0xd800) << 10) + (low - 0xdc00) + 0x10000;
201
+ bytes.push(
202
+ 0xf0 | (charCode >> 18),
203
+ 0x80 | ((charCode >> 12) & 0x3f),
204
+ 0x80 | ((charCode >> 6) & 0x3f),
205
+ 0x80 | (charCode & 0x3f)
206
+ );
207
+ } else {
208
+ bytes.push(0xe0 | (charCode >> 12), 0x80 | ((charCode >> 6) & 0x3f), 0x80 | (charCode & 0x3f));
209
+ }
210
+ }
211
+ return new Uint8Array(bytes);
212
+ }
213
+
214
+ /**
215
+ * Decode UTF-8 Uint8Array to string
216
+ */
217
+ function decodeUTF8(bytes: Uint8Array): string {
218
+ // eslint-disable-next-line no-undef
219
+ if (typeof globalThis !== 'undefined' && (globalThis as any).TextDecoder) {
220
+ // eslint-disable-next-line no-undef
221
+ return new (globalThis as any).TextDecoder().decode(bytes);
222
+ }
223
+ // Fallback
224
+ let result = '';
225
+ let i = 0;
226
+ while (i < bytes.length) {
227
+ const byte = bytes[i];
228
+ if (byte < 0x80) {
229
+ result += String.fromCharCode(byte);
230
+ i++;
231
+ } else if ((byte & 0xe0) === 0xc0) {
232
+ result += String.fromCharCode(((byte & 0x1f) << 6) | (bytes[i + 1] & 0x3f));
233
+ i += 2;
234
+ } else if ((byte & 0xf0) === 0xe0) {
235
+ result += String.fromCharCode(((byte & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f));
236
+ i += 3;
237
+ } else {
238
+ const codePoint =
239
+ ((byte & 0x07) << 18) | ((bytes[i + 1] & 0x3f) << 12) | ((bytes[i + 2] & 0x3f) << 6) | (bytes[i + 3] & 0x3f);
240
+ // Convert to surrogate pair
241
+ const offset = codePoint - 0x10000;
242
+ result += String.fromCharCode(0xd800 + (offset >> 10), 0xdc00 + (offset & 0x3ff));
243
+ i += 4;
244
+ }
245
+ }
246
+ return result;
247
+ }
@@ -1,93 +1,90 @@
1
- /**
2
- * TON Connect compatible wallet definitions
3
- * Each wallet has its own universal link format
4
- */
5
-
6
- export interface WalletDefinition {
7
- /** Wallet name for display */
8
- name: string;
9
- /** Wallet app name */
10
- appName: string;
11
- /** Universal link base URL for this wallet */
12
- universalLink: string;
13
- /** Deep link scheme (if supported) */
14
- deepLink?: string;
15
- /** Wallet icon URL (optional) */
16
- iconUrl?: string;
17
- /** Platform support */
18
- platforms: ('ios' | 'android' | 'web')[];
19
- /** Preferred return strategy for this wallet */
20
- preferredReturnStrategy?: 'back' | 'post_redirect' | 'none';
21
- /** Whether this wallet requires returnScheme in payload */
22
- requiresReturnScheme?: boolean;
23
- }
24
-
25
- /**
26
- * List of supported TON Connect wallets
27
- */
28
- export const SUPPORTED_WALLETS: WalletDefinition[] = [
29
- {
30
- name: 'Tonkeeper',
31
- appName: 'Tonkeeper',
32
- universalLink: 'https://app.tonkeeper.com/ton-connect',
33
- deepLink: 'tonkeeper://',
34
- iconUrl: 'https://tonkeeper.com/assets/tonconnect-icon.png',
35
- platforms: ['ios', 'android', 'web'], // CRITICAL FIX: Tonkeeper Web is supported
36
- preferredReturnStrategy: 'post_redirect', // CRITICAL FIX: 'back' strategy may not send callback properly, use 'post_redirect'
37
- requiresReturnScheme: true, // CRITICAL FIX: Mobile apps need returnScheme for proper callback handling
38
- },
39
- {
40
- name: 'MyTonWallet',
41
- appName: 'MyTonWallet',
42
- universalLink: 'https://connect.mytonwallet.org',
43
- deepLink: 'mytonwallet://',
44
- iconUrl: 'https://static.mytonwallet.io/icon-256.png',
45
- platforms: ['ios', 'android', 'web'],
46
- preferredReturnStrategy: 'post_redirect',
47
- requiresReturnScheme: true, // MyTonWallet requires explicit returnScheme
48
- },
49
- {
50
- name: 'Wallet in Telegram',
51
- appName: 'Wallet',
52
- universalLink: 'https://wallet.tg/ton-connect',
53
- deepLink: 'tg://',
54
- iconUrl: 'https://wallet.tg/images/logo-288.png',
55
- platforms: ['ios', 'android'],
56
- preferredReturnStrategy: 'post_redirect',
57
- requiresReturnScheme: true, // Telegram Wallet requires explicit returnScheme
58
- },
59
- {
60
- name: 'Tonhub',
61
- appName: 'Tonhub',
62
- universalLink: 'https://tonhub.com/ton-connect',
63
- deepLink: 'tonhub://',
64
- iconUrl: 'https://tonhub.com/tonconnect_logo.png',
65
- platforms: ['ios', 'android'],
66
- preferredReturnStrategy: 'post_redirect',
67
- requiresReturnScheme: true, // Tonhub requires explicit returnScheme for proper callback
68
- },
69
- ];
70
-
71
- /**
72
- * Get wallet definition by name
73
- */
74
- export function getWalletByName(name: string): WalletDefinition | undefined {
75
- return SUPPORTED_WALLETS.find(
76
- (wallet) => wallet.name.toLowerCase() === name.toLowerCase() || wallet.appName.toLowerCase() === name.toLowerCase()
77
- );
78
- }
79
-
80
- /**
81
- * Get default wallet (Tonkeeper)
82
- */
83
- export function getDefaultWallet(): WalletDefinition {
84
- return SUPPORTED_WALLETS[0]; // Tonkeeper
85
- }
86
-
87
- /**
88
- * Get all wallets for a specific platform
89
- */
90
- export function getWalletsForPlatform(platform: 'ios' | 'android' | 'web'): WalletDefinition[] {
91
- return SUPPORTED_WALLETS.filter((wallet) => wallet.platforms.includes(platform));
92
- }
93
-
1
+ /**
2
+ * TON Connect compatible wallet definitions
3
+ * Each wallet has its own universal link and bridge URL
4
+ * Data sourced from official https://github.com/ton-connect/wallets-list
5
+ */
6
+
7
+ export interface WalletDefinition {
8
+ /** Wallet name for display */
9
+ name: string;
10
+ /** Wallet app name (identifier) */
11
+ appName: string;
12
+ /** Universal link base URL for this wallet */
13
+ universalLink: string;
14
+ /** HTTP Bridge URL for SSE communication */
15
+ bridgeUrl: string;
16
+ /** Deep link scheme (if supported) */
17
+ deepLink?: string;
18
+ /** Wallet icon URL (optional) */
19
+ iconUrl?: string;
20
+ /** Platform support */
21
+ platforms: ('ios' | 'android' | 'web')[];
22
+ }
23
+
24
+ /**
25
+ * List of supported TON Connect wallets
26
+ * Bridge URLs from official wallets-v2.json
27
+ */
28
+ export const SUPPORTED_WALLETS: WalletDefinition[] = [
29
+ {
30
+ name: 'Tonkeeper',
31
+ appName: 'tonkeeper',
32
+ universalLink: 'https://app.tonkeeper.com/ton-connect',
33
+ bridgeUrl: 'https://bridge.tonapi.io/bridge',
34
+ deepLink: 'tonkeeper-tc://',
35
+ iconUrl: 'https://tonkeeper.com/assets/tonconnect-icon.png',
36
+ platforms: ['ios', 'android', 'web'],
37
+ },
38
+ {
39
+ name: 'MyTonWallet',
40
+ appName: 'mytonwallet',
41
+ universalLink: 'https://connect.mytonwallet.org',
42
+ bridgeUrl: 'https://tonconnectbridge.mytonwallet.org/bridge/',
43
+ deepLink: 'mytonwallet-tc://',
44
+ iconUrl: 'https://static.mytonwallet.io/icon-256.png',
45
+ platforms: ['ios', 'android', 'web'],
46
+ },
47
+ {
48
+ name: 'Wallet in Telegram',
49
+ appName: 'telegram-wallet',
50
+ universalLink: 'https://t.me/wallet?attach=wallet',
51
+ bridgeUrl: 'https://walletbot.me/tonconnect-bridge/bridge',
52
+ deepLink: 'tg://',
53
+ iconUrl: 'https://wallet.tg/images/logo-288.png',
54
+ platforms: ['ios', 'android'],
55
+ },
56
+ {
57
+ name: 'Tonhub',
58
+ appName: 'tonhub',
59
+ universalLink: 'https://tonhub.com/ton-connect',
60
+ bridgeUrl: 'https://connect.tonhubapi.com/tonconnect',
61
+ deepLink: 'tonhub://',
62
+ iconUrl: 'https://tonhub.com/tonconnect_logo.png',
63
+ platforms: ['ios', 'android'],
64
+ },
65
+ ];
66
+
67
+ /**
68
+ * Get wallet definition by name
69
+ */
70
+ export function getWalletByName(name: string): WalletDefinition | undefined {
71
+ return SUPPORTED_WALLETS.find(
72
+ (wallet) =>
73
+ wallet.name.toLowerCase() === name.toLowerCase() ||
74
+ wallet.appName.toLowerCase() === name.toLowerCase()
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Get default wallet (Tonkeeper)
80
+ */
81
+ export function getDefaultWallet(): WalletDefinition {
82
+ return SUPPORTED_WALLETS[0]; // Tonkeeper
83
+ }
84
+
85
+ /**
86
+ * Get all wallets for a specific platform
87
+ */
88
+ export function getWalletsForPlatform(platform: 'ios' | 'android' | 'web'): WalletDefinition[] {
89
+ return SUPPORTED_WALLETS.filter((wallet) => wallet.platforms.includes(platform));
90
+ }