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
|
@@ -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
|
+
}
|
package/src/client/TossClient.ts
CHANGED