@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
package/src/core/protocol.ts
CHANGED
|
@@ -1,443 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
type: 'connect' | 'transaction' | 'error' | 'unknown';
|
|
219
|
-
data: ConnectionResponsePayload | TransactionResponsePayload | ErrorResponse | null;
|
|
220
|
-
} {
|
|
221
|
-
try {
|
|
222
|
-
// CRITICAL FIX: Validate URL input
|
|
223
|
-
if (!url || typeof url !== 'string') {
|
|
224
|
-
return { type: 'unknown', data: null };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// CRITICAL FIX: Validate URL length (prevent DoS)
|
|
228
|
-
if (url.length > 10000) {
|
|
229
|
-
return { type: 'unknown', data: null };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// CRITICAL FIX: Validate scheme format
|
|
233
|
-
if (!scheme || typeof scheme !== 'string' || scheme.length === 0 || scheme.length > 50) {
|
|
234
|
-
return { type: 'unknown', data: null };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// CRITICAL FIX: Exact scheme matching (case-sensitive)
|
|
238
|
-
const expectedPrefix = `${scheme}://${CALLBACK_PREFIX}?`;
|
|
239
|
-
if (!url.startsWith(expectedPrefix)) {
|
|
240
|
-
return { type: 'unknown', data: null };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// CRITICAL FIX: Validate URL structure - should be exactly scheme://tonconnect?<payload>
|
|
244
|
-
// Check that there's no additional path or query params
|
|
245
|
-
const urlAfterScheme = url.substring(scheme.length + 3); // After "scheme://"
|
|
246
|
-
if (!urlAfterScheme.startsWith(`${CALLBACK_PREFIX}?`)) {
|
|
247
|
-
return { type: 'unknown', data: null };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Extract encoded payload
|
|
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
|
-
}
|
|
261
|
-
|
|
262
|
-
// CRITICAL FIX: Validate base64 payload size (prevent DoS)
|
|
263
|
-
if (encoded.length === 0 || encoded.length > 5000) {
|
|
264
|
-
return { type: 'unknown', data: null };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// CRITICAL FIX: Validate base64 characters only (after URL decoding)
|
|
268
|
-
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
269
|
-
return { type: 'unknown', data: null };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const decoded = decodeBase64URL(encoded);
|
|
273
|
-
|
|
274
|
-
// Validate decoded data is an object
|
|
275
|
-
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded)) {
|
|
276
|
-
return { type: 'unknown', data: null };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Check if it's an error response
|
|
280
|
-
if ('error' in decoded && typeof decoded.error === 'object') {
|
|
281
|
-
const errorData = decoded as ErrorResponse;
|
|
282
|
-
if (errorData.error && typeof errorData.error.code === 'number' && typeof errorData.error.message === 'string') {
|
|
283
|
-
return { type: 'error', data: errorData };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Check if it's a connection response (has session, address, publicKey)
|
|
288
|
-
if (
|
|
289
|
-
'session' in decoded &&
|
|
290
|
-
'address' in decoded &&
|
|
291
|
-
'publicKey' in decoded &&
|
|
292
|
-
typeof decoded.session === 'string' &&
|
|
293
|
-
typeof decoded.address === 'string' &&
|
|
294
|
-
typeof decoded.publicKey === 'string'
|
|
295
|
-
) {
|
|
296
|
-
return { type: 'connect', data: decoded as ConnectionResponsePayload };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Check if it's a transaction response (has boc, signature)
|
|
300
|
-
if (
|
|
301
|
-
'boc' in decoded &&
|
|
302
|
-
'signature' in decoded &&
|
|
303
|
-
typeof decoded.boc === 'string' &&
|
|
304
|
-
typeof decoded.signature === 'string'
|
|
305
|
-
) {
|
|
306
|
-
return { type: 'transaction', data: decoded as TransactionResponsePayload };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return { type: 'unknown', data: null };
|
|
310
|
-
} catch (error) {
|
|
311
|
-
// Log error for debugging but don't expose details
|
|
312
|
-
return { type: 'unknown', data: null };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Extract wallet info from connection response
|
|
318
|
-
* CRITICAL: This function assumes response has been validated by validateConnectionResponse
|
|
319
|
-
*/
|
|
320
|
-
export function extractWalletInfo(
|
|
321
|
-
response: ConnectionResponsePayload
|
|
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
|
-
|
|
328
|
-
return {
|
|
329
|
-
name: response.name,
|
|
330
|
-
appName: response.appName || response.name,
|
|
331
|
-
version: response.version || 'unknown',
|
|
332
|
-
platform: response.platform || 'unknown',
|
|
333
|
-
address: response.address,
|
|
334
|
-
publicKey: response.publicKey,
|
|
335
|
-
icon: response.icon,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Validate connection response
|
|
341
|
-
* CRITICAL FIX: Only validate truly required fields (session, address, publicKey, name)
|
|
342
|
-
* appName and version are optional - extractWalletInfo has fallbacks for them
|
|
343
|
-
*/
|
|
344
|
-
export function validateConnectionResponse(
|
|
345
|
-
response: ConnectionResponsePayload
|
|
346
|
-
): boolean {
|
|
347
|
-
return !!(
|
|
348
|
-
response.session &&
|
|
349
|
-
response.address &&
|
|
350
|
-
response.publicKey &&
|
|
351
|
-
response.name
|
|
352
|
-
// Note: appName and version are optional - extractWalletInfo handles fallbacks
|
|
353
|
-
// Some wallets may not send these fields, and that's OK
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Validate transaction response
|
|
359
|
-
*/
|
|
360
|
-
export function validateTransactionResponse(
|
|
361
|
-
response: TransactionResponsePayload
|
|
362
|
-
): boolean {
|
|
363
|
-
return !!(response.boc && response.signature);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Validate transaction request
|
|
368
|
-
*/
|
|
369
|
-
export function validateTransactionRequest(
|
|
370
|
-
request: SendTransactionRequest
|
|
371
|
-
): { valid: boolean; error?: string } {
|
|
372
|
-
if (!request.validUntil || request.validUntil <= Date.now()) {
|
|
373
|
-
return { valid: false, error: 'Transaction request has expired' };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (!request.messages || request.messages.length === 0) {
|
|
377
|
-
return { valid: false, error: 'Transaction must have at least one message' };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// CRITICAL: Validate each message
|
|
381
|
-
for (let i = 0; i < request.messages.length; i++) {
|
|
382
|
-
const msg = request.messages[i];
|
|
383
|
-
|
|
384
|
-
// Validate address
|
|
385
|
-
if (!msg.address || typeof msg.address !== 'string') {
|
|
386
|
-
return { valid: false, error: `Message ${i + 1}: Address is required and must be a string` };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// CRITICAL: Validate TON address format (EQ... or 0Q...)
|
|
390
|
-
if (!/^(EQ|0Q)[A-Za-z0-9_-]{46}$/.test(msg.address)) {
|
|
391
|
-
return { valid: false, error: `Message ${i + 1}: Invalid TON address format. Address must start with EQ or 0Q and be 48 characters long.` };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Validate amount
|
|
395
|
-
if (!msg.amount || typeof msg.amount !== 'string') {
|
|
396
|
-
return { valid: false, error: `Message ${i + 1}: Amount is required and must be a string (nanotons)` };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// CRITICAL: Validate amount is a valid positive number (nanotons)
|
|
400
|
-
try {
|
|
401
|
-
const amount = BigInt(msg.amount);
|
|
402
|
-
if (amount <= 0n) {
|
|
403
|
-
return { valid: false, error: `Message ${i + 1}: Amount must be greater than 0` };
|
|
404
|
-
}
|
|
405
|
-
// Check for reasonable maximum (prevent overflow)
|
|
406
|
-
if (amount > BigInt('1000000000000000000')) { // 1 billion TON
|
|
407
|
-
return { valid: false, error: `Message ${i + 1}: Amount exceeds maximum allowed (1 billion TON)` };
|
|
408
|
-
}
|
|
409
|
-
} catch (error) {
|
|
410
|
-
return { valid: false, error: `Message ${i + 1}: Amount must be a valid number string (nanotons)` };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Validate payload if provided (must be base64)
|
|
414
|
-
if (msg.payload !== undefined && msg.payload !== null) {
|
|
415
|
-
if (typeof msg.payload !== 'string') {
|
|
416
|
-
return { valid: false, error: `Message ${i + 1}: Payload must be a base64 string` };
|
|
417
|
-
}
|
|
418
|
-
// Basic base64 validation
|
|
419
|
-
if (msg.payload.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.payload)) {
|
|
420
|
-
return { valid: false, error: `Message ${i + 1}: Payload must be valid base64 encoded` };
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Validate stateInit if provided (must be base64)
|
|
425
|
-
if (msg.stateInit !== undefined && msg.stateInit !== null) {
|
|
426
|
-
if (typeof msg.stateInit !== 'string') {
|
|
427
|
-
return { valid: false, error: `Message ${i + 1}: StateInit must be a base64 string` };
|
|
428
|
-
}
|
|
429
|
-
// Basic base64 validation
|
|
430
|
-
if (msg.stateInit.length > 0 && !/^[A-Za-z0-9+/=]+$/.test(msg.stateInit)) {
|
|
431
|
-
return { valid: false, error: `Message ${i + 1}: StateInit must be valid base64 encoded` };
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// CRITICAL: Limit maximum number of messages (prevent DoS)
|
|
437
|
-
if (request.messages.length > 255) {
|
|
438
|
-
return { valid: false, error: 'Transaction cannot have more than 255 messages' };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return { valid: true };
|
|
442
|
-
}
|
|
443
|
-
|
|
1
|
+
/**
|
|
2
|
+
* TON Connect v2 Protocol Implementation
|
|
3
|
+
* Builds correct universal links and parses bridge responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SendTransactionRequest,
|
|
8
|
+
WalletInfo,
|
|
9
|
+
ConnectEvent,
|
|
10
|
+
ConnectErrorEvent,
|
|
11
|
+
RpcResponse,
|
|
12
|
+
RpcErrorResponse,
|
|
13
|
+
} from '../types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Protocol version
|
|
17
|
+
*/
|
|
18
|
+
const PROTOCOL_VERSION = '2';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a TON Connect v2 universal link for wallet connection
|
|
22
|
+
* Format: {universalLink}?v=2&id={sessionId}&r={connectRequest}&ret={returnStrategy}
|
|
23
|
+
*/
|
|
24
|
+
export function buildConnectUniversalLink(
|
|
25
|
+
universalLink: string,
|
|
26
|
+
sessionId: string,
|
|
27
|
+
manifestUrl: string,
|
|
28
|
+
returnStrategy: string = 'back'
|
|
29
|
+
): string {
|
|
30
|
+
// Build connect request (TON Connect v2 format)
|
|
31
|
+
const connectRequest = {
|
|
32
|
+
manifestUrl,
|
|
33
|
+
items: [{ name: 'ton_addr' }],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Build URL with proper query parameters
|
|
37
|
+
const r = JSON.stringify(connectRequest);
|
|
38
|
+
const params = [
|
|
39
|
+
`v=${PROTOCOL_VERSION}`,
|
|
40
|
+
`id=${sessionId}`,
|
|
41
|
+
`r=${encodeURIComponent(r)}`,
|
|
42
|
+
`ret=${encodeURIComponent(returnStrategy)}`,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Handle wallet universal links that may already have query params
|
|
46
|
+
const separator = universalLink.includes('?') ? '&' : '?';
|
|
47
|
+
return `${universalLink}${separator}${params.join('&')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a universal link to bring wallet to foreground (for pending transactions)
|
|
52
|
+
* Format: {universalLink}?ret={returnStrategy}
|
|
53
|
+
*/
|
|
54
|
+
export function buildReturnUniversalLink(
|
|
55
|
+
universalLink: string,
|
|
56
|
+
returnStrategy: string = 'back'
|
|
57
|
+
): string {
|
|
58
|
+
const separator = universalLink.includes('?') ? '&' : '?';
|
|
59
|
+
return `${universalLink}${separator}ret=${encodeURIComponent(returnStrategy)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a JSON-RPC request for sendTransaction
|
|
64
|
+
*/
|
|
65
|
+
export function buildSendTransactionRpcRequest(
|
|
66
|
+
request: SendTransactionRequest,
|
|
67
|
+
id: number
|
|
68
|
+
): string {
|
|
69
|
+
// TON Connect v2 sendTransaction format
|
|
70
|
+
const params = JSON.stringify({
|
|
71
|
+
valid_until: Math.floor(request.validUntil / 1000), // Convert ms to seconds
|
|
72
|
+
network: request.network === 'testnet' ? '-3' : '-239',
|
|
73
|
+
from: request.from,
|
|
74
|
+
messages: request.messages.map((msg) => ({
|
|
75
|
+
address: msg.address,
|
|
76
|
+
amount: msg.amount,
|
|
77
|
+
payload: msg.payload,
|
|
78
|
+
stateInit: msg.stateInit,
|
|
79
|
+
})),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
method: 'sendTransaction',
|
|
84
|
+
params: [params],
|
|
85
|
+
id,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a JSON-RPC request for disconnect
|
|
91
|
+
*/
|
|
92
|
+
export function buildDisconnectRpcRequest(id: number): string {
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
method: 'disconnect',
|
|
95
|
+
params: [],
|
|
96
|
+
id,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a connect response from the wallet (received via bridge, after decryption)
|
|
102
|
+
*/
|
|
103
|
+
export function parseConnectResponse(
|
|
104
|
+
decrypted: string
|
|
105
|
+
): { type: 'connect'; data: ConnectEvent } | { type: 'error'; data: ConnectErrorEvent } | null {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(decrypted);
|
|
108
|
+
|
|
109
|
+
if (parsed.event === 'connect' && parsed.payload) {
|
|
110
|
+
return { type: 'connect', data: parsed as ConnectEvent };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (parsed.event === 'connect_error' && parsed.payload) {
|
|
114
|
+
return { type: 'error', data: parsed as ConnectErrorEvent };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse an RPC response from the wallet (for sendTransaction, disconnect, etc.)
|
|
125
|
+
*/
|
|
126
|
+
export function parseRpcResponse(
|
|
127
|
+
decrypted: string
|
|
128
|
+
): { type: 'result'; data: RpcResponse } | { type: 'error'; data: RpcErrorResponse } | { type: 'event'; event: string; data: any } | null {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(decrypted);
|
|
131
|
+
|
|
132
|
+
// Check for events (disconnect, etc.)
|
|
133
|
+
if (parsed.event) {
|
|
134
|
+
return { type: 'event', event: parsed.event, data: parsed.payload || null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for RPC result
|
|
138
|
+
if ('result' in parsed && parsed.id !== undefined) {
|
|
139
|
+
return { type: 'result', data: parsed as RpcResponse };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for RPC error
|
|
143
|
+
if ('error' in parsed && parsed.id !== undefined) {
|
|
144
|
+
return { type: 'error', data: parsed as RpcErrorResponse };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract wallet info from a connect event
|
|
155
|
+
*/
|
|
156
|
+
export function extractWalletInfoFromEvent(event: ConnectEvent): WalletInfo {
|
|
157
|
+
const tonAddr = event.payload.items.find((item: any) => item.name === 'ton_addr');
|
|
158
|
+
if (!tonAddr) {
|
|
159
|
+
throw new Error('Connect response missing ton_addr item');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const device = event.payload.device || {};
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
name: device.appName || 'Unknown Wallet',
|
|
166
|
+
appName: device.appName || 'unknown',
|
|
167
|
+
version: device.appVersion || 'unknown',
|
|
168
|
+
platform: (device.platform as 'ios' | 'android' | 'unknown') || 'unknown',
|
|
169
|
+
address: tonAddr.address,
|
|
170
|
+
publicKey: tonAddr.publicKey,
|
|
171
|
+
network: tonAddr.network,
|
|
172
|
+
walletStateInit: tonAddr.walletStateInit,
|
|
173
|
+
icon: undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate transaction request
|
|
179
|
+
*/
|
|
180
|
+
export function validateTransactionRequest(
|
|
181
|
+
request: SendTransactionRequest
|
|
182
|
+
): { valid: boolean; error?: string } {
|
|
183
|
+
if (!request.validUntil || request.validUntil <= Date.now()) {
|
|
184
|
+
return { valid: false, error: 'Transaction request has expired' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!request.messages || request.messages.length === 0) {
|
|
188
|
+
return { valid: false, error: 'Transaction must have at least one message' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < request.messages.length; i++) {
|
|
192
|
+
const msg = request.messages[i];
|
|
193
|
+
|
|
194
|
+
if (!msg.address || typeof msg.address !== 'string') {
|
|
195
|
+
return { valid: false, error: `Message ${i + 1}: Address is required` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!msg.amount || typeof msg.amount !== 'string') {
|
|
199
|
+
return { valid: false, error: `Message ${i + 1}: Amount is required (nanotons string)` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const amount = BigInt(msg.amount);
|
|
204
|
+
if (amount <= 0n) {
|
|
205
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be > 0` };
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
return { valid: false, error: `Message ${i + 1}: Amount must be a valid number string` };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (request.messages.length > 255) {
|
|
213
|
+
return { valid: false, error: 'Maximum 255 messages per transaction' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { valid: true };
|
|
217
|
+
}
|