@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.
@@ -1,23 +1,27 @@
1
1
  /**
2
2
  * Core types for TON Connect Mobile SDK
3
- * These types define the protocol structure for TonConnect
3
+ * Types for TON Connect v2 protocol
4
4
  */
5
5
  /**
6
6
  * Wallet information returned after successful connection
7
7
  */
8
8
  export interface WalletInfo {
9
- /** Wallet name (e.g., "Tonkeeper", "MyTonWallet") */
9
+ /** Wallet name (e.g., "Tonkeeper") */
10
10
  name: string;
11
11
  /** Wallet app name */
12
12
  appName: string;
13
13
  /** Wallet app version */
14
14
  version: string;
15
- /** Platform (ios/android) */
15
+ /** Platform */
16
16
  platform: 'ios' | 'android' | 'unknown';
17
- /** TON address of the connected wallet */
17
+ /** TON address (raw format: workchain:hash) */
18
18
  address: string;
19
19
  /** Public key in hex format */
20
20
  publicKey: string;
21
+ /** Network identifier ("-239" = mainnet, "-3" = testnet) */
22
+ network?: string;
23
+ /** Wallet state init (base64) */
24
+ walletStateInit?: string;
21
25
  /** Wallet icon URL */
22
26
  icon?: string;
23
27
  }
@@ -25,18 +29,16 @@ export interface WalletInfo {
25
29
  * Connection status
26
30
  */
27
31
  export interface ConnectionStatus {
28
- /** Whether a wallet is currently connected */
29
32
  connected: boolean;
30
- /** Wallet information if connected, null otherwise */
31
33
  wallet: WalletInfo | null;
32
34
  }
33
35
  /**
34
36
  * Transaction message to send
35
37
  */
36
38
  export interface TransactionMessage {
37
- /** Recipient address in TON format (EQ...) */
39
+ /** Recipient address */
38
40
  address: string;
39
- /** Amount in nanotons (1 TON = 1,000,000,000 nanotons) */
41
+ /** Amount in nanotons */
40
42
  amount: string;
41
43
  /** Optional message payload (base64 encoded) */
42
44
  payload?: string;
@@ -51,128 +53,21 @@ export interface SendTransactionRequest {
51
53
  validUntil: number;
52
54
  /** Array of messages to send */
53
55
  messages: TransactionMessage[];
54
- /** Optional network (mainnet/testnet) */
56
+ /** Optional network */
55
57
  network?: 'mainnet' | 'testnet';
56
58
  /** Optional from address */
57
59
  from?: string;
58
60
  }
59
61
  /**
60
- * Transaction response from wallet
61
- */
62
- export interface TransactionResponse {
63
- /** Base64 encoded BOC of the transaction */
64
- boc: string;
65
- /** Transaction signature */
66
- signature: string;
67
- }
68
- /**
69
- * Connection request payload (sent to wallet)
70
- */
71
- export interface ConnectionRequestPayload {
72
- /** Manifest URL for the app */
73
- manifestUrl: string;
74
- /** Items requested from wallet */
75
- items: Array<{
76
- name: 'ton_addr';
77
- }>;
78
- /** Return strategy - how wallet should return to the app */
79
- returnStrategy?: 'back' | 'post_redirect' | 'none';
80
- /** Return scheme for mobile apps (required by many wallets for proper callback handling) */
81
- returnScheme?: string;
82
- }
83
- /**
84
- * Connection response payload (received from wallet)
85
- */
86
- export interface ConnectionResponsePayload {
87
- /** Session ID */
88
- session: string;
89
- /** Wallet information */
90
- name: string;
91
- /** Wallet app name */
92
- appName: string;
93
- /** Wallet version */
94
- version: string;
95
- /** Platform */
96
- platform: 'ios' | 'android' | 'unknown';
97
- /** TON address */
98
- address: string;
99
- /** Public key in hex */
100
- publicKey: string;
101
- /** Wallet icon URL */
102
- icon?: string;
103
- /** Proof (signature) for verification */
104
- proof?: {
105
- timestamp: number;
106
- domain: {
107
- lengthBytes: number;
108
- value: string;
109
- };
110
- signature: string;
111
- };
112
- }
113
- /**
114
- * Transaction request payload (sent to wallet)
115
- */
116
- export interface TransactionRequestPayload {
117
- /** Manifest URL */
118
- manifestUrl: string;
119
- /** Transaction request */
120
- request: {
121
- /** Unix timestamp (ms) when request expires */
122
- validUntil: number;
123
- /** Array of messages */
124
- messages: Array<{
125
- address: string;
126
- amount: string;
127
- payload?: string;
128
- stateInit?: string;
129
- }>;
130
- /** Optional network */
131
- network?: 'mainnet' | 'testnet';
132
- /** Optional from address */
133
- from?: string;
134
- };
135
- /** Return URL scheme (for mobile apps) */
136
- returnScheme?: string;
137
- /** Return strategy */
138
- returnStrategy?: 'back' | 'post_redirect' | 'none';
139
- }
140
- /**
141
- * Transaction response payload (received from wallet)
142
- */
143
- export interface TransactionResponsePayload {
144
- /** Base64 encoded BOC */
145
- boc: string;
146
- /** Transaction signature */
147
- signature: string;
148
- }
149
- /**
150
- * Error response from wallet
151
- */
152
- export interface ErrorResponse {
153
- /** Error code */
154
- error: {
155
- code: number;
156
- message: string;
157
- };
158
- }
159
- /**
160
- * Platform adapter interface for deep linking and storage
62
+ * Platform adapter interface
161
63
  */
162
64
  export interface PlatformAdapter {
163
- /** Open a deep link URL */
164
65
  openURL(url: string, skipCanOpenURLCheck?: boolean): Promise<boolean>;
165
- /** Get initial URL when app was opened via deep link */
166
66
  getInitialURL(): Promise<string | null>;
167
- /** Add listener for URL changes */
168
67
  addURLListener(callback: (url: string) => void): () => void;
169
- /** Store data */
170
68
  setItem(key: string, value: string): Promise<void>;
171
- /** Retrieve data */
172
69
  getItem(key: string): Promise<string | null>;
173
- /** Remove data */
174
70
  removeItem(key: string): Promise<void>;
175
- /** Generate random bytes */
176
71
  randomBytes(length: number): Promise<Uint8Array>;
177
72
  }
178
73
  /**
@@ -185,7 +80,7 @@ export type Network = 'mainnet' | 'testnet';
185
80
  export interface TonConnectMobileConfig {
186
81
  /** Manifest URL (required) */
187
82
  manifestUrl: string;
188
- /** Deep link scheme for callbacks (required) */
83
+ /** Deep link scheme for callbacks (required for return strategy) */
189
84
  scheme: string;
190
85
  /** Optional storage key prefix */
191
86
  storageKeyPrefix?: string;
@@ -193,29 +88,21 @@ export interface TonConnectMobileConfig {
193
88
  connectionTimeout?: number;
194
89
  /** Optional transaction timeout in ms (default: 300000 = 5 minutes) */
195
90
  transactionTimeout?: number;
196
- /** Skip canOpenURL check before opening URL (optional, default: true)
197
- * Set to false if you want to check if URL can be opened before attempting to open it.
198
- * Note: On Android, canOpenURL may return false for tonconnect:// even if wallet is installed.
199
- */
91
+ /** Skip canOpenURL check (default: true) */
200
92
  skipCanOpenURLCheck?: boolean;
201
- /** Preferred wallet name (optional)
202
- * If not specified, will use default wallet (Tonkeeper)
203
- * Available: 'Tonkeeper', 'MyTonWallet', 'Wallet in Telegram', 'Tonhub'
204
- */
93
+ /** Preferred wallet name */
205
94
  preferredWallet?: string;
206
- /** Network (mainnet/testnet) - default: 'mainnet' */
95
+ /** Network (default: 'mainnet') */
207
96
  network?: Network;
208
- /** TON API endpoint for balance checking (optional)
209
- * Default: 'https://toncenter.com/api/v2' for mainnet, 'https://testnet.toncenter.com/api/v2' for testnet
210
- */
97
+ /** Custom TON API endpoint */
211
98
  tonApiEndpoint?: string;
212
99
  }
213
100
  /**
214
- * Event listener callback type
101
+ * Status change callback
215
102
  */
216
103
  export type StatusChangeCallback = (status: ConnectionStatus) => void;
217
104
  /**
218
- * Event types for TonConnectMobile
105
+ * Event types
219
106
  */
220
107
  export type TonConnectEventType = 'connect' | 'disconnect' | 'transaction' | 'error' | 'statusChange';
221
108
  /**
@@ -230,23 +117,81 @@ export type TransactionStatus = 'pending' | 'confirmed' | 'failed' | 'unknown';
230
117
  * Transaction status response
231
118
  */
232
119
  export interface TransactionStatusResponse {
233
- /** Transaction status */
234
120
  status: TransactionStatus;
235
- /** Transaction hash (if available) */
236
121
  hash?: string;
237
- /** Block number (if confirmed) */
238
122
  blockNumber?: number;
239
- /** Error message (if failed) */
240
123
  error?: string;
241
124
  }
242
125
  /**
243
126
  * Balance response
244
127
  */
245
128
  export interface BalanceResponse {
246
- /** Balance in nanotons */
247
129
  balance: string;
248
- /** Balance in TON (formatted) */
249
130
  balanceTon: string;
250
- /** Network */
251
131
  network: Network;
252
132
  }
133
+ /**
134
+ * Connect event from wallet (received via bridge after decryption)
135
+ */
136
+ export interface ConnectEvent {
137
+ event: 'connect';
138
+ id: number;
139
+ payload: {
140
+ items: Array<{
141
+ name: string;
142
+ address: string;
143
+ network: string;
144
+ publicKey: string;
145
+ walletStateInit?: string;
146
+ [key: string]: any;
147
+ }>;
148
+ device: {
149
+ platform: string;
150
+ appName: string;
151
+ appVersion: string;
152
+ maxProtocolVersion: number;
153
+ features: any[];
154
+ };
155
+ };
156
+ }
157
+ /**
158
+ * Connect error event from wallet
159
+ */
160
+ export interface ConnectErrorEvent {
161
+ event: 'connect_error';
162
+ id: number;
163
+ payload: {
164
+ code: number;
165
+ message: string;
166
+ };
167
+ }
168
+ /**
169
+ * JSON-RPC response (success)
170
+ */
171
+ export interface RpcResponse {
172
+ result: string;
173
+ id: number;
174
+ }
175
+ /**
176
+ * JSON-RPC response (error)
177
+ */
178
+ export interface RpcErrorResponse {
179
+ error: {
180
+ code: number;
181
+ message: string;
182
+ };
183
+ id: number;
184
+ }
185
+ /**
186
+ * Persisted session data
187
+ */
188
+ export interface PersistedSession {
189
+ /** Hex-encoded session secret key */
190
+ sessionSecretKey: string;
191
+ /** Hex-encoded wallet public key (from bridge "from" field) */
192
+ walletPublicKey: string;
193
+ /** Wallet bridge URL */
194
+ bridgeUrl: string;
195
+ /** Wallet info */
196
+ wallet: WalletInfo;
197
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Core types for TON Connect Mobile SDK
4
- * These types define the protocol structure for TonConnect
4
+ * Types for TON Connect v2 protocol
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazium/ton-connect-mobile",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
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",
@@ -66,5 +66,4 @@
66
66
  "dist",
67
67
  "src"
68
68
  ]
69
- }
70
-
69
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * TON Connect v2 Bridge Gateway
3
+ * Handles SSE connection for receiving wallet responses and POST for sending messages
4
+ */
5
+
6
+ import { bytesToBase64, base64ToBytes } from './session';
7
+
8
+ // Type declarations
9
+ declare const XMLHttpRequest: {
10
+ new (): XMLHttpRequestInstance;
11
+ } | undefined;
12
+
13
+ interface XMLHttpRequestInstance {
14
+ readyState: number;
15
+ responseText: string;
16
+ status: number;
17
+ open(method: string, url: string, async?: boolean): void;
18
+ setRequestHeader(name: string, value: string): void;
19
+ send(data?: string | null): void;
20
+ abort(): void;
21
+ onreadystatechange: (() => void) | null;
22
+ onerror: (() => void) | null;
23
+ }
24
+
25
+ /**
26
+ * Parsed SSE event
27
+ */
28
+ interface SSEEvent {
29
+ id?: string;
30
+ data?: string;
31
+ }
32
+
33
+ /**
34
+ * Bridge message received from wallet
35
+ */
36
+ export interface BridgeIncomingMessage {
37
+ from: string; // hex-encoded sender public key
38
+ message: string; // base64-encoded encrypted message
39
+ }
40
+
41
+ /**
42
+ * Bridge Gateway for TON Connect v2 HTTP Bridge
43
+ */
44
+ export class BridgeGateway {
45
+ private xhr: XMLHttpRequestInstance | null = null;
46
+ private lastEventId: string = '';
47
+ private active: boolean = false;
48
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
49
+ private bridgeUrl: string = '';
50
+ private clientId: string = '';
51
+ private onMessageCallback: ((msg: BridgeIncomingMessage) => void) | null = null;
52
+ private onErrorCallback: ((error: Error) => void) | null = null;
53
+
54
+ /**
55
+ * Connect to the bridge SSE endpoint
56
+ * Listens for incoming messages from the wallet
57
+ */
58
+ connect(
59
+ bridgeUrl: string,
60
+ clientId: string,
61
+ onMessage: (msg: BridgeIncomingMessage) => void,
62
+ onError?: (error: Error) => void
63
+ ): void {
64
+ this.bridgeUrl = bridgeUrl;
65
+ this.clientId = clientId;
66
+ this.onMessageCallback = onMessage;
67
+ this.onErrorCallback = onError || null;
68
+ this.active = true;
69
+ this.openSSE();
70
+ }
71
+
72
+ /**
73
+ * Open SSE connection using XMLHttpRequest (works in React Native)
74
+ */
75
+ private openSSE(): void {
76
+ if (!this.active) return;
77
+
78
+ // Build URL
79
+ let url = `${this.bridgeUrl}/events?client_id=${this.clientId}`;
80
+ if (this.lastEventId) {
81
+ url += `&last_event_id=${this.lastEventId}`;
82
+ }
83
+
84
+ console.log('[Bridge] Opening SSE connection:', url);
85
+
86
+ // Use XMLHttpRequest for SSE (available in React Native)
87
+ if (typeof XMLHttpRequest === 'undefined') {
88
+ // Fallback: use fetch-based polling
89
+ this.pollWithFetch(url);
90
+ return;
91
+ }
92
+
93
+ const xhr = new XMLHttpRequest();
94
+ this.xhr = xhr;
95
+
96
+ let processedLength = 0;
97
+ let buffer = '';
98
+
99
+ xhr.onreadystatechange = () => {
100
+ // LOADING (3) or DONE (4) — process incoming data
101
+ if (xhr.readyState >= 3) {
102
+ try {
103
+ const newData = xhr.responseText.substring(processedLength);
104
+ processedLength = xhr.responseText.length;
105
+
106
+ if (newData) {
107
+ buffer += newData;
108
+ const parsed = this.parseSSE(buffer);
109
+ buffer = parsed.remaining;
110
+
111
+ for (const event of parsed.events) {
112
+ if (event.id) {
113
+ this.lastEventId = event.id;
114
+ }
115
+ if (event.data) {
116
+ this.handleEventData(event.data);
117
+ }
118
+ }
119
+ }
120
+ } catch (e) {
121
+ // Ignore parse errors, continue listening
122
+ console.warn('[Bridge] SSE parse error:', e);
123
+ }
124
+ }
125
+
126
+ // Connection closed (readyState 4) — reconnect if still active
127
+ if (xhr.readyState === 4 && this.active) {
128
+ console.log('[Bridge] SSE connection closed, reconnecting...');
129
+ this.scheduleReconnect();
130
+ }
131
+ };
132
+
133
+ xhr.onerror = () => {
134
+ console.error('[Bridge] SSE connection error');
135
+ if (this.active) {
136
+ this.scheduleReconnect();
137
+ }
138
+ };
139
+
140
+ xhr.open('GET', url, true);
141
+ xhr.setRequestHeader('Accept', 'text/event-stream');
142
+ xhr.send();
143
+ }
144
+
145
+ /**
146
+ * Fallback: poll bridge with fetch (for environments without XMLHttpRequest streaming)
147
+ */
148
+ private async pollWithFetch(url: string): Promise<void> {
149
+ while (this.active) {
150
+ try {
151
+ let pollUrl = `${this.bridgeUrl}/events?client_id=${this.clientId}`;
152
+ if (this.lastEventId) {
153
+ pollUrl += `&last_event_id=${this.lastEventId}`;
154
+ }
155
+
156
+ const controller = new AbortController();
157
+ const timeoutId = setTimeout(() => controller.abort(), 25000);
158
+
159
+ const response = await fetch(pollUrl, {
160
+ headers: { 'Accept': 'text/event-stream' },
161
+ signal: controller.signal,
162
+ });
163
+ clearTimeout(timeoutId);
164
+
165
+ const text = await response.text();
166
+ const parsed = this.parseSSE(text);
167
+
168
+ for (const event of parsed.events) {
169
+ if (event.id) {
170
+ this.lastEventId = event.id;
171
+ }
172
+ if (event.data) {
173
+ this.handleEventData(event.data);
174
+ }
175
+ }
176
+ } catch (error: any) {
177
+ if (error?.name === 'AbortError') {
178
+ // Timeout — reconnect
179
+ continue;
180
+ }
181
+ console.error('[Bridge] Fetch poll error:', error);
182
+ // Wait before retrying
183
+ await new Promise<void>((resolve) => setTimeout(() => resolve(), 2000));
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Handle a single SSE event data
190
+ */
191
+ private handleEventData(data: string): void {
192
+ try {
193
+ const parsed = JSON.parse(data) as BridgeIncomingMessage;
194
+ if (parsed.from && parsed.message) {
195
+ console.log('[Bridge] Received message from:', parsed.from.substring(0, 16) + '...');
196
+ if (this.onMessageCallback) {
197
+ this.onMessageCallback(parsed);
198
+ }
199
+ }
200
+ } catch (e) {
201
+ // Might be a heartbeat or other non-JSON data — ignore
202
+ console.log('[Bridge] Non-message event:', data.substring(0, 50));
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Schedule reconnection after a delay
208
+ */
209
+ private scheduleReconnect(): void {
210
+ if (this.reconnectTimer) {
211
+ clearTimeout(this.reconnectTimer);
212
+ }
213
+ this.reconnectTimer = setTimeout(() => {
214
+ if (this.active) {
215
+ this.openSSE();
216
+ }
217
+ }, 1500);
218
+ }
219
+
220
+ /**
221
+ * Send an encrypted message via the bridge
222
+ */
223
+ async send(
224
+ bridgeUrl: string,
225
+ fromClientId: string,
226
+ toClientId: string,
227
+ encryptedMessage: Uint8Array,
228
+ ttl: number = 300
229
+ ): Promise<void> {
230
+ const base64Message = bytesToBase64(encryptedMessage);
231
+ const url = `${bridgeUrl}/message?client_id=${fromClientId}&to=${toClientId}&ttl=${ttl}`;
232
+
233
+ console.log('[Bridge] Sending message to:', toClientId.substring(0, 16) + '...');
234
+
235
+ const response = await fetch(url, {
236
+ method: 'POST',
237
+ body: base64Message,
238
+ headers: {
239
+ 'Content-Type': 'text/plain',
240
+ },
241
+ });
242
+
243
+ if (!response.ok) {
244
+ throw new Error(`Bridge send failed: ${response.status} ${response.statusText}`);
245
+ }
246
+
247
+ console.log('[Bridge] Message sent successfully');
248
+ }
249
+
250
+ /**
251
+ * Close the bridge connection
252
+ */
253
+ close(): void {
254
+ console.log('[Bridge] Closing connection');
255
+ this.active = false;
256
+
257
+ if (this.xhr) {
258
+ this.xhr.abort();
259
+ this.xhr = null;
260
+ }
261
+
262
+ if (this.reconnectTimer) {
263
+ clearTimeout(this.reconnectTimer);
264
+ this.reconnectTimer = null;
265
+ }
266
+
267
+ this.onMessageCallback = null;
268
+ this.onErrorCallback = null;
269
+ }
270
+
271
+ /**
272
+ * Check if bridge is currently connected/active
273
+ */
274
+ get isConnected(): boolean {
275
+ return this.active;
276
+ }
277
+
278
+ /**
279
+ * Parse SSE text into events
280
+ */
281
+ private parseSSE(text: string): { events: SSEEvent[]; remaining: string } {
282
+ const events: SSEEvent[] = [];
283
+ const parts = text.split('\n\n');
284
+ const remaining = parts.pop() || '';
285
+
286
+ for (const part of parts) {
287
+ if (!part.trim()) continue;
288
+
289
+ const event: SSEEvent = {};
290
+ const lines = part.split('\n');
291
+
292
+ for (const line of lines) {
293
+ if (line.startsWith('id:')) {
294
+ event.id = line.substring(3).trim();
295
+ } else if (line.startsWith('data:')) {
296
+ event.data = (event.data || '') + line.substring(5).trim();
297
+ }
298
+ }
299
+
300
+ if (event.data || event.id) {
301
+ events.push(event);
302
+ }
303
+ }
304
+
305
+ return { events, remaining };
306
+ }
307
+ }