@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.
- package/dist/core/bridge.d.ts +61 -0
- package/dist/core/bridge.js +237 -0
- package/dist/core/crypto.d.ts +8 -19
- package/dist/core/crypto.js +15 -141
- package/dist/core/index.d.ts +5 -3
- package/dist/core/index.js +20 -17
- package/dist/core/protocol.d.ts +35 -34
- package/dist/core/protocol.js +109 -288
- package/dist/core/session.d.ts +65 -0
- package/dist/core/session.js +235 -0
- package/dist/core/wallets.d.ts +6 -6
- package/dist/core/wallets.js +17 -18
- package/dist/index.d.ts +33 -72
- package/dist/index.js +322 -769
- package/dist/react/TonConnectUIProvider.d.ts +4 -52
- package/dist/react/TonConnectUIProvider.js +18 -122
- package/dist/react/index.d.ts +1 -2
- package/dist/react/index.js +0 -1
- package/dist/types/index.d.ts +84 -139
- package/dist/types/index.js +1 -1
- package/package.json +2 -3
- package/src/core/bridge.ts +307 -0
- package/src/core/crypto.ts +62 -238
- package/src/core/index.ts +17 -7
- package/src/core/protocol.ts +217 -443
- package/src/core/session.ts +247 -0
- package/src/core/wallets.ts +90 -93
- package/src/index.ts +811 -1338
- package/src/react/TonConnectUIProvider.tsx +272 -441
- package/src/react/index.ts +23 -27
- package/src/types/index.ts +217 -272
|
@@ -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
|
+
}
|
package/src/core/wallets.ts
CHANGED
|
@@ -1,93 +1,90 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TON Connect compatible wallet definitions
|
|
3
|
-
* Each wallet has its own universal link
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
name
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*/
|
|
28
|
-
export const SUPPORTED_WALLETS: WalletDefinition[] = [
|
|
29
|
-
{
|
|
30
|
-
name: 'Tonkeeper',
|
|
31
|
-
appName: '
|
|
32
|
-
universalLink: 'https://app.tonkeeper.com/ton-connect',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
deepLink: 'mytonwallet://',
|
|
44
|
-
iconUrl: 'https://static.mytonwallet.io/icon-256.png',
|
|
45
|
-
platforms: ['ios', 'android', 'web'],
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|