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.
Files changed (93) hide show
  1. package/README.md +490 -81
  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 +27 -44
  9. package/lib/module/client/TossClient.js.map +1 -1
  10. package/lib/module/contexts/WalletContext.js +4 -4
  11. package/lib/module/contexts/WalletContext.js.map +1 -1
  12. package/lib/module/discovery.js +35 -8
  13. package/lib/module/discovery.js.map +1 -1
  14. package/lib/module/examples/offlinePaymentFlow.js +27 -2
  15. package/lib/module/examples/offlinePaymentFlow.js.map +1 -1
  16. package/lib/module/hooks/useOfflineBLETransactions.js +314 -0
  17. package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
  18. package/lib/module/index.js +13 -8
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/intent.js +198 -0
  21. package/lib/module/intent.js.map +1 -1
  22. package/lib/module/nfc.js +1 -1
  23. package/lib/module/noise.js +176 -1
  24. package/lib/module/noise.js.map +1 -1
  25. package/lib/module/reconciliation.js +155 -0
  26. package/lib/module/reconciliation.js.map +1 -1
  27. package/lib/module/services/authService.js +164 -1
  28. package/lib/module/services/authService.js.map +1 -1
  29. package/lib/module/storage/secureStorage.js +102 -0
  30. package/lib/module/storage/secureStorage.js.map +1 -1
  31. package/lib/module/storage.js +4 -4
  32. package/lib/module/sync.js +25 -1
  33. package/lib/module/sync.js.map +1 -1
  34. package/lib/module/types/nonceAccount.js +2 -0
  35. package/lib/module/types/nonceAccount.js.map +1 -0
  36. package/lib/module/types/tossUser.js +16 -1
  37. package/lib/module/types/tossUser.js.map +1 -1
  38. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
  39. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
  40. package/lib/typescript/src/ble.d.ts +31 -2
  41. package/lib/typescript/src/ble.d.ts.map +1 -1
  42. package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
  43. package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
  44. package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
  45. package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
  46. package/lib/typescript/src/client/TossClient.d.ts +10 -12
  47. package/lib/typescript/src/client/TossClient.d.ts.map +1 -1
  48. package/lib/typescript/src/discovery.d.ts +8 -2
  49. package/lib/typescript/src/discovery.d.ts.map +1 -1
  50. package/lib/typescript/src/examples/offlinePaymentFlow.d.ts +9 -1
  51. package/lib/typescript/src/examples/offlinePaymentFlow.d.ts.map +1 -1
  52. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
  53. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
  54. package/lib/typescript/src/index.d.ts +11 -4
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/intent.d.ts +26 -0
  57. package/lib/typescript/src/intent.d.ts.map +1 -1
  58. package/lib/typescript/src/noise.d.ts +62 -0
  59. package/lib/typescript/src/noise.d.ts.map +1 -1
  60. package/lib/typescript/src/reconciliation.d.ts +6 -0
  61. package/lib/typescript/src/reconciliation.d.ts.map +1 -1
  62. package/lib/typescript/src/services/authService.d.ts +26 -1
  63. package/lib/typescript/src/services/authService.d.ts.map +1 -1
  64. package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
  65. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
  66. package/lib/typescript/src/sync.d.ts +6 -1
  67. package/lib/typescript/src/sync.d.ts.map +1 -1
  68. package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
  69. package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
  70. package/lib/typescript/src/types/tossUser.d.ts +16 -0
  71. package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
  72. package/package.json +12 -1
  73. package/src/__tests__/reconciliation.test.tsx +7 -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 +36 -49
  79. package/src/contexts/WalletContext.tsx +4 -4
  80. package/src/discovery.ts +46 -8
  81. package/src/examples/offlinePaymentFlow.ts +48 -2
  82. package/src/hooks/useOfflineBLETransactions.ts +438 -0
  83. package/src/index.tsx +49 -7
  84. package/src/intent.ts +254 -0
  85. package/src/nfc.ts +4 -4
  86. package/src/noise.ts +239 -1
  87. package/src/reconciliation.ts +184 -0
  88. package/src/services/authService.ts +188 -1
  89. package/src/storage/secureStorage.ts +142 -4
  90. package/src/storage.ts +4 -4
  91. package/src/sync.ts +40 -0
  92. package/src/types/nonceAccount.ts +75 -0
  93. package/src/types/tossUser.ts +35 -2
@@ -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
+ }
@@ -9,7 +9,7 @@ import {
9
9
  import { processIntentsForSync } from '../intentManager';
10
10
  import { TossError, NetworkError, StorageError, ERROR_CODES } from '../errors';
11
11
  import { createNonceAccount, getNonce } from '../utils/nonceUtils';
12
- import { useWallet } from '../contexts/WalletContext';
12
+ // Note: TossClient is not tied to a React hook. To use wallet-provided keys in React, pass a Keypair to methods directly.
13
13
  import { syncToChain, checkSyncStatus, type SyncResult } from '../sync';
14
14
  import { detectConflicts, getReconciliationState } from '../reconciliation';
15
15
 
@@ -41,7 +41,6 @@ export class TossClient {
41
41
  };
42
42
  private nonceAccount?: Keypair;
43
43
  private nonceAuth?: PublicKey;
44
- private walletContext: ReturnType<typeof useWallet>;
45
44
 
46
45
  static createClient(config: TossConfig): TossClient {
47
46
  return new TossClient(config);
@@ -60,11 +59,6 @@ export class TossClient {
60
59
  feePayer: config.feePayer,
61
60
  } as const;
62
61
  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
62
  }
69
63
 
70
64
  private getDefaultRpcUrl(network: string): string {
@@ -172,13 +166,20 @@ export class TossClient {
172
166
  );
173
167
  }
174
168
 
175
- // Handle 'current' sender to use the wallet context
176
- const senderKeypair =
177
- sender === 'current' ? this.walletContext.keypair : sender;
169
+ // Handle 'current' sender: explicit wallet integration via React hooks is
170
+ // not available in this non-React class. Require a Keypair to be passed.
171
+ if (sender === 'current') {
172
+ throw new TossError(
173
+ 'Using "current" as sender is only supported when the client is used inside a WalletProvider. Please provide a Keypair instead.',
174
+ ERROR_CODES.SIGNATURE_VERIFICATION_FAILED
175
+ );
176
+ }
177
+
178
+ const senderKeypair = sender as Keypair;
178
179
 
179
180
  if (!senderKeypair) {
180
181
  throw new TossError(
181
- 'No sender keypair provided and no wallet is connected',
182
+ 'No sender keypair provided',
182
183
  ERROR_CODES.SIGNATURE_VERIFICATION_FAILED
183
184
  );
184
185
  }
@@ -262,10 +263,10 @@ export class TossClient {
262
263
 
263
264
  await secureStoreIntent(updatedIntent);
264
265
  return updatedIntent;
265
- } catch (error) {
266
- if (error instanceof TossError) throw error;
266
+ } catch (err) {
267
+ if (err instanceof TossError) throw err;
267
268
  throw new StorageError('Failed to update intent status', {
268
- cause: error,
269
+ cause: err,
269
270
  intentId,
270
271
  status,
271
272
  });
@@ -316,7 +317,7 @@ export class TossClient {
316
317
  try {
317
318
  return await syncToChain(
318
319
  this.connection,
319
- this.config.feePayer?.publicKey
320
+ this.config.feePayer?.publicKey?.toBase58()
320
321
  );
321
322
  } catch (error) {
322
323
  if (error instanceof TossError) throw error;
@@ -364,7 +365,14 @@ export class TossClient {
364
365
  /**
365
366
  * Create an intent from the current user's wallet
366
367
  */
368
+ /**
369
+ * Create an intent using an explicit Keypair for the sender.
370
+ * Use this method from non-React contexts. For React apps, use
371
+ * WalletProvider.createUserIntent helper wrappers that call
372
+ * TossClient.createIntent with the unlocked keypair.
373
+ */
367
374
  async createUserIntent(
375
+ senderKeypair: Keypair,
368
376
  recipient: PublicKey | string,
369
377
  amount: number,
370
378
  options: {
@@ -372,32 +380,14 @@ export class TossClient {
372
380
  useDurableNonce?: boolean;
373
381
  } = {}
374
382
  ): 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
383
  const recipientPubkey =
394
384
  typeof recipient === 'string' ? new PublicKey(recipient) : recipient;
395
385
 
396
386
  return this.createIntent(
397
- 'current', // This will use the wallet context's keypair
387
+ senderKeypair,
398
388
  recipientPubkey,
399
389
  amount,
400
- this.walletContext.user.wallet.publicKey,
390
+ senderKeypair.publicKey,
401
391
  {
402
392
  ...options,
403
393
  memo: options.memo || `TOSS transfer to ${recipientPubkey.toBase58()}`,
@@ -406,30 +396,27 @@ export class TossClient {
406
396
  }
407
397
 
408
398
  /**
409
- * Get the current user's wallet address
399
+ * The following helper methods require a WalletProvider (React) context.
400
+ * TossClient is framework-agnostic; if you need these features from a
401
+ * React app, use the WalletProvider utilities instead.
410
402
  */
411
403
  getCurrentUserAddress(): string | null {
412
- return this.walletContext.user?.wallet.publicKey.toString() || null;
404
+ throw new Error(
405
+ 'getCurrentUserAddress is only available when using WalletProvider'
406
+ );
413
407
  }
414
408
 
415
- /**
416
- * Check if the wallet is currently unlocked
417
- */
418
409
  isWalletUnlocked(): boolean {
419
- return this.walletContext.isUnlocked;
410
+ throw new Error(
411
+ 'isWalletUnlocked is only available when using WalletProvider'
412
+ );
420
413
  }
421
414
 
422
- /**
423
- * Lock the wallet
424
- */
425
415
  async lockWallet(): Promise<void> {
426
- await this.walletContext.lockWallet();
416
+ throw new Error('lockWallet is only available when using WalletProvider');
427
417
  }
428
418
 
429
- /**
430
- * Sign out the current user
431
- */
432
419
  async signOut(): Promise<void> {
433
- await this.walletContext.signOut();
420
+ throw new Error('signOut is only available when using WalletProvider');
434
421
  }
435
422
  }
@@ -31,10 +31,10 @@ export const WalletProvider: React.FC<{ children: ReactNode }> = ({
31
31
  const session = await AuthService.getSession();
32
32
  if (session) {
33
33
  // In a real app, you'd fetch the user from your backend
34
- const { user } = await AuthService.signInWithWallet(
34
+ const { user: sessionUser } = await AuthService.signInWithWallet(
35
35
  session.walletAddress
36
36
  );
37
- setUser(user);
37
+ setUser(sessionUser);
38
38
  setIsUnlocked(await AuthService.isWalletUnlocked());
39
39
  }
40
40
  } catch (error) {
@@ -77,11 +77,11 @@ export const WalletProvider: React.FC<{ children: ReactNode }> = ({
77
77
  walletAddress: string,
78
78
  isTemporary: boolean = false
79
79
  ): Promise<void> => {
80
- const { user } = await AuthService.signInWithWallet(
80
+ const { user: sessionUser } = await AuthService.signInWithWallet(
81
81
  walletAddress,
82
82
  isTemporary
83
83
  );
84
- setUser(user);
84
+ setUser(sessionUser);
85
85
  setIsUnlocked(true);
86
86
  };
87
87