@ledgerhq/cryptoassets 13.33.0-nightly.20251125074637 → 13.34.0-nightly.20251126023856
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +51 -14
- package/lib/cal-client/hooks/useTokensData.d.ts +2 -2
- package/lib/cal-client/persistence.d.ts +17 -10
- package/lib/cal-client/persistence.d.ts.map +1 -1
- package/lib/cal-client/persistence.js +84 -23
- package/lib/cal-client/persistence.js.map +1 -1
- package/lib/cal-client/state-manager/api.d.ts +157 -160
- package/lib/cal-client/state-manager/api.d.ts.map +1 -1
- package/lib/cal-client/state-manager/api.js +25 -8
- package/lib/cal-client/state-manager/api.js.map +1 -1
- package/lib-es/cal-client/hooks/useTokensData.d.ts +2 -2
- package/lib-es/cal-client/persistence.d.ts +17 -10
- package/lib-es/cal-client/persistence.d.ts.map +1 -1
- package/lib-es/cal-client/persistence.js +81 -22
- package/lib-es/cal-client/persistence.js.map +1 -1
- package/lib-es/cal-client/state-manager/api.d.ts +157 -160
- package/lib-es/cal-client/state-manager/api.d.ts.map +1 -1
- package/lib-es/cal-client/state-manager/api.js +26 -9
- package/lib-es/cal-client/state-manager/api.js.map +1 -1
- package/package.json +3 -3
- package/src/cal-client/persistence.test.ts +492 -34
- package/src/cal-client/persistence.ts +112 -32
- package/src/cal-client/state-manager/api.ts +28 -4
|
@@ -13,7 +13,7 @@ import type { ThunkDispatch } from "@reduxjs/toolkit";
|
|
|
13
13
|
* Current version of the persistence format
|
|
14
14
|
* Increment this when making breaking changes to the format
|
|
15
15
|
*/
|
|
16
|
-
export const PERSISTENCE_VERSION =
|
|
16
|
+
export const PERSISTENCE_VERSION = 2;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Serializable token format
|
|
@@ -45,11 +45,13 @@ export interface PersistedTokenEntry {
|
|
|
45
45
|
/**
|
|
46
46
|
* Root persistence format with versioning
|
|
47
47
|
*/
|
|
48
|
-
export interface
|
|
48
|
+
export interface PersistedCAL {
|
|
49
49
|
/** Format version for migration handling */
|
|
50
50
|
version: number;
|
|
51
51
|
/** Array of persisted tokens */
|
|
52
52
|
tokens: PersistedTokenEntry[];
|
|
53
|
+
/** Mapping of currencyId to X-Ledger-Commit hash */
|
|
54
|
+
hashes?: Record<string, string>;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -120,9 +122,9 @@ export function extractTokensFromState(state: StateWithCryptoAssets): PersistedT
|
|
|
120
122
|
const seenIds = new Set<string>();
|
|
121
123
|
|
|
122
124
|
// Extract tokens from fulfilled queries
|
|
123
|
-
|
|
124
|
-
for (const [_queryKey, query] of Object.entries(rtkState.queries as Record<string, any>)) {
|
|
125
|
+
for (const [_queryKey, query] of Object.entries(rtkState.queries)) {
|
|
125
126
|
if (
|
|
127
|
+
query &&
|
|
126
128
|
query.status === "fulfilled" &&
|
|
127
129
|
query.data &&
|
|
128
130
|
(query.endpointName === "findTokenById" ||
|
|
@@ -133,6 +135,7 @@ export function extractTokensFromState(state: StateWithCryptoAssets): PersistedT
|
|
|
133
135
|
// Deduplicate by token ID
|
|
134
136
|
if (token && token.id && !seenIds.has(token.id)) {
|
|
135
137
|
seenIds.add(token.id);
|
|
138
|
+
|
|
136
139
|
tokens.push({
|
|
137
140
|
data: toTokenCurrencyRaw(token),
|
|
138
141
|
timestamp: query.fulfilledTimeStamp || Date.now(),
|
|
@@ -145,6 +148,54 @@ export function extractTokensFromState(state: StateWithCryptoAssets): PersistedT
|
|
|
145
148
|
return tokens;
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Extracts hashes from getTokensSyncHash queries in RTK Query state
|
|
153
|
+
* Returns a mapping of currencyId to hash
|
|
154
|
+
*/
|
|
155
|
+
export function extractHashesFromState(state: StateWithCryptoAssets): Record<string, string> {
|
|
156
|
+
const rtkState = state[cryptoAssetsApi.reducerPath];
|
|
157
|
+
|
|
158
|
+
if (!rtkState || !rtkState.queries) {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const hashes: Record<string, string> = {};
|
|
163
|
+
|
|
164
|
+
// Extract hashes from fulfilled getTokensSyncHash queries
|
|
165
|
+
for (const [queryKey, query] of Object.entries(rtkState.queries)) {
|
|
166
|
+
if (
|
|
167
|
+
query &&
|
|
168
|
+
query.status === "fulfilled" &&
|
|
169
|
+
query.endpointName === "getTokensSyncHash" &&
|
|
170
|
+
query.data &&
|
|
171
|
+
typeof query.data === "string"
|
|
172
|
+
) {
|
|
173
|
+
// Extract currencyId from query key (format: 'getTokensSyncHash("ethereum")')
|
|
174
|
+
const match = queryKey.match(/getTokensSyncHash\("([^"]+)"\)/);
|
|
175
|
+
if (match && match[1]) {
|
|
176
|
+
hashes[match[1]] = query.data;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return hashes;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Extracts all persisted data (tokens and hashes) from RTK Query state
|
|
186
|
+
* Returns a complete PersistedCAL object ready for serialization
|
|
187
|
+
*/
|
|
188
|
+
export function extractPersistedCALFromState(state: StateWithCryptoAssets): PersistedCAL {
|
|
189
|
+
const tokens = extractTokensFromState(state);
|
|
190
|
+
const hashes = extractHashesFromState(state);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
version: PERSISTENCE_VERSION,
|
|
194
|
+
tokens,
|
|
195
|
+
...(Object.keys(hashes).length > 0 && { hashes }),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
148
199
|
/**
|
|
149
200
|
* Filters out expired tokens based on TTL
|
|
150
201
|
*/
|
|
@@ -167,35 +218,71 @@ export function filterExpiredTokens(
|
|
|
167
218
|
}
|
|
168
219
|
|
|
169
220
|
/**
|
|
170
|
-
* Restores tokens from persisted data to RTK Query cache
|
|
171
|
-
*
|
|
172
|
-
* Implements cross-caching: stores tokens by both ID and address
|
|
173
|
-
*
|
|
174
|
-
* @param dispatch - Redux dispatch function
|
|
175
|
-
* @param tokens - Array of persisted token entries
|
|
176
|
-
* @param ttl - Time-to-live in milliseconds (tokens older than this are skipped)
|
|
221
|
+
* Restores tokens from persisted data to RTK Query cache.
|
|
222
|
+
* Validates persisted hashes against current hashes and evicts cache if they differ.
|
|
177
223
|
*/
|
|
178
|
-
export function restoreTokensToCache(
|
|
224
|
+
export async function restoreTokensToCache(
|
|
179
225
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
226
|
dispatch: ThunkDispatch<any, any, any>,
|
|
181
|
-
|
|
227
|
+
persistedData: PersistedCAL,
|
|
182
228
|
ttl: number,
|
|
183
|
-
): void {
|
|
184
|
-
const validTokens = filterExpiredTokens(tokens, ttl);
|
|
185
|
-
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const validTokens = filterExpiredTokens(persistedData.tokens, ttl);
|
|
186
231
|
if (validTokens.length === 0) {
|
|
187
232
|
log("persistence", "No valid tokens to restore");
|
|
188
233
|
return;
|
|
189
234
|
}
|
|
190
235
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
const tokensByCurrency = new Map<string, PersistedTokenEntry[]>();
|
|
237
|
+
for (const entry of validTokens) {
|
|
238
|
+
const currencyId = entry.data.parentCurrencyId;
|
|
239
|
+
if (!tokensByCurrency.has(currencyId)) {
|
|
240
|
+
tokensByCurrency.set(currencyId, []);
|
|
241
|
+
}
|
|
242
|
+
tokensByCurrency.get(currencyId)!.push(entry);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const currencyIdsToEvict = new Set<string>();
|
|
246
|
+
const hashes = persistedData.hashes || {};
|
|
247
|
+
|
|
248
|
+
for (const currencyId of tokensByCurrency.keys()) {
|
|
249
|
+
const storedHash = hashes[currencyId];
|
|
250
|
+
if (!storedHash) continue;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const currentHashResult = await dispatch(
|
|
254
|
+
cryptoAssetsApi.endpoints.getTokensSyncHash.initiate(currencyId, { forceRefetch: false }),
|
|
255
|
+
);
|
|
256
|
+
const currentHash = currentHashResult.data;
|
|
257
|
+
|
|
258
|
+
if (currentHash && currentHash !== storedHash) {
|
|
259
|
+
log(
|
|
260
|
+
"persistence",
|
|
261
|
+
`Hash changed for currencyId ${currencyId}: ${storedHash} -> ${currentHash}, skipping restore`,
|
|
262
|
+
);
|
|
263
|
+
currencyIdsToEvict.add(currencyId);
|
|
198
264
|
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
log(
|
|
267
|
+
"persistence",
|
|
268
|
+
`Failed to validate hash for currencyId ${currencyId}, skipping restore`,
|
|
269
|
+
error,
|
|
270
|
+
);
|
|
271
|
+
currencyIdsToEvict.add(currencyId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tokensToRestore = validTokens.filter(
|
|
276
|
+
entry => !currencyIdsToEvict.has(entry.data.parentCurrencyId),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (tokensToRestore.length === 0) {
|
|
280
|
+
log("persistence", "No tokens to restore after hash validation");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const entries: Array<
|
|
285
|
+
| { endpointName: "findTokenById"; arg: { id: string }; value: TokenCurrency | undefined }
|
|
199
286
|
| {
|
|
200
287
|
endpointName: "findTokenByAddressInCurrency";
|
|
201
288
|
arg: { contract_address: string; network: string };
|
|
@@ -204,25 +291,19 @@ export function restoreTokensToCache(
|
|
|
204
291
|
> = [];
|
|
205
292
|
|
|
206
293
|
let skipped = 0;
|
|
207
|
-
|
|
208
|
-
for (const entry of validTokens) {
|
|
209
|
-
// Convert Raw format back to TokenCurrency
|
|
294
|
+
for (const entry of tokensToRestore) {
|
|
210
295
|
const token = fromTokenCurrencyRaw(entry.data);
|
|
211
|
-
|
|
212
296
|
if (!token) {
|
|
213
|
-
// Conversion failed (e.g., parent currency not found), skip this token
|
|
214
297
|
skipped++;
|
|
215
298
|
continue;
|
|
216
299
|
}
|
|
217
300
|
|
|
218
|
-
// Cache by ID
|
|
219
301
|
entries.push({
|
|
220
302
|
endpointName: "findTokenById",
|
|
221
303
|
arg: { id: token.id },
|
|
222
304
|
value: token,
|
|
223
305
|
});
|
|
224
306
|
|
|
225
|
-
// Cross-cache by address (for findTokenByAddressInCurrency queries)
|
|
226
307
|
entries.push({
|
|
227
308
|
endpointName: "findTokenByAddressInCurrency",
|
|
228
309
|
arg: {
|
|
@@ -233,13 +314,12 @@ export function restoreTokensToCache(
|
|
|
233
314
|
});
|
|
234
315
|
}
|
|
235
316
|
|
|
236
|
-
// Dispatch single upsertQueryEntries action with all entries
|
|
237
317
|
if (entries.length > 0) {
|
|
238
318
|
dispatch(cryptoAssetsApi.util.upsertQueryEntries(entries));
|
|
239
319
|
}
|
|
240
320
|
|
|
241
321
|
log(
|
|
242
322
|
"persistence",
|
|
243
|
-
`Restored ${
|
|
323
|
+
`Restored ${tokensToRestore.length - skipped} tokens (${entries.length} entries, ${skipped} skipped, ${currencyIdsToEvict.size} currencies evicted)`,
|
|
244
324
|
);
|
|
245
325
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createApi, fetchBaseQuery, FetchBaseQueryMeta } from "@reduxjs/toolkit/query/react";
|
|
1
|
+
import { createApi, fetchBaseQuery, FetchBaseQueryMeta, retry } from "@reduxjs/toolkit/query/react";
|
|
2
2
|
import type { ApiTokenResponse } from "../entities";
|
|
3
3
|
import { ApiTokenResponseSchema } from "../entities";
|
|
4
4
|
import { getEnv } from "@ledgerhq/live-env";
|
|
@@ -6,6 +6,7 @@ import { GetTokensDataParams, PageParam, TokensDataTags, TokensDataWithPaginatio
|
|
|
6
6
|
import { TOKEN_OUTPUT_FIELDS } from "./fields";
|
|
7
7
|
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
8
8
|
import { convertApiToken, legacyIdToApiId } from "../../api-token-converter";
|
|
9
|
+
import { log } from "@ledgerhq/logs";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -81,9 +82,8 @@ function validateAndTransformSingleTokenResponse(response: unknown): TokenCurren
|
|
|
81
82
|
return result;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
baseQuery: fetchBaseQuery({
|
|
85
|
+
const baseQueryWithRetry = retry(
|
|
86
|
+
fetchBaseQuery({
|
|
87
87
|
baseUrl: "",
|
|
88
88
|
prepareHeaders: headers => {
|
|
89
89
|
headers.set("Content-Type", "application/json");
|
|
@@ -91,6 +91,14 @@ export const cryptoAssetsApi = createApi({
|
|
|
91
91
|
return headers;
|
|
92
92
|
},
|
|
93
93
|
}),
|
|
94
|
+
{
|
|
95
|
+
maxRetries: 3,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const cryptoAssetsApi = createApi({
|
|
100
|
+
reducerPath: "cryptoAssetsApi",
|
|
101
|
+
baseQuery: baseQueryWithRetry,
|
|
94
102
|
tagTypes: [TokensDataTags.Tokens],
|
|
95
103
|
endpoints: build => ({
|
|
96
104
|
findTokenById: build.query<TokenCurrency | undefined, TokenByIdParams>({
|
|
@@ -193,6 +201,22 @@ export const cryptoAssetsApi = createApi({
|
|
|
193
201
|
};
|
|
194
202
|
}
|
|
195
203
|
},
|
|
204
|
+
async onQueryStarted(currencyId, { dispatch, queryFulfilled, getCacheEntry }) {
|
|
205
|
+
try {
|
|
206
|
+
const previousHash = getCacheEntry()?.data as string | undefined;
|
|
207
|
+
const { data: newHash } = await queryFulfilled;
|
|
208
|
+
|
|
209
|
+
if (previousHash && newHash && previousHash !== newHash) {
|
|
210
|
+
log(
|
|
211
|
+
"cryptoassets",
|
|
212
|
+
`Hash changed for currencyId ${currencyId}: ${previousHash} -> ${newHash}, evicting token cache`,
|
|
213
|
+
);
|
|
214
|
+
dispatch(cryptoAssetsApi.util.invalidateTags([TokensDataTags.Tokens]));
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Query failed, skip eviction
|
|
218
|
+
}
|
|
219
|
+
},
|
|
196
220
|
}),
|
|
197
221
|
|
|
198
222
|
getTokensData: build.infiniteQuery<TokensDataWithPagination, GetTokensDataParams, PageParam>({
|