@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.cjs
CHANGED
|
@@ -20,14 +20,23 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
DAY_MS: () => DAY_MS,
|
|
24
|
+
ErrorCode: () => ErrorCode,
|
|
25
|
+
HttpError: () => HttpError,
|
|
23
26
|
PmsClient: () => PmsClient,
|
|
24
27
|
PmsWallet: () => PmsWallet,
|
|
25
28
|
decryptPayload: () => decryptPayload,
|
|
29
|
+
effectiveValue: () => effectiveValue,
|
|
26
30
|
formatAmount: () => formatAmount,
|
|
27
31
|
fromHex: () => fromHex,
|
|
32
|
+
hashlockHash: () => hashlockHash,
|
|
28
33
|
isValidMnemonic: () => isValidMnemonic,
|
|
34
|
+
makeUnlock: () => makeUnlock,
|
|
35
|
+
multisigAddress: () => multisigAddress,
|
|
29
36
|
parseAmount: () => parseAmount,
|
|
30
|
-
|
|
37
|
+
spendableUtxos: () => spendableUtxos,
|
|
38
|
+
toHex: () => toHex,
|
|
39
|
+
txSigningMessage: () => txSigningMessage
|
|
31
40
|
});
|
|
32
41
|
module.exports = __toCommonJS(index_exports);
|
|
33
42
|
|
|
@@ -42,6 +51,7 @@ var import_english = require("@scure/bip39/wordlists/english");
|
|
|
42
51
|
// src/utils.ts
|
|
43
52
|
var import_sha2 = require("@noble/hashes/sha2");
|
|
44
53
|
var import_utils = require("@noble/hashes/utils");
|
|
54
|
+
var import_base = require("@scure/base");
|
|
45
55
|
function sha256Hash(data) {
|
|
46
56
|
return (0, import_sha2.sha256)(data);
|
|
47
57
|
}
|
|
@@ -54,12 +64,103 @@ function fromHex(hex) {
|
|
|
54
64
|
function encodeUtf8(str) {
|
|
55
65
|
return new TextEncoder().encode(str);
|
|
56
66
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
67
|
+
function toBase64(bytes) {
|
|
68
|
+
return import_base.base64.encode(bytes);
|
|
69
|
+
}
|
|
70
|
+
function txSigningMessage(networkId, inputs, outputs, fee) {
|
|
71
|
+
const canonInputs = inputs.map((i) => ({
|
|
72
|
+
out: { txid: i.out.txid, index: i.out.index }
|
|
73
|
+
}));
|
|
74
|
+
const canonOutputs = outputs.map((o) => {
|
|
75
|
+
const out = {
|
|
76
|
+
address: o.address,
|
|
77
|
+
amount: o.amount
|
|
78
|
+
};
|
|
79
|
+
if (o.asset_id !== void 0 && o.asset_id !== null) {
|
|
80
|
+
out.asset_id = o.asset_id;
|
|
81
|
+
}
|
|
82
|
+
if (o.locked_until !== void 0 && o.locked_until !== null) {
|
|
83
|
+
out.locked_until = o.locked_until;
|
|
84
|
+
}
|
|
85
|
+
if (o.spend_condition !== void 0 && o.spend_condition !== null) {
|
|
86
|
+
out.spend_condition = o.spend_condition;
|
|
87
|
+
}
|
|
88
|
+
if (o.created_at !== void 0 && o.created_at !== null) {
|
|
89
|
+
out.created_at = o.created_at;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
});
|
|
93
|
+
const canon = {
|
|
94
|
+
network_id: networkId,
|
|
95
|
+
inputs: canonInputs,
|
|
96
|
+
outputs: canonOutputs,
|
|
97
|
+
fee
|
|
98
|
+
};
|
|
99
|
+
return toHex(sha256Hash(encodeUtf8(JSON.stringify(canon))));
|
|
100
|
+
}
|
|
101
|
+
var PLAIN_VARIANT_TO_PAYLOAD_TYPE = {
|
|
102
|
+
Genesis: "Genesis",
|
|
103
|
+
Mint: "Mint",
|
|
104
|
+
TxUtxo: "Transaction",
|
|
105
|
+
Milestone: "Milestone",
|
|
106
|
+
Nft: "Nft",
|
|
107
|
+
ConfigUpdate: "ConfigUpdate",
|
|
108
|
+
Reward: "Reward",
|
|
109
|
+
EncryptedReward: "EncryptedReward",
|
|
110
|
+
TokenCreate: "TokenCreate",
|
|
111
|
+
BridgeLock: "BridgeLock",
|
|
112
|
+
BridgeMint: "BridgeMint",
|
|
113
|
+
Freeze: "Freeze",
|
|
114
|
+
Unfreeze: "Unfreeze",
|
|
115
|
+
Seize: "Seize",
|
|
116
|
+
Reverse: "Reverse",
|
|
117
|
+
ContractRegister: "ContractRegister",
|
|
118
|
+
ContractUpdate: "ContractUpdate",
|
|
119
|
+
LedgerOwnershipTransfer: "LedgerOwnershipTransfer",
|
|
120
|
+
CoordinatorKeyRotate: "CoordinatorKeyRotate"
|
|
121
|
+
};
|
|
122
|
+
var EMPTY_COMMITMENT = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
123
|
+
function computeBlockId(parents, payload, nonce) {
|
|
124
|
+
let payload_type;
|
|
125
|
+
let commitment;
|
|
126
|
+
let len_hint;
|
|
127
|
+
let key_version;
|
|
128
|
+
if (payload === void 0 || payload === null) {
|
|
129
|
+
payload_type = "None";
|
|
130
|
+
commitment = EMPTY_COMMITMENT;
|
|
131
|
+
len_hint = 0;
|
|
132
|
+
key_version = 0;
|
|
133
|
+
} else if ("Plain" in payload) {
|
|
134
|
+
const plain = payload.Plain;
|
|
135
|
+
const variant = typeof plain === "string" ? plain : Object.keys(plain)[0];
|
|
136
|
+
payload_type = PLAIN_VARIANT_TO_PAYLOAD_TYPE[variant] ?? variant;
|
|
137
|
+
const plainBytes = encodeUtf8(JSON.stringify(plain));
|
|
138
|
+
commitment = toHex(sha256Hash(plainBytes));
|
|
139
|
+
len_hint = plainBytes.length;
|
|
140
|
+
key_version = 0;
|
|
141
|
+
} else {
|
|
142
|
+
const enc = payload.Encrypted;
|
|
143
|
+
payload_type = "Encrypted";
|
|
144
|
+
commitment = enc.commitment;
|
|
145
|
+
len_hint = enc.ciphertext_b64.length;
|
|
146
|
+
key_version = enc.key_version;
|
|
147
|
+
}
|
|
148
|
+
const head = {
|
|
149
|
+
parents,
|
|
150
|
+
nonce,
|
|
151
|
+
envelope: { payload_type, commitment, len_hint, key_version }
|
|
152
|
+
};
|
|
153
|
+
return toHex(sha256Hash(encodeUtf8(JSON.stringify(head))));
|
|
154
|
+
}
|
|
155
|
+
function x25519FromAddress(address) {
|
|
156
|
+
try {
|
|
157
|
+
const decoded = import_base.bech32m.decode(address, false);
|
|
158
|
+
const bytes = import_base.bech32m.fromWords(decoded.words);
|
|
159
|
+
if (bytes.length !== 52) return void 0;
|
|
160
|
+
return toHex(bytes.slice(20));
|
|
161
|
+
} catch {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
63
164
|
}
|
|
64
165
|
function parseAmount(amount) {
|
|
65
166
|
const [whole, frac = ""] = amount.split(".");
|
|
@@ -72,6 +173,47 @@ function formatAmount(sats) {
|
|
|
72
173
|
const fracStr = frac.toString().padStart(8, "0");
|
|
73
174
|
return `${whole}.${fracStr}`;
|
|
74
175
|
}
|
|
176
|
+
var DAY_MS = 864e5;
|
|
177
|
+
function multisigAddress(m, pubkeys) {
|
|
178
|
+
const normalized = pubkeys.map((pk) => {
|
|
179
|
+
const t = pk.trim();
|
|
180
|
+
return (t.startsWith("0x") ? t.slice(2) : t).toLowerCase();
|
|
181
|
+
}).sort();
|
|
182
|
+
const parts = [
|
|
183
|
+
encodeUtf8("pms-multisig-v1"),
|
|
184
|
+
new Uint8Array([m, normalized.length]),
|
|
185
|
+
...normalized.map((pk) => encodeUtf8(pk))
|
|
186
|
+
];
|
|
187
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
188
|
+
const buf = new Uint8Array(total);
|
|
189
|
+
let off = 0;
|
|
190
|
+
for (const p of parts) {
|
|
191
|
+
buf.set(p, off);
|
|
192
|
+
off += p.length;
|
|
193
|
+
}
|
|
194
|
+
return `msig1${toHex(sha256Hash(buf).slice(0, 20))}`;
|
|
195
|
+
}
|
|
196
|
+
function hashlockHash(preimage) {
|
|
197
|
+
const bytes = typeof preimage === "string" ? encodeUtf8(preimage) : preimage;
|
|
198
|
+
return toHex(sha256Hash(bytes));
|
|
199
|
+
}
|
|
200
|
+
function effectiveValue(amount, createdAtMs, nowMs, bpsPerDay) {
|
|
201
|
+
if (createdAtMs === void 0 || createdAtMs === null || !bpsPerDay) {
|
|
202
|
+
return amount;
|
|
203
|
+
}
|
|
204
|
+
const elapsed = nowMs - createdAtMs;
|
|
205
|
+
const days = elapsed > 0 ? BigInt(Math.floor(elapsed / DAY_MS)) : 0n;
|
|
206
|
+
if (days === 0n) return amount;
|
|
207
|
+
const sats = parseAmount(amount);
|
|
208
|
+
const scaled = sats * 10000n - sats * BigInt(bpsPerDay) * days;
|
|
209
|
+
if (scaled <= 0n) return "0.00000000";
|
|
210
|
+
return formatAmount(scaled / 10000n);
|
|
211
|
+
}
|
|
212
|
+
function spendableUtxos(utxos, nowMs = Date.now()) {
|
|
213
|
+
return utxos.filter(
|
|
214
|
+
(u) => u.locked_until === void 0 || u.locked_until === null || u.locked_until <= nowMs
|
|
215
|
+
);
|
|
216
|
+
}
|
|
75
217
|
|
|
76
218
|
// src/wallet.ts
|
|
77
219
|
var PmsWallet = class _PmsWallet {
|
|
@@ -246,17 +388,57 @@ var PmsWallet = class _PmsWallet {
|
|
|
246
388
|
function isValidMnemonic(mnemonic) {
|
|
247
389
|
return (0, import_bip39.validateMnemonic)(mnemonic.trim().toLowerCase(), import_english.wordlist);
|
|
248
390
|
}
|
|
391
|
+
function makeUnlock(signers, txHashHex, opts) {
|
|
392
|
+
if (signers.length === 0) {
|
|
393
|
+
throw new Error("makeUnlock: at least one signer is required");
|
|
394
|
+
}
|
|
395
|
+
const msg = encodeUtf8(txHashHex);
|
|
396
|
+
const sign = (w) => toBase64(fromHex(w.sign(msg)));
|
|
397
|
+
const [primary, ...rest] = signers;
|
|
398
|
+
const unlock = {
|
|
399
|
+
pubkey_hex: primary.publicKeyHex,
|
|
400
|
+
signature_b64: sign(primary)
|
|
401
|
+
};
|
|
402
|
+
if (rest.length > 0) {
|
|
403
|
+
unlock.cosigners = rest.map((w) => ({
|
|
404
|
+
pubkey_hex: w.publicKeyHex,
|
|
405
|
+
signature_b64: sign(w)
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
if (opts?.preimageHex !== void 0) {
|
|
409
|
+
unlock.preimage_hex = opts.preimageHex;
|
|
410
|
+
}
|
|
411
|
+
return unlock;
|
|
412
|
+
}
|
|
249
413
|
|
|
250
414
|
// src/types.ts
|
|
251
415
|
var DEFAULT_CONFIG = {
|
|
252
416
|
networkId: "pms-mainnet",
|
|
253
|
-
protocolVersion:
|
|
417
|
+
protocolVersion: 3,
|
|
254
418
|
timeout: 3e4,
|
|
255
419
|
seedNodes: [],
|
|
256
420
|
enableRacing: true,
|
|
257
421
|
retries: 2,
|
|
258
422
|
perAttemptTimeoutMs: 4e3,
|
|
259
|
-
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
|
|
260
442
|
};
|
|
261
443
|
|
|
262
444
|
// src/crypto.ts
|
|
@@ -265,13 +447,13 @@ var import_aes = require("@noble/ciphers/aes.js");
|
|
|
265
447
|
var import_hkdf2 = require("@noble/hashes/hkdf");
|
|
266
448
|
var import_sha23 = require("@noble/hashes/sha2");
|
|
267
449
|
var import_utils3 = require("@noble/hashes/utils");
|
|
268
|
-
var
|
|
450
|
+
var import_base2 = require("@scure/base");
|
|
269
451
|
var SCHEME = "x25519+aes256gcm";
|
|
270
452
|
var HKDF_SALT = new TextEncoder().encode("pms-dek-wrap");
|
|
271
453
|
var HKDF_INFO_KEK = new TextEncoder().encode("kek-v1");
|
|
272
454
|
var HKDF_INFO_KID = new TextEncoder().encode("kid-v1");
|
|
273
455
|
function fromBase64(str) {
|
|
274
|
-
return
|
|
456
|
+
return import_base2.base64.decode(str);
|
|
275
457
|
}
|
|
276
458
|
function sha256Hex(data) {
|
|
277
459
|
return (0, import_utils3.bytesToHex)((0, import_sha23.sha256)(data));
|
|
@@ -436,11 +618,222 @@ var PmsClient = class {
|
|
|
436
618
|
}
|
|
437
619
|
/**
|
|
438
620
|
* Récupère les UTXOs d'une adresse.
|
|
621
|
+
*
|
|
622
|
+
* Depuis dag-pms v0.10.0, chaque UTXO expose aussi ses contraintes de
|
|
623
|
+
* dépense : `locked_until` (time-lock), `spend_condition`
|
|
624
|
+
* (MultiSig/HashLock) et `created_at` (base du demurrage). Utiliser
|
|
625
|
+
* [`spendableUtxos`] pour exclure les UTXOs encore verrouillés avant
|
|
626
|
+
* toute sélection de coins côté client.
|
|
439
627
|
*/
|
|
440
628
|
async getUtxos(address) {
|
|
441
629
|
const res = await this.fetch(`/v1/wallet/${address}/utxos`, void 0, { idempotent: true });
|
|
442
630
|
return res.utxos ?? [];
|
|
443
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Dernier snapshot de preuve de réserves ancré (protocole 2.6,
|
|
634
|
+
* `GET /v1/reserves/latest`). 404 si aucun snapshot n'a encore été ancré.
|
|
635
|
+
*/
|
|
636
|
+
async getLatestReserves() {
|
|
637
|
+
return this.fetch("/v1/reserves/latest", void 0, { idempotent: true });
|
|
638
|
+
}
|
|
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
|
+
}
|
|
444
837
|
// ═══════════════════════════════════════════════════════════════════════
|
|
445
838
|
// Wallet API (Custodial — Server-Side)
|
|
446
839
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -897,101 +1290,83 @@ var PmsClient = class {
|
|
|
897
1290
|
}
|
|
898
1291
|
/**
|
|
899
1292
|
* Envoie des tokens à une adresse.
|
|
900
|
-
*
|
|
1293
|
+
*
|
|
1294
|
+
* Flux aligné sur dag-pms v0.9.0 (single-writer + vérification des
|
|
1295
|
+
* signatures de transaction par l'engine) :
|
|
1296
|
+
* 1. `POST /v1/tx/prepare` — le serveur sélectionne les UTXOs et calcule
|
|
1297
|
+
* les frais.
|
|
1298
|
+
* 2. Vérification défensive côté client : l'output destinataire demandé
|
|
1299
|
+
* existe bien, et le `tx_hash` retourné correspond au message canonique
|
|
1300
|
+
* recalculé localement (`txSigningMessage`). On ne signe JAMAIS un hash
|
|
1301
|
+
* opaque non vérifié.
|
|
1302
|
+
* 3. Le wallet signe la TRANSACTION (unlocks) — pas le bloc. C'est le
|
|
1303
|
+
* serveur qui forge et signe le bloc (single-writer enforcement).
|
|
1304
|
+
* 4. `POST /v1/wallet/tx/send` avec la TX signée + les clés X25519 des
|
|
1305
|
+
* destinataires pour le chiffrement du payload.
|
|
1306
|
+
*
|
|
1307
|
+
* @param params.to - Adresse destinataire (Bech32m de préférence)
|
|
1308
|
+
* @param params.amount - Montant décimal (ex: "10.0")
|
|
1309
|
+
* @param params.wallet - Wallet signataire (propriétaire des UTXOs)
|
|
1310
|
+
* @param params.assetId - Asset ID optionnel (undefined = PMS natif)
|
|
1311
|
+
* @returns `{id, status, block_id}` — id du bloc forgé par le serveur
|
|
901
1312
|
*/
|
|
902
1313
|
async send(params) {
|
|
903
|
-
const { to, amount, wallet,
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const amountSats = parseAmount(amount);
|
|
912
|
-
const variableFee = amountSats * feeRateBps / 10000n;
|
|
913
|
-
const fee = baseFeeSats + variableFee;
|
|
914
|
-
const totalNeeded = amountSats + fee;
|
|
915
|
-
let selectedSats = 0n;
|
|
916
|
-
const selectedUtxos = [];
|
|
917
|
-
for (const utxo of utxos) {
|
|
918
|
-
selectedUtxos.push(utxo);
|
|
919
|
-
selectedSats += parseAmount(utxo.amount || "0");
|
|
920
|
-
if (selectedSats >= totalNeeded) break;
|
|
1314
|
+
const { to, amount, wallet, assetId } = params;
|
|
1315
|
+
const prepareBody = {
|
|
1316
|
+
from: wallet.address,
|
|
1317
|
+
to,
|
|
1318
|
+
amount
|
|
1319
|
+
};
|
|
1320
|
+
if (assetId !== void 0) {
|
|
1321
|
+
prepareBody.asset_id = assetId;
|
|
921
1322
|
}
|
|
922
|
-
|
|
1323
|
+
const prepared = await this.prepareTx(prepareBody);
|
|
1324
|
+
const { unsigned_tx, tx_hash } = prepared;
|
|
1325
|
+
const requestedSats = parseAmount(amount);
|
|
1326
|
+
const recipientOutput = unsigned_tx.outputs.find(
|
|
1327
|
+
(o) => o.address === to && (o.asset_id ?? void 0) === (assetId ?? void 0) && parseAmount(o.amount) >= requestedSats
|
|
1328
|
+
);
|
|
1329
|
+
if (!recipientOutput) {
|
|
923
1330
|
throw new Error(
|
|
924
|
-
`
|
|
1331
|
+
`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.`
|
|
925
1332
|
);
|
|
926
1333
|
}
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (change > 0n) {
|
|
938
|
-
outputs.push({ address: wallet.address, amount: formatAmount(change) });
|
|
1334
|
+
const localHash = txSigningMessage(
|
|
1335
|
+
this.config.networkId,
|
|
1336
|
+
unsigned_tx.inputs,
|
|
1337
|
+
unsigned_tx.outputs,
|
|
1338
|
+
unsigned_tx.fee
|
|
1339
|
+
);
|
|
1340
|
+
if (localHash !== tx_hash) {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`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.`
|
|
1343
|
+
);
|
|
939
1344
|
}
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
index: utxo.outpoint.index
|
|
944
|
-
}
|
|
945
|
-
}));
|
|
946
|
-
const txCanonical = {
|
|
947
|
-
inputs,
|
|
948
|
-
outputs,
|
|
949
|
-
fee: formatAmount(fee)
|
|
950
|
-
};
|
|
951
|
-
const txMessage = JSON.stringify(txCanonical);
|
|
952
|
-
const txSignatureHex = wallet.sign(encodeUtf8(txMessage));
|
|
953
|
-
const txSigBytes = fromHex(txSignatureHex);
|
|
954
|
-
const txSigB64 = btoa(String.fromCharCode(...txSigBytes));
|
|
955
|
-
const unlocks = inputs.map(() => ({
|
|
1345
|
+
const sigHex = wallet.sign(encodeUtf8(tx_hash));
|
|
1346
|
+
const sigB64 = toBase64(fromHex(sigHex));
|
|
1347
|
+
const unlocks = unsigned_tx.inputs.map(() => ({
|
|
956
1348
|
pubkey_hex: wallet.publicKeyHex,
|
|
957
|
-
signature_b64:
|
|
1349
|
+
signature_b64: sigB64
|
|
958
1350
|
}));
|
|
959
1351
|
const tx = {
|
|
960
|
-
inputs,
|
|
961
|
-
outputs,
|
|
962
|
-
fee:
|
|
1352
|
+
inputs: unsigned_tx.inputs,
|
|
1353
|
+
outputs: unsigned_tx.outputs,
|
|
1354
|
+
fee: unsigned_tx.fee,
|
|
963
1355
|
unlocks
|
|
964
1356
|
};
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
signer_pk_hex: wallet.publicKeyHex
|
|
979
|
-
};
|
|
980
|
-
const messageToSign = JSON.stringify(canonicalView);
|
|
981
|
-
const signatureHex = wallet.sign(encodeUtf8(messageToSign));
|
|
982
|
-
const signatureBytes = fromHex(signatureHex);
|
|
983
|
-
const signatureB64 = btoa(String.fromCharCode(...signatureBytes));
|
|
984
|
-
const wireBlock = {
|
|
985
|
-
id: blockId,
|
|
986
|
-
parents,
|
|
987
|
-
payload_json: payloadJson,
|
|
988
|
-
nonce,
|
|
989
|
-
network_id: this.config.networkId,
|
|
990
|
-
protocol_version: this.config.protocolVersion,
|
|
991
|
-
signer_pk_hex: wallet.publicKeyHex,
|
|
992
|
-
signature_hex: signatureB64
|
|
993
|
-
};
|
|
994
|
-
return this.submitBlock(wireBlock);
|
|
1357
|
+
const recipientsXpk = [wallet.x25519PublicKeyHex];
|
|
1358
|
+
const toXpk = x25519FromAddress(to);
|
|
1359
|
+
if (toXpk && !recipientsXpk.includes(toXpk)) {
|
|
1360
|
+
recipientsXpk.push(toXpk);
|
|
1361
|
+
}
|
|
1362
|
+
const resp = await this.fetch(
|
|
1363
|
+
"/v1/wallet/tx/send",
|
|
1364
|
+
{
|
|
1365
|
+
method: "POST",
|
|
1366
|
+
body: JSON.stringify({ tx, recipients_xpk: recipientsXpk })
|
|
1367
|
+
}
|
|
1368
|
+
);
|
|
1369
|
+
return { ...resp, block_id: resp.id };
|
|
995
1370
|
}
|
|
996
1371
|
// ═══════════════════════════════════════════════════════════════════════
|
|
997
1372
|
// Méthodes NFT Cube (Burn et Mint spécialisé)
|
|
@@ -999,7 +1374,15 @@ var PmsClient = class {
|
|
|
999
1374
|
/**
|
|
1000
1375
|
* Transférer un NFT à un autre propriétaire.
|
|
1001
1376
|
* Prend en charge le re-chiffrement des métadonnées via le coordinateur.
|
|
1002
|
-
*
|
|
1377
|
+
*
|
|
1378
|
+
* @deprecated Cette méthode soumet un WireBlock signé par la clé
|
|
1379
|
+
* utilisateur via `/submit/block`. Depuis dag-pms v0.9.0, l'engine tourne
|
|
1380
|
+
* en mode **single-writer** : tout bloc signé par une clé non-coordinateur
|
|
1381
|
+
* est rejeté. Cette méthode ne fonctionne que sur un réseau SANS
|
|
1382
|
+
* single-writer enforcement. Utilisez les flux server-side
|
|
1383
|
+
* (`/v1/nft/transfer/prepare` + endpoints serveur) qui forgent et signent
|
|
1384
|
+
* le bloc côté coordinateur.
|
|
1385
|
+
*
|
|
1003
1386
|
* @param params - Paramètres du transfert
|
|
1004
1387
|
* @param params.tokenId - Identifiant du NFT
|
|
1005
1388
|
* @param params.to - Adresse du nouveau propriétaire
|
|
@@ -1035,7 +1418,7 @@ var PmsClient = class {
|
|
|
1035
1418
|
const payload = { Plain: { Nft: action } };
|
|
1036
1419
|
const payloadJson = JSON.stringify(payload);
|
|
1037
1420
|
const nonce = 0;
|
|
1038
|
-
const blockId = computeBlockId(parents,
|
|
1421
|
+
const blockId = computeBlockId(parents, payload, nonce);
|
|
1039
1422
|
const canonicalView = {
|
|
1040
1423
|
id: blockId,
|
|
1041
1424
|
parents,
|
|
@@ -1067,10 +1450,17 @@ var PmsClient = class {
|
|
|
1067
1450
|
* Seul le propriétaire du NFT peut le brûler.
|
|
1068
1451
|
* Une fois brûlé, le NFT est supprimé définitivement.
|
|
1069
1452
|
*
|
|
1070
|
-
* Pour les Cubes authentiques (avec signature Authority valide),
|
|
1453
|
+
* Pour les Cubes authentiques (avec signature Authority valide),
|
|
1071
1454
|
* un remboursement est calculé selon la formule:
|
|
1072
1455
|
* `(weight * size * density) / 10000` PMS
|
|
1073
|
-
*
|
|
1456
|
+
*
|
|
1457
|
+
* @deprecated Cette méthode poste un WireBlock signé par la clé
|
|
1458
|
+
* utilisateur. Depuis dag-pms v0.9.0, l'engine tourne en mode
|
|
1459
|
+
* **single-writer** : tout bloc signé par une clé non-coordinateur est
|
|
1460
|
+
* rejeté. Cette méthode ne fonctionne que sur un réseau SANS single-writer
|
|
1461
|
+
* enforcement. Le flux supporté est le burn server-side
|
|
1462
|
+
* (`/v1/nft/burn-simple`), où le serveur forge et signe le bloc.
|
|
1463
|
+
*
|
|
1074
1464
|
* @param params - Paramètres du burn
|
|
1075
1465
|
* @param params.tokenId - Identifiant du NFT à brûler
|
|
1076
1466
|
* @param params.wallet - Wallet PMS du propriétaire (doit être l'owner actuel)
|
|
@@ -1104,7 +1494,7 @@ var PmsClient = class {
|
|
|
1104
1494
|
};
|
|
1105
1495
|
const payloadJson = JSON.stringify(payload);
|
|
1106
1496
|
const nonce = 0;
|
|
1107
|
-
const blockId = computeBlockId(parents,
|
|
1497
|
+
const blockId = computeBlockId(parents, payload, nonce);
|
|
1108
1498
|
const canonicalView = {
|
|
1109
1499
|
id: blockId,
|
|
1110
1500
|
parents,
|
|
@@ -1136,7 +1526,14 @@ var PmsClient = class {
|
|
|
1136
1526
|
}
|
|
1137
1527
|
/**
|
|
1138
1528
|
* Brûle (détruit) plusieurs NFTs en une seule transaction.
|
|
1139
|
-
*
|
|
1529
|
+
*
|
|
1530
|
+
* @deprecated Cette méthode poste un WireBlock signé par la clé
|
|
1531
|
+
* utilisateur. Depuis dag-pms v0.9.0, l'engine tourne en mode
|
|
1532
|
+
* **single-writer** : tout bloc signé par une clé non-coordinateur est
|
|
1533
|
+
* rejeté. Cette méthode ne fonctionne que sur un réseau SANS single-writer
|
|
1534
|
+
* enforcement. Le flux supporté est le burn server-side
|
|
1535
|
+
* (`/v1/nft/burn-simple`), où le serveur forge et signe le bloc.
|
|
1536
|
+
*
|
|
1140
1537
|
* @param params - Paramètres du batch burn
|
|
1141
1538
|
* @param params.tokenIds - Liste des Identifiants des NFTs à brûler
|
|
1142
1539
|
* @param params.wallet - Wallet PMS du propriétaire
|
|
@@ -1161,7 +1558,7 @@ var PmsClient = class {
|
|
|
1161
1558
|
};
|
|
1162
1559
|
const payloadJson = JSON.stringify(payload);
|
|
1163
1560
|
const nonce = 0;
|
|
1164
|
-
const blockId = computeBlockId(parents,
|
|
1561
|
+
const blockId = computeBlockId(parents, payload, nonce);
|
|
1165
1562
|
const canonicalView = {
|
|
1166
1563
|
id: blockId,
|
|
1167
1564
|
parents,
|
|
@@ -1260,6 +1657,62 @@ var PmsClient = class {
|
|
|
1260
1657
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1261
1658
|
// Helper HTTP
|
|
1262
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
|
+
}
|
|
1263
1716
|
async fetch(path, init, opts) {
|
|
1264
1717
|
return this.fetchUrl(this.config.nodeUrl, path, init, opts);
|
|
1265
1718
|
}
|
|
@@ -1267,8 +1720,26 @@ var PmsClient = class {
|
|
|
1267
1720
|
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1268
1721
|
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1269
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.
|
|
1270
1726
|
*/
|
|
1271
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) {
|
|
1272
1743
|
const attemptController = new AbortController();
|
|
1273
1744
|
const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
|
|
1274
1745
|
const signal = mergeSignalsAny(attemptController.signal, outerSignal);
|
|
@@ -1287,14 +1758,14 @@ var PmsClient = class {
|
|
|
1287
1758
|
if (!res.ok) {
|
|
1288
1759
|
const text2 = await res.text().catch(() => "");
|
|
1289
1760
|
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
1290
|
-
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
|
|
1761
|
+
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter, text2);
|
|
1291
1762
|
}
|
|
1292
1763
|
const text = await res.text();
|
|
1293
1764
|
if (!text) {
|
|
1294
|
-
return {};
|
|
1765
|
+
return { status: res.status, body: {} };
|
|
1295
1766
|
}
|
|
1296
1767
|
try {
|
|
1297
|
-
return JSON.parse(text);
|
|
1768
|
+
return { status: res.status, body: JSON.parse(text) };
|
|
1298
1769
|
} catch {
|
|
1299
1770
|
throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
|
|
1300
1771
|
}
|
|
@@ -1357,11 +1828,30 @@ var PmsClient = class {
|
|
|
1357
1828
|
var HttpError = class extends Error {
|
|
1358
1829
|
status;
|
|
1359
1830
|
retryAfter;
|
|
1360
|
-
|
|
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 = "") {
|
|
1361
1838
|
super(message);
|
|
1362
1839
|
this.name = "HttpError";
|
|
1363
1840
|
this.status = status;
|
|
1364
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
|
+
}
|
|
1365
1855
|
}
|
|
1366
1856
|
};
|
|
1367
1857
|
var PerAttemptTimeoutError = class extends Error {
|
|
@@ -1430,12 +1920,21 @@ function mergeAbortSignals(a, b) {
|
|
|
1430
1920
|
}
|
|
1431
1921
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1432
1922
|
0 && (module.exports = {
|
|
1923
|
+
DAY_MS,
|
|
1924
|
+
ErrorCode,
|
|
1925
|
+
HttpError,
|
|
1433
1926
|
PmsClient,
|
|
1434
1927
|
PmsWallet,
|
|
1435
1928
|
decryptPayload,
|
|
1929
|
+
effectiveValue,
|
|
1436
1930
|
formatAmount,
|
|
1437
1931
|
fromHex,
|
|
1932
|
+
hashlockHash,
|
|
1438
1933
|
isValidMnemonic,
|
|
1934
|
+
makeUnlock,
|
|
1935
|
+
multisigAddress,
|
|
1439
1936
|
parseAmount,
|
|
1440
|
-
|
|
1937
|
+
spendableUtxos,
|
|
1938
|
+
toHex,
|
|
1939
|
+
txSigningMessage
|
|
1441
1940
|
});
|