@empereur-rouge/pms-sdk 0.3.5 → 0.3.7
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/dist/index.cjs +155 -29
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +155 -29
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -253,7 +253,10 @@ var DEFAULT_CONFIG = {
|
|
|
253
253
|
protocolVersion: 1,
|
|
254
254
|
timeout: 3e4,
|
|
255
255
|
seedNodes: [],
|
|
256
|
-
enableRacing: true
|
|
256
|
+
enableRacing: true,
|
|
257
|
+
retries: 2,
|
|
258
|
+
perAttemptTimeoutMs: 4e3,
|
|
259
|
+
retryBaseDelayMs: 100
|
|
257
260
|
};
|
|
258
261
|
|
|
259
262
|
// src/crypto.ts
|
|
@@ -365,14 +368,14 @@ var PmsClient = class {
|
|
|
365
368
|
const res = await this.fetch("/v1/dag/tips", {
|
|
366
369
|
method: "POST",
|
|
367
370
|
body: JSON.stringify({ limit: 10 })
|
|
368
|
-
});
|
|
371
|
+
}, { idempotent: true });
|
|
369
372
|
return res;
|
|
370
373
|
}
|
|
371
374
|
/**
|
|
372
375
|
* Récupère les informations publiques du coordinateur (clés).
|
|
373
376
|
*/
|
|
374
377
|
async getCoordinatorInfo() {
|
|
375
|
-
return this.fetch("/v1/coordinator/info");
|
|
378
|
+
return this.fetch("/v1/coordinator/info", void 0, { idempotent: true });
|
|
376
379
|
}
|
|
377
380
|
/**
|
|
378
381
|
* Liste tous les ledgers actifs avec leur nombre de blocs.
|
|
@@ -396,13 +399,13 @@ var PmsClient = class {
|
|
|
396
399
|
}
|
|
397
400
|
const qs = params.toString();
|
|
398
401
|
const path = `/v1/ledgers${qs ? `?${qs}` : ""}`;
|
|
399
|
-
return (await this.fetch(path)).ledgers ?? [];
|
|
402
|
+
return (await this.fetch(path, void 0, { idempotent: true })).ledgers ?? [];
|
|
400
403
|
}
|
|
401
404
|
/**
|
|
402
405
|
* Récupère un bloc par son ID.
|
|
403
406
|
*/
|
|
404
407
|
async getBlock(blockId) {
|
|
405
|
-
const wb = await this.fetch(`/v1/blocks/${blockId}
|
|
408
|
+
const wb = await this.fetch(`/v1/blocks/${blockId}`, void 0, { idempotent: true });
|
|
406
409
|
return {
|
|
407
410
|
id: wb.id,
|
|
408
411
|
parents: wb.parents,
|
|
@@ -416,26 +419,26 @@ var PmsClient = class {
|
|
|
416
419
|
*/
|
|
417
420
|
async getSupply(assetId) {
|
|
418
421
|
const query = assetId ? `?asset_id=${encodeURIComponent(assetId)}` : "";
|
|
419
|
-
return this.fetch(`/v1/supply${query}
|
|
422
|
+
return this.fetch(`/v1/supply${query}`, void 0, { idempotent: true });
|
|
420
423
|
}
|
|
421
424
|
/**
|
|
422
425
|
* Liste tous les tokens enregistrés.
|
|
423
426
|
*/
|
|
424
427
|
async listTokens() {
|
|
425
|
-
const res = await this.fetch("/v1/tokens");
|
|
428
|
+
const res = await this.fetch("/v1/tokens", void 0, { idempotent: true });
|
|
426
429
|
return res.tokens ?? [];
|
|
427
430
|
}
|
|
428
431
|
/**
|
|
429
432
|
* Récupère les métadonnées d'un token par son asset_id.
|
|
430
433
|
*/
|
|
431
434
|
async getToken(assetId) {
|
|
432
|
-
return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}
|
|
435
|
+
return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`, void 0, { idempotent: true });
|
|
433
436
|
}
|
|
434
437
|
/**
|
|
435
438
|
* Récupère les UTXOs d'une adresse.
|
|
436
439
|
*/
|
|
437
440
|
async getUtxos(address) {
|
|
438
|
-
const res = await this.fetch(`/v1/wallet/${address}/utxos
|
|
441
|
+
const res = await this.fetch(`/v1/wallet/${address}/utxos`, void 0, { idempotent: true });
|
|
439
442
|
return res.utxos ?? [];
|
|
440
443
|
}
|
|
441
444
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -478,7 +481,7 @@ var PmsClient = class {
|
|
|
478
481
|
return this.fetch("/v1/wallet/restore/mnemonic", {
|
|
479
482
|
method: "POST",
|
|
480
483
|
body: JSON.stringify({ mnemonic })
|
|
481
|
-
});
|
|
484
|
+
}, { idempotent: true });
|
|
482
485
|
}
|
|
483
486
|
/**
|
|
484
487
|
* Restaure un wallet depuis une clé privée hexadécimale.
|
|
@@ -498,7 +501,7 @@ var PmsClient = class {
|
|
|
498
501
|
return this.fetch("/v1/wallet/restore/private-key", {
|
|
499
502
|
method: "POST",
|
|
500
503
|
body: JSON.stringify({ private_key_hex: privateKeyHex })
|
|
501
|
-
});
|
|
504
|
+
}, { idempotent: true });
|
|
502
505
|
}
|
|
503
506
|
// ═══════════════════════════════════════════════════════════════════════
|
|
504
507
|
// Préparation de Transaction (Server-Side)
|
|
@@ -533,7 +536,7 @@ var PmsClient = class {
|
|
|
533
536
|
return this.fetch("/v1/tx/prepare", {
|
|
534
537
|
method: "POST",
|
|
535
538
|
body: JSON.stringify(params)
|
|
536
|
-
});
|
|
539
|
+
}, { idempotent: true });
|
|
537
540
|
}
|
|
538
541
|
/**
|
|
539
542
|
* Récupère la balance d'une adresse.
|
|
@@ -545,7 +548,7 @@ var PmsClient = class {
|
|
|
545
548
|
const res = await this.fetch("/v1/balance", {
|
|
546
549
|
method: "POST",
|
|
547
550
|
body: JSON.stringify({ address })
|
|
548
|
-
});
|
|
551
|
+
}, { idempotent: true });
|
|
549
552
|
return res.balance;
|
|
550
553
|
}
|
|
551
554
|
/**
|
|
@@ -579,7 +582,7 @@ var PmsClient = class {
|
|
|
579
582
|
* ```
|
|
580
583
|
*/
|
|
581
584
|
async getNfts(address) {
|
|
582
|
-
const res = await this.fetch(`/v1/wallet/${address}/nfts
|
|
585
|
+
const res = await this.fetch(`/v1/wallet/${address}/nfts`, void 0, { idempotent: true });
|
|
583
586
|
return res.token_ids ?? [];
|
|
584
587
|
}
|
|
585
588
|
/**
|
|
@@ -598,7 +601,7 @@ var PmsClient = class {
|
|
|
598
601
|
* ```
|
|
599
602
|
*/
|
|
600
603
|
async getNft(tokenId) {
|
|
601
|
-
return this.fetch(`/v1/nft/${tokenId}
|
|
604
|
+
return this.fetch(`/v1/nft/${tokenId}`, void 0, { idempotent: true });
|
|
602
605
|
}
|
|
603
606
|
/**
|
|
604
607
|
* Récupère l'historique des transactions d'un wallet.
|
|
@@ -613,7 +616,7 @@ var PmsClient = class {
|
|
|
613
616
|
const res = await this.fetch("/wallet/history", {
|
|
614
617
|
method: "POST",
|
|
615
618
|
body: JSON.stringify({ bech32_addr: address, limit })
|
|
616
|
-
});
|
|
619
|
+
}, { idempotent: true });
|
|
617
620
|
if (options?.decryptionWallet) {
|
|
618
621
|
const wallet = options.decryptionWallet;
|
|
619
622
|
for (const item of res.items) {
|
|
@@ -706,7 +709,7 @@ var PmsClient = class {
|
|
|
706
709
|
}
|
|
707
710
|
const qs = params.toString();
|
|
708
711
|
const path = `/v1/wallet/${encodeURIComponent(address)}/activity${qs ? `?${qs}` : ""}`;
|
|
709
|
-
return this.fetch(path);
|
|
712
|
+
return this.fetch(path, void 0, { idempotent: true });
|
|
710
713
|
}
|
|
711
714
|
/**
|
|
712
715
|
* Écoute l'activité d'un wallet en temps réel via Server-Sent Events (SSE).
|
|
@@ -810,7 +813,7 @@ var PmsClient = class {
|
|
|
810
813
|
if (this.configCache && Date.now() < this.configCache.expires) {
|
|
811
814
|
return this.configCache.data;
|
|
812
815
|
}
|
|
813
|
-
const cfg = await this.fetch("/v1/config");
|
|
816
|
+
const cfg = await this.fetch("/v1/config", void 0, { idempotent: true });
|
|
814
817
|
this.configCache = { data: cfg, expires: Date.now() + this.CONFIG_TTL };
|
|
815
818
|
return cfg;
|
|
816
819
|
}
|
|
@@ -1257,14 +1260,18 @@ var PmsClient = class {
|
|
|
1257
1260
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1258
1261
|
// Helper HTTP
|
|
1259
1262
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1260
|
-
async fetch(path, init) {
|
|
1261
|
-
return this.fetchUrl(this.config.nodeUrl, path, init);
|
|
1263
|
+
async fetch(path, init, opts) {
|
|
1264
|
+
return this.fetchUrl(this.config.nodeUrl, path, init, opts);
|
|
1262
1265
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1266
|
+
/**
|
|
1267
|
+
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1268
|
+
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1269
|
+
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1270
|
+
*/
|
|
1271
|
+
async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
|
|
1272
|
+
const attemptController = new AbortController();
|
|
1273
|
+
const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
|
|
1274
|
+
const signal = mergeSignalsAny(attemptController.signal, outerSignal);
|
|
1268
1275
|
try {
|
|
1269
1276
|
const res = await fetch(url, {
|
|
1270
1277
|
...init,
|
|
@@ -1278,8 +1285,9 @@ var PmsClient = class {
|
|
|
1278
1285
|
signal
|
|
1279
1286
|
});
|
|
1280
1287
|
if (!res.ok) {
|
|
1281
|
-
const text2 = await res.text();
|
|
1282
|
-
|
|
1288
|
+
const text2 = await res.text().catch(() => "");
|
|
1289
|
+
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
1290
|
+
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
|
|
1283
1291
|
}
|
|
1284
1292
|
const text = await res.text();
|
|
1285
1293
|
if (!text) {
|
|
@@ -1287,14 +1295,132 @@ var PmsClient = class {
|
|
|
1287
1295
|
}
|
|
1288
1296
|
try {
|
|
1289
1297
|
return JSON.parse(text);
|
|
1290
|
-
} catch
|
|
1298
|
+
} catch {
|
|
1291
1299
|
throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
|
|
1292
1300
|
}
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
if (err && err.name === "AbortError") {
|
|
1303
|
+
if (attemptController.signal.aborted && !(outerSignal && outerSignal.aborted)) {
|
|
1304
|
+
throw new PerAttemptTimeoutError(`Per-attempt timeout after ${attemptTimeoutMs}ms`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
throw err;
|
|
1293
1308
|
} finally {
|
|
1294
|
-
clearTimeout(
|
|
1309
|
+
clearTimeout(attemptTimer);
|
|
1295
1310
|
}
|
|
1296
1311
|
}
|
|
1312
|
+
async fetchUrl(baseUrl, path, init, opts) {
|
|
1313
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1314
|
+
const outerSignal = init?.signal ?? void 0;
|
|
1315
|
+
if (!opts?.idempotent) {
|
|
1316
|
+
return this.fetchOnce(url, init, this.config.timeout, outerSignal);
|
|
1317
|
+
}
|
|
1318
|
+
const maxAttempts = 1 + Math.max(0, this.config.retries | 0);
|
|
1319
|
+
const perAttemptMs = this.config.perAttemptTimeoutMs;
|
|
1320
|
+
const baseDelayMs = this.config.retryBaseDelayMs;
|
|
1321
|
+
const overallDeadline = Date.now() + this.config.timeout;
|
|
1322
|
+
let lastErr;
|
|
1323
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1324
|
+
if (outerSignal?.aborted) {
|
|
1325
|
+
const e = new Error("Request aborted by caller");
|
|
1326
|
+
e.name = "AbortError";
|
|
1327
|
+
throw e;
|
|
1328
|
+
}
|
|
1329
|
+
if (Date.now() > overallDeadline) {
|
|
1330
|
+
throw lastErr ?? new Error(`Overall timeout (${this.config.timeout}ms) exceeded`);
|
|
1331
|
+
}
|
|
1332
|
+
try {
|
|
1333
|
+
return await this.fetchOnce(url, init, perAttemptMs, outerSignal);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
lastErr = err;
|
|
1336
|
+
if (outerSignal?.aborted) throw err;
|
|
1337
|
+
const retriable = isRetriableError(err);
|
|
1338
|
+
if (!retriable) throw err;
|
|
1339
|
+
if (attempt === maxAttempts - 1) throw err;
|
|
1340
|
+
let delay;
|
|
1341
|
+
const ra = err instanceof HttpError ? parseRetryAfterSeconds(err.retryAfter) : null;
|
|
1342
|
+
if (ra !== null) {
|
|
1343
|
+
delay = Math.min(ra * 1e3, 5e3);
|
|
1344
|
+
} else {
|
|
1345
|
+
const cap = Math.min(baseDelayMs * Math.pow(2, attempt), 2e3);
|
|
1346
|
+
delay = Math.floor(Math.random() * cap);
|
|
1347
|
+
}
|
|
1348
|
+
if (Date.now() + delay > overallDeadline) {
|
|
1349
|
+
throw err;
|
|
1350
|
+
}
|
|
1351
|
+
await sleepAbortable(delay, outerSignal);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
throw lastErr ?? new Error("Retry loop exhausted with no error captured");
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
var HttpError = class extends Error {
|
|
1358
|
+
status;
|
|
1359
|
+
retryAfter;
|
|
1360
|
+
constructor(status, message, retryAfter) {
|
|
1361
|
+
super(message);
|
|
1362
|
+
this.name = "HttpError";
|
|
1363
|
+
this.status = status;
|
|
1364
|
+
this.retryAfter = retryAfter;
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
var PerAttemptTimeoutError = class extends Error {
|
|
1368
|
+
constructor(message) {
|
|
1369
|
+
super(message);
|
|
1370
|
+
this.name = "PerAttemptTimeoutError";
|
|
1371
|
+
}
|
|
1297
1372
|
};
|
|
1373
|
+
function isRetriableError(err) {
|
|
1374
|
+
if (err instanceof PerAttemptTimeoutError) return true;
|
|
1375
|
+
if (err instanceof HttpError) {
|
|
1376
|
+
return err.status === 502 || err.status === 503 || err.status === 504;
|
|
1377
|
+
}
|
|
1378
|
+
if (err instanceof TypeError) return true;
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
function parseRetryAfterSeconds(raw) {
|
|
1382
|
+
if (!raw) return null;
|
|
1383
|
+
const n = Number(raw.trim());
|
|
1384
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
1385
|
+
return n;
|
|
1386
|
+
}
|
|
1387
|
+
function mergeSignalsAny(attemptSignal, outerSignal) {
|
|
1388
|
+
if (!outerSignal) return attemptSignal;
|
|
1389
|
+
const anyFn = AbortSignal.any;
|
|
1390
|
+
if (typeof anyFn === "function") {
|
|
1391
|
+
return anyFn([attemptSignal, outerSignal]);
|
|
1392
|
+
}
|
|
1393
|
+
const merged = new AbortController();
|
|
1394
|
+
const onAbort = (sig) => () => {
|
|
1395
|
+
if (!merged.signal.aborted) merged.abort(sig.reason);
|
|
1396
|
+
};
|
|
1397
|
+
if (attemptSignal.aborted) merged.abort(attemptSignal.reason);
|
|
1398
|
+
else attemptSignal.addEventListener("abort", onAbort(attemptSignal), { once: true });
|
|
1399
|
+
if (outerSignal.aborted) merged.abort(outerSignal.reason);
|
|
1400
|
+
else outerSignal.addEventListener("abort", onAbort(outerSignal), { once: true });
|
|
1401
|
+
return merged.signal;
|
|
1402
|
+
}
|
|
1403
|
+
function sleepAbortable(ms, signal) {
|
|
1404
|
+
if (ms <= 0) return Promise.resolve();
|
|
1405
|
+
return new Promise((resolve) => {
|
|
1406
|
+
const t = setTimeout(() => {
|
|
1407
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1408
|
+
resolve();
|
|
1409
|
+
}, ms);
|
|
1410
|
+
const onAbort = () => {
|
|
1411
|
+
clearTimeout(t);
|
|
1412
|
+
resolve();
|
|
1413
|
+
};
|
|
1414
|
+
if (signal) {
|
|
1415
|
+
if (signal.aborted) {
|
|
1416
|
+
clearTimeout(t);
|
|
1417
|
+
resolve();
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1298
1424
|
function mergeAbortSignals(a, b) {
|
|
1299
1425
|
const controller = new AbortController();
|
|
1300
1426
|
const onAbort = () => controller.abort();
|
package/dist/index.d.cts
CHANGED
|
@@ -472,6 +472,8 @@ interface WalletResponse {
|
|
|
472
472
|
public_key_hex: string;
|
|
473
473
|
/** Clé publique X25519 (hex) */
|
|
474
474
|
x25519_pub_hex: string;
|
|
475
|
+
/** Clé secrète X25519 (hex, dérivée via HKDF depuis la clé ECDSA) */
|
|
476
|
+
x25519_sk_hex: string;
|
|
475
477
|
/** Mots mnémoniques (24 mots, absent si restauré depuis clé privée) */
|
|
476
478
|
mnemonic_words?: string[];
|
|
477
479
|
}
|
|
@@ -526,6 +528,12 @@ interface PmsClientConfig {
|
|
|
526
528
|
timeout?: number;
|
|
527
529
|
/** Activer le mode racing (envoie à tous les noeuds connus) (défaut: true) */
|
|
528
530
|
enableRacing?: boolean;
|
|
531
|
+
/** Number of automatic retries for idempotent reads on transient failure. Default: 2 (so up to 3 attempts total). */
|
|
532
|
+
retries?: number;
|
|
533
|
+
/** Per-attempt timeout in ms. Default: 4000. Should be > p90 of normal latency but << overall `timeout`. */
|
|
534
|
+
perAttemptTimeoutMs?: number;
|
|
535
|
+
/** Base delay (ms) for exponential backoff with full jitter between retries. Default: 100. */
|
|
536
|
+
retryBaseDelayMs?: number;
|
|
529
537
|
}
|
|
530
538
|
/** Réponse de l'endpoint d'information du coordinateur */
|
|
531
539
|
interface CoordinatorInfoResponse {
|
|
@@ -557,11 +565,12 @@ interface WalletHistoryResp {
|
|
|
557
565
|
* - Minting: "mint"
|
|
558
566
|
* - NFT: "nft_mint", "nft_transfer_in", "nft_transfer_out", "nft_burn", "nft_use"
|
|
559
567
|
* - Bridge: "bridge_lock_in", "bridge_lock_out", "bridge_mint"
|
|
560
|
-
* - Compliance: "freeze", "unfreeze", "seized", "seize_received", "
|
|
568
|
+
* - Compliance: "freeze", "unfreeze", "seized", "seize_received", "reverse_received"
|
|
561
569
|
* - Token: "token_create"
|
|
562
570
|
* - Reward: "reward"
|
|
571
|
+
* - Encrypted: "encrypted" (payload chiffré sans clé de déchiffrement)
|
|
563
572
|
*/
|
|
564
|
-
type ActivityType = "transfer_in" | "transfer_out" | "transfer_self" | "fee_received" | "reward" | "mint" | "nft_mint" | "nft_transfer_in" | "nft_transfer_out" | "nft_burn" | "nft_use" | "bridge_lock_in" | "
|
|
573
|
+
type ActivityType = "transfer_in" | "transfer_out" | "transfer_self" | "fee_received" | "reward" | "mint" | "nft_mint" | "nft_transfer_in" | "nft_transfer_out" | "nft_burn" | "nft_use" | "bridge_lock_in" | "bridge_mint" | "freeze" | "unfreeze" | "seized" | "seize_received" | "reverse_received" | "token_create" | "encrypted";
|
|
565
574
|
/** Direction de l'activité */
|
|
566
575
|
type ActivityDirection = "in" | "out" | "info";
|
|
567
576
|
/** Élément d'activité classifié pour un wallet */
|
|
@@ -604,7 +613,7 @@ interface ActivityResp {
|
|
|
604
613
|
interface ActivityOptions {
|
|
605
614
|
/** Filtrer par type(s) d'activité (ex: ["fee_received", "transfer_in"]) */
|
|
606
615
|
type?: ActivityType[];
|
|
607
|
-
/** Nombre max d'éléments (défaut: 50, max:
|
|
616
|
+
/** Nombre max d'éléments (défaut: 50, max: 2000) */
|
|
608
617
|
limit?: number;
|
|
609
618
|
/** Cursor de pagination: timestamp */
|
|
610
619
|
after_ts?: number;
|
|
@@ -1041,6 +1050,12 @@ declare class PmsClient {
|
|
|
1041
1050
|
*/
|
|
1042
1051
|
private generateRandomHex;
|
|
1043
1052
|
private fetch;
|
|
1053
|
+
/**
|
|
1054
|
+
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1055
|
+
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1056
|
+
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1057
|
+
*/
|
|
1058
|
+
private fetchOnce;
|
|
1044
1059
|
private fetchUrl;
|
|
1045
1060
|
}
|
|
1046
1061
|
|
package/dist/index.d.ts
CHANGED
|
@@ -472,6 +472,8 @@ interface WalletResponse {
|
|
|
472
472
|
public_key_hex: string;
|
|
473
473
|
/** Clé publique X25519 (hex) */
|
|
474
474
|
x25519_pub_hex: string;
|
|
475
|
+
/** Clé secrète X25519 (hex, dérivée via HKDF depuis la clé ECDSA) */
|
|
476
|
+
x25519_sk_hex: string;
|
|
475
477
|
/** Mots mnémoniques (24 mots, absent si restauré depuis clé privée) */
|
|
476
478
|
mnemonic_words?: string[];
|
|
477
479
|
}
|
|
@@ -526,6 +528,12 @@ interface PmsClientConfig {
|
|
|
526
528
|
timeout?: number;
|
|
527
529
|
/** Activer le mode racing (envoie à tous les noeuds connus) (défaut: true) */
|
|
528
530
|
enableRacing?: boolean;
|
|
531
|
+
/** Number of automatic retries for idempotent reads on transient failure. Default: 2 (so up to 3 attempts total). */
|
|
532
|
+
retries?: number;
|
|
533
|
+
/** Per-attempt timeout in ms. Default: 4000. Should be > p90 of normal latency but << overall `timeout`. */
|
|
534
|
+
perAttemptTimeoutMs?: number;
|
|
535
|
+
/** Base delay (ms) for exponential backoff with full jitter between retries. Default: 100. */
|
|
536
|
+
retryBaseDelayMs?: number;
|
|
529
537
|
}
|
|
530
538
|
/** Réponse de l'endpoint d'information du coordinateur */
|
|
531
539
|
interface CoordinatorInfoResponse {
|
|
@@ -557,11 +565,12 @@ interface WalletHistoryResp {
|
|
|
557
565
|
* - Minting: "mint"
|
|
558
566
|
* - NFT: "nft_mint", "nft_transfer_in", "nft_transfer_out", "nft_burn", "nft_use"
|
|
559
567
|
* - Bridge: "bridge_lock_in", "bridge_lock_out", "bridge_mint"
|
|
560
|
-
* - Compliance: "freeze", "unfreeze", "seized", "seize_received", "
|
|
568
|
+
* - Compliance: "freeze", "unfreeze", "seized", "seize_received", "reverse_received"
|
|
561
569
|
* - Token: "token_create"
|
|
562
570
|
* - Reward: "reward"
|
|
571
|
+
* - Encrypted: "encrypted" (payload chiffré sans clé de déchiffrement)
|
|
563
572
|
*/
|
|
564
|
-
type ActivityType = "transfer_in" | "transfer_out" | "transfer_self" | "fee_received" | "reward" | "mint" | "nft_mint" | "nft_transfer_in" | "nft_transfer_out" | "nft_burn" | "nft_use" | "bridge_lock_in" | "
|
|
573
|
+
type ActivityType = "transfer_in" | "transfer_out" | "transfer_self" | "fee_received" | "reward" | "mint" | "nft_mint" | "nft_transfer_in" | "nft_transfer_out" | "nft_burn" | "nft_use" | "bridge_lock_in" | "bridge_mint" | "freeze" | "unfreeze" | "seized" | "seize_received" | "reverse_received" | "token_create" | "encrypted";
|
|
565
574
|
/** Direction de l'activité */
|
|
566
575
|
type ActivityDirection = "in" | "out" | "info";
|
|
567
576
|
/** Élément d'activité classifié pour un wallet */
|
|
@@ -604,7 +613,7 @@ interface ActivityResp {
|
|
|
604
613
|
interface ActivityOptions {
|
|
605
614
|
/** Filtrer par type(s) d'activité (ex: ["fee_received", "transfer_in"]) */
|
|
606
615
|
type?: ActivityType[];
|
|
607
|
-
/** Nombre max d'éléments (défaut: 50, max:
|
|
616
|
+
/** Nombre max d'éléments (défaut: 50, max: 2000) */
|
|
608
617
|
limit?: number;
|
|
609
618
|
/** Cursor de pagination: timestamp */
|
|
610
619
|
after_ts?: number;
|
|
@@ -1041,6 +1050,12 @@ declare class PmsClient {
|
|
|
1041
1050
|
*/
|
|
1042
1051
|
private generateRandomHex;
|
|
1043
1052
|
private fetch;
|
|
1053
|
+
/**
|
|
1054
|
+
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1055
|
+
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1056
|
+
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1057
|
+
*/
|
|
1058
|
+
private fetchOnce;
|
|
1044
1059
|
private fetchUrl;
|
|
1045
1060
|
}
|
|
1046
1061
|
|
package/dist/index.js
CHANGED
|
@@ -220,7 +220,10 @@ var DEFAULT_CONFIG = {
|
|
|
220
220
|
protocolVersion: 1,
|
|
221
221
|
timeout: 3e4,
|
|
222
222
|
seedNodes: [],
|
|
223
|
-
enableRacing: true
|
|
223
|
+
enableRacing: true,
|
|
224
|
+
retries: 2,
|
|
225
|
+
perAttemptTimeoutMs: 4e3,
|
|
226
|
+
retryBaseDelayMs: 100
|
|
224
227
|
};
|
|
225
228
|
|
|
226
229
|
// src/crypto.ts
|
|
@@ -332,14 +335,14 @@ var PmsClient = class {
|
|
|
332
335
|
const res = await this.fetch("/v1/dag/tips", {
|
|
333
336
|
method: "POST",
|
|
334
337
|
body: JSON.stringify({ limit: 10 })
|
|
335
|
-
});
|
|
338
|
+
}, { idempotent: true });
|
|
336
339
|
return res;
|
|
337
340
|
}
|
|
338
341
|
/**
|
|
339
342
|
* Récupère les informations publiques du coordinateur (clés).
|
|
340
343
|
*/
|
|
341
344
|
async getCoordinatorInfo() {
|
|
342
|
-
return this.fetch("/v1/coordinator/info");
|
|
345
|
+
return this.fetch("/v1/coordinator/info", void 0, { idempotent: true });
|
|
343
346
|
}
|
|
344
347
|
/**
|
|
345
348
|
* Liste tous les ledgers actifs avec leur nombre de blocs.
|
|
@@ -363,13 +366,13 @@ var PmsClient = class {
|
|
|
363
366
|
}
|
|
364
367
|
const qs = params.toString();
|
|
365
368
|
const path = `/v1/ledgers${qs ? `?${qs}` : ""}`;
|
|
366
|
-
return (await this.fetch(path)).ledgers ?? [];
|
|
369
|
+
return (await this.fetch(path, void 0, { idempotent: true })).ledgers ?? [];
|
|
367
370
|
}
|
|
368
371
|
/**
|
|
369
372
|
* Récupère un bloc par son ID.
|
|
370
373
|
*/
|
|
371
374
|
async getBlock(blockId) {
|
|
372
|
-
const wb = await this.fetch(`/v1/blocks/${blockId}
|
|
375
|
+
const wb = await this.fetch(`/v1/blocks/${blockId}`, void 0, { idempotent: true });
|
|
373
376
|
return {
|
|
374
377
|
id: wb.id,
|
|
375
378
|
parents: wb.parents,
|
|
@@ -383,26 +386,26 @@ var PmsClient = class {
|
|
|
383
386
|
*/
|
|
384
387
|
async getSupply(assetId) {
|
|
385
388
|
const query = assetId ? `?asset_id=${encodeURIComponent(assetId)}` : "";
|
|
386
|
-
return this.fetch(`/v1/supply${query}
|
|
389
|
+
return this.fetch(`/v1/supply${query}`, void 0, { idempotent: true });
|
|
387
390
|
}
|
|
388
391
|
/**
|
|
389
392
|
* Liste tous les tokens enregistrés.
|
|
390
393
|
*/
|
|
391
394
|
async listTokens() {
|
|
392
|
-
const res = await this.fetch("/v1/tokens");
|
|
395
|
+
const res = await this.fetch("/v1/tokens", void 0, { idempotent: true });
|
|
393
396
|
return res.tokens ?? [];
|
|
394
397
|
}
|
|
395
398
|
/**
|
|
396
399
|
* Récupère les métadonnées d'un token par son asset_id.
|
|
397
400
|
*/
|
|
398
401
|
async getToken(assetId) {
|
|
399
|
-
return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}
|
|
402
|
+
return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`, void 0, { idempotent: true });
|
|
400
403
|
}
|
|
401
404
|
/**
|
|
402
405
|
* Récupère les UTXOs d'une adresse.
|
|
403
406
|
*/
|
|
404
407
|
async getUtxos(address) {
|
|
405
|
-
const res = await this.fetch(`/v1/wallet/${address}/utxos
|
|
408
|
+
const res = await this.fetch(`/v1/wallet/${address}/utxos`, void 0, { idempotent: true });
|
|
406
409
|
return res.utxos ?? [];
|
|
407
410
|
}
|
|
408
411
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -445,7 +448,7 @@ var PmsClient = class {
|
|
|
445
448
|
return this.fetch("/v1/wallet/restore/mnemonic", {
|
|
446
449
|
method: "POST",
|
|
447
450
|
body: JSON.stringify({ mnemonic })
|
|
448
|
-
});
|
|
451
|
+
}, { idempotent: true });
|
|
449
452
|
}
|
|
450
453
|
/**
|
|
451
454
|
* Restaure un wallet depuis une clé privée hexadécimale.
|
|
@@ -465,7 +468,7 @@ var PmsClient = class {
|
|
|
465
468
|
return this.fetch("/v1/wallet/restore/private-key", {
|
|
466
469
|
method: "POST",
|
|
467
470
|
body: JSON.stringify({ private_key_hex: privateKeyHex })
|
|
468
|
-
});
|
|
471
|
+
}, { idempotent: true });
|
|
469
472
|
}
|
|
470
473
|
// ═══════════════════════════════════════════════════════════════════════
|
|
471
474
|
// Préparation de Transaction (Server-Side)
|
|
@@ -500,7 +503,7 @@ var PmsClient = class {
|
|
|
500
503
|
return this.fetch("/v1/tx/prepare", {
|
|
501
504
|
method: "POST",
|
|
502
505
|
body: JSON.stringify(params)
|
|
503
|
-
});
|
|
506
|
+
}, { idempotent: true });
|
|
504
507
|
}
|
|
505
508
|
/**
|
|
506
509
|
* Récupère la balance d'une adresse.
|
|
@@ -512,7 +515,7 @@ var PmsClient = class {
|
|
|
512
515
|
const res = await this.fetch("/v1/balance", {
|
|
513
516
|
method: "POST",
|
|
514
517
|
body: JSON.stringify({ address })
|
|
515
|
-
});
|
|
518
|
+
}, { idempotent: true });
|
|
516
519
|
return res.balance;
|
|
517
520
|
}
|
|
518
521
|
/**
|
|
@@ -546,7 +549,7 @@ var PmsClient = class {
|
|
|
546
549
|
* ```
|
|
547
550
|
*/
|
|
548
551
|
async getNfts(address) {
|
|
549
|
-
const res = await this.fetch(`/v1/wallet/${address}/nfts
|
|
552
|
+
const res = await this.fetch(`/v1/wallet/${address}/nfts`, void 0, { idempotent: true });
|
|
550
553
|
return res.token_ids ?? [];
|
|
551
554
|
}
|
|
552
555
|
/**
|
|
@@ -565,7 +568,7 @@ var PmsClient = class {
|
|
|
565
568
|
* ```
|
|
566
569
|
*/
|
|
567
570
|
async getNft(tokenId) {
|
|
568
|
-
return this.fetch(`/v1/nft/${tokenId}
|
|
571
|
+
return this.fetch(`/v1/nft/${tokenId}`, void 0, { idempotent: true });
|
|
569
572
|
}
|
|
570
573
|
/**
|
|
571
574
|
* Récupère l'historique des transactions d'un wallet.
|
|
@@ -580,7 +583,7 @@ var PmsClient = class {
|
|
|
580
583
|
const res = await this.fetch("/wallet/history", {
|
|
581
584
|
method: "POST",
|
|
582
585
|
body: JSON.stringify({ bech32_addr: address, limit })
|
|
583
|
-
});
|
|
586
|
+
}, { idempotent: true });
|
|
584
587
|
if (options?.decryptionWallet) {
|
|
585
588
|
const wallet = options.decryptionWallet;
|
|
586
589
|
for (const item of res.items) {
|
|
@@ -673,7 +676,7 @@ var PmsClient = class {
|
|
|
673
676
|
}
|
|
674
677
|
const qs = params.toString();
|
|
675
678
|
const path = `/v1/wallet/${encodeURIComponent(address)}/activity${qs ? `?${qs}` : ""}`;
|
|
676
|
-
return this.fetch(path);
|
|
679
|
+
return this.fetch(path, void 0, { idempotent: true });
|
|
677
680
|
}
|
|
678
681
|
/**
|
|
679
682
|
* Écoute l'activité d'un wallet en temps réel via Server-Sent Events (SSE).
|
|
@@ -777,7 +780,7 @@ var PmsClient = class {
|
|
|
777
780
|
if (this.configCache && Date.now() < this.configCache.expires) {
|
|
778
781
|
return this.configCache.data;
|
|
779
782
|
}
|
|
780
|
-
const cfg = await this.fetch("/v1/config");
|
|
783
|
+
const cfg = await this.fetch("/v1/config", void 0, { idempotent: true });
|
|
781
784
|
this.configCache = { data: cfg, expires: Date.now() + this.CONFIG_TTL };
|
|
782
785
|
return cfg;
|
|
783
786
|
}
|
|
@@ -1224,14 +1227,18 @@ var PmsClient = class {
|
|
|
1224
1227
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1225
1228
|
// Helper HTTP
|
|
1226
1229
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1227
|
-
async fetch(path, init) {
|
|
1228
|
-
return this.fetchUrl(this.config.nodeUrl, path, init);
|
|
1230
|
+
async fetch(path, init, opts) {
|
|
1231
|
+
return this.fetchUrl(this.config.nodeUrl, path, init, opts);
|
|
1229
1232
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1233
|
+
/**
|
|
1234
|
+
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1235
|
+
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1236
|
+
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1237
|
+
*/
|
|
1238
|
+
async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
|
|
1239
|
+
const attemptController = new AbortController();
|
|
1240
|
+
const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
|
|
1241
|
+
const signal = mergeSignalsAny(attemptController.signal, outerSignal);
|
|
1235
1242
|
try {
|
|
1236
1243
|
const res = await fetch(url, {
|
|
1237
1244
|
...init,
|
|
@@ -1245,8 +1252,9 @@ var PmsClient = class {
|
|
|
1245
1252
|
signal
|
|
1246
1253
|
});
|
|
1247
1254
|
if (!res.ok) {
|
|
1248
|
-
const text2 = await res.text();
|
|
1249
|
-
|
|
1255
|
+
const text2 = await res.text().catch(() => "");
|
|
1256
|
+
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
1257
|
+
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
|
|
1250
1258
|
}
|
|
1251
1259
|
const text = await res.text();
|
|
1252
1260
|
if (!text) {
|
|
@@ -1254,14 +1262,132 @@ var PmsClient = class {
|
|
|
1254
1262
|
}
|
|
1255
1263
|
try {
|
|
1256
1264
|
return JSON.parse(text);
|
|
1257
|
-
} catch
|
|
1265
|
+
} catch {
|
|
1258
1266
|
throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
|
|
1259
1267
|
}
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
if (err && err.name === "AbortError") {
|
|
1270
|
+
if (attemptController.signal.aborted && !(outerSignal && outerSignal.aborted)) {
|
|
1271
|
+
throw new PerAttemptTimeoutError(`Per-attempt timeout after ${attemptTimeoutMs}ms`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
throw err;
|
|
1260
1275
|
} finally {
|
|
1261
|
-
clearTimeout(
|
|
1276
|
+
clearTimeout(attemptTimer);
|
|
1262
1277
|
}
|
|
1263
1278
|
}
|
|
1279
|
+
async fetchUrl(baseUrl, path, init, opts) {
|
|
1280
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1281
|
+
const outerSignal = init?.signal ?? void 0;
|
|
1282
|
+
if (!opts?.idempotent) {
|
|
1283
|
+
return this.fetchOnce(url, init, this.config.timeout, outerSignal);
|
|
1284
|
+
}
|
|
1285
|
+
const maxAttempts = 1 + Math.max(0, this.config.retries | 0);
|
|
1286
|
+
const perAttemptMs = this.config.perAttemptTimeoutMs;
|
|
1287
|
+
const baseDelayMs = this.config.retryBaseDelayMs;
|
|
1288
|
+
const overallDeadline = Date.now() + this.config.timeout;
|
|
1289
|
+
let lastErr;
|
|
1290
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1291
|
+
if (outerSignal?.aborted) {
|
|
1292
|
+
const e = new Error("Request aborted by caller");
|
|
1293
|
+
e.name = "AbortError";
|
|
1294
|
+
throw e;
|
|
1295
|
+
}
|
|
1296
|
+
if (Date.now() > overallDeadline) {
|
|
1297
|
+
throw lastErr ?? new Error(`Overall timeout (${this.config.timeout}ms) exceeded`);
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
return await this.fetchOnce(url, init, perAttemptMs, outerSignal);
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
lastErr = err;
|
|
1303
|
+
if (outerSignal?.aborted) throw err;
|
|
1304
|
+
const retriable = isRetriableError(err);
|
|
1305
|
+
if (!retriable) throw err;
|
|
1306
|
+
if (attempt === maxAttempts - 1) throw err;
|
|
1307
|
+
let delay;
|
|
1308
|
+
const ra = err instanceof HttpError ? parseRetryAfterSeconds(err.retryAfter) : null;
|
|
1309
|
+
if (ra !== null) {
|
|
1310
|
+
delay = Math.min(ra * 1e3, 5e3);
|
|
1311
|
+
} else {
|
|
1312
|
+
const cap = Math.min(baseDelayMs * Math.pow(2, attempt), 2e3);
|
|
1313
|
+
delay = Math.floor(Math.random() * cap);
|
|
1314
|
+
}
|
|
1315
|
+
if (Date.now() + delay > overallDeadline) {
|
|
1316
|
+
throw err;
|
|
1317
|
+
}
|
|
1318
|
+
await sleepAbortable(delay, outerSignal);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
throw lastErr ?? new Error("Retry loop exhausted with no error captured");
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
var HttpError = class extends Error {
|
|
1325
|
+
status;
|
|
1326
|
+
retryAfter;
|
|
1327
|
+
constructor(status, message, retryAfter) {
|
|
1328
|
+
super(message);
|
|
1329
|
+
this.name = "HttpError";
|
|
1330
|
+
this.status = status;
|
|
1331
|
+
this.retryAfter = retryAfter;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
var PerAttemptTimeoutError = class extends Error {
|
|
1335
|
+
constructor(message) {
|
|
1336
|
+
super(message);
|
|
1337
|
+
this.name = "PerAttemptTimeoutError";
|
|
1338
|
+
}
|
|
1264
1339
|
};
|
|
1340
|
+
function isRetriableError(err) {
|
|
1341
|
+
if (err instanceof PerAttemptTimeoutError) return true;
|
|
1342
|
+
if (err instanceof HttpError) {
|
|
1343
|
+
return err.status === 502 || err.status === 503 || err.status === 504;
|
|
1344
|
+
}
|
|
1345
|
+
if (err instanceof TypeError) return true;
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
function parseRetryAfterSeconds(raw) {
|
|
1349
|
+
if (!raw) return null;
|
|
1350
|
+
const n = Number(raw.trim());
|
|
1351
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
1352
|
+
return n;
|
|
1353
|
+
}
|
|
1354
|
+
function mergeSignalsAny(attemptSignal, outerSignal) {
|
|
1355
|
+
if (!outerSignal) return attemptSignal;
|
|
1356
|
+
const anyFn = AbortSignal.any;
|
|
1357
|
+
if (typeof anyFn === "function") {
|
|
1358
|
+
return anyFn([attemptSignal, outerSignal]);
|
|
1359
|
+
}
|
|
1360
|
+
const merged = new AbortController();
|
|
1361
|
+
const onAbort = (sig) => () => {
|
|
1362
|
+
if (!merged.signal.aborted) merged.abort(sig.reason);
|
|
1363
|
+
};
|
|
1364
|
+
if (attemptSignal.aborted) merged.abort(attemptSignal.reason);
|
|
1365
|
+
else attemptSignal.addEventListener("abort", onAbort(attemptSignal), { once: true });
|
|
1366
|
+
if (outerSignal.aborted) merged.abort(outerSignal.reason);
|
|
1367
|
+
else outerSignal.addEventListener("abort", onAbort(outerSignal), { once: true });
|
|
1368
|
+
return merged.signal;
|
|
1369
|
+
}
|
|
1370
|
+
function sleepAbortable(ms, signal) {
|
|
1371
|
+
if (ms <= 0) return Promise.resolve();
|
|
1372
|
+
return new Promise((resolve) => {
|
|
1373
|
+
const t = setTimeout(() => {
|
|
1374
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1375
|
+
resolve();
|
|
1376
|
+
}, ms);
|
|
1377
|
+
const onAbort = () => {
|
|
1378
|
+
clearTimeout(t);
|
|
1379
|
+
resolve();
|
|
1380
|
+
};
|
|
1381
|
+
if (signal) {
|
|
1382
|
+
if (signal.aborted) {
|
|
1383
|
+
clearTimeout(t);
|
|
1384
|
+
resolve();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1265
1391
|
function mergeAbortSignals(a, b) {
|
|
1266
1392
|
const controller = new AbortController();
|
|
1267
1393
|
const onAbort = () => controller.abort();
|
package/package.json
CHANGED