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,331 @@
1
+ /**
2
+ * Complete Offline Payment Flow Example
3
+ *
4
+ * Demonstrates the full TOSS lifecycle per Section 12 of the Technical Paper:
5
+ * 1. Sender constructs and signs payment intent
6
+ * 2. Intent is exchanged offline via BLE/NFC/QR
7
+ * 3. Both devices store pending intent
8
+ * 4. When connectivity is restored, devices reconcile
9
+ * 5. Intent is submitted onchain with deterministic outcome
10
+ */
11
+
12
+ import { Connection, Keypair, PublicKey } from '@solana/web3.js';
13
+ import { createIntent, type SolanaIntent, verifyIntent } from '../intent';
14
+ import {
15
+ secureStoreIntent,
16
+ getAllSecureIntents,
17
+ } from '../storage/secureStorage';
18
+ import {
19
+ deviceDiscovery,
20
+ intentExchange,
21
+ MultiDeviceConflictResolver,
22
+ type PeerDevice,
23
+ } from '../discovery';
24
+ import { syncToChain } from '../sync';
25
+ import { TossError } from '../errors';
26
+
27
+ /**
28
+ * Example: Sender initiates offline payment
29
+ *
30
+ * This simulates a sender who wants to send lamports to a recipient
31
+ * while offline. The intent is created, signed, and stored locally.
32
+ */
33
+ export async function exampleInitiateOfflinePayment(
34
+ senderKeypair: Keypair,
35
+ recipientAddress: string,
36
+ amountLamports: number,
37
+ connection: Connection
38
+ ): Promise<SolanaIntent> {
39
+ console.log('📝 Creating offline payment intent...');
40
+
41
+ // Create the intent (this is done offline, no network needed)
42
+ const intent = await createIntent(
43
+ senderKeypair,
44
+ new PublicKey(recipientAddress),
45
+ amountLamports,
46
+ connection,
47
+ {
48
+ expiresIn: 24 * 60 * 60, // Valid for 24 hours
49
+ }
50
+ );
51
+
52
+ console.log(`✅ Intent created: ${intent.id}`);
53
+ console.log(` From: ${intent.from}`);
54
+ console.log(` To: ${intent.to}`);
55
+ console.log(` Amount: ${intent.amount} lamports`);
56
+ console.log(` Expires at: ${new Date(intent.expiry * 1000).toISOString()}`);
57
+
58
+ // Store locally
59
+ await secureStoreIntent(intent);
60
+ console.log('💾 Intent stored securely locally\n');
61
+
62
+ return intent;
63
+ }
64
+
65
+ /**
66
+ * Example: Intent exchange via proximity (BLE/NFC)
67
+ *
68
+ * One device has an intent it wants to share with a nearby peer.
69
+ * This demonstrates the intent exchange protocol.
70
+ */
71
+ export async function exampleExchangeIntentWithPeer(
72
+ intent: SolanaIntent,
73
+ localDeviceId: string,
74
+ peerDeviceId: string,
75
+ peerDevice: PeerDevice
76
+ ): Promise<void> {
77
+ console.log('📡 Initiating intent exchange with peer...');
78
+ console.log(` Local Device: ${localDeviceId}`);
79
+ console.log(` Peer Device: ${peerDeviceId}`);
80
+
81
+ // Register the peer
82
+ deviceDiscovery.registerPeer(peerDevice);
83
+ console.log(`✅ Peer registered: ${peerDevice.id}`);
84
+
85
+ // Create an exchange request
86
+ const exchangeRequest = intentExchange.createRequest(
87
+ intent,
88
+ localDeviceId,
89
+ undefined,
90
+ 5 * 60 // 5 minute expiry
91
+ );
92
+
93
+ console.log(`📨 Exchange request created: ${exchangeRequest.requestId}`);
94
+ console.log(` Intent ID: ${intent.id}`);
95
+ console.log(` Amount: ${intent.amount} lamports`);
96
+
97
+ // In a real scenario, this request would be transmitted via BLE/NFC/QR
98
+ // For this example, we'll simulate the peer receiving and accepting it
99
+
100
+ // Simulate peer accepting the request
101
+ const response = intentExchange.createResponse(
102
+ exchangeRequest.requestId,
103
+ peerDeviceId,
104
+ 'accepted',
105
+ undefined,
106
+ [intent.id]
107
+ );
108
+
109
+ console.log(`\n✅ Peer accepted exchange`);
110
+ console.log(` Status: ${response.status}`);
111
+ console.log(
112
+ ` Acknowledged intents: ${response.acknowledgedIntentIds?.join(', ')}\n`
113
+ );
114
+
115
+ // In a real app, the peer would now have the intent in their local storage
116
+ }
117
+
118
+ /**
119
+ * Example: Multiple devices create conflicting intents
120
+ *
121
+ * Demonstrates TOSS's deterministic conflict resolution when
122
+ * multiple offline devices create intents for the same action.
123
+ */
124
+ export async function exampleMultiDeviceConflict(
125
+ connection: Connection
126
+ ): Promise<void> {
127
+ console.log('🔄 Simulating multi-device conflict scenario...\n');
128
+
129
+ // Create keypair for "Device A"
130
+ const senderKeypair = Keypair.generate();
131
+ const recipient = new PublicKey('11111111111111111111111111111111');
132
+ const amount = 1000000; // 0.001 SOL
133
+
134
+ // Both devices create identical intents (same sender, recipient, amount)
135
+ // but at slightly different times
136
+ const intentA = await createIntent(
137
+ senderKeypair,
138
+ recipient,
139
+ amount,
140
+ connection,
141
+ { expiresIn: 3600 }
142
+ );
143
+
144
+ // Simulate Device B creating same intent 1 second later
145
+ await new Promise((resolve) => setTimeout(resolve, 1000));
146
+
147
+ const intentB = await createIntent(
148
+ senderKeypair, // Same sender!
149
+ recipient,
150
+ amount,
151
+ connection,
152
+ { expiresIn: 3600 }
153
+ );
154
+
155
+ console.log('❌ Conflict Detected!');
156
+ console.log(
157
+ ` Device A created intent: ${intentA.id} at ${intentA.createdAt}`
158
+ );
159
+ console.log(
160
+ ` Device B created intent: ${intentB.id} at ${intentB.createdAt}`
161
+ );
162
+ console.log(` Both intents: Same sender, same recipient, same amount\n`);
163
+
164
+ // Use the conflict resolver
165
+ const conflictingIntents = [intentA, intentB];
166
+ const resolution =
167
+ MultiDeviceConflictResolver.resolveConflicts(conflictingIntents);
168
+
169
+ console.log('⚖️ Deterministic Resolution Applied:');
170
+ console.log(` Winner: ${resolution.winner.id}`);
171
+ console.log(` Winner nonce: ${resolution.winner.nonce}`);
172
+ console.log(
173
+ ` Winner timestamp: ${new Date(resolution.winner.createdAt * 1000).toISOString()}`
174
+ );
175
+
176
+ console.log(
177
+ `\n Losers: ${resolution.losers.map((i: SolanaIntent) => i.id).join(', ')}`
178
+ );
179
+ console.log(` (These intents will be marked failed during settlement)\n`);
180
+ }
181
+
182
+ /**
183
+ * Example: Full offline-to-settlement flow
184
+ *
185
+ * Shows the complete journey of an intent from creation to onchain settlement.
186
+ */
187
+ export async function exampleCompleteOfflineFlow(
188
+ senderKeypair: Keypair,
189
+ recipientAddress: string,
190
+ amountLamports: number,
191
+ connection: Connection
192
+ ): Promise<void> {
193
+ console.log('='.repeat(60));
194
+ console.log('🚀 TOSS Complete Offline Payment Flow');
195
+ console.log('='.repeat(60) + '\n');
196
+
197
+ try {
198
+ // Step 1: Create intent offline
199
+ console.log('STEP 1: Offline Intent Creation');
200
+ console.log('-'.repeat(60));
201
+ const intent = await exampleInitiateOfflinePayment(
202
+ senderKeypair,
203
+ recipientAddress,
204
+ amountLamports,
205
+ connection
206
+ );
207
+
208
+ // Step 2: Simulate peer device discovery and exchange
209
+ console.log('STEP 2: Peer Discovery & Intent Exchange');
210
+ console.log('-'.repeat(60));
211
+ const peerDevice: PeerDevice = {
212
+ id: 'device_peer_001',
213
+ lastSeen: Date.now(),
214
+ transport: 'ble',
215
+ signalStrength: -45, // dBm
216
+ trustScore: 75,
217
+ };
218
+
219
+ await exampleExchangeIntentWithPeer(
220
+ intent,
221
+ 'device_local_001',
222
+ 'device_peer_001',
223
+ peerDevice
224
+ );
225
+
226
+ // Step 3: Device reconnects and initiates synchronisation
227
+ console.log('STEP 3: Synchronisation with Solana');
228
+ console.log('-'.repeat(60));
229
+ console.log('📱 Device reconnected to network...');
230
+ console.log('🔄 Initiating sync with Solana blockchain...\n');
231
+
232
+ // Check sync status
233
+ const syncResult = await syncToChain(connection);
234
+
235
+ console.log('📊 Sync Results:');
236
+ console.log(
237
+ ` Successful settlements: ${syncResult.successfulSettlements.length}`
238
+ );
239
+ console.log(
240
+ ` Failed settlements: ${syncResult.failedSettlements.length}`
241
+ );
242
+ console.log(
243
+ ` Detected conflicts: ${syncResult.detectedConflicts.length}`
244
+ );
245
+ console.log(` Overall complete: ${syncResult.isComplete}\n`);
246
+
247
+ if (syncResult.successfulSettlements.length > 0) {
248
+ console.log('✅ Successful Settlements:');
249
+ for (const settlement of syncResult.successfulSettlements) {
250
+ console.log(` Intent ${settlement.intentId}`);
251
+ console.log(` Signature: ${settlement.signature}`);
252
+ console.log(
253
+ ` Timestamp: ${new Date(settlement.timestamp * 1000).toISOString()}`
254
+ );
255
+ }
256
+ console.log();
257
+ }
258
+
259
+ if (syncResult.failedSettlements.length > 0) {
260
+ console.log('❌ Failed Settlements:');
261
+ for (const settlement of syncResult.failedSettlements) {
262
+ console.log(` Intent ${settlement.intentId}`);
263
+ console.log(` Reason: ${settlement.error}`);
264
+ }
265
+ console.log();
266
+ }
267
+
268
+ if (syncResult.detectedConflicts.length > 0) {
269
+ console.log('⚠️ Detected Conflicts:');
270
+ for (const conflict of syncResult.detectedConflicts) {
271
+ console.log(` Intent ${conflict.intentId}: ${conflict.conflict}`);
272
+ }
273
+ console.log();
274
+ }
275
+
276
+ // Step 4: Verify final state
277
+ console.log('STEP 4: Final State Verification');
278
+ console.log('-'.repeat(60));
279
+ const allIntents = await getAllSecureIntents();
280
+ const settledIntents = allIntents.filter(
281
+ (i: SolanaIntent) => i.status === 'settled'
282
+ );
283
+ const failedIntents = allIntents.filter(
284
+ (i: SolanaIntent) => i.status === 'failed'
285
+ );
286
+
287
+ console.log(`📦 Intent Storage:
288
+ Total intents: ${allIntents.length}
289
+ Settled: ${settledIntents.length}
290
+ Failed: ${failedIntents.length}\n`);
291
+
292
+ console.log('✨ Flow complete!\n');
293
+ console.log('='.repeat(60));
294
+ } catch (error) {
295
+ console.error('❌ Error during offline flow:', error);
296
+ if (error instanceof TossError) {
297
+ console.error(` Error code: ${(error as TossError).code}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Example: Verify intent before exchange
304
+ *
305
+ * Good practice: receivers should verify intent signatures
306
+ * before accepting and storing them.
307
+ */
308
+ export async function exampleVerifyIntentBeforeAcceptance(
309
+ intent: SolanaIntent,
310
+ connection: Connection
311
+ ): Promise<boolean> {
312
+ console.log('🔐 Verifying intent signature...');
313
+
314
+ try {
315
+ const isValid = await verifyIntent(intent, connection);
316
+
317
+ if (isValid) {
318
+ console.log('✅ Intent signature is valid');
319
+ console.log(` From: ${intent.from}`);
320
+ console.log(` To: ${intent.to}`);
321
+ console.log(` Amount: ${intent.amount} lamports`);
322
+ return true;
323
+ } else {
324
+ console.log('❌ Intent signature is invalid');
325
+ return false;
326
+ }
327
+ } catch (error) {
328
+ console.error('❌ Verification failed:', error);
329
+ return false;
330
+ }
331
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,61 @@
1
+ // Core types and intents
2
+ export { createIntent, type SolanaIntent, type IntentStatus } from './intent';
3
+
4
+ // Intent management
5
+ export {
6
+ verifyIntentSignature,
7
+ isIntentExpired,
8
+ updateIntentStatus,
9
+ validateIntent,
10
+ processIntentsForSync,
11
+ filterExpiredIntents,
12
+ } from './intentManager';
13
+
14
+ // Storage
15
+ export {
16
+ storePendingIntent,
17
+ getPendingIntents,
18
+ clearPendingIntents,
19
+ } from './storage';
20
+
21
+ // Transport methods
22
+ export { startTossScan, requestBLEPermissions } from './ble';
23
+ export { initNFC, readNFCUser, writeUserToNFC, writeIntentToNFC } from './nfc';
24
+ export { QRScanner } from './qr';
25
+
26
+ // Client
27
+ export { TossClient, type TossConfig } from './client/TossClient';
28
+
29
+ // Create client instance
30
+ import { TossClient } from './client/TossClient';
31
+ export const createClient = TossClient.createClient;
32
+
33
+ // Sync and settlement
34
+ export { syncToChain, checkSyncStatus, type SyncResult } from './sync';
35
+
36
+ // Reconciliation and conflict detection
37
+ export {
38
+ reconcilePendingIntents,
39
+ settleIntent,
40
+ validateIntentOnchain,
41
+ buildTransactionFromIntent,
42
+ submitTransactionToChain,
43
+ detectConflicts,
44
+ getReconciliationState,
45
+ type SettlementResult,
46
+ type ReconciliationState,
47
+ } from './reconciliation';
48
+
49
+ // Device discovery and intent exchange
50
+ export {
51
+ DeviceDiscoveryService,
52
+ IntentExchangeProtocol,
53
+ IntentRoutingService,
54
+ MultiDeviceConflictResolver,
55
+ deviceDiscovery,
56
+ intentExchange,
57
+ intentRouting,
58
+ type PeerDevice,
59
+ type IntentExchangeRequest,
60
+ type IntentExchangeResponse,
61
+ } from './discovery';
package/src/intent.ts ADDED
@@ -0,0 +1,328 @@
1
+ import { PublicKey, Keypair, Connection } from '@solana/web3.js';
2
+ import type { AccountInfo } from '@solana/web3.js';
3
+ import bs58 from 'bs58';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import { sign } from 'tweetnacl';
6
+ import nacl from 'tweetnacl';
7
+ import {
8
+ encryptForArciumInternal,
9
+ type ArciumEncryptedOutput,
10
+ } from './internal/arciumHelper';
11
+ // Nonce management now handled internally
12
+
13
+ /**
14
+ * Status of an intent in its lifecycle
15
+ */
16
+ export type IntentStatus = 'pending' | 'settled' | 'failed' | 'expired';
17
+
18
+ /**
19
+ * Core type for an offline intent following TOSS specification
20
+ */
21
+ export interface SolanaIntent {
22
+ // Core fields
23
+ id: string; // Unique identifier for the intent
24
+ from: string; // Sender's public key
25
+ to: string; // Recipient's public key
26
+ amount: number; // Amount in lamports
27
+ nonce: number; // For replay protection
28
+ expiry: number; // Unix timestamp in seconds
29
+ signature: string; // Signature of the intent
30
+ status: IntentStatus;
31
+ createdAt: number;
32
+ updatedAt: number;
33
+ error?: string;
34
+
35
+ // Transaction metadata
36
+ blockhash?: string; // Optional: For transaction construction
37
+ feePayer?: string; // Optional: Public key of fee payer
38
+ signatures?: string[]; // Optional: Transaction signatures
39
+ serialized?: string; // Optional: Serialized transaction
40
+ nonceAccount?: string; // Optional: Public key of the nonce account
41
+ nonceAuth?: string; // Optional: Public key authorized to use the nonce
42
+
43
+ // Privacy features
44
+ encrypted?: ArciumEncryptedOutput; // Optional encrypted payload
45
+ }
46
+
47
+ /**
48
+ * Options for creating a new intent
49
+ */
50
+ export interface CreateIntentOptions {
51
+ /** Whether to encrypt the transaction details using Arcium */
52
+ privateTransaction?: boolean;
53
+ /** Program ID for Arcium encryption */
54
+ mxeProgramId?: PublicKey;
55
+ /** Provider for blockchain access */
56
+ provider?: any; // AnchorProvider or similar
57
+ /** Expiry time in seconds from now (default: 1 hour) */
58
+ expiresIn?: number;
59
+ /** Custom nonce (auto-generated if not provided) */
60
+ nonce?: number;
61
+ /** Keypair for the nonce account (for durable transactions) */
62
+ nonceAccount?: Keypair;
63
+ /** Public key authorized to use the nonce account */
64
+ nonceAuth?: PublicKey;
65
+ /** Fee payer for the transaction (defaults to sender) */
66
+ feePayer?: PublicKey | string;
67
+ }
68
+
69
+ /**
70
+ * Manages nonce values for transaction replay protection
71
+ */
72
+ class NonceManager {
73
+ private nonceStore: Map<string, { nonce: number; lastUsed: number }> =
74
+ new Map();
75
+ private readonly NONCE_EXPIRY = 5 * 60 * 1000; // 5 minutes
76
+
77
+ async getNextNonce(
78
+ publicKey: PublicKey,
79
+ connection: Connection
80
+ ): Promise<number> {
81
+ const key = publicKey.toBase58();
82
+ const now = Date.now();
83
+
84
+ // Clean up old nonces
85
+ this.cleanupNonces();
86
+
87
+ try {
88
+ // Get nonce from chain
89
+ const accountInfo = await connection.getAccountInfo(publicKey);
90
+ const chainNonce = accountInfo
91
+ ? this.extractNonceFromAccountInfo(accountInfo)
92
+ : 0;
93
+
94
+ // Get or initialize stored nonce
95
+ const stored = this.nonceStore.get(key) || {
96
+ nonce: chainNonce,
97
+ lastUsed: now,
98
+ };
99
+ const nextNonce = Math.max(stored.nonce + 1, chainNonce + 1);
100
+
101
+ // Update store
102
+ this.nonceStore.set(key, { nonce: nextNonce, lastUsed: now });
103
+ return nextNonce;
104
+ } catch (error) {
105
+ console.warn(
106
+ 'Failed to get nonce from chain, using in-memory nonce',
107
+ error
108
+ );
109
+ const stored = this.nonceStore.get(key) || { nonce: 0, lastUsed: now };
110
+ const nextNonce = stored.nonce + 1;
111
+ this.nonceStore.set(key, { nonce: nextNonce, lastUsed: now });
112
+ return nextNonce;
113
+ }
114
+ }
115
+
116
+ private extractNonceFromAccountInfo(
117
+ accountInfo: AccountInfo<Buffer>
118
+ ): number {
119
+ // For SystemProgram accounts, nonce is typically stored in the first 8 bytes
120
+ const data = accountInfo.data;
121
+ return data?.length >= 8 ? data.readUInt32LE(0) : 0;
122
+ }
123
+
124
+ private cleanupNonces() {
125
+ const now = Date.now();
126
+ for (const [key, value] of this.nonceStore.entries()) {
127
+ if (now - value.lastUsed > this.NONCE_EXPIRY) {
128
+ this.nonceStore.delete(key);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ export const nonceManager = new NonceManager();
135
+
136
+ /**
137
+ * Creates a signed intent that can be verified offline
138
+ */
139
+ export async function createSignedIntent(
140
+ sender: Keypair,
141
+ recipient: PublicKey,
142
+ amount: number,
143
+ connection: Connection,
144
+ options: CreateIntentOptions = {}
145
+ ): Promise<SolanaIntent> {
146
+ const now = Math.floor(Date.now() / 1000);
147
+ const defaultExpiry = 24 * 60 * 60; // 24 hours default
148
+
149
+ // Get latest blockhash and nonce
150
+ const [{ blockhash }, nonce] = await Promise.all([
151
+ connection.getLatestBlockhash(),
152
+ options.nonce !== undefined
153
+ ? Promise.resolve(options.nonce)
154
+ : nonceManager.getNextNonce(sender.publicKey, connection),
155
+ ]);
156
+
157
+ // Create base intent
158
+ const baseIntent: Omit<SolanaIntent, 'signature'> = {
159
+ id: uuidv4(),
160
+ from: sender.publicKey.toBase58(),
161
+ to: recipient.toBase58(),
162
+ amount,
163
+ nonce,
164
+ expiry: now + (options.expiresIn || defaultExpiry),
165
+ blockhash,
166
+ feePayer: options.nonceAuth?.toBase58() || sender.publicKey.toBase58(),
167
+ status: 'pending',
168
+ createdAt: now,
169
+ updatedAt: now,
170
+ ...(options.nonceAccount && options.nonceAuth
171
+ ? {
172
+ nonceAccount: options.nonceAccount.publicKey.toBase58(),
173
+ nonceAuth: options.nonceAuth.toBase58(),
174
+ }
175
+ : {}),
176
+ };
177
+
178
+ // Sign the intent
179
+ const signature = sign(
180
+ Buffer.from(JSON.stringify(baseIntent)),
181
+ sender.secretKey
182
+ );
183
+
184
+ return {
185
+ ...baseIntent,
186
+ signature: bs58.encode(signature),
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Verifies the signature, nonce, and expiry of an intent
192
+ */
193
+ export async function verifyIntent(
194
+ intent: SolanaIntent,
195
+ connection?: Connection
196
+ ): Promise<boolean> {
197
+ try {
198
+ // Basic validation
199
+ if (!intent.signature || !intent.from || !intent.to) {
200
+ return false;
201
+ }
202
+
203
+ // Check if intent is expired
204
+ if (isIntentExpired(intent)) {
205
+ return false;
206
+ }
207
+
208
+ // Verify signature
209
+ const signature = bs58.decode(intent.signature);
210
+ const message = Buffer.from(
211
+ JSON.stringify({ ...intent, signature: undefined })
212
+ );
213
+ const publicKey = new PublicKey(intent.from).toBytes();
214
+
215
+ try {
216
+ // Use nacl.sign.detached.verify for detached signature verification
217
+ // Requires: message, detached signature, public key
218
+ const publicKeyUint8 = new Uint8Array(publicKey);
219
+ const signatureUint8 = new Uint8Array(signature);
220
+
221
+ const verified = nacl.sign.detached.verify(
222
+ Buffer.from(message),
223
+ signatureUint8,
224
+ publicKeyUint8
225
+ );
226
+
227
+ if (!verified) {
228
+ return false;
229
+ }
230
+ } catch (error) {
231
+ console.error('Signature verification failed:', error);
232
+ return false;
233
+ }
234
+
235
+ // Verify nonce if connection is provided
236
+ if (connection) {
237
+ try {
238
+ const accountInfo = await connection.getAccountInfo(
239
+ new PublicKey(intent.from)
240
+ );
241
+ if (accountInfo) {
242
+ const currentNonce =
243
+ accountInfo.data?.length >= 8
244
+ ? accountInfo.data.readUInt32LE(0)
245
+ : 0;
246
+ if (intent.nonce <= currentNonce) {
247
+ return false; // Nonce too low or reused
248
+ }
249
+ }
250
+ } catch (error) {
251
+ console.warn('Failed to verify nonce:', error);
252
+ // Continue without nonce verification if we can't check the chain
253
+ }
254
+ }
255
+
256
+ return true;
257
+ } catch (error) {
258
+ console.error('Intent verification failed:', error);
259
+ return false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Creates an offline Solana intent following TOSS specification.
265
+ * If privateTransaction is true, encrypts internal data with Arcium.
266
+ */
267
+ export async function createIntent(
268
+ sender: Keypair,
269
+ recipient: PublicKey,
270
+ amount: number,
271
+ connection: Connection,
272
+ options: CreateIntentOptions = {}
273
+ ): Promise<SolanaIntent> {
274
+ // First create and sign the intent
275
+ const intent = await createSignedIntent(
276
+ sender,
277
+ recipient,
278
+ amount,
279
+ connection,
280
+ options
281
+ );
282
+
283
+ // If private transaction, encrypt the intent data
284
+ if (options.privateTransaction) {
285
+ if (!options.mxeProgramId) {
286
+ throw new Error('MXE Program ID is required for private transactions');
287
+ }
288
+ if (!options.provider) {
289
+ throw new Error('Provider is required for private transactions');
290
+ }
291
+
292
+ const plaintextValues: bigint[] = [
293
+ BigInt(amount),
294
+ // Include additional fields for privacy as needed
295
+ ];
296
+
297
+ intent.encrypted = await encryptForArciumInternal(
298
+ options.mxeProgramId,
299
+ plaintextValues,
300
+ options.provider
301
+ );
302
+ }
303
+
304
+ return intent;
305
+ }
306
+
307
+ /**
308
+ * Checks if an intent has expired
309
+ */
310
+ export function isIntentExpired(intent: SolanaIntent): boolean {
311
+ return intent.expiry <= Math.floor(Date.now() / 1000);
312
+ }
313
+
314
+ /**
315
+ * Updates the status of an intent
316
+ */
317
+ export function updateIntentStatus(
318
+ intent: SolanaIntent,
319
+ status: IntentStatus,
320
+ error?: string
321
+ ): SolanaIntent {
322
+ return {
323
+ ...intent,
324
+ status,
325
+ error,
326
+ updatedAt: Math.floor(Date.now() / 1000),
327
+ };
328
+ }