@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for TonConnect
|
|
3
|
+
* Uses tweetnacl for signature verification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Type declarations for runtime globals
|
|
7
|
+
declare const console: {
|
|
8
|
+
warn(...args: unknown[]): void;
|
|
9
|
+
error(...args: unknown[]): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
declare const crypto: {
|
|
13
|
+
getRandomValues(array: Uint8Array): Uint8Array;
|
|
14
|
+
} | undefined;
|
|
15
|
+
|
|
16
|
+
declare const require: {
|
|
17
|
+
(id: string): any;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
21
|
+
const nacl = require('tweetnacl');
|
|
22
|
+
import { ConnectionResponsePayload } from '../types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decode base64 string to Uint8Array
|
|
26
|
+
*/
|
|
27
|
+
function decodeBase64(base64: string): Uint8Array {
|
|
28
|
+
// Remove padding and convert URL-safe to standard base64
|
|
29
|
+
const cleanBase64 = base64.replace(/-/g, '+').replace(/_/g, '/');
|
|
30
|
+
const padded = cleanBase64 + '='.repeat((4 - (cleanBase64.length % 4)) % 4);
|
|
31
|
+
|
|
32
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
33
|
+
const bytes: number[] = [];
|
|
34
|
+
let buffer = 0;
|
|
35
|
+
let bitsCollected = 0;
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < padded.length; i++) {
|
|
38
|
+
const ch = padded[i];
|
|
39
|
+
if (ch === '=') break;
|
|
40
|
+
|
|
41
|
+
const index = chars.indexOf(ch);
|
|
42
|
+
if (index === -1) continue;
|
|
43
|
+
|
|
44
|
+
buffer = (buffer << 6) | index;
|
|
45
|
+
bitsCollected += 6;
|
|
46
|
+
|
|
47
|
+
if (bitsCollected >= 8) {
|
|
48
|
+
bitsCollected -= 8;
|
|
49
|
+
bytes.push((buffer >> bitsCollected) & 0xff);
|
|
50
|
+
buffer &= (1 << bitsCollected) - 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Uint8Array(bytes);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* TextEncoder type declaration
|
|
59
|
+
*/
|
|
60
|
+
declare const TextEncoder: {
|
|
61
|
+
new (): {
|
|
62
|
+
encode(input: string): Uint8Array;
|
|
63
|
+
};
|
|
64
|
+
} | undefined;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get TextEncoder with fallback
|
|
68
|
+
*/
|
|
69
|
+
function getTextEncoder(): { encode(input: string): Uint8Array } {
|
|
70
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
71
|
+
return new TextEncoder();
|
|
72
|
+
}
|
|
73
|
+
// Fallback implementation for older React Native
|
|
74
|
+
throw new Error('TextEncoder is not available. Please use React Native 0.59+ or add a polyfill.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Verify connection proof signature
|
|
79
|
+
* The proof is signed by the wallet to verify authenticity
|
|
80
|
+
*/
|
|
81
|
+
export function verifyConnectionProof(
|
|
82
|
+
response: ConnectionResponsePayload,
|
|
83
|
+
manifestUrl: string
|
|
84
|
+
): boolean {
|
|
85
|
+
// HIGH FIX: Log warning if proof is missing but allow for compatibility
|
|
86
|
+
if (!response.proof) {
|
|
87
|
+
console.warn('TON Connect: Connection proof missing - wallet may not support proof verification');
|
|
88
|
+
// Allow connection for compatibility, but log warning
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const { timestamp, domain, signature } = response.proof;
|
|
94
|
+
|
|
95
|
+
// Validate proof structure
|
|
96
|
+
if (typeof timestamp !== 'number' || !domain || typeof domain.lengthBytes !== 'number' || typeof domain.value !== 'string' || typeof signature !== 'string') {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Build the message that was signed
|
|
101
|
+
// Format: <timestamp>.<domain_length>.<domain_value>.<address>.<publicKey>
|
|
102
|
+
const domainLength = domain.lengthBytes;
|
|
103
|
+
const message = `${timestamp}.${domainLength}.${domain.value}.${response.address}.${response.publicKey}`;
|
|
104
|
+
|
|
105
|
+
// Convert public key from hex to Uint8Array
|
|
106
|
+
const publicKeyBytes = hexToBytes(response.publicKey);
|
|
107
|
+
if (publicKeyBytes.length !== 32) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Convert signature from base64 to Uint8Array
|
|
112
|
+
const signatureBytes = decodeBase64(signature);
|
|
113
|
+
if (signatureBytes.length !== 64) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Verify signature using nacl
|
|
118
|
+
const encoder = getTextEncoder();
|
|
119
|
+
const messageBytes = encoder.encode(message);
|
|
120
|
+
return nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// If verification fails, return false
|
|
123
|
+
console.error('TON Connect: Proof verification error:', error);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Verify transaction signature
|
|
130
|
+
*
|
|
131
|
+
* WARNING: This function only performs basic format validation.
|
|
132
|
+
* Full signature verification requires parsing the BOC (Bag of Cells) and
|
|
133
|
+
* verifying the signature against the transaction hash, which requires
|
|
134
|
+
* TON library integration (@ton/core or @ton/crypto).
|
|
135
|
+
*
|
|
136
|
+
* For production use, transaction signatures should be verified server-side
|
|
137
|
+
* using proper TON libraries.
|
|
138
|
+
*
|
|
139
|
+
* @returns false - Always returns false to be safe until proper implementation
|
|
140
|
+
*/
|
|
141
|
+
export function verifyTransactionSignature(
|
|
142
|
+
boc: string,
|
|
143
|
+
signature: string,
|
|
144
|
+
publicKey: string
|
|
145
|
+
): boolean {
|
|
146
|
+
// CRITICAL FIX: This function does not actually verify signatures
|
|
147
|
+
// It only checks format. For security, we return false until proper implementation.
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// Basic format validation
|
|
151
|
+
if (!boc || typeof boc !== 'string' || boc.length === 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (!signature || typeof signature !== 'string' || signature.length === 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (!publicKey || typeof publicKey !== 'string' || publicKey.length === 0) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Convert public key from hex to Uint8Array
|
|
162
|
+
const publicKeyBytes = hexToBytes(publicKey);
|
|
163
|
+
if (publicKeyBytes.length !== 32) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Convert signature from base64 to Uint8Array
|
|
168
|
+
const signatureBytes = decodeBase64(signature);
|
|
169
|
+
if (signatureBytes.length !== 64) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Convert BOC from base64 to Uint8Array
|
|
174
|
+
const bocBytes = decodeBase64(boc);
|
|
175
|
+
if (bocBytes.length === 0) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// CRITICAL: Return false - actual signature verification requires TON library
|
|
180
|
+
// TODO: Integrate @ton/core or @ton/crypto for proper BOC parsing and signature verification
|
|
181
|
+
console.warn('TON Connect: Transaction signature verification not fully implemented. Signature format is valid but not cryptographically verified. Verify server-side using @ton/core.');
|
|
182
|
+
return false; // Fail-safe: reject until properly implemented
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('TON Connect: Transaction signature verification error:', error);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Convert hex string to Uint8Array
|
|
191
|
+
*/
|
|
192
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
193
|
+
// Remove 0x prefix if present
|
|
194
|
+
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
195
|
+
|
|
196
|
+
// Handle odd-length hex strings
|
|
197
|
+
const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex;
|
|
198
|
+
|
|
199
|
+
const bytes = new Uint8Array(paddedHex.length / 2);
|
|
200
|
+
for (let i = 0; i < paddedHex.length; i += 2) {
|
|
201
|
+
bytes[i / 2] = parseInt(paddedHex.substr(i, 2), 16);
|
|
202
|
+
}
|
|
203
|
+
return bytes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate cryptographically secure random bytes
|
|
208
|
+
*/
|
|
209
|
+
function getSecureRandomBytes(length: number): Uint8Array {
|
|
210
|
+
const bytes = new Uint8Array(length);
|
|
211
|
+
|
|
212
|
+
// Try to use crypto.getRandomValues (available in React Native with polyfill)
|
|
213
|
+
// eslint-disable-next-line no-undef
|
|
214
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).crypto && (globalThis as any).crypto.getRandomValues) {
|
|
215
|
+
(globalThis as any).crypto.getRandomValues(bytes);
|
|
216
|
+
return bytes;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// HIGH FIX: Throw error instead of using insecure Math.random()
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Cryptographically secure random number generation not available. ' +
|
|
222
|
+
'Please install react-native-get-random-values or use React Native 0.59+'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate random session ID
|
|
228
|
+
*/
|
|
229
|
+
export function generateSessionId(): string {
|
|
230
|
+
// HIGH FIX: Use secure random bytes
|
|
231
|
+
const bytes = getSecureRandomBytes(32);
|
|
232
|
+
|
|
233
|
+
// Convert to hex string
|
|
234
|
+
return Array.from(bytes)
|
|
235
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
236
|
+
.join('');
|
|
237
|
+
}
|
|
238
|
+
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core TonConnect protocol implementation
|
|
3
|
+
* Pure TypeScript, no platform dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Type declarations for runtime globals
|
|
7
|
+
declare const TextEncoder: {
|
|
8
|
+
new (): {
|
|
9
|
+
encode(input: string): Uint8Array;
|
|
10
|
+
};
|
|
11
|
+
} | undefined;
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ConnectionRequestPayload,
|
|
15
|
+
ConnectionResponsePayload,
|
|
16
|
+
TransactionRequestPayload,
|
|
17
|
+
TransactionResponsePayload,
|
|
18
|
+
ErrorResponse,
|
|
19
|
+
WalletInfo,
|
|
20
|
+
TransactionMessage,
|
|
21
|
+
SendTransactionRequest,
|
|
22
|
+
} from '../types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* TonConnect protocol constants
|
|
26
|
+
*/
|
|
27
|
+
const PROTOCOL_VERSION = '2';
|
|
28
|
+
const CONNECT_PREFIX = 'tonconnect://connect';
|
|
29
|
+
const SEND_TRANSACTION_PREFIX = 'tonconnect://send-transaction';
|
|
30
|
+
const CALLBACK_PREFIX = 'tonconnect';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get TextEncoder with availability check
|
|
34
|
+
*/
|
|
35
|
+
function getTextEncoder(): { encode(input: string): Uint8Array } {
|
|
36
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
37
|
+
return new TextEncoder();
|
|
38
|
+
}
|
|
39
|
+
throw new Error('TextEncoder is not available. Please use React Native 0.59+ or add a polyfill.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Encode string to base64
|
|
44
|
+
*/
|
|
45
|
+
function base64Encode(str: string): string {
|
|
46
|
+
// Use TextEncoder to convert string to bytes
|
|
47
|
+
const encoder = getTextEncoder();
|
|
48
|
+
const bytes = encoder.encode(str);
|
|
49
|
+
|
|
50
|
+
// Convert bytes to base64
|
|
51
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
52
|
+
let result = '';
|
|
53
|
+
let i = 0;
|
|
54
|
+
|
|
55
|
+
while (i < bytes.length) {
|
|
56
|
+
const a = bytes[i++];
|
|
57
|
+
const b = i < bytes.length ? bytes[i++] : 0;
|
|
58
|
+
const c = i < bytes.length ? bytes[i++] : 0;
|
|
59
|
+
|
|
60
|
+
const bitmap = (a << 16) | (b << 8) | c;
|
|
61
|
+
|
|
62
|
+
result += chars.charAt((bitmap >> 18) & 63);
|
|
63
|
+
result += chars.charAt((bitmap >> 12) & 63);
|
|
64
|
+
result += i - 2 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
|
|
65
|
+
result += i - 1 < bytes.length ? chars.charAt(bitmap & 63) : '=';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decode base64 to string
|
|
73
|
+
*/
|
|
74
|
+
function base64Decode(base64: string): string {
|
|
75
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
76
|
+
let buffer = 0;
|
|
77
|
+
let bitsCollected = 0;
|
|
78
|
+
let result = '';
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < base64.length; i++) {
|
|
81
|
+
const ch = base64[i];
|
|
82
|
+
if (ch === '=') break;
|
|
83
|
+
|
|
84
|
+
const index = chars.indexOf(ch);
|
|
85
|
+
if (index === -1) continue;
|
|
86
|
+
|
|
87
|
+
buffer = (buffer << 6) | index;
|
|
88
|
+
bitsCollected += 6;
|
|
89
|
+
|
|
90
|
+
if (bitsCollected >= 8) {
|
|
91
|
+
bitsCollected -= 8;
|
|
92
|
+
result += String.fromCharCode((buffer >> bitsCollected) & 0xff);
|
|
93
|
+
buffer &= (1 << bitsCollected) - 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Encode JSON to base64 URL-safe string
|
|
102
|
+
*/
|
|
103
|
+
export function encodeBase64URL(data: unknown): string {
|
|
104
|
+
const json = JSON.stringify(data);
|
|
105
|
+
const base64 = base64Encode(json);
|
|
106
|
+
// Convert to URL-safe base64
|
|
107
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decode base64 URL-safe string to JSON
|
|
112
|
+
*/
|
|
113
|
+
export function decodeBase64URL<T>(encoded: string): T {
|
|
114
|
+
// Convert from URL-safe base64
|
|
115
|
+
const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
|
116
|
+
// Add padding if needed
|
|
117
|
+
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
118
|
+
const json = base64Decode(padded);
|
|
119
|
+
return JSON.parse(json) as T;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build connection request URL
|
|
124
|
+
* Format: tonconnect://connect?<base64_encoded_payload>
|
|
125
|
+
*/
|
|
126
|
+
export function buildConnectionRequest(
|
|
127
|
+
manifestUrl: string,
|
|
128
|
+
returnScheme: string
|
|
129
|
+
): string {
|
|
130
|
+
const payload: ConnectionRequestPayload = {
|
|
131
|
+
manifestUrl,
|
|
132
|
+
items: [{ name: 'ton_addr' }],
|
|
133
|
+
returnStrategy: 'back',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const encoded = encodeBase64URL(payload);
|
|
137
|
+
return `${CONNECT_PREFIX}?${encoded}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build transaction request URL
|
|
142
|
+
* Format: tonconnect://send-transaction?<base64_encoded_payload>
|
|
143
|
+
*/
|
|
144
|
+
export function buildTransactionRequest(
|
|
145
|
+
manifestUrl: string,
|
|
146
|
+
request: SendTransactionRequest,
|
|
147
|
+
returnScheme: string
|
|
148
|
+
): string {
|
|
149
|
+
const payload: TransactionRequestPayload = {
|
|
150
|
+
manifestUrl,
|
|
151
|
+
request: {
|
|
152
|
+
validUntil: request.validUntil,
|
|
153
|
+
messages: request.messages.map((msg) => ({
|
|
154
|
+
address: msg.address,
|
|
155
|
+
amount: msg.amount,
|
|
156
|
+
payload: msg.payload,
|
|
157
|
+
stateInit: msg.stateInit,
|
|
158
|
+
})),
|
|
159
|
+
network: request.network,
|
|
160
|
+
from: request.from,
|
|
161
|
+
},
|
|
162
|
+
returnStrategy: 'back',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const encoded = encodeBase64URL(payload);
|
|
166
|
+
return `${SEND_TRANSACTION_PREFIX}?${encoded}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse callback URL
|
|
171
|
+
* Format: <scheme>://tonconnect?<base64_encoded_response>
|
|
172
|
+
*/
|
|
173
|
+
export function parseCallbackURL(url: string, scheme: string): {
|
|
174
|
+
type: 'connect' | 'transaction' | 'error' | 'unknown';
|
|
175
|
+
data: ConnectionResponsePayload | TransactionResponsePayload | ErrorResponse | null;
|
|
176
|
+
} {
|
|
177
|
+
try {
|
|
178
|
+
// CRITICAL FIX: Validate URL input
|
|
179
|
+
if (!url || typeof url !== 'string') {
|
|
180
|
+
return { type: 'unknown', data: null };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// CRITICAL FIX: Validate URL length (prevent DoS)
|
|
184
|
+
if (url.length > 10000) {
|
|
185
|
+
return { type: 'unknown', data: null };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// CRITICAL FIX: Validate scheme format
|
|
189
|
+
if (!scheme || typeof scheme !== 'string' || scheme.length === 0 || scheme.length > 50) {
|
|
190
|
+
return { type: 'unknown', data: null };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// CRITICAL FIX: Exact scheme matching (case-sensitive)
|
|
194
|
+
const expectedPrefix = `${scheme}://${CALLBACK_PREFIX}?`;
|
|
195
|
+
if (!url.startsWith(expectedPrefix)) {
|
|
196
|
+
return { type: 'unknown', data: null };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// CRITICAL FIX: Validate URL structure - should be exactly scheme://tonconnect?<payload>
|
|
200
|
+
// Check that there's no additional path or query params
|
|
201
|
+
const urlAfterScheme = url.substring(scheme.length + 3); // After "scheme://"
|
|
202
|
+
if (!urlAfterScheme.startsWith(`${CALLBACK_PREFIX}?`)) {
|
|
203
|
+
return { type: 'unknown', data: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extract encoded payload
|
|
207
|
+
const encoded = url.substring(expectedPrefix.length);
|
|
208
|
+
|
|
209
|
+
// CRITICAL FIX: Validate base64 payload size (prevent DoS)
|
|
210
|
+
if (encoded.length === 0 || encoded.length > 5000) {
|
|
211
|
+
return { type: 'unknown', data: null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// CRITICAL FIX: Validate base64 characters only
|
|
215
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
216
|
+
return { type: 'unknown', data: null };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const decoded = decodeBase64URL(encoded);
|
|
220
|
+
|
|
221
|
+
// Validate decoded data is an object
|
|
222
|
+
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded)) {
|
|
223
|
+
return { type: 'unknown', data: null };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if it's an error response
|
|
227
|
+
if ('error' in decoded && typeof decoded.error === 'object') {
|
|
228
|
+
const errorData = decoded as ErrorResponse;
|
|
229
|
+
if (errorData.error && typeof errorData.error.code === 'number' && typeof errorData.error.message === 'string') {
|
|
230
|
+
return { type: 'error', data: errorData };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check if it's a connection response (has session, address, publicKey)
|
|
235
|
+
if (
|
|
236
|
+
'session' in decoded &&
|
|
237
|
+
'address' in decoded &&
|
|
238
|
+
'publicKey' in decoded &&
|
|
239
|
+
typeof decoded.session === 'string' &&
|
|
240
|
+
typeof decoded.address === 'string' &&
|
|
241
|
+
typeof decoded.publicKey === 'string'
|
|
242
|
+
) {
|
|
243
|
+
return { type: 'connect', data: decoded as ConnectionResponsePayload };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if it's a transaction response (has boc, signature)
|
|
247
|
+
if (
|
|
248
|
+
'boc' in decoded &&
|
|
249
|
+
'signature' in decoded &&
|
|
250
|
+
typeof decoded.boc === 'string' &&
|
|
251
|
+
typeof decoded.signature === 'string'
|
|
252
|
+
) {
|
|
253
|
+
return { type: 'transaction', data: decoded as TransactionResponsePayload };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { type: 'unknown', data: null };
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Log error for debugging but don't expose details
|
|
259
|
+
return { type: 'unknown', data: null };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Extract wallet info from connection response
|
|
265
|
+
*/
|
|
266
|
+
export function extractWalletInfo(
|
|
267
|
+
response: ConnectionResponsePayload
|
|
268
|
+
): WalletInfo {
|
|
269
|
+
return {
|
|
270
|
+
name: response.name,
|
|
271
|
+
appName: response.appName,
|
|
272
|
+
version: response.version,
|
|
273
|
+
platform: response.platform || 'unknown',
|
|
274
|
+
address: response.address,
|
|
275
|
+
publicKey: response.publicKey,
|
|
276
|
+
icon: response.icon,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate connection response
|
|
282
|
+
*/
|
|
283
|
+
export function validateConnectionResponse(
|
|
284
|
+
response: ConnectionResponsePayload
|
|
285
|
+
): boolean {
|
|
286
|
+
return !!(
|
|
287
|
+
response.session &&
|
|
288
|
+
response.address &&
|
|
289
|
+
response.publicKey &&
|
|
290
|
+
response.name &&
|
|
291
|
+
response.appName &&
|
|
292
|
+
response.version
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Validate transaction response
|
|
298
|
+
*/
|
|
299
|
+
export function validateTransactionResponse(
|
|
300
|
+
response: TransactionResponsePayload
|
|
301
|
+
): boolean {
|
|
302
|
+
return !!(response.boc && response.signature);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Validate transaction request
|
|
307
|
+
*/
|
|
308
|
+
export function validateTransactionRequest(
|
|
309
|
+
request: SendTransactionRequest
|
|
310
|
+
): { valid: boolean; error?: string } {
|
|
311
|
+
if (!request.validUntil || request.validUntil <= Date.now()) {
|
|
312
|
+
return { valid: false, error: 'Transaction request has expired' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!request.messages || request.messages.length === 0) {
|
|
316
|
+
return { valid: false, error: 'Transaction must have at least one message' };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const msg of request.messages) {
|
|
320
|
+
if (!msg.address) {
|
|
321
|
+
return { valid: false, error: 'Message address is required' };
|
|
322
|
+
}
|
|
323
|
+
if (!msg.amount || isNaN(Number(msg.amount))) {
|
|
324
|
+
return { valid: false, error: 'Message amount must be a valid number' };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { valid: true };
|
|
329
|
+
}
|
|
330
|
+
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for runtime globals
|
|
3
|
+
* Separated from main file to avoid webpack parsing issues
|
|
4
|
+
* These are available in React Native and Node.js environments
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
declare const require: {
|
|
8
|
+
(id: string): any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
declare const console: {
|
|
12
|
+
warn(...args: unknown[]): void;
|
|
13
|
+
error(...args: unknown[]): void;
|
|
14
|
+
log(...args: unknown[]): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
declare function setTimeout(callback: () => void, delay: number): number;
|
|
18
|
+
declare function clearTimeout(timeoutId: number): void;
|
|
19
|
+
|