@empereur-rouge/pms-sdk 0.5.0 → 0.7.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.
- package/README.md +76 -0
- package/dist/index.cjs +318 -5
- package/dist/index.d.cts +453 -2
- package/dist/index.d.ts +453 -2
- package/dist/index.js +316 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ Le client permet d'interagir avec l'API REST des nœuds PMS.
|
|
|
118
118
|
const client = new PmsClient({
|
|
119
119
|
nodeUrl: "https://node.pms.network", // URL du nœud principal
|
|
120
120
|
apiKey: "pk_live_votre_cle_ici", // Clé API (obligatoire)
|
|
121
|
+
adminToken: "admin_token_ici", // Optionnel: requis pour les méthodes admin (gouvernance, setConfig)
|
|
121
122
|
seedNodes: [ // Optionnel: nœuds de secours
|
|
122
123
|
"https://node2.pms.network",
|
|
123
124
|
"https://node3.pms.network",
|
|
@@ -132,6 +133,9 @@ const client = new PmsClient({
|
|
|
132
133
|
> [!IMPORTANT]
|
|
133
134
|
> La clé API est **obligatoire**. Obtenez-la depuis votre dashboard PMS ou auprès de l'administrateur du réseau.
|
|
134
135
|
|
|
136
|
+
> [!WARNING]
|
|
137
|
+
> `adminToken` n'est requis que pour les méthodes admin (gouvernance `propose`/`enact`/`cancel`, `setConfig`). Il est envoyé en `Authorization: Bearer <adminToken>`, uniquement vers le nœud principal (jamais vers les seeds lors du racing). **Ne jamais l'exposer côté navigateur ni le committer.** Les lectures publiques de gouvernance n'en ont pas besoin.
|
|
138
|
+
|
|
135
139
|
#### Wallet (Custodial — Server-Side)
|
|
136
140
|
|
|
137
141
|
Ces méthodes créent/restaurent des wallets **côté serveur**. L'adresse retournée est au format Bech32 (ex: `8e1a...`).
|
|
@@ -448,6 +452,78 @@ const result = await client.burnNfts({
|
|
|
448
452
|
|
|
449
453
|
---
|
|
450
454
|
|
|
455
|
+
### Gouvernance & Émission
|
|
456
|
+
|
|
457
|
+
La gouvernance est ancrée dans le DAG : un changement de config peut être **proposé** (avec un palier et un timelock), puis **enacté** une fois le timelock écoulé, ou **annulé**. Les lectures sont publiques ; les écritures requièrent `adminToken`.
|
|
458
|
+
|
|
459
|
+
#### Lectures publiques (aucun `adminToken` requis)
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
// Propositions en attente (statut "pending")
|
|
463
|
+
const pending = await client.getGovernancePending();
|
|
464
|
+
// [{ proposal_id, tier, status: "pending", reason, announced_at_ms, enact_after_ms, update, proposal_block_id, enact_block_id, cancel_block_id }]
|
|
465
|
+
|
|
466
|
+
// Propositions terminées (enactées ou annulées)
|
|
467
|
+
const history = await client.getGovernanceHistory();
|
|
468
|
+
|
|
469
|
+
// Blocs de gouvernance ancrés dans le DAG
|
|
470
|
+
const { count, blocks } = await client.getGovernanceBlocks();
|
|
471
|
+
// blocks: [{ block_id, kind: "proposal" | "enact" | "cancel", proposal_id, tier, status, update, reason, announced_at_ms, enact_after_ms }]
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### Actions admin (requièrent `adminToken`)
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// Proposer un changement de config (ici, le couloir d'émission)
|
|
478
|
+
const proposal = await client.proposeGovernance({
|
|
479
|
+
update: { SetEmissionCorridor: { ceiling_bps: 1500, floor_bps: 0, target_bps: 1000, epoch_duration_sec: 86400 } },
|
|
480
|
+
tier: "constitution", // "operator" | "policy" | "constitution" (minuscules)
|
|
481
|
+
reason: "raise emission ceiling for Q3",
|
|
482
|
+
});
|
|
483
|
+
// { status: "ok", proposal_id, block_id, tier, announced_at_ms, enact_after_ms }
|
|
484
|
+
|
|
485
|
+
// Enacter une proposition dont le timelock est écoulé
|
|
486
|
+
await client.enactProposal(proposal.proposal_id, "timelock elapsed");
|
|
487
|
+
// { status: "ok", proposal_id, block_id }
|
|
488
|
+
|
|
489
|
+
// Annuler une proposition en attente
|
|
490
|
+
await client.cancelProposal(proposal.proposal_id);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### `setConfig` — 200 (appliqué) vs 202 (timelocké)
|
|
494
|
+
|
|
495
|
+
Un **durcissement** (ex: réduire un plafond, désactiver le mint) est appliqué instantanément ; un **assouplissement** est placé sous timelock de gouvernance. Le SDK expose les deux cas via une union discriminée sur `applied` — **un 202 n'est jamais traité comme un succès appliqué** :
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
const r = await client.setConfig({ SetMintEnabled: { enabled: false } });
|
|
499
|
+
if (r.applied) {
|
|
500
|
+
// HTTP 200 — en vigueur immédiatement
|
|
501
|
+
console.log("appliqué:", r.updateApplied, "bloc:", r.enactBlockId);
|
|
502
|
+
} else {
|
|
503
|
+
// HTTP 202 — proposé, PAS encore en vigueur
|
|
504
|
+
console.log("timelocké, enact après", new Date(r.enactAfterMs), "via", r.proposalId);
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### Codes d'erreur stables
|
|
509
|
+
|
|
510
|
+
Le moteur renvoie ses erreurs au format `{ "code": NNNN, "message": "..." }`. Branchez sur le `code` numérique (stable), pas sur le message :
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import { ErrorCode, HttpError } from "@empereur-rouge/pms-sdk";
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
await client.enactProposal(proposalId);
|
|
517
|
+
} catch (e) {
|
|
518
|
+
if (e instanceof HttpError && e.code === ErrorCode.GovernanceRejected) {
|
|
519
|
+
// 3071 — timelock non écoulé, proposition non en attente, ou id inconnu
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Codes notables : GovernanceRejected (3071), MintDisabled (5031), MissingAuth (1001), InvalidAuth (1002).
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
451
527
|
### API Avancée
|
|
452
528
|
|
|
453
529
|
Pour les cas d'usage avancés (construction manuelle de blocs, chiffrement custom), importez depuis le module `advanced` :
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DAY_MS: () => DAY_MS,
|
|
24
|
+
ErrorCode: () => ErrorCode,
|
|
25
|
+
HttpError: () => HttpError,
|
|
24
26
|
PmsClient: () => PmsClient,
|
|
25
27
|
PmsWallet: () => PmsWallet,
|
|
26
28
|
decryptPayload: () => decryptPayload,
|
|
@@ -418,7 +420,25 @@ var DEFAULT_CONFIG = {
|
|
|
418
420
|
enableRacing: true,
|
|
419
421
|
retries: 2,
|
|
420
422
|
perAttemptTimeoutMs: 4e3,
|
|
421
|
-
retryBaseDelayMs: 100
|
|
423
|
+
retryBaseDelayMs: 100,
|
|
424
|
+
// Vide par défaut : les méthodes admin lèvent une erreur explicite si
|
|
425
|
+
// appelées sans jeton (voir PmsClient.requireAdminToken).
|
|
426
|
+
adminToken: ""
|
|
427
|
+
};
|
|
428
|
+
var ErrorCode = {
|
|
429
|
+
/** En-tête d'authentification manquant (admin requis). */
|
|
430
|
+
MissingAuth: 1001,
|
|
431
|
+
/** Jeton d'authentification invalide. */
|
|
432
|
+
InvalidAuth: 1002,
|
|
433
|
+
/**
|
|
434
|
+
* Proposition de gouvernance rejetée (timelock non écoulé, proposition
|
|
435
|
+
* non en attente, ou identifiant inconnu). HTTP 409.
|
|
436
|
+
*/
|
|
437
|
+
GovernanceRejected: 3071,
|
|
438
|
+
/** Émission native (mint) désactivée. HTTP 503. */
|
|
439
|
+
MintDisabled: 5031,
|
|
440
|
+
/** Erreur interne générique (dette : handler non encore migré). */
|
|
441
|
+
Internal: 9999
|
|
422
442
|
};
|
|
423
443
|
|
|
424
444
|
// src/crypto.ts
|
|
@@ -617,6 +637,204 @@ var PmsClient = class {
|
|
|
617
637
|
return this.fetch("/v1/reserves/latest", void 0, { idempotent: true });
|
|
618
638
|
}
|
|
619
639
|
// ═══════════════════════════════════════════════════════════════════════
|
|
640
|
+
// Gouvernance — lectures publiques (GET, sans authentification)
|
|
641
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
642
|
+
/**
|
|
643
|
+
* Liste les propositions de gouvernance EN ATTENTE (statut `pending`).
|
|
644
|
+
*
|
|
645
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
646
|
+
*
|
|
647
|
+
* @returns Le tableau des propositions en attente (vide si aucune).
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```typescript
|
|
651
|
+
* const pending = await client.getGovernancePending();
|
|
652
|
+
* for (const p of pending) {
|
|
653
|
+
* console.log(`${p.proposal_id} enacts after ${new Date(p.enact_after_ms)}`);
|
|
654
|
+
* }
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
async getGovernancePending() {
|
|
658
|
+
const res = await this.fetch(
|
|
659
|
+
"/v1/governance/pending",
|
|
660
|
+
void 0,
|
|
661
|
+
{ idempotent: true }
|
|
662
|
+
);
|
|
663
|
+
return res.pending ?? [];
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Liste les propositions de gouvernance TERMINÉES (enactées ou annulées).
|
|
667
|
+
*
|
|
668
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
669
|
+
*
|
|
670
|
+
* @returns Le tableau de l'historique des propositions (vide si aucune).
|
|
671
|
+
*/
|
|
672
|
+
async getGovernanceHistory() {
|
|
673
|
+
const res = await this.fetch(
|
|
674
|
+
"/v1/governance/history",
|
|
675
|
+
void 0,
|
|
676
|
+
{ idempotent: true }
|
|
677
|
+
);
|
|
678
|
+
return res.history ?? [];
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Liste les blocs de gouvernance ancrés dans le DAG (un par proposition,
|
|
682
|
+
* enactment, ou annulation).
|
|
683
|
+
*
|
|
684
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
685
|
+
*
|
|
686
|
+
* @returns `{ count, blocks }` — le nombre et la liste des blocs.
|
|
687
|
+
*/
|
|
688
|
+
async getGovernanceBlocks() {
|
|
689
|
+
return this.fetch(
|
|
690
|
+
"/v1/governance/blocks",
|
|
691
|
+
void 0,
|
|
692
|
+
{ idempotent: true }
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
696
|
+
// Gouvernance — actions ADMIN (POST, requièrent adminToken)
|
|
697
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
698
|
+
/**
|
|
699
|
+
* Soumet une proposition de gouvernance ancrée dans le DAG.
|
|
700
|
+
*
|
|
701
|
+
* Méthode ADMIN — requiert `adminToken` dans la config du client
|
|
702
|
+
* (envoyé en `Authorization: Bearer <adminToken>`). Lève une erreur
|
|
703
|
+
* explicite avant tout appel réseau si le jeton est absent.
|
|
704
|
+
*
|
|
705
|
+
* @param req.update - Mise à jour de config à proposer.
|
|
706
|
+
* @param req.tier - Palier de gouvernance (minuscules, voir
|
|
707
|
+
* [`GovernanceTier`]).
|
|
708
|
+
* @param req.reason - Raison libre (optionnelle).
|
|
709
|
+
* @returns `{ status, proposal_id, block_id, tier, announced_at_ms,
|
|
710
|
+
* enact_after_ms }`.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```typescript
|
|
714
|
+
* const res = await client.proposeGovernance({
|
|
715
|
+
* update: { SetEmissionCorridor: { ceiling_bps: 1500, floor_bps: 0, target_bps: 1000, epoch_duration_sec: 86400 } },
|
|
716
|
+
* tier: "constitution",
|
|
717
|
+
* reason: "raise emission ceiling for Q3",
|
|
718
|
+
* });
|
|
719
|
+
* console.log(res.proposal_id, "enacts after", new Date(res.enact_after_ms));
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
async proposeGovernance(req) {
|
|
723
|
+
return this.fetchAdmin("proposeGovernance", "/admin/governance/propose", {
|
|
724
|
+
method: "POST",
|
|
725
|
+
body: JSON.stringify({
|
|
726
|
+
update: req.update,
|
|
727
|
+
tier: req.tier,
|
|
728
|
+
reason: req.reason ?? ""
|
|
729
|
+
})
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Enacte une proposition de gouvernance dont le timelock est écoulé.
|
|
734
|
+
*
|
|
735
|
+
* Méthode ADMIN — requiert `adminToken`. Le moteur rejette (HTTP 409,
|
|
736
|
+
* code [`ErrorCode.GovernanceRejected`] = 3071) si le timelock n'est pas
|
|
737
|
+
* écoulé, si la proposition n'est pas en attente, ou si l'identifiant est
|
|
738
|
+
* inconnu.
|
|
739
|
+
*
|
|
740
|
+
* @param proposalId - Identifiant de la proposition à enacter.
|
|
741
|
+
* @param reason - Raison libre (optionnelle).
|
|
742
|
+
* @returns `{ status, proposal_id, block_id }`.
|
|
743
|
+
*/
|
|
744
|
+
async enactProposal(proposalId, reason) {
|
|
745
|
+
return this.fetchAdmin(
|
|
746
|
+
"enactProposal",
|
|
747
|
+
`/admin/governance/enact/${encodeURIComponent(proposalId)}`,
|
|
748
|
+
{
|
|
749
|
+
method: "POST",
|
|
750
|
+
body: JSON.stringify({ reason: reason ?? "" })
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Annule une proposition de gouvernance en attente.
|
|
756
|
+
*
|
|
757
|
+
* Méthode ADMIN — requiert `adminToken`. Rejet HTTP 409 / code 3071 si la
|
|
758
|
+
* proposition n'est pas en attente ou si l'identifiant est inconnu.
|
|
759
|
+
*
|
|
760
|
+
* @param proposalId - Identifiant de la proposition à annuler.
|
|
761
|
+
* @param reason - Raison libre (optionnelle).
|
|
762
|
+
* @returns `{ status, proposal_id, block_id }`.
|
|
763
|
+
*/
|
|
764
|
+
async cancelProposal(proposalId, reason) {
|
|
765
|
+
return this.fetchAdmin(
|
|
766
|
+
"cancelProposal",
|
|
767
|
+
`/admin/governance/cancel/${encodeURIComponent(proposalId)}`,
|
|
768
|
+
{
|
|
769
|
+
method: "POST",
|
|
770
|
+
body: JSON.stringify({ reason: reason ?? "" })
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Applique (ou propose) une mise à jour de configuration.
|
|
776
|
+
*
|
|
777
|
+
* Méthode ADMIN — requiert `adminToken`. Le moteur distingue DEUX issues
|
|
778
|
+
* selon le sens du changement, et le SDK les EXPOSE explicitement via une
|
|
779
|
+
* union discriminée sur `applied` — un 202 n'est JAMAIS traité comme un
|
|
780
|
+
* succès appliqué :
|
|
781
|
+
* - **HTTP 200** (durcissement) → `{ applied: true, ... }` : la mise à
|
|
782
|
+
* jour est instantanément en vigueur.
|
|
783
|
+
* - **HTTP 202** (assouplissement) → `{ applied: false, proposalId,
|
|
784
|
+
* enactAfterMs, ... }` : la mise à jour est placée sous timelock de
|
|
785
|
+
* gouvernance, PAS encore appliquée. Elle s'auto-enacte à l'expiration
|
|
786
|
+
* du timelock, ou via [`enactProposal`].
|
|
787
|
+
*
|
|
788
|
+
* @param update - Mise à jour de config (envoyée brute, sans wrapper).
|
|
789
|
+
* @returns Une [`ConfigChangeResult`] discriminée sur `applied`.
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* ```typescript
|
|
793
|
+
* const r = await client.setConfig({ SetMintEnabled: { enabled: false } });
|
|
794
|
+
* if (r.applied) {
|
|
795
|
+
* console.log("appliqué instantanément:", r.updateApplied);
|
|
796
|
+
* } else {
|
|
797
|
+
* console.log("timelocké, enact après", new Date(r.enactAfterMs));
|
|
798
|
+
* }
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
async setConfig(update) {
|
|
802
|
+
const { status, body } = await this.fetchAdminWithStatus(
|
|
803
|
+
"setConfig",
|
|
804
|
+
"/admin/config",
|
|
805
|
+
{
|
|
806
|
+
method: "POST",
|
|
807
|
+
body: JSON.stringify(update)
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
const isApplied = status === 200 && body.status !== "proposed";
|
|
811
|
+
if (isApplied) {
|
|
812
|
+
return {
|
|
813
|
+
applied: true,
|
|
814
|
+
status: "applied",
|
|
815
|
+
mode: body.mode ?? "",
|
|
816
|
+
updateApplied: body.update_applied ?? "",
|
|
817
|
+
tier: body.tier ?? "",
|
|
818
|
+
proposalId: body.proposal_id ?? "",
|
|
819
|
+
proposalBlockId: body.proposal_block_id ?? "",
|
|
820
|
+
enactBlockId: body.enact_block_id ?? "",
|
|
821
|
+
config: body.config ?? null
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
applied: false,
|
|
826
|
+
status: "proposed",
|
|
827
|
+
mode: body.mode ?? "",
|
|
828
|
+
update: body.update ?? "",
|
|
829
|
+
tier: body.tier ?? "",
|
|
830
|
+
proposalId: body.proposal_id ?? "",
|
|
831
|
+
proposalBlockId: body.proposal_block_id ?? "",
|
|
832
|
+
announcedAtMs: body.announced_at_ms ?? 0,
|
|
833
|
+
enactAfterMs: body.enact_after_ms ?? 0,
|
|
834
|
+
message: body.message ?? ""
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
620
838
|
// Wallet API (Custodial — Server-Side)
|
|
621
839
|
// ═══════════════════════════════════════════════════════════════════════
|
|
622
840
|
/**
|
|
@@ -1439,6 +1657,62 @@ var PmsClient = class {
|
|
|
1439
1657
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1440
1658
|
// Helper HTTP
|
|
1441
1659
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1660
|
+
/**
|
|
1661
|
+
* @internal
|
|
1662
|
+
* Garantit qu'un `adminToken` est configuré avant un appel admin.
|
|
1663
|
+
* Lève une erreur claire (sans appel réseau) sinon.
|
|
1664
|
+
*/
|
|
1665
|
+
requireAdminToken(method) {
|
|
1666
|
+
const token = this.config.adminToken;
|
|
1667
|
+
if (!token || token.trim() === "") {
|
|
1668
|
+
throw new Error(
|
|
1669
|
+
`PmsClient.${method}() requires an admin token. Set it via new PmsClient({ ..., adminToken: '<token>' }). Admin methods send it as 'Authorization: Bearer <adminToken>'. Public reads (getGovernancePending/History/Blocks) do NOT need it.`
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
return token;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* @internal
|
|
1676
|
+
* Effectue un appel admin authentifié (en-tête `Authorization: Bearer`).
|
|
1677
|
+
*
|
|
1678
|
+
* Les écritures admin ne sont PAS idempotentes — pas de retry automatique
|
|
1679
|
+
* (single-attempt via la branche non-idempotente de `fetchUrl`). L'auth est
|
|
1680
|
+
* injectée par appel (pas dans le bloc d'en-têtes partagé de `fetchOnce`)
|
|
1681
|
+
* pour éviter de fuiter le jeton admin vers des nœuds découverts/seeds lors
|
|
1682
|
+
* du racing — cf. `submitBlockRacing`. `method` ne sert qu'au libellé de
|
|
1683
|
+
* l'erreur "jeton manquant".
|
|
1684
|
+
*/
|
|
1685
|
+
async fetchAdmin(method, path, init) {
|
|
1686
|
+
return this.fetchUrl(this.config.nodeUrl, path, this.withAdminAuth(method, init));
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* @internal
|
|
1690
|
+
* Variante d'appel admin qui expose le STATUT HTTP en plus du corps parsé.
|
|
1691
|
+
* Nécessaire pour [`setConfig`], qui doit distinguer 200 (applied) de 202
|
|
1692
|
+
* (proposed) — deux corps de réponse différents pour deux issues. Délègue à
|
|
1693
|
+
* la même primitive `fetchOnceWithStatus` que tous les autres appels, donc
|
|
1694
|
+
* hérite du merge de signal et du tagging d'abort sans duplication.
|
|
1695
|
+
*/
|
|
1696
|
+
async fetchAdminWithStatus(method, path, init) {
|
|
1697
|
+
const url = `${this.config.nodeUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1698
|
+
const auth = this.withAdminAuth(method, init);
|
|
1699
|
+
return this.fetchOnceWithStatus(url, auth, this.config.timeout, auth.signal ?? void 0);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* @internal
|
|
1703
|
+
* Vérifie la présence du jeton admin (sinon erreur claire, sans réseau) et
|
|
1704
|
+
* injecte l'en-tête `Authorization: Bearer <token>` dans un RequestInit.
|
|
1705
|
+
*/
|
|
1706
|
+
withAdminAuth(method, init) {
|
|
1707
|
+
const token = this.requireAdminToken(method);
|
|
1708
|
+
return {
|
|
1709
|
+
...init,
|
|
1710
|
+
headers: {
|
|
1711
|
+
...init?.headers,
|
|
1712
|
+
"Authorization": `Bearer ${token}`
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1442
1716
|
async fetch(path, init, opts) {
|
|
1443
1717
|
return this.fetchUrl(this.config.nodeUrl, path, init, opts);
|
|
1444
1718
|
}
|
|
@@ -1446,8 +1720,26 @@ var PmsClient = class {
|
|
|
1446
1720
|
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1447
1721
|
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1448
1722
|
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1723
|
+
*
|
|
1724
|
+
* Thin wrapper over `fetchOnceWithStatus` that discards the HTTP status —
|
|
1725
|
+
* every caller that only needs the parsed body goes through here.
|
|
1449
1726
|
*/
|
|
1450
1727
|
async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
|
|
1728
|
+
return (await this.fetchOnceWithStatus(url, init, attemptTimeoutMs, outerSignal)).body;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Single-attempt HTTP request returning BOTH the numeric HTTP status and the
|
|
1732
|
+
* parsed body. The shared fetch primitive: applies the default headers
|
|
1733
|
+
* (`Content-Type`, public `X-API-Key`), merges the caller's signal with the
|
|
1734
|
+
* per-attempt timeout, tags per-attempt aborts as `PerAttemptTimeoutError`
|
|
1735
|
+
* (so the retry loop can act on them), and constructs `HttpError` on non-2xx.
|
|
1736
|
+
*
|
|
1737
|
+
* NOTE: admin `Authorization: Bearer` is NOT injected here — it is added
|
|
1738
|
+
* per-admin-call by `withAdminAuth` so the admin token never leaks to
|
|
1739
|
+
* discovered/seed nodes during `submitBlockRacing`. Only the public,
|
|
1740
|
+
* scoped `X-API-Key` is broadcast.
|
|
1741
|
+
*/
|
|
1742
|
+
async fetchOnceWithStatus(url, init, attemptTimeoutMs, outerSignal) {
|
|
1451
1743
|
const attemptController = new AbortController();
|
|
1452
1744
|
const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
|
|
1453
1745
|
const signal = mergeSignalsAny(attemptController.signal, outerSignal);
|
|
@@ -1466,14 +1758,14 @@ var PmsClient = class {
|
|
|
1466
1758
|
if (!res.ok) {
|
|
1467
1759
|
const text2 = await res.text().catch(() => "");
|
|
1468
1760
|
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
1469
|
-
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
|
|
1761
|
+
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter, text2);
|
|
1470
1762
|
}
|
|
1471
1763
|
const text = await res.text();
|
|
1472
1764
|
if (!text) {
|
|
1473
|
-
return {};
|
|
1765
|
+
return { status: res.status, body: {} };
|
|
1474
1766
|
}
|
|
1475
1767
|
try {
|
|
1476
|
-
return JSON.parse(text);
|
|
1768
|
+
return { status: res.status, body: JSON.parse(text) };
|
|
1477
1769
|
} catch {
|
|
1478
1770
|
throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
|
|
1479
1771
|
}
|
|
@@ -1536,11 +1828,30 @@ var PmsClient = class {
|
|
|
1536
1828
|
var HttpError = class extends Error {
|
|
1537
1829
|
status;
|
|
1538
1830
|
retryAfter;
|
|
1539
|
-
|
|
1831
|
+
/** Raw response body text (may be empty). */
|
|
1832
|
+
body;
|
|
1833
|
+
/** Stable numeric error code parsed from `{code,message}`, if present. */
|
|
1834
|
+
code;
|
|
1835
|
+
/** Engine-provided message from `{code,message}`, if present. */
|
|
1836
|
+
engineMessage;
|
|
1837
|
+
constructor(status, message, retryAfter, body = "") {
|
|
1540
1838
|
super(message);
|
|
1541
1839
|
this.name = "HttpError";
|
|
1542
1840
|
this.status = status;
|
|
1543
1841
|
this.retryAfter = retryAfter;
|
|
1842
|
+
this.body = body;
|
|
1843
|
+
if (body) {
|
|
1844
|
+
try {
|
|
1845
|
+
const parsed = JSON.parse(body);
|
|
1846
|
+
if (parsed && typeof parsed === "object" && typeof parsed.code === "number") {
|
|
1847
|
+
this.code = parsed.code;
|
|
1848
|
+
if (typeof parsed.message === "string") {
|
|
1849
|
+
this.engineMessage = parsed.message;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1544
1855
|
}
|
|
1545
1856
|
};
|
|
1546
1857
|
var PerAttemptTimeoutError = class extends Error {
|
|
@@ -1610,6 +1921,8 @@ function mergeAbortSignals(a, b) {
|
|
|
1610
1921
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1611
1922
|
0 && (module.exports = {
|
|
1612
1923
|
DAY_MS,
|
|
1924
|
+
ErrorCode,
|
|
1925
|
+
HttpError,
|
|
1613
1926
|
PmsClient,
|
|
1614
1927
|
PmsWallet,
|
|
1615
1928
|
decryptPayload,
|