toss-expo-sdk 0.1.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.
Files changed (116) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +292 -0
  3. package/lib/module/ble.js +103 -0
  4. package/lib/module/ble.js.map +1 -0
  5. package/lib/module/client/TossClient.js +324 -0
  6. package/lib/module/client/TossClient.js.map +1 -0
  7. package/lib/module/client/index.js +4 -0
  8. package/lib/module/client/index.js.map +1 -0
  9. package/lib/module/contexts/WalletContext.js +99 -0
  10. package/lib/module/contexts/WalletContext.js.map +1 -0
  11. package/lib/module/discovery.js +434 -0
  12. package/lib/module/discovery.js.map +1 -0
  13. package/lib/module/errors.js +47 -0
  14. package/lib/module/errors.js.map +1 -0
  15. package/lib/module/examples/offlinePaymentFlow.js +234 -0
  16. package/lib/module/examples/offlinePaymentFlow.js.map +1 -0
  17. package/lib/module/index.js +32 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/module/intent.js +223 -0
  20. package/lib/module/intent.js.map +1 -0
  21. package/lib/module/intentManager.js +145 -0
  22. package/lib/module/intentManager.js.map +1 -0
  23. package/lib/module/internal/arciumHelper.js +50 -0
  24. package/lib/module/internal/arciumHelper.js.map +1 -0
  25. package/lib/module/nfc.js +54 -0
  26. package/lib/module/nfc.js.map +1 -0
  27. package/lib/module/noise.js +14 -0
  28. package/lib/module/noise.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/qr.js +57 -0
  31. package/lib/module/qr.js.map +1 -0
  32. package/lib/module/reconciliation.js +329 -0
  33. package/lib/module/reconciliation.js.map +1 -0
  34. package/lib/module/services/authService.js +205 -0
  35. package/lib/module/services/authService.js.map +1 -0
  36. package/lib/module/storage/secureStorage.js +89 -0
  37. package/lib/module/storage/secureStorage.js.map +1 -0
  38. package/lib/module/storage.js +16 -0
  39. package/lib/module/storage.js.map +1 -0
  40. package/lib/module/sync.js +64 -0
  41. package/lib/module/sync.js.map +1 -0
  42. package/lib/module/types/tossUser.js +41 -0
  43. package/lib/module/types/tossUser.js.map +1 -0
  44. package/lib/module/utils/nonceUtils.js +38 -0
  45. package/lib/module/utils/nonceUtils.js.map +1 -0
  46. package/lib/typescript/package.json +1 -0
  47. package/lib/typescript/src/__tests__/index.test.d.ts +1 -0
  48. package/lib/typescript/src/__tests__/index.test.d.ts.map +1 -0
  49. package/lib/typescript/src/__tests__/reconciliation.test.d.ts +6 -0
  50. package/lib/typescript/src/__tests__/reconciliation.test.d.ts.map +1 -0
  51. package/lib/typescript/src/ble.d.ts +10 -0
  52. package/lib/typescript/src/ble.d.ts.map +1 -0
  53. package/lib/typescript/src/client/TossClient.d.ts +110 -0
  54. package/lib/typescript/src/client/TossClient.d.ts.map +1 -0
  55. package/lib/typescript/src/client/index.d.ts +3 -0
  56. package/lib/typescript/src/client/index.d.ts.map +1 -0
  57. package/lib/typescript/src/contexts/WalletContext.d.ts +20 -0
  58. package/lib/typescript/src/contexts/WalletContext.d.ts.map +1 -0
  59. package/lib/typescript/src/discovery.d.ts +188 -0
  60. package/lib/typescript/src/discovery.d.ts.map +1 -0
  61. package/lib/typescript/src/errors.d.ts +27 -0
  62. package/lib/typescript/src/errors.d.ts.map +1 -0
  63. package/lib/typescript/src/examples/offlinePaymentFlow.d.ts +48 -0
  64. package/lib/typescript/src/examples/offlinePaymentFlow.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +13 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/intent.d.ts +84 -0
  68. package/lib/typescript/src/intent.d.ts.map +1 -0
  69. package/lib/typescript/src/intentManager.d.ts +46 -0
  70. package/lib/typescript/src/intentManager.d.ts.map +1 -0
  71. package/lib/typescript/src/internal/arciumHelper.d.ts +19 -0
  72. package/lib/typescript/src/internal/arciumHelper.d.ts.map +1 -0
  73. package/lib/typescript/src/nfc.d.ts +7 -0
  74. package/lib/typescript/src/nfc.d.ts.map +1 -0
  75. package/lib/typescript/src/noise.d.ts +5 -0
  76. package/lib/typescript/src/noise.d.ts.map +1 -0
  77. package/lib/typescript/src/qr.d.ts +6 -0
  78. package/lib/typescript/src/qr.d.ts.map +1 -0
  79. package/lib/typescript/src/reconciliation.d.ts +65 -0
  80. package/lib/typescript/src/reconciliation.d.ts.map +1 -0
  81. package/lib/typescript/src/services/authService.d.ts +55 -0
  82. package/lib/typescript/src/services/authService.d.ts.map +1 -0
  83. package/lib/typescript/src/storage/secureStorage.d.ts +7 -0
  84. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -0
  85. package/lib/typescript/src/storage.d.ts +4 -0
  86. package/lib/typescript/src/storage.d.ts.map +1 -0
  87. package/lib/typescript/src/sync.d.ts +40 -0
  88. package/lib/typescript/src/sync.d.ts.map +1 -0
  89. package/lib/typescript/src/types/tossUser.d.ts +39 -0
  90. package/lib/typescript/src/types/tossUser.d.ts.map +1 -0
  91. package/lib/typescript/src/utils/nonceUtils.d.ts +8 -0
  92. package/lib/typescript/src/utils/nonceUtils.d.ts.map +1 -0
  93. package/package.json +176 -0
  94. package/src/__tests__/index.test.tsx +1 -0
  95. package/src/__tests__/reconciliation.test.tsx +361 -0
  96. package/src/ble.ts +138 -0
  97. package/src/client/TossClient.ts +435 -0
  98. package/src/client/index.ts +2 -0
  99. package/src/contexts/WalletContext.tsx +127 -0
  100. package/src/discovery.ts +542 -0
  101. package/src/errors.ts +51 -0
  102. package/src/examples/offlinePaymentFlow.ts +331 -0
  103. package/src/index.tsx +61 -0
  104. package/src/intent.ts +328 -0
  105. package/src/intentManager.ts +164 -0
  106. package/src/internal/arciumHelper.ts +58 -0
  107. package/src/nfc.ts +57 -0
  108. package/src/noise.ts +9 -0
  109. package/src/qr.tsx +65 -0
  110. package/src/reconciliation.ts +421 -0
  111. package/src/services/authService.ts +238 -0
  112. package/src/storage/secureStorage.ts +100 -0
  113. package/src/storage.ts +17 -0
  114. package/src/sync.ts +101 -0
  115. package/src/types/tossUser.ts +81 -0
  116. package/src/utils/nonceUtils.ts +56 -0
@@ -0,0 +1,435 @@
1
+ import { Connection, Keypair, PublicKey } from '@solana/web3.js';
2
+ import type { SolanaIntent, IntentStatus } from '../intent';
3
+ import { createIntent } from '../intent';
4
+ import {
5
+ secureStoreIntent,
6
+ getSecureIntent,
7
+ getAllSecureIntents,
8
+ } from '../storage/secureStorage';
9
+ import { processIntentsForSync } from '../intentManager';
10
+ import { TossError, NetworkError, StorageError, ERROR_CODES } from '../errors';
11
+ import { createNonceAccount, getNonce } from '../utils/nonceUtils';
12
+ import { useWallet } from '../contexts/WalletContext';
13
+ import { syncToChain, checkSyncStatus, type SyncResult } from '../sync';
14
+ import { detectConflicts, getReconciliationState } from '../reconciliation';
15
+
16
+ export type TossConfig = {
17
+ projectId: string;
18
+ mode?: 'devnet' | 'testnet' | 'mainnet-beta';
19
+ privateTransactions?: boolean;
20
+ provider?: any; // AnchorProvider or similar
21
+ sync?: {
22
+ syncBackupDb?: boolean;
23
+ dbUrl?: string;
24
+ };
25
+ rpcUrl?: string;
26
+ maxRetries?: number;
27
+ retryDelay?: number;
28
+ feePayer?: Keypair; // Optional fee payer
29
+ };
30
+
31
+ const DEFAULT_RETRY_OPTIONS = {
32
+ maxRetries: 3,
33
+ retryDelay: 1000, // 1 second
34
+ };
35
+
36
+ export class TossClient {
37
+ private connection: Connection;
38
+ private config: Required<Omit<TossConfig, 'rpcUrl' | 'feePayer'>> & {
39
+ rpcUrl: string;
40
+ feePayer?: Keypair;
41
+ };
42
+ private nonceAccount?: Keypair;
43
+ private nonceAuth?: PublicKey;
44
+ private walletContext: ReturnType<typeof useWallet>;
45
+
46
+ static createClient(config: TossConfig): TossClient {
47
+ return new TossClient(config);
48
+ }
49
+
50
+ private constructor(config: TossConfig) {
51
+ this.config = {
52
+ projectId: config.projectId,
53
+ mode: config.mode || 'devnet',
54
+ privateTransactions: config.privateTransactions || false,
55
+ provider: config.provider,
56
+ sync: config.sync || { syncBackupDb: false },
57
+ rpcUrl: config.rpcUrl || this.getDefaultRpcUrl(config.mode || 'devnet'),
58
+ maxRetries: config.maxRetries ?? DEFAULT_RETRY_OPTIONS.maxRetries,
59
+ retryDelay: config.retryDelay ?? DEFAULT_RETRY_OPTIONS.retryDelay,
60
+ feePayer: config.feePayer,
61
+ } as const;
62
+ this.connection = new Connection(this.config.rpcUrl, 'confirmed');
63
+ this.walletContext = useWallet();
64
+
65
+ if (!this.walletContext) {
66
+ throw new Error('TossClient must be used within a WalletProvider');
67
+ }
68
+ }
69
+
70
+ private getDefaultRpcUrl(network: string): string {
71
+ const urls = {
72
+ 'devnet': 'https://api.devnet.solana.com',
73
+ 'testnet': 'https://api.testnet.solana.com',
74
+ 'mainnet-beta': 'https://api.mainnet-beta.solana.com',
75
+ };
76
+ return urls[network as keyof typeof urls] || urls.devnet;
77
+ }
78
+
79
+ private async withRetry<T>(
80
+ fn: () => Promise<T>,
81
+ context: string
82
+ ): Promise<T> {
83
+ let lastError: Error | null = null;
84
+ const { maxRetries, retryDelay } = this.config;
85
+
86
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
87
+ try {
88
+ return await fn();
89
+ } catch (error) {
90
+ lastError = error as Error;
91
+
92
+ // Don't retry on validation errors
93
+ if (error instanceof TossError) {
94
+ throw error;
95
+ }
96
+
97
+ // Exponential backoff
98
+ if (attempt < maxRetries) {
99
+ const delay = retryDelay * Math.pow(2, attempt - 1);
100
+ await new Promise((resolve) => setTimeout(resolve, delay));
101
+ }
102
+ }
103
+ }
104
+
105
+ throw new NetworkError(
106
+ `Failed after ${maxRetries} attempts: ${lastError?.message}`,
107
+ { context, cause: lastError }
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Initialize a nonce account for durable transactions
113
+ * @param amount SOL amount to fund the nonce account with (default: 1 SOL)
114
+ * @returns Object containing nonce account and authority public keys
115
+ */
116
+ async initializeNonceAccount(
117
+ amount = 1
118
+ ): Promise<{ nonceAccount: string; nonceAuth: string }> {
119
+ if (!this.config.feePayer) {
120
+ throw new Error(
121
+ 'Fee payer keypair is required for nonce account creation'
122
+ );
123
+ }
124
+
125
+ const { nonceAccount, nonceAuth } = await createNonceAccount(
126
+ this.connection,
127
+ this.config.feePayer,
128
+ undefined,
129
+ amount * 1e9 // Convert SOL to lamports
130
+ );
131
+
132
+ this.nonceAccount = nonceAccount;
133
+ this.nonceAuth = nonceAuth;
134
+
135
+ return {
136
+ nonceAccount: nonceAccount.publicKey.toBase58(),
137
+ nonceAuth: nonceAuth.toBase58(),
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Get the current nonce value from the nonce account
143
+ * @returns The current nonce value as a base58 string
144
+ */
145
+ async getCurrentNonce(): Promise<string> {
146
+ if (!this.nonceAccount) {
147
+ throw new Error(
148
+ 'Nonce account not initialized. Call initializeNonceAccount() first.'
149
+ );
150
+ }
151
+ return getNonce(this.connection, this.nonceAccount.publicKey);
152
+ }
153
+
154
+ async createIntent(
155
+ sender: Keypair | 'current',
156
+ recipient: PublicKey | string,
157
+ amount: number,
158
+ feePayer?: PublicKey | string,
159
+ options: {
160
+ expiresIn?: number;
161
+ nonce?: number;
162
+ useDurableNonce?: boolean;
163
+ memo?: string;
164
+ } = {}
165
+ ): Promise<SolanaIntent> {
166
+ return this.withRetry(async () => {
167
+ try {
168
+ if (!this.connection) {
169
+ throw new TossError(
170
+ 'Connection not initialized',
171
+ ERROR_CODES.NETWORK_ERROR
172
+ );
173
+ }
174
+
175
+ // Handle 'current' sender to use the wallet context
176
+ const senderKeypair =
177
+ sender === 'current' ? this.walletContext.keypair : sender;
178
+
179
+ if (!senderKeypair) {
180
+ throw new TossError(
181
+ 'No sender keypair provided and no wallet is connected',
182
+ ERROR_CODES.SIGNATURE_VERIFICATION_FAILED
183
+ );
184
+ }
185
+
186
+ // Convert string addresses to PublicKey if needed
187
+ const recipientPubkey =
188
+ typeof recipient === 'string' ? new PublicKey(recipient) : recipient;
189
+
190
+ const feePayerPubkey = feePayer
191
+ ? typeof feePayer === 'string'
192
+ ? new PublicKey(feePayer)
193
+ : feePayer
194
+ : senderKeypair.publicKey;
195
+
196
+ // Set up nonce account and auth if using durable nonce
197
+ if (options.useDurableNonce && this.nonceAccount && this.nonceAuth) {
198
+ // nonce account and auth are passed via options to createIntent
199
+ }
200
+
201
+ // Get the latest blockhash (no need to store it as it's handled internally)
202
+ await this.connection.getLatestBlockhash();
203
+
204
+ const intent = await createIntent(
205
+ senderKeypair,
206
+ recipientPubkey,
207
+ amount,
208
+ this.connection,
209
+ {
210
+ ...options,
211
+ feePayer: feePayerPubkey,
212
+ nonceAccount: options.useDurableNonce
213
+ ? this.nonceAccount
214
+ : undefined,
215
+ nonceAuth: options.useDurableNonce ? this.nonceAuth : undefined,
216
+ }
217
+ );
218
+
219
+ await secureStoreIntent(intent);
220
+ return intent;
221
+ } catch (error) {
222
+ if (error instanceof TossError) throw error;
223
+ throw new TossError(
224
+ 'Failed to create intent',
225
+ ERROR_CODES.TRANSACTION_FAILED,
226
+ { cause: error }
227
+ );
228
+ }
229
+ }, 'createIntent');
230
+ }
231
+
232
+ async getIntents(): Promise<SolanaIntent[]> {
233
+ return this.withRetry(async () => {
234
+ try {
235
+ return await getAllSecureIntents();
236
+ } catch (error) {
237
+ if (error instanceof TossError) throw error;
238
+ throw new StorageError('Failed to retrieve intents', { cause: error });
239
+ }
240
+ }, 'getIntents');
241
+ }
242
+
243
+ async updateIntentStatus(
244
+ intentId: string,
245
+ status: IntentStatus,
246
+ error?: string
247
+ ): Promise<SolanaIntent | null> {
248
+ return this.withRetry(async () => {
249
+ try {
250
+ const intent = await getSecureIntent(intentId);
251
+ if (!intent) return null;
252
+
253
+ // Ensure updatedAt is a number (timestamp)
254
+ const updatedAt = Date.now();
255
+
256
+ const updatedIntent: SolanaIntent = {
257
+ ...intent,
258
+ status,
259
+ updatedAt,
260
+ ...(error ? { error } : {}),
261
+ };
262
+
263
+ await secureStoreIntent(updatedIntent);
264
+ return updatedIntent;
265
+ } catch (error) {
266
+ if (error instanceof TossError) throw error;
267
+ throw new StorageError('Failed to update intent status', {
268
+ cause: error,
269
+ intentId,
270
+ status,
271
+ });
272
+ }
273
+ }, 'updateIntentStatus');
274
+ }
275
+
276
+ async sync(): Promise<SolanaIntent[]> {
277
+ return this.withRetry(async () => {
278
+ try {
279
+ const intents = await getAllSecureIntents();
280
+
281
+ if (!this.config.sync?.syncBackupDb || !this.config.sync.dbUrl) {
282
+ return intents;
283
+ }
284
+
285
+ // Process intents for sync and update their statuses
286
+ const processedIntents = await processIntentsForSync(
287
+ intents,
288
+ this.connection
289
+ );
290
+
291
+ // Save updated intents
292
+ await Promise.all(
293
+ processedIntents.map((intent) => secureStoreIntent(intent))
294
+ );
295
+
296
+ return processedIntents;
297
+ } catch (error) {
298
+ if (error instanceof TossError) throw error;
299
+ throw new NetworkError('Failed to sync intents', { cause: error });
300
+ }
301
+ }, 'sync');
302
+ }
303
+
304
+ /**
305
+ * Full synchronisation with Solana blockchain (TOSS Section 9)
306
+ *
307
+ * Performs complete reconciliation:
308
+ * - Detects conflicts with onchain state
309
+ * - Settles all pending intents
310
+ * - Updates local state deterministically
311
+ *
312
+ * @returns Complete sync results including settlements, conflicts, and final state
313
+ */
314
+ async fullSync(): Promise<SyncResult> {
315
+ return this.withRetry(async () => {
316
+ try {
317
+ return await syncToChain(
318
+ this.connection,
319
+ this.config.feePayer?.publicKey
320
+ );
321
+ } catch (error) {
322
+ if (error instanceof TossError) throw error;
323
+ throw new NetworkError('Full sync failed', { cause: error });
324
+ }
325
+ }, 'fullSync');
326
+ }
327
+
328
+ /**
329
+ * Check synchronisation status without settling
330
+ *
331
+ * Lightweight operation to query current reconciliation state
332
+ * without committing any settlements to the blockchain.
333
+ */
334
+ async checkSyncStatus() {
335
+ return this.withRetry(async () => {
336
+ return await checkSyncStatus(this.connection);
337
+ }, 'checkSyncStatus');
338
+ }
339
+
340
+ /**
341
+ * Detect conflicts between local intents and onchain state
342
+ *
343
+ * Useful for monitoring and alerting users to potential issues
344
+ * before attempting settlement.
345
+ */
346
+ async detectIntentConflicts() {
347
+ return this.withRetry(async () => {
348
+ return await detectConflicts(this.connection);
349
+ }, 'detectIntentConflicts');
350
+ }
351
+
352
+ /**
353
+ * Get current reconciliation state
354
+ *
355
+ * Returns summary of processed, failed, and conflicting intents
356
+ * for UI updates or logging.
357
+ */
358
+ async getReconciliationStatus() {
359
+ return this.withRetry(async () => {
360
+ return await getReconciliationState(this.connection);
361
+ }, 'getReconciliationStatus');
362
+ }
363
+
364
+ /**
365
+ * Create an intent from the current user's wallet
366
+ */
367
+ async createUserIntent(
368
+ recipient: PublicKey | string,
369
+ amount: number,
370
+ options: {
371
+ memo?: string;
372
+ useDurableNonce?: boolean;
373
+ } = {}
374
+ ): Promise<SolanaIntent> {
375
+ if (!this.walletContext.user) {
376
+ throw new TossError(
377
+ 'No user is currently signed in',
378
+ ERROR_CODES.SIGNATURE_VERIFICATION_FAILED
379
+ );
380
+ }
381
+
382
+ // Ensure wallet is unlocked
383
+ if (!this.walletContext.isUnlocked) {
384
+ const unlocked = await this.walletContext.unlockWallet();
385
+ if (!unlocked) {
386
+ throw new TossError(
387
+ 'Wallet is locked',
388
+ ERROR_CODES.SIGNATURE_VERIFICATION_FAILED
389
+ );
390
+ }
391
+ }
392
+
393
+ const recipientPubkey =
394
+ typeof recipient === 'string' ? new PublicKey(recipient) : recipient;
395
+
396
+ return this.createIntent(
397
+ 'current', // This will use the wallet context's keypair
398
+ recipientPubkey,
399
+ amount,
400
+ this.walletContext.user.wallet.publicKey,
401
+ {
402
+ ...options,
403
+ memo: options.memo || `TOSS transfer to ${recipientPubkey.toBase58()}`,
404
+ }
405
+ );
406
+ }
407
+
408
+ /**
409
+ * Get the current user's wallet address
410
+ */
411
+ getCurrentUserAddress(): string | null {
412
+ return this.walletContext.user?.wallet.publicKey.toString() || null;
413
+ }
414
+
415
+ /**
416
+ * Check if the wallet is currently unlocked
417
+ */
418
+ isWalletUnlocked(): boolean {
419
+ return this.walletContext.isUnlocked;
420
+ }
421
+
422
+ /**
423
+ * Lock the wallet
424
+ */
425
+ async lockWallet(): Promise<void> {
426
+ await this.walletContext.lockWallet();
427
+ }
428
+
429
+ /**
430
+ * Sign out the current user
431
+ */
432
+ async signOut(): Promise<void> {
433
+ await this.walletContext.signOut();
434
+ }
435
+ }
@@ -0,0 +1,2 @@
1
+ export * from './TossClient';
2
+ export type { TossConfig } from './TossClient';
@@ -0,0 +1,127 @@
1
+ import { createContext, useContext, useState, useEffect } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { Keypair } from '@solana/web3.js';
4
+ import { AuthService } from '../services/authService';
5
+ import type { TossUser } from '../types/tossUser';
6
+
7
+ type WalletContextType = {
8
+ isInitialized: boolean;
9
+ isUnlocked: boolean;
10
+ user: TossUser | null;
11
+ keypair: Keypair | null;
12
+ unlockWallet: () => Promise<boolean>;
13
+ lockWallet: () => Promise<void>;
14
+ signIn: (walletAddress: string, isTemporary?: boolean) => Promise<void>;
15
+ signOut: () => Promise<void>;
16
+ };
17
+
18
+ const WalletContext = createContext<WalletContextType | undefined>(undefined);
19
+
20
+ export const WalletProvider: React.FC<{ children: ReactNode }> = ({
21
+ children,
22
+ }) => {
23
+ const [isInitialized, setIsInitialized] = useState(false);
24
+ const [isUnlocked, setIsUnlocked] = useState(false);
25
+ const [user, setUser] = useState<TossUser | null>(null);
26
+ const [keypair, setKeypair] = useState<Keypair | null>(null);
27
+
28
+ useEffect(() => {
29
+ const checkAuth = async () => {
30
+ try {
31
+ const session = await AuthService.getSession();
32
+ if (session) {
33
+ // In a real app, you'd fetch the user from your backend
34
+ const { user } = await AuthService.signInWithWallet(
35
+ session.walletAddress
36
+ );
37
+ setUser(user);
38
+ setIsUnlocked(await AuthService.isWalletUnlocked());
39
+ }
40
+ } catch (error) {
41
+ console.error('Auth check failed:', error);
42
+ } finally {
43
+ setIsInitialized(true);
44
+ }
45
+ };
46
+
47
+ checkAuth();
48
+ }, []);
49
+
50
+ const unlockWallet = async (): Promise<boolean> => {
51
+ try {
52
+ // Biometric authentication REQUIRED - throws if not authorized
53
+ const unlockedKeypair = await AuthService.unlockWalletWithBiometrics();
54
+ if (unlockedKeypair) {
55
+ // Keypair held ONLY in React state memory (NOT persisted to disk)
56
+ setKeypair(unlockedKeypair);
57
+ setIsUnlocked(true);
58
+ return true;
59
+ }
60
+ return false;
61
+ } catch (error) {
62
+ console.error('Biometric authentication failed:', error);
63
+ setIsUnlocked(false);
64
+ setKeypair(null);
65
+ return false;
66
+ }
67
+ };
68
+
69
+ const lockWallet = async (): Promise<void> => {
70
+ // Clear keypair from memory (from React state)
71
+ // Encrypted keypair remains stored securely (requires biometric to re-unlock)
72
+ setKeypair(null);
73
+ setIsUnlocked(false);
74
+ };
75
+
76
+ const signIn = async (
77
+ walletAddress: string,
78
+ isTemporary: boolean = false
79
+ ): Promise<void> => {
80
+ const { user } = await AuthService.signInWithWallet(
81
+ walletAddress,
82
+ isTemporary
83
+ );
84
+ setUser(user);
85
+ setIsUnlocked(true);
86
+ };
87
+
88
+ const signOut = async (): Promise<void> => {
89
+ // Clear session from storage
90
+ await AuthService.signOut();
91
+
92
+ // Clear all memory (state)
93
+ setUser(null);
94
+ setKeypair(null); // Remove from RAM
95
+ setIsUnlocked(false);
96
+
97
+ // NOTE: Encrypted keypair remains in SecureStore
98
+ // It can only be accessed again with biometric authentication
99
+ };
100
+
101
+ return (
102
+ <WalletContext.Provider
103
+ value={{
104
+ isInitialized,
105
+ isUnlocked,
106
+ user,
107
+ keypair,
108
+ unlockWallet,
109
+ lockWallet,
110
+ signIn,
111
+ signOut,
112
+ }}
113
+ >
114
+ {children}
115
+ </WalletContext.Provider>
116
+ );
117
+ };
118
+
119
+ export const useWallet = (): WalletContextType => {
120
+ const context = useContext(WalletContext);
121
+ if (context === undefined) {
122
+ throw new Error('useWallet must be used within a WalletProvider');
123
+ }
124
+ return context;
125
+ };
126
+
127
+ export default WalletContext;