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.
- package/README.md +380 -25
- package/lib/module/ble.js +59 -4
- package/lib/module/ble.js.map +1 -1
- package/lib/module/client/BLETransactionHandler.js +277 -0
- package/lib/module/client/BLETransactionHandler.js.map +1 -0
- package/lib/module/client/NonceAccountManager.js +364 -0
- package/lib/module/client/NonceAccountManager.js.map +1 -0
- package/lib/module/client/TossClient.js +1 -1
- package/lib/module/client/TossClient.js.map +1 -1
- package/lib/module/examples/enhancedFeaturesFlow.js +233 -0
- package/lib/module/examples/enhancedFeaturesFlow.js.map +1 -0
- package/lib/module/examples/offlinePaymentFlow.js +27 -27
- package/lib/module/examples/offlinePaymentFlow.js.map +1 -1
- package/lib/module/hooks/useOfflineBLETransactions.js +314 -0
- package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
- package/lib/module/index.js +18 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/intent.js +129 -0
- package/lib/module/intent.js.map +1 -1
- package/lib/module/noise.js +175 -0
- package/lib/module/noise.js.map +1 -1
- package/lib/module/qr.js +2 -2
- package/lib/module/reconciliation.js +155 -0
- package/lib/module/reconciliation.js.map +1 -1
- package/lib/module/services/authService.js +166 -3
- package/lib/module/services/authService.js.map +1 -1
- package/lib/module/storage/secureStorage.js +102 -0
- package/lib/module/storage/secureStorage.js.map +1 -1
- package/lib/module/sync.js +25 -1
- package/lib/module/sync.js.map +1 -1
- package/lib/module/types/nonceAccount.js +2 -0
- package/lib/module/types/nonceAccount.js.map +1 -0
- package/lib/module/types/tossUser.js +16 -1
- package/lib/module/types/tossUser.js.map +1 -1
- package/lib/module/utils/compression.js +210 -0
- package/lib/module/utils/compression.js.map +1 -0
- package/lib/module/wifi.js +311 -0
- package/lib/module/wifi.js.map +1 -0
- package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
- package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
- package/lib/typescript/src/ble.d.ts +31 -2
- package/lib/typescript/src/ble.d.ts.map +1 -1
- package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
- package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
- package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
- package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
- package/lib/typescript/src/examples/enhancedFeaturesFlow.d.ts +45 -0
- package/lib/typescript/src/examples/enhancedFeaturesFlow.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
- package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/intent.d.ts +15 -0
- package/lib/typescript/src/intent.d.ts.map +1 -1
- package/lib/typescript/src/noise.d.ts +62 -0
- package/lib/typescript/src/noise.d.ts.map +1 -1
- package/lib/typescript/src/reconciliation.d.ts +6 -0
- package/lib/typescript/src/reconciliation.d.ts.map +1 -1
- package/lib/typescript/src/services/authService.d.ts +26 -1
- package/lib/typescript/src/services/authService.d.ts.map +1 -1
- package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
- package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
- package/lib/typescript/src/sync.d.ts +6 -1
- package/lib/typescript/src/sync.d.ts.map +1 -1
- package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
- package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
- package/lib/typescript/src/types/tossUser.d.ts +16 -0
- package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
- package/lib/typescript/src/utils/compression.d.ts +52 -0
- package/lib/typescript/src/utils/compression.d.ts.map +1 -0
- package/lib/typescript/src/wifi.d.ts +116 -0
- package/lib/typescript/src/wifi.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/solana-program-simple.test.ts +256 -0
- package/src/ble.ts +105 -4
- package/src/client/BLETransactionHandler.ts +364 -0
- package/src/client/NonceAccountManager.ts +444 -0
- package/src/client/TossClient.ts +1 -1
- package/src/examples/enhancedFeaturesFlow.ts +272 -0
- package/src/examples/offlinePaymentFlow.ts +27 -27
- package/src/hooks/useOfflineBLETransactions.ts +438 -0
- package/src/index.tsx +52 -6
- package/src/intent.ts +166 -0
- package/src/noise.ts +238 -0
- package/src/qr.tsx +2 -2
- package/src/reconciliation.ts +184 -0
- package/src/services/authService.ts +190 -3
- package/src/storage/secureStorage.ts +138 -0
- package/src/sync.ts +40 -0
- package/src/types/nonceAccount.ts +75 -0
- package/src/types/tossUser.ts +35 -2
- package/src/utils/compression.ts +247 -0
- package/src/wifi.ts +401 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as SecureStore from 'expo-secure-store';
|
|
2
|
-
import { Keypair, PublicKey } from '@solana/web3.js';
|
|
2
|
+
import { Keypair, PublicKey, Connection } from '@solana/web3.js';
|
|
3
3
|
import * as LocalAuthentication from 'expo-local-authentication';
|
|
4
4
|
import type { TossUser } from '../types/tossUser';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
+
import { NonceAccountManager } from '../client/NonceAccountManager';
|
|
6
7
|
|
|
7
8
|
export const SESSION_KEY = 'toss_user_session';
|
|
8
9
|
const WALLET_KEY = 'toss_encrypted_wallet';
|
|
9
10
|
const BIOMETRIC_SALT_KEY = 'toss_biometric_salt';
|
|
11
|
+
const NONCE_ACCOUNT_KEY = 'toss_nonce_account';
|
|
10
12
|
|
|
11
13
|
type UserSession = {
|
|
12
14
|
id: string;
|
|
@@ -43,6 +45,10 @@ export class AuthService {
|
|
|
43
45
|
lastActive: new Date().toISOString(),
|
|
44
46
|
client: 'mobile',
|
|
45
47
|
},
|
|
48
|
+
security: {
|
|
49
|
+
biometricEnabled: false,
|
|
50
|
+
nonceAccountRequiresBiometric: true,
|
|
51
|
+
},
|
|
46
52
|
status: 'active',
|
|
47
53
|
lastSeen: new Date().toISOString(),
|
|
48
54
|
tossFeatures: {
|
|
@@ -50,6 +56,8 @@ export class AuthService {
|
|
|
50
56
|
canReceive: true,
|
|
51
57
|
isPrivateTxEnabled: true,
|
|
52
58
|
maxTransactionAmount: 10000,
|
|
59
|
+
offlineTransactionsEnabled: false,
|
|
60
|
+
nonceAccountEnabled: false,
|
|
53
61
|
},
|
|
54
62
|
createdAt: new Date().toISOString(),
|
|
55
63
|
updatedAt: new Date().toISOString(),
|
|
@@ -158,7 +166,7 @@ export class AuthService {
|
|
|
158
166
|
): Promise<void> {
|
|
159
167
|
if (!useBiometrics) {
|
|
160
168
|
throw new Error(
|
|
161
|
-
'
|
|
169
|
+
' SECURITY ERROR: Biometric protection is mandatory for wallet security'
|
|
162
170
|
);
|
|
163
171
|
}
|
|
164
172
|
|
|
@@ -168,7 +176,7 @@ export class AuthService {
|
|
|
168
176
|
|
|
169
177
|
if (!hasHardware || !isEnrolled) {
|
|
170
178
|
throw new Error(
|
|
171
|
-
'
|
|
179
|
+
' Biometric authentication required but not configured on device'
|
|
172
180
|
);
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -235,4 +243,183 @@ export class AuthService {
|
|
|
235
243
|
await SecureStore.deleteItemAsync(BIOMETRIC_SALT_KEY);
|
|
236
244
|
await SecureStore.deleteItemAsync(SESSION_KEY);
|
|
237
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create a secure durable nonce account for offline transactions
|
|
249
|
+
* REQUIRES biometric authentication for maximum security
|
|
250
|
+
*
|
|
251
|
+
* This creates a nonce account that enables:
|
|
252
|
+
* - Offline transaction creation (with replay protection)
|
|
253
|
+
* - Biometric-protected signing
|
|
254
|
+
* - Encrypted storage with Noise Protocol support
|
|
255
|
+
*/
|
|
256
|
+
static async createSecureNonceAccount(
|
|
257
|
+
user: TossUser,
|
|
258
|
+
connection: Connection,
|
|
259
|
+
userKeypair: Keypair
|
|
260
|
+
): Promise<TossUser> {
|
|
261
|
+
// Verify biometric is available and enrolled
|
|
262
|
+
const hasHardware = await LocalAuthentication.hasHardwareAsync();
|
|
263
|
+
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
|
|
264
|
+
|
|
265
|
+
if (!hasHardware || !isEnrolled) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
' Biometric authentication required but not configured on this device'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Require biometric verification before creating nonce account
|
|
272
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
273
|
+
promptMessage: 'Biometric verification required to create nonce account',
|
|
274
|
+
fallbackLabel: 'Use PIN',
|
|
275
|
+
disableDeviceFallback: false,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (!result.success) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'Biometric verification failed - nonce account creation denied'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
// Initialize nonce account manager
|
|
286
|
+
const nonceManager = new NonceAccountManager(connection);
|
|
287
|
+
|
|
288
|
+
// Generate nonce authority keypair (separate from user wallet for security)
|
|
289
|
+
const nonceAuthorityKeypair = Keypair.generate();
|
|
290
|
+
|
|
291
|
+
// Create the nonce account
|
|
292
|
+
const nonceAccountInfo = await nonceManager.createNonceAccount(
|
|
293
|
+
user,
|
|
294
|
+
nonceAuthorityKeypair,
|
|
295
|
+
userKeypair.publicKey,
|
|
296
|
+
{
|
|
297
|
+
requireBiometric: true,
|
|
298
|
+
securityLevel: 'high',
|
|
299
|
+
persistToSecureStorage: true,
|
|
300
|
+
autoRenew: true,
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Update user with nonce account information
|
|
305
|
+
const updatedUser: TossUser = {
|
|
306
|
+
...user,
|
|
307
|
+
nonceAccount: {
|
|
308
|
+
address: new PublicKey(nonceAccountInfo.address),
|
|
309
|
+
authorizedSigner: new PublicKey(nonceAccountInfo.authorizedSigner),
|
|
310
|
+
isBiometricProtected: true,
|
|
311
|
+
status: 'active',
|
|
312
|
+
},
|
|
313
|
+
security: {
|
|
314
|
+
...user.security,
|
|
315
|
+
biometricEnabled: true,
|
|
316
|
+
nonceAccountRequiresBiometric: true,
|
|
317
|
+
lastBiometricVerification: Math.floor(Date.now() / 1000),
|
|
318
|
+
},
|
|
319
|
+
tossFeatures: {
|
|
320
|
+
...user.tossFeatures,
|
|
321
|
+
offlineTransactionsEnabled: true,
|
|
322
|
+
nonceAccountEnabled: true,
|
|
323
|
+
},
|
|
324
|
+
updatedAt: new Date().toISOString(),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return updatedUser;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const errorMessage =
|
|
330
|
+
error instanceof Error ? error.message : String(error);
|
|
331
|
+
throw new Error(`Failed to create nonce account: ${errorMessage}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Enable offline transactions for a user with nonce account support
|
|
337
|
+
* Ensures all security measures are in place
|
|
338
|
+
*/
|
|
339
|
+
static async enableOfflineTransactions(user: TossUser): Promise<TossUser> {
|
|
340
|
+
// Verify user has nonce account
|
|
341
|
+
if (!user.nonceAccount) {
|
|
342
|
+
throw new Error('User does not have a nonce account. Create one first.');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Verify biometric is enabled
|
|
346
|
+
if (!user.security.biometricEnabled) {
|
|
347
|
+
throw new Error('Biometric authentication must be enabled first');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Update user with offline transactions enabled
|
|
351
|
+
return {
|
|
352
|
+
...user,
|
|
353
|
+
tossFeatures: {
|
|
354
|
+
...user.tossFeatures,
|
|
355
|
+
offlineTransactionsEnabled: true,
|
|
356
|
+
nonceAccountEnabled: true,
|
|
357
|
+
},
|
|
358
|
+
updatedAt: new Date().toISOString(),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Verify nonce account is accessible and valid
|
|
364
|
+
* Requires biometric authentication
|
|
365
|
+
*/
|
|
366
|
+
static async verifyNonceAccountAccess(userId: string): Promise<boolean> {
|
|
367
|
+
try {
|
|
368
|
+
// Verify biometric
|
|
369
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
370
|
+
promptMessage: 'Verify nonce account access with biometric',
|
|
371
|
+
fallbackLabel: 'Use PIN',
|
|
372
|
+
disableDeviceFallback: false,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (!result.success) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check if nonce account exists in secure storage
|
|
380
|
+
const storageKey = `${NONCE_ACCOUNT_KEY}_${userId}`;
|
|
381
|
+
const stored = await SecureStore.getItemAsync(storageKey);
|
|
382
|
+
|
|
383
|
+
return stored !== null;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('Failed to verify nonce account access:', error);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Revoke nonce account (security measure)
|
|
392
|
+
* Requires biometric verification
|
|
393
|
+
*/
|
|
394
|
+
static async revokeNonceAccount(
|
|
395
|
+
userId: string,
|
|
396
|
+
user: TossUser
|
|
397
|
+
): Promise<TossUser> {
|
|
398
|
+
// Require biometric verification
|
|
399
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
400
|
+
promptMessage: 'Biometric verification required to revoke nonce account',
|
|
401
|
+
fallbackLabel: 'Use PIN',
|
|
402
|
+
disableDeviceFallback: false,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!result.success) {
|
|
406
|
+
throw new Error('Biometric verification failed - revocation denied');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Remove nonce account from storage
|
|
410
|
+
const storageKey = `${NONCE_ACCOUNT_KEY}_${userId}`;
|
|
411
|
+
await SecureStore.deleteItemAsync(storageKey);
|
|
412
|
+
|
|
413
|
+
// Update user
|
|
414
|
+
return {
|
|
415
|
+
...user,
|
|
416
|
+
nonceAccount: undefined,
|
|
417
|
+
tossFeatures: {
|
|
418
|
+
...user.tossFeatures,
|
|
419
|
+
offlineTransactionsEnabled: false,
|
|
420
|
+
nonceAccountEnabled: false,
|
|
421
|
+
},
|
|
422
|
+
updatedAt: new Date().toISOString(),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
238
425
|
}
|
|
@@ -98,3 +98,141 @@ export async function clearAllSecureIntents(): Promise<void> {
|
|
|
98
98
|
throw new StorageError('Failed to clear all intents', { cause: error });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GAP #1: Cleanup expired intents from local storage
|
|
104
|
+
* Per TOSS Paper Section 8: Local state is append-only until settlement confirmation
|
|
105
|
+
*/
|
|
106
|
+
export async function cleanupExpiredIntents(): Promise<number> {
|
|
107
|
+
try {
|
|
108
|
+
const intents = await getAllSecureIntents();
|
|
109
|
+
const now = Math.floor(Date.now() / 1000);
|
|
110
|
+
let cleanedCount = 0;
|
|
111
|
+
|
|
112
|
+
for (const intent of intents) {
|
|
113
|
+
if (intent.expiry < now) {
|
|
114
|
+
await removeSecureIntent(intent.id);
|
|
115
|
+
cleanedCount++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return cleanedCount;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new StorageError('Failed to cleanup expired intents', {
|
|
122
|
+
cause: error,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* GAP #2: Store and retrieve reconciliation state
|
|
129
|
+
* Per TOSS Paper Section 9: Track which intents have been synced/failed/conflicted
|
|
130
|
+
*/
|
|
131
|
+
const RECONCILIATION_STATE_KEY = 'toss_reconciliation_state_';
|
|
132
|
+
|
|
133
|
+
export interface ReconciliationStateData {
|
|
134
|
+
userId: string;
|
|
135
|
+
lastSyncTime: number;
|
|
136
|
+
lastSyncSlot: number;
|
|
137
|
+
processedIntents: string[]; // Intent IDs successfully settled
|
|
138
|
+
failedIntents: string[]; // Intent IDs that failed/were rejected
|
|
139
|
+
conflictingIntents: string[]; // Intent IDs with detected conflicts
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function saveReconciliationState(
|
|
143
|
+
userId: string,
|
|
144
|
+
state: Partial<ReconciliationStateData>
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
try {
|
|
147
|
+
const key = `${RECONCILIATION_STATE_KEY}${userId}`;
|
|
148
|
+
const existing = await SecureStore.getItemAsync(key);
|
|
149
|
+
const currentState: ReconciliationStateData = existing
|
|
150
|
+
? JSON.parse(existing)
|
|
151
|
+
: {
|
|
152
|
+
userId,
|
|
153
|
+
lastSyncTime: 0,
|
|
154
|
+
lastSyncSlot: 0,
|
|
155
|
+
processedIntents: [],
|
|
156
|
+
failedIntents: [],
|
|
157
|
+
conflictingIntents: [],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const merged: ReconciliationStateData = {
|
|
161
|
+
...currentState,
|
|
162
|
+
...state,
|
|
163
|
+
userId, // Always preserve userId
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
await SecureStore.setItemAsync(key, JSON.stringify(merged));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new StorageError('Failed to save reconciliation state', {
|
|
169
|
+
cause: error,
|
|
170
|
+
userId,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function getReconciliationState(
|
|
176
|
+
userId: string
|
|
177
|
+
): Promise<ReconciliationStateData> {
|
|
178
|
+
try {
|
|
179
|
+
const key = `${RECONCILIATION_STATE_KEY}${userId}`;
|
|
180
|
+
const value = await SecureStore.getItemAsync(key);
|
|
181
|
+
|
|
182
|
+
if (value) {
|
|
183
|
+
return JSON.parse(value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
userId,
|
|
188
|
+
lastSyncTime: 0,
|
|
189
|
+
lastSyncSlot: 0,
|
|
190
|
+
processedIntents: [],
|
|
191
|
+
failedIntents: [],
|
|
192
|
+
conflictingIntents: [],
|
|
193
|
+
};
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new StorageError('Failed to retrieve reconciliation state', {
|
|
196
|
+
cause: error,
|
|
197
|
+
userId,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function updateReconciliationState(
|
|
203
|
+
userId: string,
|
|
204
|
+
intentId: string,
|
|
205
|
+
status: 'processed' | 'failed' | 'conflicted'
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
try {
|
|
208
|
+
const current = await getReconciliationState(userId);
|
|
209
|
+
|
|
210
|
+
// Remove from all lists first
|
|
211
|
+
current.processedIntents = current.processedIntents.filter(
|
|
212
|
+
(id) => id !== intentId
|
|
213
|
+
);
|
|
214
|
+
current.failedIntents = current.failedIntents.filter(
|
|
215
|
+
(id) => id !== intentId
|
|
216
|
+
);
|
|
217
|
+
current.conflictingIntents = current.conflictingIntents.filter(
|
|
218
|
+
(id) => id !== intentId
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Add to appropriate list
|
|
222
|
+
if (status === 'processed') {
|
|
223
|
+
current.processedIntents.push(intentId);
|
|
224
|
+
} else if (status === 'failed') {
|
|
225
|
+
current.failedIntents.push(intentId);
|
|
226
|
+
} else if (status === 'conflicted') {
|
|
227
|
+
current.conflictingIntents.push(intentId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await saveReconciliationState(userId, current);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new StorageError('Failed to update reconciliation state', {
|
|
233
|
+
cause: error,
|
|
234
|
+
userId,
|
|
235
|
+
intentId,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/sync.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Implements Section 9 of the TOSS Technical Paper:
|
|
5
5
|
* Upon regaining connectivity, devices initiate reconciliation with onchain state.
|
|
6
6
|
* All offline artifacts are verified onchain and settled with deterministic outcomes.
|
|
7
|
+
*
|
|
8
|
+
* GAP #2 FIX: Track synchronization state persistently
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { Connection, PublicKey } from '@solana/web3.js';
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
type SettlementResult,
|
|
15
17
|
type ReconciliationState,
|
|
16
18
|
} from './reconciliation';
|
|
19
|
+
import { updateReconciliationState } from './storage/secureStorage';
|
|
17
20
|
import { NetworkError } from './errors';
|
|
18
21
|
|
|
19
22
|
export interface SyncResult {
|
|
@@ -40,12 +43,16 @@ export interface SyncResult {
|
|
|
40
43
|
* 2. Settle all pending intents
|
|
41
44
|
* 3. Update local state with results
|
|
42
45
|
*
|
|
46
|
+
* GAP #2 FIX: Persist reconciliation state for future queries
|
|
47
|
+
*
|
|
43
48
|
* @param connection Connection to Solana RPC
|
|
49
|
+
* @param userId User ID for state tracking (required for persistence)
|
|
44
50
|
* @param feePayer Optional fee payer keypair public key
|
|
45
51
|
* @returns Detailed sync results including conflicts and settlements
|
|
46
52
|
*/
|
|
47
53
|
export async function syncToChain(
|
|
48
54
|
connection: Connection,
|
|
55
|
+
userId?: string,
|
|
49
56
|
feePayer?: PublicKey
|
|
50
57
|
): Promise<SyncResult> {
|
|
51
58
|
const syncTimestamp = Math.floor(Date.now() / 1000);
|
|
@@ -71,6 +78,39 @@ export async function syncToChain(
|
|
|
71
78
|
// Step 4: Get final reconciliation state
|
|
72
79
|
const reconciliationState = await getReconciliationState(connection);
|
|
73
80
|
|
|
81
|
+
// GAP #2 FIX: Persist reconciliation state to storage
|
|
82
|
+
if (userId) {
|
|
83
|
+
try {
|
|
84
|
+
// Update individual intent statuses
|
|
85
|
+
for (const settlement of successfulSettlements) {
|
|
86
|
+
await updateReconciliationState(
|
|
87
|
+
userId,
|
|
88
|
+
settlement.intentId,
|
|
89
|
+
'processed'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
for (const settlement of failedSettlements) {
|
|
93
|
+
await updateReconciliationState(
|
|
94
|
+
userId,
|
|
95
|
+
settlement.intentId,
|
|
96
|
+
'failed'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
for (const conflict of detectedConflicts) {
|
|
100
|
+
await updateReconciliationState(
|
|
101
|
+
userId,
|
|
102
|
+
conflict.intentId,
|
|
103
|
+
'conflicted'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} catch (storageError) {
|
|
107
|
+
console.warn(
|
|
108
|
+
'Failed to update reconciliation state storage:',
|
|
109
|
+
storageError
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
74
114
|
const isComplete =
|
|
75
115
|
failedSettlements.length === 0 && detectedConflicts.length === 0;
|
|
76
116
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a durable nonce account for offline transaction support
|
|
3
|
+
* Enables secure offline transaction creation with replay protection
|
|
4
|
+
*/
|
|
5
|
+
export interface NonceAccountInfo {
|
|
6
|
+
// Account Identity
|
|
7
|
+
address: string; // Public key of the nonce account
|
|
8
|
+
owner: string; // Public key of owner/authority
|
|
9
|
+
authorizedSigner: string; // Public key authorized to use this nonce
|
|
10
|
+
|
|
11
|
+
// Nonce State
|
|
12
|
+
currentNonce: number; // Current nonce value
|
|
13
|
+
lastUsedNonce: number; // Last consumed nonce
|
|
14
|
+
blockhash: string; // Associated blockhash
|
|
15
|
+
|
|
16
|
+
// Security
|
|
17
|
+
isBiometricProtected: boolean; // Requires biometric to use
|
|
18
|
+
createdAt: number; // Unix timestamp
|
|
19
|
+
lastModified: number; // Unix timestamp
|
|
20
|
+
|
|
21
|
+
// Storage
|
|
22
|
+
isStoredSecurely: boolean; // In secure enclave
|
|
23
|
+
encryptedData?: string; // Optional encrypted backup
|
|
24
|
+
minRentLamports?: number; // Minimum rent exemption amount
|
|
25
|
+
status?: 'active' | 'expired' | 'revoked'; // Account lifecycle status
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for creating a durable nonce account
|
|
30
|
+
*/
|
|
31
|
+
export interface CreateNonceAccountOptions {
|
|
32
|
+
// Biometric & Security
|
|
33
|
+
requireBiometric?: boolean; // Default: true (mandatory)
|
|
34
|
+
securityLevel?: 'standard' | 'high' | 'maximum'; // Default: 'standard'
|
|
35
|
+
|
|
36
|
+
// Nonce Config
|
|
37
|
+
minNonceCount?: number; // Minimum nonces to maintain (default: 1)
|
|
38
|
+
maxNonceCount?: number; // Maximum nonces to cache (default: 100)
|
|
39
|
+
|
|
40
|
+
// Storage
|
|
41
|
+
persistToSecureStorage?: boolean; // Default: true
|
|
42
|
+
allowCloudBackup?: boolean; // Default: false
|
|
43
|
+
|
|
44
|
+
// Lifecycle
|
|
45
|
+
autoRenew?: boolean; // Auto-renew when close to expiry (default: true)
|
|
46
|
+
expiryDays?: number; // Account expiry in days (default: 365)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Nonce account cache entry for efficient offline usage
|
|
51
|
+
*/
|
|
52
|
+
export interface NonceAccountCacheEntry {
|
|
53
|
+
accountInfo: NonceAccountInfo;
|
|
54
|
+
nonces: number[];
|
|
55
|
+
expiresAt: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Represents a transaction prepared for offline use with nonce account
|
|
60
|
+
*/
|
|
61
|
+
export interface OfflineTransaction {
|
|
62
|
+
id: string;
|
|
63
|
+
nonceAccount: string;
|
|
64
|
+
nonce: number;
|
|
65
|
+
transaction: string; // Base64 or serialized transaction
|
|
66
|
+
signature?: string;
|
|
67
|
+
status: 'prepared' | 'signed' | 'submitted' | 'confirmed' | 'failed';
|
|
68
|
+
createdAt: number;
|
|
69
|
+
expiresAt: number;
|
|
70
|
+
metadata?: {
|
|
71
|
+
description?: string;
|
|
72
|
+
tags?: string[];
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/types/tossUser.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { PublicKey } from '@solana/web3.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Represents a TOSS wallet user in the ecosystem
|
|
5
|
+
* Enhanced with secure nonce account support for offline transactions
|
|
5
6
|
*/
|
|
6
7
|
export type TossUser = {
|
|
7
8
|
// Core Identity
|
|
@@ -9,13 +10,22 @@ export type TossUser = {
|
|
|
9
10
|
username: string; // @handle format (e.g., '@alice')
|
|
10
11
|
displayName?: string; // Optional display name
|
|
11
12
|
|
|
12
|
-
// TOSS Wallet
|
|
13
|
+
// TOSS Wallet (Primary wallet)
|
|
13
14
|
wallet: {
|
|
14
15
|
publicKey: PublicKey; // Main wallet address
|
|
15
16
|
isVerified: boolean; // KYC/verification status
|
|
16
17
|
createdAt: string; // ISO timestamp
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// Nonce Account (For offline transactions with replay protection)
|
|
21
|
+
nonceAccount?: {
|
|
22
|
+
address: PublicKey; // Public key of the nonce account
|
|
23
|
+
authorizedSigner: PublicKey; // Public key authorized to use this nonce
|
|
24
|
+
isBiometricProtected: boolean; // Requires biometric authentication
|
|
25
|
+
expiresAt?: number; // Unix timestamp of expiry
|
|
26
|
+
status: 'active' | 'expired' | 'revoked'; // Account status
|
|
27
|
+
};
|
|
28
|
+
|
|
19
29
|
// Device & Session
|
|
20
30
|
device: {
|
|
21
31
|
id: string; // Unique device ID
|
|
@@ -24,16 +34,26 @@ export type TossUser = {
|
|
|
24
34
|
client: 'mobile' | 'web' | 'desktop';
|
|
25
35
|
};
|
|
26
36
|
|
|
37
|
+
// Security & Biometrics
|
|
38
|
+
security: {
|
|
39
|
+
biometricEnabled: boolean; // Biometric authentication enabled
|
|
40
|
+
biometricSalt?: string; // Salt for biometric derivation
|
|
41
|
+
nonceAccountRequiresBiometric: boolean; // Nonce operations require biometric
|
|
42
|
+
lastBiometricVerification?: number; // Unix timestamp
|
|
43
|
+
};
|
|
44
|
+
|
|
27
45
|
// Status
|
|
28
46
|
status: 'active' | 'inactive' | 'restricted';
|
|
29
47
|
lastSeen: string; // ISO timestamp
|
|
30
48
|
|
|
31
|
-
// TOSS-specific
|
|
49
|
+
// TOSS-specific Features
|
|
32
50
|
tossFeatures: {
|
|
33
51
|
canSend: boolean;
|
|
34
52
|
canReceive: boolean;
|
|
35
53
|
isPrivateTxEnabled: boolean;
|
|
36
54
|
maxTransactionAmount: number; // In lamports
|
|
55
|
+
offlineTransactionsEnabled?: boolean; // Can create offline transactions
|
|
56
|
+
nonceAccountEnabled?: boolean; // Has durable nonce account
|
|
37
57
|
};
|
|
38
58
|
|
|
39
59
|
// Timestamps
|
|
@@ -62,12 +82,23 @@ export const exampleTossUser: TossUser = {
|
|
|
62
82
|
isVerified: true,
|
|
63
83
|
createdAt: '2023-01-01T00:00:00Z',
|
|
64
84
|
},
|
|
85
|
+
nonceAccount: {
|
|
86
|
+
address: new PublicKey('22222222222222222222222222222222'),
|
|
87
|
+
authorizedSigner: new PublicKey('33333333333333333333333333333333'),
|
|
88
|
+
isBiometricProtected: true,
|
|
89
|
+
status: 'active',
|
|
90
|
+
},
|
|
65
91
|
device: {
|
|
66
92
|
id: 'dev_xyz789',
|
|
67
93
|
name: 'Alice iPhone',
|
|
68
94
|
lastActive: new Date().toISOString(),
|
|
69
95
|
client: 'mobile',
|
|
70
96
|
},
|
|
97
|
+
security: {
|
|
98
|
+
biometricEnabled: true,
|
|
99
|
+
nonceAccountRequiresBiometric: true,
|
|
100
|
+
lastBiometricVerification: Math.floor(Date.now() / 1000),
|
|
101
|
+
},
|
|
71
102
|
status: 'active',
|
|
72
103
|
lastSeen: new Date().toISOString(),
|
|
73
104
|
tossFeatures: {
|
|
@@ -75,6 +106,8 @@ export const exampleTossUser: TossUser = {
|
|
|
75
106
|
canReceive: true,
|
|
76
107
|
isPrivateTxEnabled: true,
|
|
77
108
|
maxTransactionAmount: 1000000000, // 1 SOL in lamports
|
|
109
|
+
offlineTransactionsEnabled: true,
|
|
110
|
+
nonceAccountEnabled: true,
|
|
78
111
|
},
|
|
79
112
|
createdAt: '2023-01-01T00:00:00Z',
|
|
80
113
|
updatedAt: new Date().toISOString(),
|