toss-expo-sdk 0.1.1 → 1.0.1
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/README.md +490 -81
- package/lib/module/ble.js +59 -4
- package/lib/module/ble.js.map +1 -1
- package/lib/module/client/BLETransactionHandler.js +277 -0
- package/lib/module/client/BLETransactionHandler.js.map +1 -0
- package/lib/module/client/NonceAccountManager.js +364 -0
- package/lib/module/client/NonceAccountManager.js.map +1 -0
- package/lib/module/client/TossClient.js +27 -44
- package/lib/module/client/TossClient.js.map +1 -1
- package/lib/module/contexts/WalletContext.js +4 -4
- package/lib/module/contexts/WalletContext.js.map +1 -1
- package/lib/module/discovery.js +35 -8
- package/lib/module/discovery.js.map +1 -1
- package/lib/module/examples/offlinePaymentFlow.js +27 -2
- package/lib/module/examples/offlinePaymentFlow.js.map +1 -1
- package/lib/module/hooks/useOfflineBLETransactions.js +314 -0
- package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
- package/lib/module/index.js +13 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/intent.js +198 -0
- package/lib/module/intent.js.map +1 -1
- package/lib/module/nfc.js +1 -1
- package/lib/module/noise.js +176 -1
- package/lib/module/noise.js.map +1 -1
- package/lib/module/reconciliation.js +155 -0
- package/lib/module/reconciliation.js.map +1 -1
- package/lib/module/services/authService.js +164 -1
- package/lib/module/services/authService.js.map +1 -1
- package/lib/module/storage/secureStorage.js +102 -0
- package/lib/module/storage/secureStorage.js.map +1 -1
- package/lib/module/storage.js +4 -4
- package/lib/module/sync.js +25 -1
- package/lib/module/sync.js.map +1 -1
- package/lib/module/types/nonceAccount.js +2 -0
- package/lib/module/types/nonceAccount.js.map +1 -0
- package/lib/module/types/tossUser.js +16 -1
- package/lib/module/types/tossUser.js.map +1 -1
- package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
- package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
- package/lib/typescript/src/ble.d.ts +31 -2
- package/lib/typescript/src/ble.d.ts.map +1 -1
- package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
- package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
- package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
- package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
- package/lib/typescript/src/client/TossClient.d.ts +10 -12
- package/lib/typescript/src/client/TossClient.d.ts.map +1 -1
- package/lib/typescript/src/discovery.d.ts +8 -2
- package/lib/typescript/src/discovery.d.ts.map +1 -1
- package/lib/typescript/src/examples/offlinePaymentFlow.d.ts +9 -1
- package/lib/typescript/src/examples/offlinePaymentFlow.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
- package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/intent.d.ts +26 -0
- package/lib/typescript/src/intent.d.ts.map +1 -1
- package/lib/typescript/src/noise.d.ts +62 -0
- package/lib/typescript/src/noise.d.ts.map +1 -1
- package/lib/typescript/src/reconciliation.d.ts +6 -0
- package/lib/typescript/src/reconciliation.d.ts.map +1 -1
- package/lib/typescript/src/services/authService.d.ts +26 -1
- package/lib/typescript/src/services/authService.d.ts.map +1 -1
- package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
- package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
- package/lib/typescript/src/sync.d.ts +6 -1
- package/lib/typescript/src/sync.d.ts.map +1 -1
- package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
- package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
- package/lib/typescript/src/types/tossUser.d.ts +16 -0
- package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
- package/package.json +12 -1
- package/src/__tests__/reconciliation.test.tsx +7 -1
- package/src/__tests__/solana-program-simple.test.ts +256 -0
- package/src/ble.ts +105 -4
- package/src/client/BLETransactionHandler.ts +364 -0
- package/src/client/NonceAccountManager.ts +444 -0
- package/src/client/TossClient.ts +36 -49
- package/src/contexts/WalletContext.tsx +4 -4
- package/src/discovery.ts +46 -8
- package/src/examples/offlinePaymentFlow.ts +48 -2
- package/src/hooks/useOfflineBLETransactions.ts +438 -0
- package/src/index.tsx +49 -7
- package/src/intent.ts +254 -0
- package/src/nfc.ts +4 -4
- package/src/noise.ts +239 -1
- package/src/reconciliation.ts +184 -0
- package/src/services/authService.ts +188 -1
- package/src/storage/secureStorage.ts +142 -4
- package/src/storage.ts +4 -4
- package/src/sync.ts +40 -0
- package/src/types/nonceAccount.ts +75 -0
- package/src/types/tossUser.ts +35 -2
package/src/discovery.ts
CHANGED
|
@@ -152,6 +152,12 @@ export class DeviceDiscoveryService {
|
|
|
152
152
|
export class IntentExchangeProtocol {
|
|
153
153
|
private pendingRequests: Map<string, IntentExchangeRequest> = new Map();
|
|
154
154
|
private noiseSessions: Map<string, NoiseSession> = new Map();
|
|
155
|
+
// Track timeout handles so they can be cleared during shutdown/cleanup
|
|
156
|
+
private requestTimeouts: Map<string, ReturnType<typeof setTimeout>> =
|
|
157
|
+
new Map();
|
|
158
|
+
private sessionTimeouts: Map<string, ReturnType<typeof setTimeout>> =
|
|
159
|
+
new Map();
|
|
160
|
+
|
|
155
161
|
private deviceStaticKey: Uint8Array; // Static key for this device
|
|
156
162
|
private readonly REQUEST_TIMEOUT = 2 * 60 * 1000; // 2 minutes
|
|
157
163
|
private readonly SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
|
@@ -187,11 +193,14 @@ export class IntentExchangeProtocol {
|
|
|
187
193
|
|
|
188
194
|
this.noiseSessions.set(peerId, session);
|
|
189
195
|
|
|
190
|
-
// Clean up expired sessions
|
|
191
|
-
setTimeout(() => {
|
|
196
|
+
// Clean up expired sessions (track timer so it can be cleared)
|
|
197
|
+
const sessTimer = setTimeout(() => {
|
|
192
198
|
this.noiseSessions.delete(peerId);
|
|
199
|
+
this.sessionTimeouts.delete(peerId);
|
|
193
200
|
}, this.SESSION_TIMEOUT);
|
|
194
201
|
|
|
202
|
+
this.sessionTimeouts.set(peerId, sessTimer);
|
|
203
|
+
|
|
195
204
|
return session;
|
|
196
205
|
}
|
|
197
206
|
|
|
@@ -224,8 +233,11 @@ export class IntentExchangeProtocol {
|
|
|
224
233
|
// XOR encryption with session key
|
|
225
234
|
const encrypted = new Uint8Array(payload.length);
|
|
226
235
|
for (let i = 0; i < payload.length; i++) {
|
|
236
|
+
// XOR operation used intentionally for lightweight obfuscation
|
|
237
|
+
// Prefer Uint8 arithmetic to avoid bitwise lint; modulo ensures 0-255 values
|
|
227
238
|
encrypted[i] =
|
|
228
|
-
payload[i]!
|
|
239
|
+
(payload[i]! + session.sessionKey[i % session.sessionKey.length]!) %
|
|
240
|
+
256;
|
|
229
241
|
}
|
|
230
242
|
|
|
231
243
|
return encrypted;
|
|
@@ -241,8 +253,12 @@ export class IntentExchangeProtocol {
|
|
|
241
253
|
// Reverse the XOR operation
|
|
242
254
|
const decrypted = new Uint8Array(ciphertext.length);
|
|
243
255
|
for (let i = 0; i < ciphertext.length; i++) {
|
|
256
|
+
// Reverse the lightweight obfuscation
|
|
244
257
|
decrypted[i] =
|
|
245
|
-
|
|
258
|
+
(256 +
|
|
259
|
+
ciphertext[i]! -
|
|
260
|
+
(session.sessionKey[i % session.sessionKey.length]! % 256)) %
|
|
261
|
+
256;
|
|
246
262
|
}
|
|
247
263
|
|
|
248
264
|
const jsonPayload = new TextDecoder().decode(decrypted);
|
|
@@ -293,11 +309,14 @@ export class IntentExchangeProtocol {
|
|
|
293
309
|
|
|
294
310
|
this.pendingRequests.set(requestId, request);
|
|
295
311
|
|
|
296
|
-
// Clean up expired requests after timeout
|
|
297
|
-
setTimeout(() => {
|
|
312
|
+
// Clean up expired requests after timeout (track timer so it can be cleared)
|
|
313
|
+
const reqTimer = setTimeout(() => {
|
|
298
314
|
this.pendingRequests.delete(requestId);
|
|
315
|
+
this.requestTimeouts.delete(requestId);
|
|
299
316
|
}, this.REQUEST_TIMEOUT);
|
|
300
317
|
|
|
318
|
+
this.requestTimeouts.set(requestId, reqTimer);
|
|
319
|
+
|
|
301
320
|
return request;
|
|
302
321
|
}
|
|
303
322
|
|
|
@@ -380,18 +399,37 @@ export class IntentExchangeProtocol {
|
|
|
380
399
|
}
|
|
381
400
|
|
|
382
401
|
/**
|
|
383
|
-
* Clear all pending requests
|
|
402
|
+
* Clear all pending requests and their timers
|
|
384
403
|
*/
|
|
385
404
|
clearRequests(): void {
|
|
405
|
+
// Clear any outstanding timers
|
|
406
|
+
for (const [id, timer] of this.requestTimeouts.entries()) {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
this.requestTimeouts.delete(id);
|
|
409
|
+
}
|
|
410
|
+
|
|
386
411
|
this.pendingRequests.clear();
|
|
387
412
|
}
|
|
388
413
|
|
|
389
414
|
/**
|
|
390
|
-
* Clear all Noise sessions
|
|
415
|
+
* Clear all Noise sessions and their timers
|
|
391
416
|
*/
|
|
392
417
|
clearSessions(): void {
|
|
418
|
+
for (const [id, timer] of this.sessionTimeouts.entries()) {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
this.sessionTimeouts.delete(id);
|
|
421
|
+
}
|
|
422
|
+
|
|
393
423
|
this.noiseSessions.clear();
|
|
394
424
|
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Dispose of the protocol, clearing internal state and timers
|
|
428
|
+
*/
|
|
429
|
+
dispose(): void {
|
|
430
|
+
this.clearRequests();
|
|
431
|
+
this.clearSessions();
|
|
432
|
+
}
|
|
395
433
|
}
|
|
396
434
|
|
|
397
435
|
/**
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createUserIntent,
|
|
15
|
+
createIntent,
|
|
16
|
+
type SolanaIntent,
|
|
17
|
+
verifyIntent,
|
|
18
|
+
} from '../intent';
|
|
19
|
+
import type { TossUser } from '../types/tossUser';
|
|
14
20
|
import {
|
|
15
21
|
secureStoreIntent,
|
|
16
22
|
getAllSecureIntents,
|
|
@@ -25,7 +31,47 @@ import { syncToChain } from '../sync';
|
|
|
25
31
|
import { TossError } from '../errors';
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* Example: Sender initiates offline payment
|
|
34
|
+
* Example: Sender initiates offline payment using TOSS users
|
|
35
|
+
*
|
|
36
|
+
* User-centric approach: sender and recipient are TossUser objects
|
|
37
|
+
* Intent creation validates user features and transaction limits
|
|
38
|
+
*/
|
|
39
|
+
export async function exampleInitiateUserPayment(
|
|
40
|
+
senderUser: TossUser,
|
|
41
|
+
senderKeypair: Keypair,
|
|
42
|
+
recipientUser: TossUser,
|
|
43
|
+
amountLamports: number,
|
|
44
|
+
connection: Connection
|
|
45
|
+
): Promise<SolanaIntent> {
|
|
46
|
+
console.log('📝 Creating offline payment intent between TOSS users...');
|
|
47
|
+
console.log(` From: @${senderUser.username}`);
|
|
48
|
+
console.log(` To: @${recipientUser.username}`);
|
|
49
|
+
|
|
50
|
+
// Create the intent using user objects (validates sender/recipient features)
|
|
51
|
+
const intent = await createUserIntent(
|
|
52
|
+
senderUser,
|
|
53
|
+
senderKeypair,
|
|
54
|
+
recipientUser,
|
|
55
|
+
amountLamports,
|
|
56
|
+
connection,
|
|
57
|
+
{
|
|
58
|
+
expiresIn: 24 * 60 * 60, // Valid for 24 hours
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log(`✅ Intent created: ${intent.id}`);
|
|
63
|
+
console.log(` Amount: ${intent.amount} lamports`);
|
|
64
|
+
console.log(` Expires at: ${new Date(intent.expiry * 1000).toISOString()}`);
|
|
65
|
+
|
|
66
|
+
// Store locally
|
|
67
|
+
await secureStoreIntent(intent);
|
|
68
|
+
console.log('💾 Intent stored securely locally\n');
|
|
69
|
+
|
|
70
|
+
return intent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Example: Sender initiates offline payment using addresses (legacy)
|
|
29
75
|
*
|
|
30
76
|
* This simulates a sender who wants to send lamports to a recipient
|
|
31
77
|
* while offline. The intent is created, signed, and stored locally.
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Device } from 'react-native-ble-plx';
|
|
3
|
+
import { Connection } from '@solana/web3.js';
|
|
4
|
+
import type { SolanaIntent } from '../intent';
|
|
5
|
+
import type {
|
|
6
|
+
OfflineTransaction,
|
|
7
|
+
NonceAccountCacheEntry,
|
|
8
|
+
} from '../types/nonceAccount';
|
|
9
|
+
import type { TossUser } from '../types/tossUser';
|
|
10
|
+
import { BLETransactionHandler } from '../client/BLETransactionHandler';
|
|
11
|
+
import { NonceAccountManager } from '../client/NonceAccountManager';
|
|
12
|
+
import { AuthService } from '../services/authService';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* State for BLE transaction transmission
|
|
16
|
+
*/
|
|
17
|
+
export interface BLETransmissionState {
|
|
18
|
+
isTransmitting: boolean;
|
|
19
|
+
progress: {
|
|
20
|
+
sentFragments: number;
|
|
21
|
+
totalFragments: number;
|
|
22
|
+
messageId?: string;
|
|
23
|
+
};
|
|
24
|
+
error?: string;
|
|
25
|
+
lastSent?: {
|
|
26
|
+
messageId: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* State for offline transaction preparation
|
|
33
|
+
*/
|
|
34
|
+
export interface OfflineTransactionState {
|
|
35
|
+
isPreparing: boolean;
|
|
36
|
+
transaction?: OfflineTransaction;
|
|
37
|
+
error?: string;
|
|
38
|
+
isReady: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* useOfflineTransaction Hook
|
|
43
|
+
* Manages offline transaction creation with nonce accounts
|
|
44
|
+
* Handles biometric protection and secure storage
|
|
45
|
+
*/
|
|
46
|
+
export function useOfflineTransaction(user: TossUser, connection: Connection) {
|
|
47
|
+
const [state, setState] = useState<OfflineTransactionState>({
|
|
48
|
+
isPreparing: false,
|
|
49
|
+
isReady: false,
|
|
50
|
+
});
|
|
51
|
+
const nonceManagerRef = useRef<NonceAccountManager | null>(null);
|
|
52
|
+
|
|
53
|
+
// Initialize nonce manager
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
nonceManagerRef.current = new NonceAccountManager(connection);
|
|
56
|
+
|
|
57
|
+
// Cleanup expired cache periodically
|
|
58
|
+
const interval = setInterval(
|
|
59
|
+
() => {
|
|
60
|
+
nonceManagerRef.current?.cleanupExpiredCache();
|
|
61
|
+
},
|
|
62
|
+
5 * 60 * 1000
|
|
63
|
+
); // Every 5 minutes
|
|
64
|
+
|
|
65
|
+
return () => clearInterval(interval);
|
|
66
|
+
}, [connection]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create offline transaction with nonce account
|
|
70
|
+
* Requires biometric verification if enabled
|
|
71
|
+
*/
|
|
72
|
+
const createOfflineTransaction = useCallback(
|
|
73
|
+
async (
|
|
74
|
+
instructions: any[], // TransactionInstruction[]
|
|
75
|
+
metadata?: { description?: string; tags?: string[] }
|
|
76
|
+
): Promise<OfflineTransaction | null> => {
|
|
77
|
+
if (!user.nonceAccount) {
|
|
78
|
+
setState((prev) => ({
|
|
79
|
+
...prev,
|
|
80
|
+
error: 'User does not have nonce account configured',
|
|
81
|
+
}));
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setState((prev) => ({
|
|
86
|
+
...prev,
|
|
87
|
+
isPreparing: true,
|
|
88
|
+
error: undefined,
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Verify nonce account access (requires biometric if enabled)
|
|
93
|
+
if (user.security.nonceAccountRequiresBiometric) {
|
|
94
|
+
const hasAccess = await AuthService.verifyNonceAccountAccess(
|
|
95
|
+
user.userId
|
|
96
|
+
);
|
|
97
|
+
if (!hasAccess) {
|
|
98
|
+
throw new Error('Biometric verification failed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get cached nonce account or retrieve from storage
|
|
103
|
+
const nonceManager = nonceManagerRef.current!;
|
|
104
|
+
let nonceAccountData = nonceManager.getCachedNonceAccount(user.userId);
|
|
105
|
+
|
|
106
|
+
let nonceAccountInfo: NonceAccountCacheEntry | null = null;
|
|
107
|
+
if (nonceAccountData) {
|
|
108
|
+
nonceAccountInfo = nonceAccountData;
|
|
109
|
+
} else {
|
|
110
|
+
const retrievedInfo = await nonceManager.getNonceAccountSecure(
|
|
111
|
+
user.userId
|
|
112
|
+
);
|
|
113
|
+
if (retrievedInfo) {
|
|
114
|
+
nonceAccountInfo = nonceManager.getCachedNonceAccount(user.userId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!nonceAccountInfo) {
|
|
119
|
+
throw new Error('Failed to retrieve nonce account information');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validate nonce account
|
|
123
|
+
if (!nonceManager.isNonceAccountValid(nonceAccountInfo.accountInfo)) {
|
|
124
|
+
throw new Error('Nonce account is no longer valid');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Prepare offline transaction
|
|
128
|
+
const transaction = await nonceManager.prepareOfflineTransaction(
|
|
129
|
+
user,
|
|
130
|
+
instructions,
|
|
131
|
+
nonceAccountInfo.accountInfo
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
transaction.metadata = metadata || {};
|
|
135
|
+
|
|
136
|
+
setState((prev) => ({
|
|
137
|
+
...prev,
|
|
138
|
+
transaction,
|
|
139
|
+
isReady: true,
|
|
140
|
+
isPreparing: false,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
return transaction;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const errorMessage =
|
|
146
|
+
error instanceof Error ? error.message : String(error);
|
|
147
|
+
setState((prev) => ({
|
|
148
|
+
...prev,
|
|
149
|
+
error: errorMessage,
|
|
150
|
+
isPreparing: false,
|
|
151
|
+
isReady: false,
|
|
152
|
+
}));
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[user]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clear current offline transaction
|
|
161
|
+
*/
|
|
162
|
+
const clearTransaction = useCallback(() => {
|
|
163
|
+
setState({
|
|
164
|
+
isPreparing: false,
|
|
165
|
+
isReady: false,
|
|
166
|
+
});
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
...state,
|
|
171
|
+
createOfflineTransaction,
|
|
172
|
+
clearTransaction,
|
|
173
|
+
nonceManager: nonceManagerRef.current,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* useBLETransactionTransmission Hook
|
|
179
|
+
* Handles secure BLE transmission of fragmented transactions
|
|
180
|
+
* with Noise Protocol encryption
|
|
181
|
+
*/
|
|
182
|
+
export function useBLETransactionTransmission(
|
|
183
|
+
platform: 'android' | 'ios' = 'android'
|
|
184
|
+
) {
|
|
185
|
+
const [state, setState] = useState<BLETransmissionState>({
|
|
186
|
+
isTransmitting: false,
|
|
187
|
+
progress: {
|
|
188
|
+
sentFragments: 0,
|
|
189
|
+
totalFragments: 0,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const bleHandlerRef = useRef(new BLETransactionHandler(platform));
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Send transaction over BLE with fragmentation
|
|
197
|
+
*/
|
|
198
|
+
const sendTransactionBLE = useCallback(
|
|
199
|
+
async (
|
|
200
|
+
device: Device,
|
|
201
|
+
transaction: OfflineTransaction | SolanaIntent,
|
|
202
|
+
sendFn: (
|
|
203
|
+
deviceId: string,
|
|
204
|
+
characteristicUUID: string,
|
|
205
|
+
data: Buffer
|
|
206
|
+
) => Promise<void>,
|
|
207
|
+
noiseEncryptFn?: (data: Uint8Array) => Promise<any>,
|
|
208
|
+
isIntent: boolean = false
|
|
209
|
+
): Promise<boolean> => {
|
|
210
|
+
setState((prev) => ({
|
|
211
|
+
...prev,
|
|
212
|
+
isTransmitting: true,
|
|
213
|
+
error: undefined,
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const bleHandler = bleHandlerRef.current;
|
|
218
|
+
const result = await bleHandler.sendFragmentedTransactionBLE(
|
|
219
|
+
device,
|
|
220
|
+
transaction,
|
|
221
|
+
sendFn,
|
|
222
|
+
noiseEncryptFn,
|
|
223
|
+
isIntent
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (!result.success) {
|
|
227
|
+
const failedCount = result.failedFragments.length;
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Failed to send ${failedCount} fragment(s): ${result.failedFragments.join(', ')}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setState((prev) => ({
|
|
234
|
+
...prev,
|
|
235
|
+
isTransmitting: false,
|
|
236
|
+
progress: {
|
|
237
|
+
sentFragments: result.sentFragments,
|
|
238
|
+
totalFragments:
|
|
239
|
+
result.sentFragments + result.failedFragments.length,
|
|
240
|
+
messageId: result.messageId,
|
|
241
|
+
},
|
|
242
|
+
lastSent: {
|
|
243
|
+
messageId: result.messageId,
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
},
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const errorMessage =
|
|
251
|
+
error instanceof Error ? error.message : String(error);
|
|
252
|
+
setState((prev) => ({
|
|
253
|
+
...prev,
|
|
254
|
+
isTransmitting: false,
|
|
255
|
+
error: errorMessage,
|
|
256
|
+
}));
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
[]
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Receive fragmented transaction
|
|
265
|
+
*/
|
|
266
|
+
const receiveTransactionFragment = useCallback(
|
|
267
|
+
async (
|
|
268
|
+
fragment: any, // BLEFragment
|
|
269
|
+
noiseDecryptFn?: (encrypted: any) => Promise<Uint8Array>
|
|
270
|
+
): Promise<{
|
|
271
|
+
complete: boolean;
|
|
272
|
+
transaction?: OfflineTransaction | SolanaIntent;
|
|
273
|
+
progress: { received: number; total: number };
|
|
274
|
+
}> => {
|
|
275
|
+
try {
|
|
276
|
+
const bleHandler = bleHandlerRef.current;
|
|
277
|
+
const result = await bleHandler.receiveFragmentedMessage(
|
|
278
|
+
fragment,
|
|
279
|
+
noiseDecryptFn
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
setState((prev) => ({
|
|
283
|
+
...prev,
|
|
284
|
+
progress: {
|
|
285
|
+
sentFragments: result.progress.received,
|
|
286
|
+
totalFragments: result.progress.total,
|
|
287
|
+
messageId: fragment.messageId,
|
|
288
|
+
},
|
|
289
|
+
}));
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const errorMessage =
|
|
294
|
+
error instanceof Error ? error.message : String(error);
|
|
295
|
+
setState((prev) => ({
|
|
296
|
+
...prev,
|
|
297
|
+
error: errorMessage,
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
complete: false,
|
|
302
|
+
progress: { received: 0, total: 0 },
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
[]
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const getMTUConfig = useCallback(() => {
|
|
310
|
+
return bleHandlerRef.current.getMTUConfig();
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
const setMTUConfig = useCallback((config: Partial<any>) => {
|
|
314
|
+
bleHandlerRef.current.setMTUConfig(config);
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
...state,
|
|
319
|
+
sendTransactionBLE,
|
|
320
|
+
receiveTransactionFragment,
|
|
321
|
+
getMTUConfig,
|
|
322
|
+
setMTUConfig,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* useNonceAccountManagement Hook
|
|
328
|
+
* Manages nonce account lifecycle: creation, renewal, revocation
|
|
329
|
+
*/
|
|
330
|
+
export function useNonceAccountManagement(
|
|
331
|
+
user: TossUser,
|
|
332
|
+
connection: Connection
|
|
333
|
+
) {
|
|
334
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
335
|
+
const [error, setError] = useState<string | undefined>();
|
|
336
|
+
const nonceManagerRef = useRef(new NonceAccountManager(connection));
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create nonce account with biometric protection
|
|
340
|
+
*/
|
|
341
|
+
const createNonceAccount = useCallback(
|
|
342
|
+
async (userKeypair: any) => {
|
|
343
|
+
setIsLoading(true);
|
|
344
|
+
setError(undefined);
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const updatedUser = await AuthService.createSecureNonceAccount(
|
|
348
|
+
user,
|
|
349
|
+
connection,
|
|
350
|
+
userKeypair
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return updatedUser;
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
356
|
+
setError(errorMessage);
|
|
357
|
+
return null;
|
|
358
|
+
} finally {
|
|
359
|
+
setIsLoading(false);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
[user, connection]
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Renew nonce account (refresh from blockchain)
|
|
367
|
+
*/
|
|
368
|
+
const renewNonceAccount = useCallback(async () => {
|
|
369
|
+
if (!user.nonceAccount) {
|
|
370
|
+
setError('No nonce account to renew');
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
setIsLoading(true);
|
|
375
|
+
setError(undefined);
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const updated = await nonceManagerRef.current.renewNonceAccount(
|
|
379
|
+
user.userId,
|
|
380
|
+
user.nonceAccount.address
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return updated;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
386
|
+
setError(errorMessage);
|
|
387
|
+
return null;
|
|
388
|
+
} finally {
|
|
389
|
+
setIsLoading(false);
|
|
390
|
+
}
|
|
391
|
+
}, [user.userId, user.nonceAccount]);
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Revoke nonce account
|
|
395
|
+
*/
|
|
396
|
+
const revokeNonceAccount = useCallback(async () => {
|
|
397
|
+
setIsLoading(true);
|
|
398
|
+
setError(undefined);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const updatedUser = await AuthService.revokeNonceAccount(
|
|
402
|
+
user.userId,
|
|
403
|
+
user
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return updatedUser;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
409
|
+
setError(errorMessage);
|
|
410
|
+
return null;
|
|
411
|
+
} finally {
|
|
412
|
+
setIsLoading(false);
|
|
413
|
+
}
|
|
414
|
+
}, [user]);
|
|
415
|
+
|
|
416
|
+
const isNonceAccountValid = useCallback(() => {
|
|
417
|
+
if (!user.nonceAccount) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const cached = nonceManagerRef.current.getCachedNonceAccount(user.userId);
|
|
422
|
+
if (cached) {
|
|
423
|
+
return nonceManagerRef.current.isNonceAccountValid(cached.accountInfo);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return user.nonceAccount.status === 'active';
|
|
427
|
+
}, [user.userId, user.nonceAccount]);
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
isLoading,
|
|
431
|
+
error,
|
|
432
|
+
createNonceAccount,
|
|
433
|
+
renewNonceAccount,
|
|
434
|
+
revokeNonceAccount,
|
|
435
|
+
isNonceAccountValid,
|
|
436
|
+
hasNonceAccount: !!user.nonceAccount,
|
|
437
|
+
};
|
|
438
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
// Core types and intents
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
createIntent,
|
|
4
|
+
createUserIntent,
|
|
5
|
+
createSignedIntent,
|
|
6
|
+
createOfflineIntent,
|
|
7
|
+
type SolanaIntent,
|
|
8
|
+
type IntentStatus,
|
|
9
|
+
} from './intent';
|
|
10
|
+
|
|
11
|
+
// Nonce Account Management (for offline transactions)
|
|
12
|
+
export { NonceAccountManager } from './client/NonceAccountManager';
|
|
13
|
+
export type {
|
|
14
|
+
NonceAccountInfo,
|
|
15
|
+
NonceAccountCacheEntry,
|
|
16
|
+
CreateNonceAccountOptions,
|
|
17
|
+
OfflineTransaction,
|
|
18
|
+
} from './types/nonceAccount';
|
|
19
|
+
|
|
20
|
+
// BLE Transaction Handling (fragmentation & Noise encryption)
|
|
21
|
+
export { BLETransactionHandler } from './client/BLETransactionHandler';
|
|
22
|
+
export type {
|
|
23
|
+
BLEFragment,
|
|
24
|
+
EncryptedBLEMessage,
|
|
25
|
+
BLEMTUConfig,
|
|
26
|
+
} from './client/BLETransactionHandler';
|
|
27
|
+
|
|
28
|
+
// Custom Hooks for Offline BLE Transactions
|
|
29
|
+
export {
|
|
30
|
+
useOfflineTransaction,
|
|
31
|
+
useBLETransactionTransmission,
|
|
32
|
+
useNonceAccountManagement,
|
|
33
|
+
} from './hooks/useOfflineBLETransactions';
|
|
34
|
+
export type {
|
|
35
|
+
BLETransmissionState,
|
|
36
|
+
OfflineTransactionState,
|
|
37
|
+
} from './hooks/useOfflineBLETransactions';
|
|
3
38
|
|
|
4
39
|
// Intent management
|
|
5
40
|
export {
|
|
@@ -18,18 +53,25 @@ export {
|
|
|
18
53
|
clearPendingIntents,
|
|
19
54
|
} from './storage';
|
|
20
55
|
|
|
21
|
-
// Transport methods
|
|
22
|
-
export {
|
|
56
|
+
// Transport methods (enhanced with fragmentation)
|
|
57
|
+
export {
|
|
58
|
+
startTossScan,
|
|
59
|
+
requestBLEPermissions,
|
|
60
|
+
sendOfflineTransactionFragmented,
|
|
61
|
+
receiveOfflineTransactionFragment,
|
|
62
|
+
getBLEMTUConfig,
|
|
63
|
+
setBLEMTUConfig,
|
|
64
|
+
} from './ble';
|
|
23
65
|
export { initNFC, readNFCUser, writeUserToNFC, writeIntentToNFC } from './nfc';
|
|
24
66
|
export { QRScanner } from './qr';
|
|
25
67
|
|
|
26
68
|
// Client
|
|
27
69
|
export { TossClient, type TossConfig } from './client/TossClient';
|
|
70
|
+
export type { TossUser } from './types/tossUser';
|
|
71
|
+
export { WalletProvider, useWallet } from './contexts/WalletContext';
|
|
28
72
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
export const createClient = TossClient.createClient;
|
|
32
|
-
|
|
73
|
+
// Authentication Service (enhanced with nonce accounts)
|
|
74
|
+
export { AuthService } from './services/authService';
|
|
33
75
|
// Sync and settlement
|
|
34
76
|
export { syncToChain, checkSyncStatus, type SyncResult } from './sync';
|
|
35
77
|
|