@empereur-rouge/pms-sdk 0.3.8 → 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 +605 -106
- package/dist/index.d.cts +846 -124
- package/dist/index.d.ts +846 -124
- package/dist/index.js +595 -105
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { wordlist } from "@scure/bip39/wordlists/english";
|
|
|
9
9
|
// src/utils.ts
|
|
10
10
|
import { sha256 } from "@noble/hashes/sha2";
|
|
11
11
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
12
|
+
import { base64, bech32m } from "@scure/base";
|
|
12
13
|
function sha256Hash(data) {
|
|
13
14
|
return sha256(data);
|
|
14
15
|
}
|
|
@@ -21,12 +22,103 @@ function fromHex(hex) {
|
|
|
21
22
|
function encodeUtf8(str) {
|
|
22
23
|
return new TextEncoder().encode(str);
|
|
23
24
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
25
|
+
function toBase64(bytes) {
|
|
26
|
+
return base64.encode(bytes);
|
|
27
|
+
}
|
|
28
|
+
function txSigningMessage(networkId, inputs, outputs, fee) {
|
|
29
|
+
const canonInputs = inputs.map((i) => ({
|
|
30
|
+
out: { txid: i.out.txid, index: i.out.index }
|
|
31
|
+
}));
|
|
32
|
+
const canonOutputs = outputs.map((o) => {
|
|
33
|
+
const out = {
|
|
34
|
+
address: o.address,
|
|
35
|
+
amount: o.amount
|
|
36
|
+
};
|
|
37
|
+
if (o.asset_id !== void 0 && o.asset_id !== null) {
|
|
38
|
+
out.asset_id = o.asset_id;
|
|
39
|
+
}
|
|
40
|
+
if (o.locked_until !== void 0 && o.locked_until !== null) {
|
|
41
|
+
out.locked_until = o.locked_until;
|
|
42
|
+
}
|
|
43
|
+
if (o.spend_condition !== void 0 && o.spend_condition !== null) {
|
|
44
|
+
out.spend_condition = o.spend_condition;
|
|
45
|
+
}
|
|
46
|
+
if (o.created_at !== void 0 && o.created_at !== null) {
|
|
47
|
+
out.created_at = o.created_at;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
});
|
|
51
|
+
const canon = {
|
|
52
|
+
network_id: networkId,
|
|
53
|
+
inputs: canonInputs,
|
|
54
|
+
outputs: canonOutputs,
|
|
55
|
+
fee
|
|
56
|
+
};
|
|
57
|
+
return toHex(sha256Hash(encodeUtf8(JSON.stringify(canon))));
|
|
58
|
+
}
|
|
59
|
+
var PLAIN_VARIANT_TO_PAYLOAD_TYPE = {
|
|
60
|
+
Genesis: "Genesis",
|
|
61
|
+
Mint: "Mint",
|
|
62
|
+
TxUtxo: "Transaction",
|
|
63
|
+
Milestone: "Milestone",
|
|
64
|
+
Nft: "Nft",
|
|
65
|
+
ConfigUpdate: "ConfigUpdate",
|
|
66
|
+
Reward: "Reward",
|
|
67
|
+
EncryptedReward: "EncryptedReward",
|
|
68
|
+
TokenCreate: "TokenCreate",
|
|
69
|
+
BridgeLock: "BridgeLock",
|
|
70
|
+
BridgeMint: "BridgeMint",
|
|
71
|
+
Freeze: "Freeze",
|
|
72
|
+
Unfreeze: "Unfreeze",
|
|
73
|
+
Seize: "Seize",
|
|
74
|
+
Reverse: "Reverse",
|
|
75
|
+
ContractRegister: "ContractRegister",
|
|
76
|
+
ContractUpdate: "ContractUpdate",
|
|
77
|
+
LedgerOwnershipTransfer: "LedgerOwnershipTransfer",
|
|
78
|
+
CoordinatorKeyRotate: "CoordinatorKeyRotate"
|
|
79
|
+
};
|
|
80
|
+
var EMPTY_COMMITMENT = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
81
|
+
function computeBlockId(parents, payload, nonce) {
|
|
82
|
+
let payload_type;
|
|
83
|
+
let commitment;
|
|
84
|
+
let len_hint;
|
|
85
|
+
let key_version;
|
|
86
|
+
if (payload === void 0 || payload === null) {
|
|
87
|
+
payload_type = "None";
|
|
88
|
+
commitment = EMPTY_COMMITMENT;
|
|
89
|
+
len_hint = 0;
|
|
90
|
+
key_version = 0;
|
|
91
|
+
} else if ("Plain" in payload) {
|
|
92
|
+
const plain = payload.Plain;
|
|
93
|
+
const variant = typeof plain === "string" ? plain : Object.keys(plain)[0];
|
|
94
|
+
payload_type = PLAIN_VARIANT_TO_PAYLOAD_TYPE[variant] ?? variant;
|
|
95
|
+
const plainBytes = encodeUtf8(JSON.stringify(plain));
|
|
96
|
+
commitment = toHex(sha256Hash(plainBytes));
|
|
97
|
+
len_hint = plainBytes.length;
|
|
98
|
+
key_version = 0;
|
|
99
|
+
} else {
|
|
100
|
+
const enc = payload.Encrypted;
|
|
101
|
+
payload_type = "Encrypted";
|
|
102
|
+
commitment = enc.commitment;
|
|
103
|
+
len_hint = enc.ciphertext_b64.length;
|
|
104
|
+
key_version = enc.key_version;
|
|
105
|
+
}
|
|
106
|
+
const head = {
|
|
107
|
+
parents,
|
|
108
|
+
nonce,
|
|
109
|
+
envelope: { payload_type, commitment, len_hint, key_version }
|
|
110
|
+
};
|
|
111
|
+
return toHex(sha256Hash(encodeUtf8(JSON.stringify(head))));
|
|
112
|
+
}
|
|
113
|
+
function x25519FromAddress(address) {
|
|
114
|
+
try {
|
|
115
|
+
const decoded = bech32m.decode(address, false);
|
|
116
|
+
const bytes = bech32m.fromWords(decoded.words);
|
|
117
|
+
if (bytes.length !== 52) return void 0;
|
|
118
|
+
return toHex(bytes.slice(20));
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
30
122
|
}
|
|
31
123
|
function parseAmount(amount) {
|
|
32
124
|
const [whole, frac = ""] = amount.split(".");
|
|
@@ -39,6 +131,47 @@ function formatAmount(sats) {
|
|
|
39
131
|
const fracStr = frac.toString().padStart(8, "0");
|
|
40
132
|
return `${whole}.${fracStr}`;
|
|
41
133
|
}
|
|
134
|
+
var DAY_MS = 864e5;
|
|
135
|
+
function multisigAddress(m, pubkeys) {
|
|
136
|
+
const normalized = pubkeys.map((pk) => {
|
|
137
|
+
const t = pk.trim();
|
|
138
|
+
return (t.startsWith("0x") ? t.slice(2) : t).toLowerCase();
|
|
139
|
+
}).sort();
|
|
140
|
+
const parts = [
|
|
141
|
+
encodeUtf8("pms-multisig-v1"),
|
|
142
|
+
new Uint8Array([m, normalized.length]),
|
|
143
|
+
...normalized.map((pk) => encodeUtf8(pk))
|
|
144
|
+
];
|
|
145
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
146
|
+
const buf = new Uint8Array(total);
|
|
147
|
+
let off = 0;
|
|
148
|
+
for (const p of parts) {
|
|
149
|
+
buf.set(p, off);
|
|
150
|
+
off += p.length;
|
|
151
|
+
}
|
|
152
|
+
return `msig1${toHex(sha256Hash(buf).slice(0, 20))}`;
|
|
153
|
+
}
|
|
154
|
+
function hashlockHash(preimage) {
|
|
155
|
+
const bytes = typeof preimage === "string" ? encodeUtf8(preimage) : preimage;
|
|
156
|
+
return toHex(sha256Hash(bytes));
|
|
157
|
+
}
|
|
158
|
+
function effectiveValue(amount, createdAtMs, nowMs, bpsPerDay) {
|
|
159
|
+
if (createdAtMs === void 0 || createdAtMs === null || !bpsPerDay) {
|
|
160
|
+
return amount;
|
|
161
|
+
}
|
|
162
|
+
const elapsed = nowMs - createdAtMs;
|
|
163
|
+
const days = elapsed > 0 ? BigInt(Math.floor(elapsed / DAY_MS)) : 0n;
|
|
164
|
+
if (days === 0n) return amount;
|
|
165
|
+
const sats = parseAmount(amount);
|
|
166
|
+
const scaled = sats * 10000n - sats * BigInt(bpsPerDay) * days;
|
|
167
|
+
if (scaled <= 0n) return "0.00000000";
|
|
168
|
+
return formatAmount(scaled / 10000n);
|
|
169
|
+
}
|
|
170
|
+
function spendableUtxos(utxos, nowMs = Date.now()) {
|
|
171
|
+
return utxos.filter(
|
|
172
|
+
(u) => u.locked_until === void 0 || u.locked_until === null || u.locked_until <= nowMs
|
|
173
|
+
);
|
|
174
|
+
}
|
|
42
175
|
|
|
43
176
|
// src/wallet.ts
|
|
44
177
|
var PmsWallet = class _PmsWallet {
|
|
@@ -213,17 +346,57 @@ var PmsWallet = class _PmsWallet {
|
|
|
213
346
|
function isValidMnemonic(mnemonic) {
|
|
214
347
|
return validateMnemonic(mnemonic.trim().toLowerCase(), wordlist);
|
|
215
348
|
}
|
|
349
|
+
function makeUnlock(signers, txHashHex, opts) {
|
|
350
|
+
if (signers.length === 0) {
|
|
351
|
+
throw new Error("makeUnlock: at least one signer is required");
|
|
352
|
+
}
|
|
353
|
+
const msg = encodeUtf8(txHashHex);
|
|
354
|
+
const sign = (w) => toBase64(fromHex(w.sign(msg)));
|
|
355
|
+
const [primary, ...rest] = signers;
|
|
356
|
+
const unlock = {
|
|
357
|
+
pubkey_hex: primary.publicKeyHex,
|
|
358
|
+
signature_b64: sign(primary)
|
|
359
|
+
};
|
|
360
|
+
if (rest.length > 0) {
|
|
361
|
+
unlock.cosigners = rest.map((w) => ({
|
|
362
|
+
pubkey_hex: w.publicKeyHex,
|
|
363
|
+
signature_b64: sign(w)
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
if (opts?.preimageHex !== void 0) {
|
|
367
|
+
unlock.preimage_hex = opts.preimageHex;
|
|
368
|
+
}
|
|
369
|
+
return unlock;
|
|
370
|
+
}
|
|
216
371
|
|
|
217
372
|
// src/types.ts
|
|
218
373
|
var DEFAULT_CONFIG = {
|
|
219
374
|
networkId: "pms-mainnet",
|
|
220
|
-
protocolVersion:
|
|
375
|
+
protocolVersion: 3,
|
|
221
376
|
timeout: 3e4,
|
|
222
377
|
seedNodes: [],
|
|
223
378
|
enableRacing: true,
|
|
224
379
|
retries: 2,
|
|
225
380
|
perAttemptTimeoutMs: 4e3,
|
|
226
|
-
retryBaseDelayMs: 100
|
|
381
|
+
retryBaseDelayMs: 100,
|
|
382
|
+
// Vide par défaut : les méthodes admin lèvent une erreur explicite si
|
|
383
|
+
// appelées sans jeton (voir PmsClient.requireAdminToken).
|
|
384
|
+
adminToken: ""
|
|
385
|
+
};
|
|
386
|
+
var ErrorCode = {
|
|
387
|
+
/** En-tête d'authentification manquant (admin requis). */
|
|
388
|
+
MissingAuth: 1001,
|
|
389
|
+
/** Jeton d'authentification invalide. */
|
|
390
|
+
InvalidAuth: 1002,
|
|
391
|
+
/**
|
|
392
|
+
* Proposition de gouvernance rejetée (timelock non écoulé, proposition
|
|
393
|
+
* non en attente, ou identifiant inconnu). HTTP 409.
|
|
394
|
+
*/
|
|
395
|
+
GovernanceRejected: 3071,
|
|
396
|
+
/** Émission native (mint) désactivée. HTTP 503. */
|
|
397
|
+
MintDisabled: 5031,
|
|
398
|
+
/** Erreur interne générique (dette : handler non encore migré). */
|
|
399
|
+
Internal: 9999
|
|
227
400
|
};
|
|
228
401
|
|
|
229
402
|
// src/crypto.ts
|
|
@@ -232,13 +405,13 @@ import { gcm } from "@noble/ciphers/aes.js";
|
|
|
232
405
|
import { hkdf as hkdf2 } from "@noble/hashes/hkdf";
|
|
233
406
|
import { sha256 as sha2563 } from "@noble/hashes/sha2";
|
|
234
407
|
import { randomBytes, bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils";
|
|
235
|
-
import { base64 } from "@scure/base";
|
|
408
|
+
import { base64 as base642 } from "@scure/base";
|
|
236
409
|
var SCHEME = "x25519+aes256gcm";
|
|
237
410
|
var HKDF_SALT = new TextEncoder().encode("pms-dek-wrap");
|
|
238
411
|
var HKDF_INFO_KEK = new TextEncoder().encode("kek-v1");
|
|
239
412
|
var HKDF_INFO_KID = new TextEncoder().encode("kid-v1");
|
|
240
413
|
function fromBase64(str) {
|
|
241
|
-
return
|
|
414
|
+
return base642.decode(str);
|
|
242
415
|
}
|
|
243
416
|
function sha256Hex(data) {
|
|
244
417
|
return bytesToHex2(sha2563(data));
|
|
@@ -403,11 +576,222 @@ var PmsClient = class {
|
|
|
403
576
|
}
|
|
404
577
|
/**
|
|
405
578
|
* Récupère les UTXOs d'une adresse.
|
|
579
|
+
*
|
|
580
|
+
* Depuis dag-pms v0.10.0, chaque UTXO expose aussi ses contraintes de
|
|
581
|
+
* dépense : `locked_until` (time-lock), `spend_condition`
|
|
582
|
+
* (MultiSig/HashLock) et `created_at` (base du demurrage). Utiliser
|
|
583
|
+
* [`spendableUtxos`] pour exclure les UTXOs encore verrouillés avant
|
|
584
|
+
* toute sélection de coins côté client.
|
|
406
585
|
*/
|
|
407
586
|
async getUtxos(address) {
|
|
408
587
|
const res = await this.fetch(`/v1/wallet/${address}/utxos`, void 0, { idempotent: true });
|
|
409
588
|
return res.utxos ?? [];
|
|
410
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Dernier snapshot de preuve de réserves ancré (protocole 2.6,
|
|
592
|
+
* `GET /v1/reserves/latest`). 404 si aucun snapshot n'a encore été ancré.
|
|
593
|
+
*/
|
|
594
|
+
async getLatestReserves() {
|
|
595
|
+
return this.fetch("/v1/reserves/latest", void 0, { idempotent: true });
|
|
596
|
+
}
|
|
597
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
598
|
+
// Gouvernance — lectures publiques (GET, sans authentification)
|
|
599
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
600
|
+
/**
|
|
601
|
+
* Liste les propositions de gouvernance EN ATTENTE (statut `pending`).
|
|
602
|
+
*
|
|
603
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
604
|
+
*
|
|
605
|
+
* @returns Le tableau des propositions en attente (vide si aucune).
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* const pending = await client.getGovernancePending();
|
|
610
|
+
* for (const p of pending) {
|
|
611
|
+
* console.log(`${p.proposal_id} enacts after ${new Date(p.enact_after_ms)}`);
|
|
612
|
+
* }
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
async getGovernancePending() {
|
|
616
|
+
const res = await this.fetch(
|
|
617
|
+
"/v1/governance/pending",
|
|
618
|
+
void 0,
|
|
619
|
+
{ idempotent: true }
|
|
620
|
+
);
|
|
621
|
+
return res.pending ?? [];
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Liste les propositions de gouvernance TERMINÉES (enactées ou annulées).
|
|
625
|
+
*
|
|
626
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
627
|
+
*
|
|
628
|
+
* @returns Le tableau de l'historique des propositions (vide si aucune).
|
|
629
|
+
*/
|
|
630
|
+
async getGovernanceHistory() {
|
|
631
|
+
const res = await this.fetch(
|
|
632
|
+
"/v1/governance/history",
|
|
633
|
+
void 0,
|
|
634
|
+
{ idempotent: true }
|
|
635
|
+
);
|
|
636
|
+
return res.history ?? [];
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Liste les blocs de gouvernance ancrés dans le DAG (un par proposition,
|
|
640
|
+
* enactment, ou annulation).
|
|
641
|
+
*
|
|
642
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
643
|
+
*
|
|
644
|
+
* @returns `{ count, blocks }` — le nombre et la liste des blocs.
|
|
645
|
+
*/
|
|
646
|
+
async getGovernanceBlocks() {
|
|
647
|
+
return this.fetch(
|
|
648
|
+
"/v1/governance/blocks",
|
|
649
|
+
void 0,
|
|
650
|
+
{ idempotent: true }
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
654
|
+
// Gouvernance — actions ADMIN (POST, requièrent adminToken)
|
|
655
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
656
|
+
/**
|
|
657
|
+
* Soumet une proposition de gouvernance ancrée dans le DAG.
|
|
658
|
+
*
|
|
659
|
+
* Méthode ADMIN — requiert `adminToken` dans la config du client
|
|
660
|
+
* (envoyé en `Authorization: Bearer <adminToken>`). Lève une erreur
|
|
661
|
+
* explicite avant tout appel réseau si le jeton est absent.
|
|
662
|
+
*
|
|
663
|
+
* @param req.update - Mise à jour de config à proposer.
|
|
664
|
+
* @param req.tier - Palier de gouvernance (minuscules, voir
|
|
665
|
+
* [`GovernanceTier`]).
|
|
666
|
+
* @param req.reason - Raison libre (optionnelle).
|
|
667
|
+
* @returns `{ status, proposal_id, block_id, tier, announced_at_ms,
|
|
668
|
+
* enact_after_ms }`.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```typescript
|
|
672
|
+
* const res = await client.proposeGovernance({
|
|
673
|
+
* update: { SetEmissionCorridor: { ceiling_bps: 1500, floor_bps: 0, target_bps: 1000, epoch_duration_sec: 86400 } },
|
|
674
|
+
* tier: "constitution",
|
|
675
|
+
* reason: "raise emission ceiling for Q3",
|
|
676
|
+
* });
|
|
677
|
+
* console.log(res.proposal_id, "enacts after", new Date(res.enact_after_ms));
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
async proposeGovernance(req) {
|
|
681
|
+
return this.fetchAdmin("proposeGovernance", "/admin/governance/propose", {
|
|
682
|
+
method: "POST",
|
|
683
|
+
body: JSON.stringify({
|
|
684
|
+
update: req.update,
|
|
685
|
+
tier: req.tier,
|
|
686
|
+
reason: req.reason ?? ""
|
|
687
|
+
})
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Enacte une proposition de gouvernance dont le timelock est écoulé.
|
|
692
|
+
*
|
|
693
|
+
* Méthode ADMIN — requiert `adminToken`. Le moteur rejette (HTTP 409,
|
|
694
|
+
* code [`ErrorCode.GovernanceRejected`] = 3071) si le timelock n'est pas
|
|
695
|
+
* écoulé, si la proposition n'est pas en attente, ou si l'identifiant est
|
|
696
|
+
* inconnu.
|
|
697
|
+
*
|
|
698
|
+
* @param proposalId - Identifiant de la proposition à enacter.
|
|
699
|
+
* @param reason - Raison libre (optionnelle).
|
|
700
|
+
* @returns `{ status, proposal_id, block_id }`.
|
|
701
|
+
*/
|
|
702
|
+
async enactProposal(proposalId, reason) {
|
|
703
|
+
return this.fetchAdmin(
|
|
704
|
+
"enactProposal",
|
|
705
|
+
`/admin/governance/enact/${encodeURIComponent(proposalId)}`,
|
|
706
|
+
{
|
|
707
|
+
method: "POST",
|
|
708
|
+
body: JSON.stringify({ reason: reason ?? "" })
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Annule une proposition de gouvernance en attente.
|
|
714
|
+
*
|
|
715
|
+
* Méthode ADMIN — requiert `adminToken`. Rejet HTTP 409 / code 3071 si la
|
|
716
|
+
* proposition n'est pas en attente ou si l'identifiant est inconnu.
|
|
717
|
+
*
|
|
718
|
+
* @param proposalId - Identifiant de la proposition à annuler.
|
|
719
|
+
* @param reason - Raison libre (optionnelle).
|
|
720
|
+
* @returns `{ status, proposal_id, block_id }`.
|
|
721
|
+
*/
|
|
722
|
+
async cancelProposal(proposalId, reason) {
|
|
723
|
+
return this.fetchAdmin(
|
|
724
|
+
"cancelProposal",
|
|
725
|
+
`/admin/governance/cancel/${encodeURIComponent(proposalId)}`,
|
|
726
|
+
{
|
|
727
|
+
method: "POST",
|
|
728
|
+
body: JSON.stringify({ reason: reason ?? "" })
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Applique (ou propose) une mise à jour de configuration.
|
|
734
|
+
*
|
|
735
|
+
* Méthode ADMIN — requiert `adminToken`. Le moteur distingue DEUX issues
|
|
736
|
+
* selon le sens du changement, et le SDK les EXPOSE explicitement via une
|
|
737
|
+
* union discriminée sur `applied` — un 202 n'est JAMAIS traité comme un
|
|
738
|
+
* succès appliqué :
|
|
739
|
+
* - **HTTP 200** (durcissement) → `{ applied: true, ... }` : la mise à
|
|
740
|
+
* jour est instantanément en vigueur.
|
|
741
|
+
* - **HTTP 202** (assouplissement) → `{ applied: false, proposalId,
|
|
742
|
+
* enactAfterMs, ... }` : la mise à jour est placée sous timelock de
|
|
743
|
+
* gouvernance, PAS encore appliquée. Elle s'auto-enacte à l'expiration
|
|
744
|
+
* du timelock, ou via [`enactProposal`].
|
|
745
|
+
*
|
|
746
|
+
* @param update - Mise à jour de config (envoyée brute, sans wrapper).
|
|
747
|
+
* @returns Une [`ConfigChangeResult`] discriminée sur `applied`.
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* const r = await client.setConfig({ SetMintEnabled: { enabled: false } });
|
|
752
|
+
* if (r.applied) {
|
|
753
|
+
* console.log("appliqué instantanément:", r.updateApplied);
|
|
754
|
+
* } else {
|
|
755
|
+
* console.log("timelocké, enact après", new Date(r.enactAfterMs));
|
|
756
|
+
* }
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
async setConfig(update) {
|
|
760
|
+
const { status, body } = await this.fetchAdminWithStatus(
|
|
761
|
+
"setConfig",
|
|
762
|
+
"/admin/config",
|
|
763
|
+
{
|
|
764
|
+
method: "POST",
|
|
765
|
+
body: JSON.stringify(update)
|
|
766
|
+
}
|
|
767
|
+
);
|
|
768
|
+
const isApplied = status === 200 && body.status !== "proposed";
|
|
769
|
+
if (isApplied) {
|
|
770
|
+
return {
|
|
771
|
+
applied: true,
|
|
772
|
+
status: "applied",
|
|
773
|
+
mode: body.mode ?? "",
|
|
774
|
+
updateApplied: body.update_applied ?? "",
|
|
775
|
+
tier: body.tier ?? "",
|
|
776
|
+
proposalId: body.proposal_id ?? "",
|
|
777
|
+
proposalBlockId: body.proposal_block_id ?? "",
|
|
778
|
+
enactBlockId: body.enact_block_id ?? "",
|
|
779
|
+
config: body.config ?? null
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
applied: false,
|
|
784
|
+
status: "proposed",
|
|
785
|
+
mode: body.mode ?? "",
|
|
786
|
+
update: body.update ?? "",
|
|
787
|
+
tier: body.tier ?? "",
|
|
788
|
+
proposalId: body.proposal_id ?? "",
|
|
789
|
+
proposalBlockId: body.proposal_block_id ?? "",
|
|
790
|
+
announcedAtMs: body.announced_at_ms ?? 0,
|
|
791
|
+
enactAfterMs: body.enact_after_ms ?? 0,
|
|
792
|
+
message: body.message ?? ""
|
|
793
|
+
};
|
|
794
|
+
}
|
|
411
795
|
// ═══════════════════════════════════════════════════════════════════════
|
|
412
796
|
// Wallet API (Custodial — Server-Side)
|
|
413
797
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -864,101 +1248,83 @@ var PmsClient = class {
|
|
|
864
1248
|
}
|
|
865
1249
|
/**
|
|
866
1250
|
* Envoie des tokens à une adresse.
|
|
867
|
-
*
|
|
1251
|
+
*
|
|
1252
|
+
* Flux aligné sur dag-pms v0.9.0 (single-writer + vérification des
|
|
1253
|
+
* signatures de transaction par l'engine) :
|
|
1254
|
+
* 1. `POST /v1/tx/prepare` — le serveur sélectionne les UTXOs et calcule
|
|
1255
|
+
* les frais.
|
|
1256
|
+
* 2. Vérification défensive côté client : l'output destinataire demandé
|
|
1257
|
+
* existe bien, et le `tx_hash` retourné correspond au message canonique
|
|
1258
|
+
* recalculé localement (`txSigningMessage`). On ne signe JAMAIS un hash
|
|
1259
|
+
* opaque non vérifié.
|
|
1260
|
+
* 3. Le wallet signe la TRANSACTION (unlocks) — pas le bloc. C'est le
|
|
1261
|
+
* serveur qui forge et signe le bloc (single-writer enforcement).
|
|
1262
|
+
* 4. `POST /v1/wallet/tx/send` avec la TX signée + les clés X25519 des
|
|
1263
|
+
* destinataires pour le chiffrement du payload.
|
|
1264
|
+
*
|
|
1265
|
+
* @param params.to - Adresse destinataire (Bech32m de préférence)
|
|
1266
|
+
* @param params.amount - Montant décimal (ex: "10.0")
|
|
1267
|
+
* @param params.wallet - Wallet signataire (propriétaire des UTXOs)
|
|
1268
|
+
* @param params.assetId - Asset ID optionnel (undefined = PMS natif)
|
|
1269
|
+
* @returns `{id, status, block_id}` — id du bloc forgé par le serveur
|
|
868
1270
|
*/
|
|
869
1271
|
async send(params) {
|
|
870
|
-
const { to, amount, wallet,
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const amountSats = parseAmount(amount);
|
|
879
|
-
const variableFee = amountSats * feeRateBps / 10000n;
|
|
880
|
-
const fee = baseFeeSats + variableFee;
|
|
881
|
-
const totalNeeded = amountSats + fee;
|
|
882
|
-
let selectedSats = 0n;
|
|
883
|
-
const selectedUtxos = [];
|
|
884
|
-
for (const utxo of utxos) {
|
|
885
|
-
selectedUtxos.push(utxo);
|
|
886
|
-
selectedSats += parseAmount(utxo.amount || "0");
|
|
887
|
-
if (selectedSats >= totalNeeded) break;
|
|
1272
|
+
const { to, amount, wallet, assetId } = params;
|
|
1273
|
+
const prepareBody = {
|
|
1274
|
+
from: wallet.address,
|
|
1275
|
+
to,
|
|
1276
|
+
amount
|
|
1277
|
+
};
|
|
1278
|
+
if (assetId !== void 0) {
|
|
1279
|
+
prepareBody.asset_id = assetId;
|
|
888
1280
|
}
|
|
889
|
-
|
|
1281
|
+
const prepared = await this.prepareTx(prepareBody);
|
|
1282
|
+
const { unsigned_tx, tx_hash } = prepared;
|
|
1283
|
+
const requestedSats = parseAmount(amount);
|
|
1284
|
+
const recipientOutput = unsigned_tx.outputs.find(
|
|
1285
|
+
(o) => o.address === to && (o.asset_id ?? void 0) === (assetId ?? void 0) && parseAmount(o.amount) >= requestedSats
|
|
1286
|
+
);
|
|
1287
|
+
if (!recipientOutput) {
|
|
890
1288
|
throw new Error(
|
|
891
|
-
`
|
|
1289
|
+
`Prepared transaction does not contain the requested output (to=${to}, amount=${amount}, asset=${assetId ?? "PMS"}). Refusing to sign a transaction that does not pay the intended recipient.`
|
|
892
1290
|
);
|
|
893
1291
|
}
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
if (change > 0n) {
|
|
905
|
-
outputs.push({ address: wallet.address, amount: formatAmount(change) });
|
|
1292
|
+
const localHash = txSigningMessage(
|
|
1293
|
+
this.config.networkId,
|
|
1294
|
+
unsigned_tx.inputs,
|
|
1295
|
+
unsigned_tx.outputs,
|
|
1296
|
+
unsigned_tx.fee
|
|
1297
|
+
);
|
|
1298
|
+
if (localHash !== tx_hash) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`tx_hash mismatch: server returned ${tx_hash}, locally computed ${localHash}. Either the client networkId ("${this.config.networkId}") does not match the node's network, or the server response was tampered with. Refusing to sign.`
|
|
1301
|
+
);
|
|
906
1302
|
}
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
index: utxo.outpoint.index
|
|
911
|
-
}
|
|
912
|
-
}));
|
|
913
|
-
const txCanonical = {
|
|
914
|
-
inputs,
|
|
915
|
-
outputs,
|
|
916
|
-
fee: formatAmount(fee)
|
|
917
|
-
};
|
|
918
|
-
const txMessage = JSON.stringify(txCanonical);
|
|
919
|
-
const txSignatureHex = wallet.sign(encodeUtf8(txMessage));
|
|
920
|
-
const txSigBytes = fromHex(txSignatureHex);
|
|
921
|
-
const txSigB64 = btoa(String.fromCharCode(...txSigBytes));
|
|
922
|
-
const unlocks = inputs.map(() => ({
|
|
1303
|
+
const sigHex = wallet.sign(encodeUtf8(tx_hash));
|
|
1304
|
+
const sigB64 = toBase64(fromHex(sigHex));
|
|
1305
|
+
const unlocks = unsigned_tx.inputs.map(() => ({
|
|
923
1306
|
pubkey_hex: wallet.publicKeyHex,
|
|
924
|
-
signature_b64:
|
|
1307
|
+
signature_b64: sigB64
|
|
925
1308
|
}));
|
|
926
1309
|
const tx = {
|
|
927
|
-
inputs,
|
|
928
|
-
outputs,
|
|
929
|
-
fee:
|
|
1310
|
+
inputs: unsigned_tx.inputs,
|
|
1311
|
+
outputs: unsigned_tx.outputs,
|
|
1312
|
+
fee: unsigned_tx.fee,
|
|
930
1313
|
unlocks
|
|
931
1314
|
};
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
signer_pk_hex: wallet.publicKeyHex
|
|
946
|
-
};
|
|
947
|
-
const messageToSign = JSON.stringify(canonicalView);
|
|
948
|
-
const signatureHex = wallet.sign(encodeUtf8(messageToSign));
|
|
949
|
-
const signatureBytes = fromHex(signatureHex);
|
|
950
|
-
const signatureB64 = btoa(String.fromCharCode(...signatureBytes));
|
|
951
|
-
const wireBlock = {
|
|
952
|
-
id: blockId,
|
|
953
|
-
parents,
|
|
954
|
-
payload_json: payloadJson,
|
|
955
|
-
nonce,
|
|
956
|
-
network_id: this.config.networkId,
|
|
957
|
-
protocol_version: this.config.protocolVersion,
|
|
958
|
-
signer_pk_hex: wallet.publicKeyHex,
|
|
959
|
-
signature_hex: signatureB64
|
|
960
|
-
};
|
|
961
|
-
return this.submitBlock(wireBlock);
|
|
1315
|
+
const recipientsXpk = [wallet.x25519PublicKeyHex];
|
|
1316
|
+
const toXpk = x25519FromAddress(to);
|
|
1317
|
+
if (toXpk && !recipientsXpk.includes(toXpk)) {
|
|
1318
|
+
recipientsXpk.push(toXpk);
|
|
1319
|
+
}
|
|
1320
|
+
const resp = await this.fetch(
|
|
1321
|
+
"/v1/wallet/tx/send",
|
|
1322
|
+
{
|
|
1323
|
+
method: "POST",
|
|
1324
|
+
body: JSON.stringify({ tx, recipients_xpk: recipientsXpk })
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
return { ...resp, block_id: resp.id };
|
|
962
1328
|
}
|
|
963
1329
|
// ═══════════════════════════════════════════════════════════════════════
|
|
964
1330
|
// Méthodes NFT Cube (Burn et Mint spécialisé)
|
|
@@ -966,7 +1332,15 @@ var PmsClient = class {
|
|
|
966
1332
|
/**
|
|
967
1333
|
* Transférer un NFT à un autre propriétaire.
|
|
968
1334
|
* Prend en charge le re-chiffrement des métadonnées via le coordinateur.
|
|
969
|
-
*
|
|
1335
|
+
*
|
|
1336
|
+
* @deprecated Cette méthode soumet un WireBlock signé par la clé
|
|
1337
|
+
* utilisateur via `/submit/block`. Depuis dag-pms v0.9.0, l'engine tourne
|
|
1338
|
+
* en mode **single-writer** : tout bloc signé par une clé non-coordinateur
|
|
1339
|
+
* est rejeté. Cette méthode ne fonctionne que sur un réseau SANS
|
|
1340
|
+
* single-writer enforcement. Utilisez les flux server-side
|
|
1341
|
+
* (`/v1/nft/transfer/prepare` + endpoints serveur) qui forgent et signent
|
|
1342
|
+
* le bloc côté coordinateur.
|
|
1343
|
+
*
|
|
970
1344
|
* @param params - Paramètres du transfert
|
|
971
1345
|
* @param params.tokenId - Identifiant du NFT
|
|
972
1346
|
* @param params.to - Adresse du nouveau propriétaire
|
|
@@ -1002,7 +1376,7 @@ var PmsClient = class {
|
|
|
1002
1376
|
const payload = { Plain: { Nft: action } };
|
|
1003
1377
|
const payloadJson = JSON.stringify(payload);
|
|
1004
1378
|
const nonce = 0;
|
|
1005
|
-
const blockId = computeBlockId(parents,
|
|
1379
|
+
const blockId = computeBlockId(parents, payload, nonce);
|
|
1006
1380
|
const canonicalView = {
|
|
1007
1381
|
id: blockId,
|
|
1008
1382
|
parents,
|
|
@@ -1034,10 +1408,17 @@ var PmsClient = class {
|
|
|
1034
1408
|
* Seul le propriétaire du NFT peut le brûler.
|
|
1035
1409
|
* Une fois brûlé, le NFT est supprimé définitivement.
|
|
1036
1410
|
*
|
|
1037
|
-
* Pour les Cubes authentiques (avec signature Authority valide),
|
|
1411
|
+
* Pour les Cubes authentiques (avec signature Authority valide),
|
|
1038
1412
|
* un remboursement est calculé selon la formule:
|
|
1039
1413
|
* `(weight * size * density) / 10000` PMS
|
|
1040
|
-
*
|
|
1414
|
+
*
|
|
1415
|
+
* @deprecated Cette méthode poste un WireBlock signé par la clé
|
|
1416
|
+
* utilisateur. Depuis dag-pms v0.9.0, l'engine tourne en mode
|
|
1417
|
+
* **single-writer** : tout bloc signé par une clé non-coordinateur est
|
|
1418
|
+
* rejeté. Cette méthode ne fonctionne que sur un réseau SANS single-writer
|
|
1419
|
+
* enforcement. Le flux supporté est le burn server-side
|
|
1420
|
+
* (`/v1/nft/burn-simple`), où le serveur forge et signe le bloc.
|
|
1421
|
+
*
|
|
1041
1422
|
* @param params - Paramètres du burn
|
|
1042
1423
|
* @param params.tokenId - Identifiant du NFT à brûler
|
|
1043
1424
|
* @param params.wallet - Wallet PMS du propriétaire (doit être l'owner actuel)
|
|
@@ -1071,7 +1452,7 @@ var PmsClient = class {
|
|
|
1071
1452
|
};
|
|
1072
1453
|
const payloadJson = JSON.stringify(payload);
|
|
1073
1454
|
const nonce = 0;
|
|
1074
|
-
const blockId = computeBlockId(parents,
|
|
1455
|
+
const blockId = computeBlockId(parents, payload, nonce);
|
|
1075
1456
|
const canonicalView = {
|
|
1076
1457
|
id: blockId,
|
|
1077
1458
|
parents,
|
|
@@ -1103,7 +1484,14 @@ var PmsClient = class {
|
|
|
1103
1484
|
}
|
|
1104
1485
|
/**
|
|
1105
1486
|
* Brûle (détruit) plusieurs NFTs en une seule transaction.
|
|
1106
|
-
*
|
|
1487
|
+
*
|
|
1488
|
+
* @deprecated Cette méthode poste un WireBlock signé par la clé
|
|
1489
|
+
* utilisateur. Depuis dag-pms v0.9.0, l'engine tourne en mode
|
|
1490
|
+
* **single-writer** : tout bloc signé par une clé non-coordinateur est
|
|
1491
|
+
* rejeté. Cette méthode ne fonctionne que sur un réseau SANS single-writer
|
|
1492
|
+
* enforcement. Le flux supporté est le burn server-side
|
|
1493
|
+
* (`/v1/nft/burn-simple`), où le serveur forge et signe le bloc.
|
|
1494
|
+
*
|
|
1107
1495
|
* @param params - Paramètres du batch burn
|
|
1108
1496
|
* @param params.tokenIds - Liste des Identifiants des NFTs à brûler
|
|
1109
1497
|
* @param params.wallet - Wallet PMS du propriétaire
|
|
@@ -1128,7 +1516,7 @@ var PmsClient = class {
|
|
|
1128
1516
|
};
|
|
1129
1517
|
const payloadJson = JSON.stringify(payload);
|
|
1130
1518
|
const nonce = 0;
|
|
1131
|
-
const blockId = computeBlockId(parents,
|
|
1519
|
+
const blockId = computeBlockId(parents, payload, nonce);
|
|
1132
1520
|
const canonicalView = {
|
|
1133
1521
|
id: blockId,
|
|
1134
1522
|
parents,
|
|
@@ -1227,6 +1615,62 @@ var PmsClient = class {
|
|
|
1227
1615
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1228
1616
|
// Helper HTTP
|
|
1229
1617
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1618
|
+
/**
|
|
1619
|
+
* @internal
|
|
1620
|
+
* Garantit qu'un `adminToken` est configuré avant un appel admin.
|
|
1621
|
+
* Lève une erreur claire (sans appel réseau) sinon.
|
|
1622
|
+
*/
|
|
1623
|
+
requireAdminToken(method) {
|
|
1624
|
+
const token = this.config.adminToken;
|
|
1625
|
+
if (!token || token.trim() === "") {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
`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.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
return token;
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* @internal
|
|
1634
|
+
* Effectue un appel admin authentifié (en-tête `Authorization: Bearer`).
|
|
1635
|
+
*
|
|
1636
|
+
* Les écritures admin ne sont PAS idempotentes — pas de retry automatique
|
|
1637
|
+
* (single-attempt via la branche non-idempotente de `fetchUrl`). L'auth est
|
|
1638
|
+
* injectée par appel (pas dans le bloc d'en-têtes partagé de `fetchOnce`)
|
|
1639
|
+
* pour éviter de fuiter le jeton admin vers des nœuds découverts/seeds lors
|
|
1640
|
+
* du racing — cf. `submitBlockRacing`. `method` ne sert qu'au libellé de
|
|
1641
|
+
* l'erreur "jeton manquant".
|
|
1642
|
+
*/
|
|
1643
|
+
async fetchAdmin(method, path, init) {
|
|
1644
|
+
return this.fetchUrl(this.config.nodeUrl, path, this.withAdminAuth(method, init));
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* @internal
|
|
1648
|
+
* Variante d'appel admin qui expose le STATUT HTTP en plus du corps parsé.
|
|
1649
|
+
* Nécessaire pour [`setConfig`], qui doit distinguer 200 (applied) de 202
|
|
1650
|
+
* (proposed) — deux corps de réponse différents pour deux issues. Délègue à
|
|
1651
|
+
* la même primitive `fetchOnceWithStatus` que tous les autres appels, donc
|
|
1652
|
+
* hérite du merge de signal et du tagging d'abort sans duplication.
|
|
1653
|
+
*/
|
|
1654
|
+
async fetchAdminWithStatus(method, path, init) {
|
|
1655
|
+
const url = `${this.config.nodeUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1656
|
+
const auth = this.withAdminAuth(method, init);
|
|
1657
|
+
return this.fetchOnceWithStatus(url, auth, this.config.timeout, auth.signal ?? void 0);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* @internal
|
|
1661
|
+
* Vérifie la présence du jeton admin (sinon erreur claire, sans réseau) et
|
|
1662
|
+
* injecte l'en-tête `Authorization: Bearer <token>` dans un RequestInit.
|
|
1663
|
+
*/
|
|
1664
|
+
withAdminAuth(method, init) {
|
|
1665
|
+
const token = this.requireAdminToken(method);
|
|
1666
|
+
return {
|
|
1667
|
+
...init,
|
|
1668
|
+
headers: {
|
|
1669
|
+
...init?.headers,
|
|
1670
|
+
"Authorization": `Bearer ${token}`
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1230
1674
|
async fetch(path, init, opts) {
|
|
1231
1675
|
return this.fetchUrl(this.config.nodeUrl, path, init, opts);
|
|
1232
1676
|
}
|
|
@@ -1234,8 +1678,26 @@ var PmsClient = class {
|
|
|
1234
1678
|
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1235
1679
|
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1236
1680
|
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1681
|
+
*
|
|
1682
|
+
* Thin wrapper over `fetchOnceWithStatus` that discards the HTTP status —
|
|
1683
|
+
* every caller that only needs the parsed body goes through here.
|
|
1237
1684
|
*/
|
|
1238
1685
|
async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
|
|
1686
|
+
return (await this.fetchOnceWithStatus(url, init, attemptTimeoutMs, outerSignal)).body;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Single-attempt HTTP request returning BOTH the numeric HTTP status and the
|
|
1690
|
+
* parsed body. The shared fetch primitive: applies the default headers
|
|
1691
|
+
* (`Content-Type`, public `X-API-Key`), merges the caller's signal with the
|
|
1692
|
+
* per-attempt timeout, tags per-attempt aborts as `PerAttemptTimeoutError`
|
|
1693
|
+
* (so the retry loop can act on them), and constructs `HttpError` on non-2xx.
|
|
1694
|
+
*
|
|
1695
|
+
* NOTE: admin `Authorization: Bearer` is NOT injected here — it is added
|
|
1696
|
+
* per-admin-call by `withAdminAuth` so the admin token never leaks to
|
|
1697
|
+
* discovered/seed nodes during `submitBlockRacing`. Only the public,
|
|
1698
|
+
* scoped `X-API-Key` is broadcast.
|
|
1699
|
+
*/
|
|
1700
|
+
async fetchOnceWithStatus(url, init, attemptTimeoutMs, outerSignal) {
|
|
1239
1701
|
const attemptController = new AbortController();
|
|
1240
1702
|
const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
|
|
1241
1703
|
const signal = mergeSignalsAny(attemptController.signal, outerSignal);
|
|
@@ -1254,14 +1716,14 @@ var PmsClient = class {
|
|
|
1254
1716
|
if (!res.ok) {
|
|
1255
1717
|
const text2 = await res.text().catch(() => "");
|
|
1256
1718
|
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
1257
|
-
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
|
|
1719
|
+
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter, text2);
|
|
1258
1720
|
}
|
|
1259
1721
|
const text = await res.text();
|
|
1260
1722
|
if (!text) {
|
|
1261
|
-
return {};
|
|
1723
|
+
return { status: res.status, body: {} };
|
|
1262
1724
|
}
|
|
1263
1725
|
try {
|
|
1264
|
-
return JSON.parse(text);
|
|
1726
|
+
return { status: res.status, body: JSON.parse(text) };
|
|
1265
1727
|
} catch {
|
|
1266
1728
|
throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
|
|
1267
1729
|
}
|
|
@@ -1324,11 +1786,30 @@ var PmsClient = class {
|
|
|
1324
1786
|
var HttpError = class extends Error {
|
|
1325
1787
|
status;
|
|
1326
1788
|
retryAfter;
|
|
1327
|
-
|
|
1789
|
+
/** Raw response body text (may be empty). */
|
|
1790
|
+
body;
|
|
1791
|
+
/** Stable numeric error code parsed from `{code,message}`, if present. */
|
|
1792
|
+
code;
|
|
1793
|
+
/** Engine-provided message from `{code,message}`, if present. */
|
|
1794
|
+
engineMessage;
|
|
1795
|
+
constructor(status, message, retryAfter, body = "") {
|
|
1328
1796
|
super(message);
|
|
1329
1797
|
this.name = "HttpError";
|
|
1330
1798
|
this.status = status;
|
|
1331
1799
|
this.retryAfter = retryAfter;
|
|
1800
|
+
this.body = body;
|
|
1801
|
+
if (body) {
|
|
1802
|
+
try {
|
|
1803
|
+
const parsed = JSON.parse(body);
|
|
1804
|
+
if (parsed && typeof parsed === "object" && typeof parsed.code === "number") {
|
|
1805
|
+
this.code = parsed.code;
|
|
1806
|
+
if (typeof parsed.message === "string") {
|
|
1807
|
+
this.engineMessage = parsed.message;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1332
1813
|
}
|
|
1333
1814
|
};
|
|
1334
1815
|
var PerAttemptTimeoutError = class extends Error {
|
|
@@ -1396,12 +1877,21 @@ function mergeAbortSignals(a, b) {
|
|
|
1396
1877
|
return controller.signal;
|
|
1397
1878
|
}
|
|
1398
1879
|
export {
|
|
1880
|
+
DAY_MS,
|
|
1881
|
+
ErrorCode,
|
|
1882
|
+
HttpError,
|
|
1399
1883
|
PmsClient,
|
|
1400
1884
|
PmsWallet,
|
|
1401
1885
|
decryptPayload,
|
|
1886
|
+
effectiveValue,
|
|
1402
1887
|
formatAmount,
|
|
1403
1888
|
fromHex,
|
|
1889
|
+
hashlockHash,
|
|
1404
1890
|
isValidMnemonic,
|
|
1891
|
+
makeUnlock,
|
|
1892
|
+
multisigAddress,
|
|
1405
1893
|
parseAmount,
|
|
1406
|
-
|
|
1894
|
+
spendableUtxos,
|
|
1895
|
+
toHex,
|
|
1896
|
+
txSigningMessage
|
|
1407
1897
|
};
|