@blazium/ton-connect-mobile 1.0.0
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/LICENSE +22 -0
- package/README.md +271 -0
- package/dist/adapters/expo.d.ts +25 -0
- package/dist/adapters/expo.js +145 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/react-native.d.ts +25 -0
- package/dist/adapters/react-native.js +133 -0
- package/dist/adapters/web.d.ts +27 -0
- package/dist/adapters/web.js +147 -0
- package/dist/core/crypto.d.ts +28 -0
- package/dist/core/crypto.js +183 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +21 -0
- package/dist/core/protocol.d.ts +50 -0
- package/dist/core/protocol.js +260 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +502 -0
- package/dist/types/index.d.ts +192 -0
- package/dist/types/index.js +6 -0
- package/package.json +57 -0
- package/src/adapters/expo.ts +160 -0
- package/src/adapters/index.ts +8 -0
- package/src/adapters/react-native.ts +148 -0
- package/src/adapters/web.ts +176 -0
- package/src/core/crypto.ts +238 -0
- package/src/core/index.ts +7 -0
- package/src/core/protocol.ts +330 -0
- package/src/index.d.ts +19 -0
- package/src/index.ts +578 -0
- package/src/types/index.ts +206 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core TonConnect protocol implementation
|
|
4
|
+
* Pure TypeScript, no platform dependencies
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.encodeBase64URL = encodeBase64URL;
|
|
8
|
+
exports.decodeBase64URL = decodeBase64URL;
|
|
9
|
+
exports.buildConnectionRequest = buildConnectionRequest;
|
|
10
|
+
exports.buildTransactionRequest = buildTransactionRequest;
|
|
11
|
+
exports.parseCallbackURL = parseCallbackURL;
|
|
12
|
+
exports.extractWalletInfo = extractWalletInfo;
|
|
13
|
+
exports.validateConnectionResponse = validateConnectionResponse;
|
|
14
|
+
exports.validateTransactionResponse = validateTransactionResponse;
|
|
15
|
+
exports.validateTransactionRequest = validateTransactionRequest;
|
|
16
|
+
/**
|
|
17
|
+
* TonConnect protocol constants
|
|
18
|
+
*/
|
|
19
|
+
const PROTOCOL_VERSION = '2';
|
|
20
|
+
const CONNECT_PREFIX = 'tonconnect://connect';
|
|
21
|
+
const SEND_TRANSACTION_PREFIX = 'tonconnect://send-transaction';
|
|
22
|
+
const CALLBACK_PREFIX = 'tonconnect';
|
|
23
|
+
/**
|
|
24
|
+
* Get TextEncoder with availability check
|
|
25
|
+
*/
|
|
26
|
+
function getTextEncoder() {
|
|
27
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
28
|
+
return new TextEncoder();
|
|
29
|
+
}
|
|
30
|
+
throw new Error('TextEncoder is not available. Please use React Native 0.59+ or add a polyfill.');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Encode string to base64
|
|
34
|
+
*/
|
|
35
|
+
function base64Encode(str) {
|
|
36
|
+
// Use TextEncoder to convert string to bytes
|
|
37
|
+
const encoder = getTextEncoder();
|
|
38
|
+
const bytes = encoder.encode(str);
|
|
39
|
+
// Convert bytes to base64
|
|
40
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
41
|
+
let result = '';
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < bytes.length) {
|
|
44
|
+
const a = bytes[i++];
|
|
45
|
+
const b = i < bytes.length ? bytes[i++] : 0;
|
|
46
|
+
const c = i < bytes.length ? bytes[i++] : 0;
|
|
47
|
+
const bitmap = (a << 16) | (b << 8) | c;
|
|
48
|
+
result += chars.charAt((bitmap >> 18) & 63);
|
|
49
|
+
result += chars.charAt((bitmap >> 12) & 63);
|
|
50
|
+
result += i - 2 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
|
|
51
|
+
result += i - 1 < bytes.length ? chars.charAt(bitmap & 63) : '=';
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decode base64 to string
|
|
57
|
+
*/
|
|
58
|
+
function base64Decode(base64) {
|
|
59
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
60
|
+
let buffer = 0;
|
|
61
|
+
let bitsCollected = 0;
|
|
62
|
+
let result = '';
|
|
63
|
+
for (let i = 0; i < base64.length; i++) {
|
|
64
|
+
const ch = base64[i];
|
|
65
|
+
if (ch === '=')
|
|
66
|
+
break;
|
|
67
|
+
const index = chars.indexOf(ch);
|
|
68
|
+
if (index === -1)
|
|
69
|
+
continue;
|
|
70
|
+
buffer = (buffer << 6) | index;
|
|
71
|
+
bitsCollected += 6;
|
|
72
|
+
if (bitsCollected >= 8) {
|
|
73
|
+
bitsCollected -= 8;
|
|
74
|
+
result += String.fromCharCode((buffer >> bitsCollected) & 0xff);
|
|
75
|
+
buffer &= (1 << bitsCollected) - 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Encode JSON to base64 URL-safe string
|
|
82
|
+
*/
|
|
83
|
+
function encodeBase64URL(data) {
|
|
84
|
+
const json = JSON.stringify(data);
|
|
85
|
+
const base64 = base64Encode(json);
|
|
86
|
+
// Convert to URL-safe base64
|
|
87
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decode base64 URL-safe string to JSON
|
|
91
|
+
*/
|
|
92
|
+
function decodeBase64URL(encoded) {
|
|
93
|
+
// Convert from URL-safe base64
|
|
94
|
+
const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
|
95
|
+
// Add padding if needed
|
|
96
|
+
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
97
|
+
const json = base64Decode(padded);
|
|
98
|
+
return JSON.parse(json);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build connection request URL
|
|
102
|
+
* Format: tonconnect://connect?<base64_encoded_payload>
|
|
103
|
+
*/
|
|
104
|
+
function buildConnectionRequest(manifestUrl, returnScheme) {
|
|
105
|
+
const payload = {
|
|
106
|
+
manifestUrl,
|
|
107
|
+
items: [{ name: 'ton_addr' }],
|
|
108
|
+
returnStrategy: 'back',
|
|
109
|
+
};
|
|
110
|
+
const encoded = encodeBase64URL(payload);
|
|
111
|
+
return `${CONNECT_PREFIX}?${encoded}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build transaction request URL
|
|
115
|
+
* Format: tonconnect://send-transaction?<base64_encoded_payload>
|
|
116
|
+
*/
|
|
117
|
+
function buildTransactionRequest(manifestUrl, request, returnScheme) {
|
|
118
|
+
const payload = {
|
|
119
|
+
manifestUrl,
|
|
120
|
+
request: {
|
|
121
|
+
validUntil: request.validUntil,
|
|
122
|
+
messages: request.messages.map((msg) => ({
|
|
123
|
+
address: msg.address,
|
|
124
|
+
amount: msg.amount,
|
|
125
|
+
payload: msg.payload,
|
|
126
|
+
stateInit: msg.stateInit,
|
|
127
|
+
})),
|
|
128
|
+
network: request.network,
|
|
129
|
+
from: request.from,
|
|
130
|
+
},
|
|
131
|
+
returnStrategy: 'back',
|
|
132
|
+
};
|
|
133
|
+
const encoded = encodeBase64URL(payload);
|
|
134
|
+
return `${SEND_TRANSACTION_PREFIX}?${encoded}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Parse callback URL
|
|
138
|
+
* Format: <scheme>://tonconnect?<base64_encoded_response>
|
|
139
|
+
*/
|
|
140
|
+
function parseCallbackURL(url, scheme) {
|
|
141
|
+
try {
|
|
142
|
+
// CRITICAL FIX: Validate URL input
|
|
143
|
+
if (!url || typeof url !== 'string') {
|
|
144
|
+
return { type: 'unknown', data: null };
|
|
145
|
+
}
|
|
146
|
+
// CRITICAL FIX: Validate URL length (prevent DoS)
|
|
147
|
+
if (url.length > 10000) {
|
|
148
|
+
return { type: 'unknown', data: null };
|
|
149
|
+
}
|
|
150
|
+
// CRITICAL FIX: Validate scheme format
|
|
151
|
+
if (!scheme || typeof scheme !== 'string' || scheme.length === 0 || scheme.length > 50) {
|
|
152
|
+
return { type: 'unknown', data: null };
|
|
153
|
+
}
|
|
154
|
+
// CRITICAL FIX: Exact scheme matching (case-sensitive)
|
|
155
|
+
const expectedPrefix = `${scheme}://${CALLBACK_PREFIX}?`;
|
|
156
|
+
if (!url.startsWith(expectedPrefix)) {
|
|
157
|
+
return { type: 'unknown', data: null };
|
|
158
|
+
}
|
|
159
|
+
// CRITICAL FIX: Validate URL structure - should be exactly scheme://tonconnect?<payload>
|
|
160
|
+
// Check that there's no additional path or query params
|
|
161
|
+
const urlAfterScheme = url.substring(scheme.length + 3); // After "scheme://"
|
|
162
|
+
if (!urlAfterScheme.startsWith(`${CALLBACK_PREFIX}?`)) {
|
|
163
|
+
return { type: 'unknown', data: null };
|
|
164
|
+
}
|
|
165
|
+
// Extract encoded payload
|
|
166
|
+
const encoded = url.substring(expectedPrefix.length);
|
|
167
|
+
// CRITICAL FIX: Validate base64 payload size (prevent DoS)
|
|
168
|
+
if (encoded.length === 0 || encoded.length > 5000) {
|
|
169
|
+
return { type: 'unknown', data: null };
|
|
170
|
+
}
|
|
171
|
+
// CRITICAL FIX: Validate base64 characters only
|
|
172
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
173
|
+
return { type: 'unknown', data: null };
|
|
174
|
+
}
|
|
175
|
+
const decoded = decodeBase64URL(encoded);
|
|
176
|
+
// Validate decoded data is an object
|
|
177
|
+
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded)) {
|
|
178
|
+
return { type: 'unknown', data: null };
|
|
179
|
+
}
|
|
180
|
+
// Check if it's an error response
|
|
181
|
+
if ('error' in decoded && typeof decoded.error === 'object') {
|
|
182
|
+
const errorData = decoded;
|
|
183
|
+
if (errorData.error && typeof errorData.error.code === 'number' && typeof errorData.error.message === 'string') {
|
|
184
|
+
return { type: 'error', data: errorData };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Check if it's a connection response (has session, address, publicKey)
|
|
188
|
+
if ('session' in decoded &&
|
|
189
|
+
'address' in decoded &&
|
|
190
|
+
'publicKey' in decoded &&
|
|
191
|
+
typeof decoded.session === 'string' &&
|
|
192
|
+
typeof decoded.address === 'string' &&
|
|
193
|
+
typeof decoded.publicKey === 'string') {
|
|
194
|
+
return { type: 'connect', data: decoded };
|
|
195
|
+
}
|
|
196
|
+
// Check if it's a transaction response (has boc, signature)
|
|
197
|
+
if ('boc' in decoded &&
|
|
198
|
+
'signature' in decoded &&
|
|
199
|
+
typeof decoded.boc === 'string' &&
|
|
200
|
+
typeof decoded.signature === 'string') {
|
|
201
|
+
return { type: 'transaction', data: decoded };
|
|
202
|
+
}
|
|
203
|
+
return { type: 'unknown', data: null };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// Log error for debugging but don't expose details
|
|
207
|
+
return { type: 'unknown', data: null };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extract wallet info from connection response
|
|
212
|
+
*/
|
|
213
|
+
function extractWalletInfo(response) {
|
|
214
|
+
return {
|
|
215
|
+
name: response.name,
|
|
216
|
+
appName: response.appName,
|
|
217
|
+
version: response.version,
|
|
218
|
+
platform: response.platform || 'unknown',
|
|
219
|
+
address: response.address,
|
|
220
|
+
publicKey: response.publicKey,
|
|
221
|
+
icon: response.icon,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Validate connection response
|
|
226
|
+
*/
|
|
227
|
+
function validateConnectionResponse(response) {
|
|
228
|
+
return !!(response.session &&
|
|
229
|
+
response.address &&
|
|
230
|
+
response.publicKey &&
|
|
231
|
+
response.name &&
|
|
232
|
+
response.appName &&
|
|
233
|
+
response.version);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Validate transaction response
|
|
237
|
+
*/
|
|
238
|
+
function validateTransactionResponse(response) {
|
|
239
|
+
return !!(response.boc && response.signature);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Validate transaction request
|
|
243
|
+
*/
|
|
244
|
+
function validateTransactionRequest(request) {
|
|
245
|
+
if (!request.validUntil || request.validUntil <= Date.now()) {
|
|
246
|
+
return { valid: false, error: 'Transaction request has expired' };
|
|
247
|
+
}
|
|
248
|
+
if (!request.messages || request.messages.length === 0) {
|
|
249
|
+
return { valid: false, error: 'Transaction must have at least one message' };
|
|
250
|
+
}
|
|
251
|
+
for (const msg of request.messages) {
|
|
252
|
+
if (!msg.address) {
|
|
253
|
+
return { valid: false, error: 'Message address is required' };
|
|
254
|
+
}
|
|
255
|
+
if (!msg.amount || isNaN(Number(msg.amount))) {
|
|
256
|
+
return { valid: false, error: 'Message amount must be a valid number' };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { valid: true };
|
|
260
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TON Connect Mobile SDK
|
|
3
|
+
* Production-ready implementation for React Native and Expo
|
|
4
|
+
*/
|
|
5
|
+
import { TonConnectMobileConfig, ConnectionStatus, WalletInfo, SendTransactionRequest, StatusChangeCallback } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Custom error classes
|
|
8
|
+
*/
|
|
9
|
+
export declare class TonConnectError extends Error {
|
|
10
|
+
code?: string | undefined;
|
|
11
|
+
constructor(message: string, code?: string | undefined);
|
|
12
|
+
}
|
|
13
|
+
export declare class ConnectionTimeoutError extends TonConnectError {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export declare class TransactionTimeoutError extends TonConnectError {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
export declare class UserRejectedError extends TonConnectError {
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
22
|
+
export declare class ConnectionInProgressError extends TonConnectError {
|
|
23
|
+
constructor();
|
|
24
|
+
}
|
|
25
|
+
export declare class TransactionInProgressError extends TonConnectError {
|
|
26
|
+
constructor();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Main TON Connect Mobile SDK class
|
|
30
|
+
*/
|
|
31
|
+
export declare class TonConnectMobile {
|
|
32
|
+
private adapter;
|
|
33
|
+
private config;
|
|
34
|
+
private statusChangeCallbacks;
|
|
35
|
+
private currentStatus;
|
|
36
|
+
private urlUnsubscribe;
|
|
37
|
+
private connectionPromise;
|
|
38
|
+
private transactionPromise;
|
|
39
|
+
constructor(config: TonConnectMobileConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Create platform adapter based on available modules
|
|
42
|
+
*/
|
|
43
|
+
private createAdapter;
|
|
44
|
+
/**
|
|
45
|
+
* Set up URL listener for wallet callbacks
|
|
46
|
+
*/
|
|
47
|
+
private setupURLListener;
|
|
48
|
+
/**
|
|
49
|
+
* Handle callback from wallet
|
|
50
|
+
*/
|
|
51
|
+
private handleCallback;
|
|
52
|
+
/**
|
|
53
|
+
* Handle connection response from wallet
|
|
54
|
+
*/
|
|
55
|
+
private handleConnectionResponse;
|
|
56
|
+
/**
|
|
57
|
+
* Handle transaction response from wallet
|
|
58
|
+
*/
|
|
59
|
+
private handleTransactionResponse;
|
|
60
|
+
/**
|
|
61
|
+
* Reject current promise with error
|
|
62
|
+
*/
|
|
63
|
+
private rejectWithError;
|
|
64
|
+
/**
|
|
65
|
+
* Connect to wallet
|
|
66
|
+
*/
|
|
67
|
+
connect(): Promise<WalletInfo>;
|
|
68
|
+
/**
|
|
69
|
+
* Send transaction
|
|
70
|
+
*/
|
|
71
|
+
sendTransaction(request: SendTransactionRequest): Promise<{
|
|
72
|
+
boc: string;
|
|
73
|
+
signature: string;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Disconnect from wallet
|
|
77
|
+
*/
|
|
78
|
+
disconnect(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Get current connection status
|
|
81
|
+
*/
|
|
82
|
+
getStatus(): ConnectionStatus;
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe to status changes
|
|
85
|
+
*/
|
|
86
|
+
onStatusChange(callback: StatusChangeCallback): () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Notify all status change callbacks
|
|
89
|
+
*/
|
|
90
|
+
private notifyStatusChange;
|
|
91
|
+
/**
|
|
92
|
+
* Validate session ID format
|
|
93
|
+
*/
|
|
94
|
+
private validateSessionId;
|
|
95
|
+
/**
|
|
96
|
+
* Save session to storage
|
|
97
|
+
*/
|
|
98
|
+
private saveSession;
|
|
99
|
+
/**
|
|
100
|
+
* Load session from storage
|
|
101
|
+
*/
|
|
102
|
+
private loadSession;
|
|
103
|
+
/**
|
|
104
|
+
* Clear session from storage
|
|
105
|
+
*/
|
|
106
|
+
private clearSession;
|
|
107
|
+
/**
|
|
108
|
+
* Cleanup resources
|
|
109
|
+
*/
|
|
110
|
+
destroy(): void;
|
|
111
|
+
}
|
|
112
|
+
export * from './types';
|