toss-expo-sdk 0.1.2 → 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 (73) hide show
  1. package/README.md +368 -15
  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/hooks/useOfflineBLETransactions.js +314 -0
  11. package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
  12. package/lib/module/index.js +12 -8
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/intent.js +129 -0
  15. package/lib/module/intent.js.map +1 -1
  16. package/lib/module/noise.js +175 -0
  17. package/lib/module/noise.js.map +1 -1
  18. package/lib/module/reconciliation.js +155 -0
  19. package/lib/module/reconciliation.js.map +1 -1
  20. package/lib/module/services/authService.js +164 -1
  21. package/lib/module/services/authService.js.map +1 -1
  22. package/lib/module/storage/secureStorage.js +102 -0
  23. package/lib/module/storage/secureStorage.js.map +1 -1
  24. package/lib/module/sync.js +25 -1
  25. package/lib/module/sync.js.map +1 -1
  26. package/lib/module/types/nonceAccount.js +2 -0
  27. package/lib/module/types/nonceAccount.js.map +1 -0
  28. package/lib/module/types/tossUser.js +16 -1
  29. package/lib/module/types/tossUser.js.map +1 -1
  30. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
  31. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
  32. package/lib/typescript/src/ble.d.ts +31 -2
  33. package/lib/typescript/src/ble.d.ts.map +1 -1
  34. package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
  35. package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
  36. package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
  37. package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
  38. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
  39. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +9 -4
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/intent.d.ts +15 -0
  43. package/lib/typescript/src/intent.d.ts.map +1 -1
  44. package/lib/typescript/src/noise.d.ts +62 -0
  45. package/lib/typescript/src/noise.d.ts.map +1 -1
  46. package/lib/typescript/src/reconciliation.d.ts +6 -0
  47. package/lib/typescript/src/reconciliation.d.ts.map +1 -1
  48. package/lib/typescript/src/services/authService.d.ts +26 -1
  49. package/lib/typescript/src/services/authService.d.ts.map +1 -1
  50. package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
  51. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
  52. package/lib/typescript/src/sync.d.ts +6 -1
  53. package/lib/typescript/src/sync.d.ts.map +1 -1
  54. package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
  55. package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
  56. package/lib/typescript/src/types/tossUser.d.ts +16 -0
  57. package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/__tests__/solana-program-simple.test.ts +256 -0
  60. package/src/ble.ts +105 -4
  61. package/src/client/BLETransactionHandler.ts +364 -0
  62. package/src/client/NonceAccountManager.ts +444 -0
  63. package/src/client/TossClient.ts +1 -1
  64. package/src/hooks/useOfflineBLETransactions.ts +438 -0
  65. package/src/index.tsx +40 -6
  66. package/src/intent.ts +166 -0
  67. package/src/noise.ts +238 -0
  68. package/src/reconciliation.ts +184 -0
  69. package/src/services/authService.ts +188 -1
  70. package/src/storage/secureStorage.ts +138 -0
  71. package/src/sync.ts +40 -0
  72. package/src/types/nonceAccount.ts +75 -0
  73. package/src/types/tossUser.ts +35 -2
@@ -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
+ }
@@ -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(),