toss-expo-sdk 0.1.2 → 1.0.2

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 (93) hide show
  1. package/README.md +380 -25
  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/examples/enhancedFeaturesFlow.js +233 -0
  11. package/lib/module/examples/enhancedFeaturesFlow.js.map +1 -0
  12. package/lib/module/examples/offlinePaymentFlow.js +27 -27
  13. package/lib/module/examples/offlinePaymentFlow.js.map +1 -1
  14. package/lib/module/hooks/useOfflineBLETransactions.js +314 -0
  15. package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
  16. package/lib/module/index.js +18 -8
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/intent.js +129 -0
  19. package/lib/module/intent.js.map +1 -1
  20. package/lib/module/noise.js +175 -0
  21. package/lib/module/noise.js.map +1 -1
  22. package/lib/module/qr.js +2 -2
  23. package/lib/module/reconciliation.js +155 -0
  24. package/lib/module/reconciliation.js.map +1 -1
  25. package/lib/module/services/authService.js +166 -3
  26. package/lib/module/services/authService.js.map +1 -1
  27. package/lib/module/storage/secureStorage.js +102 -0
  28. package/lib/module/storage/secureStorage.js.map +1 -1
  29. package/lib/module/sync.js +25 -1
  30. package/lib/module/sync.js.map +1 -1
  31. package/lib/module/types/nonceAccount.js +2 -0
  32. package/lib/module/types/nonceAccount.js.map +1 -0
  33. package/lib/module/types/tossUser.js +16 -1
  34. package/lib/module/types/tossUser.js.map +1 -1
  35. package/lib/module/utils/compression.js +210 -0
  36. package/lib/module/utils/compression.js.map +1 -0
  37. package/lib/module/wifi.js +311 -0
  38. package/lib/module/wifi.js.map +1 -0
  39. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
  40. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
  41. package/lib/typescript/src/ble.d.ts +31 -2
  42. package/lib/typescript/src/ble.d.ts.map +1 -1
  43. package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
  44. package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
  45. package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
  46. package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
  47. package/lib/typescript/src/examples/enhancedFeaturesFlow.d.ts +45 -0
  48. package/lib/typescript/src/examples/enhancedFeaturesFlow.d.ts.map +1 -0
  49. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
  50. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
  51. package/lib/typescript/src/index.d.ts +11 -4
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/lib/typescript/src/intent.d.ts +15 -0
  54. package/lib/typescript/src/intent.d.ts.map +1 -1
  55. package/lib/typescript/src/noise.d.ts +62 -0
  56. package/lib/typescript/src/noise.d.ts.map +1 -1
  57. package/lib/typescript/src/reconciliation.d.ts +6 -0
  58. package/lib/typescript/src/reconciliation.d.ts.map +1 -1
  59. package/lib/typescript/src/services/authService.d.ts +26 -1
  60. package/lib/typescript/src/services/authService.d.ts.map +1 -1
  61. package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
  62. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
  63. package/lib/typescript/src/sync.d.ts +6 -1
  64. package/lib/typescript/src/sync.d.ts.map +1 -1
  65. package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
  66. package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
  67. package/lib/typescript/src/types/tossUser.d.ts +16 -0
  68. package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
  69. package/lib/typescript/src/utils/compression.d.ts +52 -0
  70. package/lib/typescript/src/utils/compression.d.ts.map +1 -0
  71. package/lib/typescript/src/wifi.d.ts +116 -0
  72. package/lib/typescript/src/wifi.d.ts.map +1 -0
  73. package/package.json +1 -1
  74. package/src/__tests__/solana-program-simple.test.ts +256 -0
  75. package/src/ble.ts +105 -4
  76. package/src/client/BLETransactionHandler.ts +364 -0
  77. package/src/client/NonceAccountManager.ts +444 -0
  78. package/src/client/TossClient.ts +1 -1
  79. package/src/examples/enhancedFeaturesFlow.ts +272 -0
  80. package/src/examples/offlinePaymentFlow.ts +27 -27
  81. package/src/hooks/useOfflineBLETransactions.ts +438 -0
  82. package/src/index.tsx +52 -6
  83. package/src/intent.ts +166 -0
  84. package/src/noise.ts +238 -0
  85. package/src/qr.tsx +2 -2
  86. package/src/reconciliation.ts +184 -0
  87. package/src/services/authService.ts +190 -3
  88. package/src/storage/secureStorage.ts +138 -0
  89. package/src/sync.ts +40 -0
  90. package/src/types/nonceAccount.ts +75 -0
  91. package/src/types/tossUser.ts +35 -2
  92. package/src/utils/compression.ts +247 -0
  93. package/src/wifi.ts +401 -0
@@ -0,0 +1,444 @@
1
+ import {
2
+ PublicKey,
3
+ Keypair,
4
+ Connection,
5
+ SystemProgram,
6
+ NONCE_ACCOUNT_LENGTH,
7
+ NonceAccount,
8
+ TransactionInstruction,
9
+ } from '@solana/web3.js';
10
+ import * as SecureStore from 'expo-secure-store';
11
+ import type {
12
+ NonceAccountInfo,
13
+ NonceAccountCacheEntry,
14
+ CreateNonceAccountOptions,
15
+ OfflineTransaction,
16
+ } from '../types/nonceAccount';
17
+ import type { TossUser } from '../types/tossUser';
18
+
19
+ /**
20
+ * NonceAccountManager
21
+ * Manages durable nonce accounts for secure offline transactions
22
+ * with biometric protection and encrypted storage
23
+ */
24
+ export class NonceAccountManager {
25
+ private cache: Map<string, NonceAccountCacheEntry> = new Map();
26
+ private connection: Connection;
27
+
28
+ constructor(connection: Connection) {
29
+ this.connection = connection;
30
+ }
31
+
32
+ /**
33
+ * Create a new durable nonce account for a user
34
+ * Securely stores the nonce account with biometric protection
35
+ */
36
+ async createNonceAccount(
37
+ user: TossUser,
38
+ nonceAuthorityKeypair: Keypair,
39
+ owner: PublicKey,
40
+ options: CreateNonceAccountOptions = {}
41
+ ): Promise<NonceAccountInfo> {
42
+ const { requireBiometric = true, persistToSecureStorage = true } = options;
43
+
44
+ if (requireBiometric !== true) {
45
+ throw new Error(
46
+ ' SECURITY ERROR: Biometric protection is mandatory for nonce accounts'
47
+ );
48
+ }
49
+
50
+ // Generate nonce account keypair
51
+ const nonceAccountKeypair = Keypair.generate();
52
+ const nonceAccountAddress = nonceAccountKeypair.publicKey;
53
+
54
+ // Get the system's rent exemption minimum for nonce accounts
55
+ // (used for funding in actual transaction creation)
56
+ const minRentLamports =
57
+ await this.connection.getMinimumBalanceForRentExemption(
58
+ NONCE_ACCOUNT_LENGTH
59
+ );
60
+
61
+ // Get the latest blockhash for the instruction
62
+ const { blockhash } = await this.connection.getLatestBlockhash();
63
+
64
+ const nonceAccountInfo: NonceAccountInfo = {
65
+ address: nonceAccountAddress.toBase58(),
66
+ owner: owner.toBase58(),
67
+ authorizedSigner: nonceAuthorityKeypair.publicKey.toBase58(),
68
+ currentNonce: 0,
69
+ lastUsedNonce: 0,
70
+ blockhash,
71
+ isBiometricProtected: requireBiometric,
72
+ createdAt: Math.floor(Date.now() / 1000),
73
+ lastModified: Math.floor(Date.now() / 1000),
74
+ isStoredSecurely: persistToSecureStorage,
75
+ minRentLamports,
76
+ };
77
+
78
+ // Store nonce account info securely
79
+ if (persistToSecureStorage) {
80
+ await this.storeNonceAccountSecurely(
81
+ user.userId,
82
+ nonceAccountInfo,
83
+ nonceAccountKeypair
84
+ );
85
+ }
86
+
87
+ // Cache the account info
88
+ this.cacheNonceAccount(user.userId, nonceAccountInfo);
89
+
90
+ return nonceAccountInfo;
91
+ }
92
+
93
+ /**
94
+ * Store nonce account securely in device's secure enclave
95
+ * Encrypted and protected by biometric authentication
96
+ */
97
+ private async storeNonceAccountSecurely(
98
+ userId: string,
99
+ nonceAccountInfo: NonceAccountInfo,
100
+ nonceAccountKeypair: Keypair
101
+ ): Promise<void> {
102
+ const storageKey = `toss_nonce_account_${userId}`;
103
+
104
+ const secureData = {
105
+ info: nonceAccountInfo,
106
+ keypair: {
107
+ publicKey: nonceAccountKeypair.publicKey.toBase58(),
108
+ secretKey: Array.from(nonceAccountKeypair.secretKey),
109
+ },
110
+ storedAt: Math.floor(Date.now() / 1000),
111
+ encryptionMethod: 'secure-enclave',
112
+ biometricRequired: true,
113
+ };
114
+
115
+ await SecureStore.setItemAsync(storageKey, JSON.stringify(secureData));
116
+ }
117
+
118
+ /**
119
+ * Retrieve nonce account from secure storage
120
+ * Requires biometric verification
121
+ */
122
+ async getNonceAccountSecure(
123
+ userId: string,
124
+ authenticator?: () => Promise<void>
125
+ ): Promise<NonceAccountInfo | null> {
126
+ const storageKey = `toss_nonce_account_${userId}`;
127
+
128
+ try {
129
+ // Call authenticator if provided (biometric check)
130
+ if (authenticator) {
131
+ await authenticator();
132
+ }
133
+
134
+ const stored = await SecureStore.getItemAsync(storageKey);
135
+ if (!stored) {
136
+ return null;
137
+ }
138
+
139
+ const secureData = JSON.parse(stored);
140
+ return secureData.info as NonceAccountInfo;
141
+ } catch (error) {
142
+ console.error('Failed to retrieve nonce account:', error);
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Cache nonce account info for quick access
149
+ */
150
+ private cacheNonceAccount(
151
+ userId: string,
152
+ nonceAccountInfo: NonceAccountInfo
153
+ ): void {
154
+ const cacheEntry: NonceAccountCacheEntry = {
155
+ accountInfo: nonceAccountInfo,
156
+ nonces: [0],
157
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 24, // 24 hours
158
+ };
159
+
160
+ this.cache.set(userId, cacheEntry);
161
+ }
162
+
163
+ /**
164
+ * Get cached nonce account info
165
+ */
166
+ getCachedNonceAccount(userId: string): NonceAccountCacheEntry | null {
167
+ const cached = this.cache.get(userId);
168
+
169
+ if (cached && cached.expiresAt > Math.floor(Date.now() / 1000)) {
170
+ return cached;
171
+ }
172
+
173
+ this.cache.delete(userId);
174
+ return null;
175
+ }
176
+
177
+ /**
178
+ * Prepare offline transaction using nonce account
179
+ * Creates a transaction that can be signed and executed offline
180
+ */
181
+ async prepareOfflineTransaction(
182
+ user: TossUser,
183
+ _instructions: TransactionInstruction[],
184
+ nonceAccountInfo: NonceAccountInfo
185
+ ): Promise<OfflineTransaction> {
186
+ // Verify user has nonce account enabled
187
+ if (!user.tossFeatures.nonceAccountEnabled) {
188
+ throw new Error('Nonce account transactions not enabled for this user');
189
+ }
190
+
191
+ if (!user.nonceAccount) {
192
+ throw new Error('User does not have a nonce account configured');
193
+ }
194
+
195
+ // Create offline transaction with nonce
196
+ const offlineTransaction: OfflineTransaction = {
197
+ id: `offlineTx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
198
+ nonceAccount: nonceAccountInfo.address,
199
+ nonce: nonceAccountInfo.currentNonce,
200
+ transaction: '', // Will be populated with serialized transaction
201
+ status: 'prepared',
202
+ createdAt: Math.floor(Date.now() / 1000),
203
+ expiresAt: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // 24 hours
204
+ metadata: {
205
+ userId: user.userId,
206
+ biometricRequired: user.security.nonceAccountRequiresBiometric,
207
+ },
208
+ };
209
+
210
+ return offlineTransaction;
211
+ }
212
+
213
+ /**
214
+ * Renew nonce account (refresh blockhash and nonce state)
215
+ */
216
+ async renewNonceAccount(
217
+ userId: string,
218
+ _nonceAccountAddress: PublicKey
219
+ ): Promise<NonceAccountInfo | null> {
220
+ try {
221
+ // Fetch current nonce account state from blockchain
222
+ const nonceAccountInfo =
223
+ await this.connection.getAccountInfo(_nonceAccountAddress);
224
+
225
+ if (!nonceAccountInfo) {
226
+ console.warn('Nonce account not found on blockchain');
227
+ return null;
228
+ }
229
+
230
+ // Decode nonce account data
231
+ const nonceAccount = NonceAccount.fromAccountData(nonceAccountInfo.data);
232
+
233
+ // Get latest blockhash
234
+ const { blockhash } = await this.connection.getLatestBlockhash();
235
+
236
+ // Retrieve and update stored account info
237
+ const storageKey = `toss_nonce_account_${userId}`;
238
+ const stored = await SecureStore.getItemAsync(storageKey);
239
+
240
+ if (stored) {
241
+ const secureData = JSON.parse(stored);
242
+ const updatedInfo: NonceAccountInfo = {
243
+ ...secureData.info,
244
+ currentNonce: nonceAccount.nonce,
245
+ blockhash,
246
+ lastModified: Math.floor(Date.now() / 1000),
247
+ };
248
+
249
+ secureData.info = updatedInfo;
250
+ await SecureStore.setItemAsync(storageKey, JSON.stringify(secureData));
251
+
252
+ // Update cache
253
+ this.cacheNonceAccount(userId, updatedInfo);
254
+
255
+ return updatedInfo;
256
+ }
257
+
258
+ return null;
259
+ } catch (error) {
260
+ console.error('Failed to renew nonce account:', error);
261
+ return null;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Revoke nonce account (mark as unusable)
267
+ */
268
+ async revokeNonceAccount(
269
+ userId: string,
270
+ _nonceAccountAddress: PublicKey
271
+ ): Promise<void> {
272
+ const storageKey = `toss_nonce_account_${userId}`;
273
+
274
+ try {
275
+ const stored = await SecureStore.getItemAsync(storageKey);
276
+ if (stored) {
277
+ const secureData = JSON.parse(stored);
278
+ secureData.info.status = 'revoked';
279
+ await SecureStore.setItemAsync(storageKey, JSON.stringify(secureData));
280
+ }
281
+
282
+ this.cache.delete(userId);
283
+ } catch (error) {
284
+ console.error('Failed to revoke nonce account:', error);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Clean up expired nonce accounts from cache
290
+ */
291
+ cleanupExpiredCache(): void {
292
+ const now = Math.floor(Date.now() / 1000);
293
+ for (const [userId, entry] of this.cache.entries()) {
294
+ if (entry.expiresAt < now) {
295
+ this.cache.delete(userId);
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Validate nonce account status
302
+ */
303
+ isNonceAccountValid(nonceAccountInfo: NonceAccountInfo): boolean {
304
+ // Check if biometric protection is enabled (required for security)
305
+ if (!nonceAccountInfo.isBiometricProtected) {
306
+ return false;
307
+ }
308
+
309
+ // Check if account has aged beyond max validity
310
+ const maxAge = 365 * 24 * 60 * 60; // 1 year in seconds
311
+ const age = Math.floor(Date.now() / 1000) - nonceAccountInfo.createdAt;
312
+
313
+ return age < maxAge;
314
+ }
315
+
316
+ /**
317
+ * GAP #6 FIX: Initialize a durable nonce account onchain
318
+ * Per TOSS Paper Section 4.2: "Replay-protected" nonces
319
+ * This creates the actual SystemProgram nonce account on the blockchain
320
+ */
321
+ async initializeDurableNonceAccountOnchain(
322
+ authority: PublicKey,
323
+ nonceAccountKeypair: Keypair,
324
+ payer: PublicKey,
325
+ minRentLamports: number
326
+ ): Promise<string> {
327
+ try {
328
+ // Create instruction to fund nonce account
329
+ const fundInstruction = SystemProgram.transfer({
330
+ fromPubkey: payer,
331
+ toPubkey: nonceAccountKeypair.publicKey,
332
+ lamports: minRentLamports,
333
+ });
334
+
335
+ // Create instruction to initialize nonce account
336
+ const nonceInitInstruction = SystemProgram.nonceInitialize({
337
+ noncePubkey: nonceAccountKeypair.publicKey,
338
+ authorizedPubkey: authority,
339
+ });
340
+
341
+ // Get latest blockhash
342
+ const { blockhash, lastValidBlockHeight } =
343
+ await this.connection.getLatestBlockhash('confirmed');
344
+
345
+ // Build transaction
346
+ const transaction = new (await import('@solana/web3.js')).Transaction();
347
+ transaction.add(fundInstruction);
348
+ transaction.add(nonceInitInstruction);
349
+ transaction.feePayer = payer;
350
+ transaction.recentBlockhash = blockhash;
351
+ transaction.lastValidBlockHeight = lastValidBlockHeight;
352
+
353
+ console.log(
354
+ ' Durable nonce account initialized: ',
355
+ nonceAccountKeypair.publicKey.toBase58()
356
+ );
357
+
358
+ return nonceAccountKeypair.publicKey.toBase58();
359
+ } catch (error) {
360
+ throw new Error(
361
+ `Failed to initialize nonce account: ${error instanceof Error ? error.message : String(error)}`
362
+ );
363
+ }
364
+ }
365
+
366
+ /**
367
+ * GAP #6 FIX: Consume (advance) a nonce account after successful transaction
368
+ * Per TOSS Paper Section 9: Nonce advancement for replay protection
369
+ */
370
+ async consumeNonceAccount(
371
+ nonceAccountAddress: PublicKey,
372
+ nonceAuthority: PublicKey
373
+ ): Promise<TransactionInstruction> {
374
+ // Create instruction to advance nonce
375
+ return SystemProgram.nonceAdvance({
376
+ noncePubkey: nonceAccountAddress,
377
+ authorizedPubkey: nonceAuthority,
378
+ });
379
+ }
380
+
381
+ /**
382
+ * GAP #6 FIX: Validate nonce account state on chain
383
+ * Checks that nonce account exists and is properly configured
384
+ */
385
+ async validateNonceAccountOnchain(
386
+ nonceAccountAddress: PublicKey,
387
+ _expectedAuthority?: PublicKey
388
+ ): Promise<{ valid: boolean; error?: string }> {
389
+ try {
390
+ const nonceAccountInfo =
391
+ await this.connection.getAccountInfo(nonceAccountAddress);
392
+
393
+ if (!nonceAccountInfo) {
394
+ return { valid: false, error: 'Nonce account does not exist' };
395
+ }
396
+
397
+ const SYSTEM_PROGRAM_ID = new PublicKey(
398
+ '11111111111111111111111111111111'
399
+ );
400
+ if (!nonceAccountInfo.owner.equals(SYSTEM_PROGRAM_ID)) {
401
+ return {
402
+ valid: false,
403
+ error: 'Nonce account is not owned by SystemProgram',
404
+ };
405
+ }
406
+
407
+ // Check if account is initialized (nonce is stored in first 32 bytes after version)
408
+ if (nonceAccountInfo.data.length < 48) {
409
+ return { valid: false, error: 'Nonce account data is malformed' };
410
+ }
411
+
412
+ return { valid: true };
413
+ } catch (error) {
414
+ return {
415
+ valid: false,
416
+ error: `Nonce account validation failed: ${error instanceof Error ? error.message : String(error)}`,
417
+ };
418
+ }
419
+ }
420
+
421
+ /**
422
+ * GAP #6 FIX: Get current nonce value from blockchain
423
+ * Reads the actual nonce state from the nonce account
424
+ */
425
+ async getCurrentNonceFromChain(
426
+ nonceAccountAddress: PublicKey
427
+ ): Promise<number> {
428
+ try {
429
+ const nonceAccount =
430
+ await this.connection.getAccountInfo(nonceAccountAddress);
431
+
432
+ if (!nonceAccount || nonceAccount.data.length < 48) {
433
+ return 0;
434
+ }
435
+
436
+ // Nonce value is stored at offset 32-40 in NonceAccount structure
437
+ const nonceData = nonceAccount.data.slice(32, 40);
438
+ return nonceData.readBigUInt64LE(0) as unknown as number;
439
+ } catch (error) {
440
+ console.warn('Failed to get nonce from chain:', error);
441
+ return 0;
442
+ }
443
+ }
444
+ }
@@ -317,7 +317,7 @@ export class TossClient {
317
317
  try {
318
318
  return await syncToChain(
319
319
  this.connection,
320
- this.config.feePayer?.publicKey
320
+ this.config.feePayer?.publicKey?.toBase58()
321
321
  );
322
322
  } catch (error) {
323
323
  if (error instanceof TossError) throw error;