toss-expo-sdk 0.1.2 → 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.
Files changed (73) hide show
  1. package/README.md +368 -15
  2. package/lib/module/ble.js +59 -4
  3. package/lib/module/ble.js.map +1 -1
  4. package/lib/module/client/BLETransactionHandler.js +277 -0
  5. package/lib/module/client/BLETransactionHandler.js.map +1 -0
  6. package/lib/module/client/NonceAccountManager.js +364 -0
  7. package/lib/module/client/NonceAccountManager.js.map +1 -0
  8. package/lib/module/client/TossClient.js +1 -1
  9. package/lib/module/client/TossClient.js.map +1 -1
  10. package/lib/module/hooks/useOfflineBLETransactions.js +314 -0
  11. package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
  12. package/lib/module/index.js +12 -8
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/intent.js +129 -0
  15. package/lib/module/intent.js.map +1 -1
  16. package/lib/module/noise.js +175 -0
  17. package/lib/module/noise.js.map +1 -1
  18. package/lib/module/reconciliation.js +155 -0
  19. package/lib/module/reconciliation.js.map +1 -1
  20. package/lib/module/services/authService.js +164 -1
  21. package/lib/module/services/authService.js.map +1 -1
  22. package/lib/module/storage/secureStorage.js +102 -0
  23. package/lib/module/storage/secureStorage.js.map +1 -1
  24. package/lib/module/sync.js +25 -1
  25. package/lib/module/sync.js.map +1 -1
  26. package/lib/module/types/nonceAccount.js +2 -0
  27. package/lib/module/types/nonceAccount.js.map +1 -0
  28. package/lib/module/types/tossUser.js +16 -1
  29. package/lib/module/types/tossUser.js.map +1 -1
  30. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
  31. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
  32. package/lib/typescript/src/ble.d.ts +31 -2
  33. package/lib/typescript/src/ble.d.ts.map +1 -1
  34. package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
  35. package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
  36. package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
  37. package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
  38. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
  39. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +9 -4
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/intent.d.ts +15 -0
  43. package/lib/typescript/src/intent.d.ts.map +1 -1
  44. package/lib/typescript/src/noise.d.ts +62 -0
  45. package/lib/typescript/src/noise.d.ts.map +1 -1
  46. package/lib/typescript/src/reconciliation.d.ts +6 -0
  47. package/lib/typescript/src/reconciliation.d.ts.map +1 -1
  48. package/lib/typescript/src/services/authService.d.ts +26 -1
  49. package/lib/typescript/src/services/authService.d.ts.map +1 -1
  50. package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
  51. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
  52. package/lib/typescript/src/sync.d.ts +6 -1
  53. package/lib/typescript/src/sync.d.ts.map +1 -1
  54. package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
  55. package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
  56. package/lib/typescript/src/types/tossUser.d.ts +16 -0
  57. package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/__tests__/solana-program-simple.test.ts +256 -0
  60. package/src/ble.ts +105 -4
  61. package/src/client/BLETransactionHandler.ts +364 -0
  62. package/src/client/NonceAccountManager.ts +444 -0
  63. package/src/client/TossClient.ts +1 -1
  64. package/src/hooks/useOfflineBLETransactions.ts +438 -0
  65. package/src/index.tsx +40 -6
  66. package/src/intent.ts +166 -0
  67. package/src/noise.ts +238 -0
  68. package/src/reconciliation.ts +184 -0
  69. package/src/services/authService.ts +188 -1
  70. package/src/storage/secureStorage.ts +138 -0
  71. package/src/sync.ts +40 -0
  72. package/src/types/nonceAccount.ts +75 -0
  73. package/src/types/tossUser.ts +35 -2
@@ -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
@@ -3,10 +3,39 @@ export {
3
3
  createIntent,
4
4
  createUserIntent,
5
5
  createSignedIntent,
6
+ createOfflineIntent,
6
7
  type SolanaIntent,
7
8
  type IntentStatus,
8
9
  } from './intent';
9
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';
38
+
10
39
  // Intent management
11
40
  export {
12
41
  verifyIntentSignature,
@@ -24,8 +53,15 @@ export {
24
53
  clearPendingIntents,
25
54
  } from './storage';
26
55
 
27
- // Transport methods
28
- export { startTossScan, requestBLEPermissions } from './ble';
56
+ // Transport methods (enhanced with fragmentation)
57
+ export {
58
+ startTossScan,
59
+ requestBLEPermissions,
60
+ sendOfflineTransactionFragmented,
61
+ receiveOfflineTransactionFragment,
62
+ getBLEMTUConfig,
63
+ setBLEMTUConfig,
64
+ } from './ble';
29
65
  export { initNFC, readNFCUser, writeUserToNFC, writeIntentToNFC } from './nfc';
30
66
  export { QRScanner } from './qr';
31
67
 
@@ -34,10 +70,8 @@ export { TossClient, type TossConfig } from './client/TossClient';
34
70
  export type { TossUser } from './types/tossUser';
35
71
  export { WalletProvider, useWallet } from './contexts/WalletContext';
36
72
 
37
- // Create client instance
38
- import { TossClient } from './client/TossClient';
39
- export const createClient = TossClient.createClient;
40
-
73
+ // Authentication Service (enhanced with nonce accounts)
74
+ export { AuthService } from './services/authService';
41
75
  // Sync and settlement
42
76
  export { syncToChain, checkSyncStatus, type SyncResult } from './sync';
43
77
 
package/src/intent.ts CHANGED
@@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
5
5
  import { sign } from 'tweetnacl';
6
6
  import nacl from 'tweetnacl';
7
7
  import type { TossUser, TossUserContext } from './types/tossUser';
8
+ import type { OfflineTransaction } from './types/nonceAccount';
8
9
  import {
9
10
  encryptForArciumInternal,
10
11
  type ArciumEncryptedOutput,
@@ -18,6 +19,7 @@ export type IntentStatus = 'pending' | 'settled' | 'failed' | 'expired';
18
19
 
19
20
  /**
20
21
  * Core type for an offline intent following TOSS specification
22
+ * Enhanced with durable nonce account support
21
23
  */
22
24
  export interface SolanaIntent {
23
25
  // Core fields
@@ -43,6 +45,12 @@ export interface SolanaIntent {
43
45
  serialized?: string; // Optional: Serialized transaction
44
46
  nonceAccount?: string; // Optional: Public key of the nonce account
45
47
  nonceAuth?: string; // Optional: Public key authorized to use the nonce
48
+ nonceAccountAddress?: string; // Durable nonce account address (from nonce account)
49
+ nonceAccountAuth?: string; // Authority for durable nonce account
50
+
51
+ // Offline transaction support
52
+ offlineTransaction?: OfflineTransaction; // Associated offline transaction
53
+ requiresBiometric?: boolean; // Requires biometric to sign/execute
46
54
 
47
55
  // Privacy features
48
56
  encrypted?: ArciumEncryptedOutput; // Optional encrypted payload
@@ -143,6 +151,8 @@ export const nonceManager = new NonceManager();
143
151
  /**
144
152
  * Creates a signed intent between two TOSS users (User-centric API)
145
153
  * Recommended for application developers - validates user wallets
154
+ *
155
+ * GAP #8 FIX: Requires biometric authentication for sensitive transactions
146
156
  */
147
157
  export async function createUserIntent(
148
158
  senderUser: TossUser,
@@ -152,6 +162,29 @@ export async function createUserIntent(
152
162
  connection: Connection,
153
163
  options: CreateIntentOptions = {}
154
164
  ): Promise<SolanaIntent> {
165
+ // GAP #8 FIX: Require biometric verification if enabled
166
+ if (senderUser.security?.biometricEnabled) {
167
+ try {
168
+ const LocalAuthentication = await import('expo-local-authentication');
169
+ const compatible = await LocalAuthentication.default.hasHardwareAsync();
170
+ if (compatible) {
171
+ const authenticated =
172
+ await LocalAuthentication.default.authenticateAsync({
173
+ disableDeviceFallback: false,
174
+ });
175
+
176
+ if (!authenticated.success) {
177
+ throw new Error('Biometric authentication failed');
178
+ }
179
+ }
180
+ } catch (error) {
181
+ console.warn(
182
+ 'Biometric verification not available, proceeding without',
183
+ error
184
+ );
185
+ }
186
+ }
187
+
155
188
  // Verify sender's keypair matches their wallet
156
189
  if (
157
190
  senderKeypair.publicKey.toBase58() !==
@@ -414,3 +447,136 @@ export function updateIntentStatus(
414
447
  updatedAt: Math.floor(Date.now() / 1000),
415
448
  };
416
449
  }
450
+ /**
451
+ * Creates an offline intent with durable nonce account support
452
+ * Enables replay-protected offline transactions using nonce accounts
453
+ * Requires biometric authentication for enhanced security
454
+ */
455
+ export async function createOfflineIntent(
456
+ senderUser: TossUser,
457
+ senderKeypair: Keypair,
458
+ recipientUser: TossUser,
459
+ amount: number,
460
+ nonceAccountInfo: any, // NonceAccountInfo from NonceAccountManager
461
+ connection: Connection,
462
+ options: CreateIntentOptions = {}
463
+ ): Promise<SolanaIntent> {
464
+ // Verify sender has nonce account enabled
465
+ if (
466
+ !senderUser.nonceAccount ||
467
+ !senderUser.tossFeatures.offlineTransactionsEnabled
468
+ ) {
469
+ throw new Error('Offline transactions not enabled for this user');
470
+ }
471
+
472
+ // Verify sender's keypair matches their wallet
473
+ if (
474
+ senderKeypair.publicKey.toBase58() !==
475
+ senderUser.wallet.publicKey.toBase58()
476
+ ) {
477
+ throw new Error('Sender keypair does not match user wallet');
478
+ }
479
+
480
+ // Verify both users can transact
481
+ if (!senderUser.tossFeatures.canSend) {
482
+ throw new Error('Sender account is not enabled for sending');
483
+ }
484
+ if (!recipientUser.tossFeatures.canReceive) {
485
+ throw new Error('Recipient account is not enabled for receiving');
486
+ }
487
+
488
+ // Verify transaction amount is within limits
489
+ if (amount > senderUser.tossFeatures.maxTransactionAmount) {
490
+ throw new Error(
491
+ `Transaction amount exceeds limit of ${senderUser.tossFeatures.maxTransactionAmount} lamports`
492
+ );
493
+ }
494
+
495
+ const now = Math.floor(Date.now() / 1000);
496
+ const defaultExpiry = 24 * 60 * 60; // 24 hours default
497
+
498
+ // Get latest blockhash for nonce account
499
+ const { blockhash } = await connection.getLatestBlockhash();
500
+
501
+ // Prepare user contexts
502
+ const senderCtx: TossUserContext = {
503
+ userId: senderUser.userId,
504
+ username: senderUser.username,
505
+ wallet: {
506
+ publicKey: senderUser.wallet.publicKey,
507
+ isVerified: senderUser.wallet.isVerified,
508
+ createdAt: senderUser.wallet.createdAt,
509
+ },
510
+ status: senderUser.status,
511
+ deviceId: senderUser.device.id,
512
+ sessionId: uuidv4(),
513
+ };
514
+
515
+ const recipientCtx: TossUserContext = {
516
+ userId: recipientUser.userId,
517
+ username: recipientUser.username,
518
+ wallet: {
519
+ publicKey: recipientUser.wallet.publicKey,
520
+ isVerified: recipientUser.wallet.isVerified,
521
+ createdAt: recipientUser.wallet.createdAt,
522
+ },
523
+ status: recipientUser.status,
524
+ deviceId: recipientUser.device.id,
525
+ sessionId: uuidv4(),
526
+ };
527
+
528
+ // Create base intent with nonce account support
529
+ const baseIntent: Omit<SolanaIntent, 'signature'> = {
530
+ id: uuidv4(),
531
+ from: senderKeypair.publicKey.toBase58(),
532
+ to: recipientUser.wallet.publicKey.toBase58(),
533
+ fromUser: senderCtx,
534
+ toUser: recipientCtx,
535
+ amount,
536
+ nonce: nonceAccountInfo.currentNonce,
537
+ expiry: now + (options.expiresIn || defaultExpiry),
538
+ blockhash,
539
+ feePayer: senderKeypair.publicKey.toBase58(),
540
+ status: 'pending',
541
+ createdAt: now,
542
+ updatedAt: now,
543
+ // Nonce account support
544
+ nonceAccountAddress: nonceAccountInfo.address,
545
+ nonceAccountAuth: nonceAccountInfo.authorizedSigner,
546
+ requiresBiometric: senderUser.security.nonceAccountRequiresBiometric,
547
+ // Backward compatibility
548
+ nonceAccount: nonceAccountInfo.address,
549
+ nonceAuth: nonceAccountInfo.authorizedSigner,
550
+ };
551
+
552
+ // Sign the intent
553
+ const signature = sign(
554
+ Buffer.from(JSON.stringify(baseIntent)),
555
+ senderKeypair.secretKey
556
+ );
557
+
558
+ const intent: SolanaIntent = {
559
+ ...baseIntent,
560
+ signature: bs58.encode(signature),
561
+ };
562
+
563
+ // If private transaction, encrypt the intent data
564
+ if (options.privateTransaction) {
565
+ if (!options.mxeProgramId) {
566
+ throw new Error('MXE Program ID is required for private transactions');
567
+ }
568
+ if (!options.provider) {
569
+ throw new Error('Provider is required for private transactions');
570
+ }
571
+
572
+ const plaintextValues: bigint[] = [BigInt(amount)];
573
+
574
+ intent.encrypted = await encryptForArciumInternal(
575
+ options.mxeProgramId,
576
+ plaintextValues,
577
+ options.provider
578
+ );
579
+ }
580
+
581
+ return intent;
582
+ }