@empereur-rouge/pms-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1062 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PmsClient: () => PmsClient,
24
+ PmsWallet: () => PmsWallet,
25
+ decryptPayload: () => decryptPayload,
26
+ formatAmount: () => formatAmount,
27
+ fromHex: () => fromHex,
28
+ isValidMnemonic: () => isValidMnemonic,
29
+ parseAmount: () => parseAmount,
30
+ toHex: () => toHex
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/wallet.ts
35
+ var import_secp256k1 = require("@noble/curves/secp256k1");
36
+ var import_ed25519 = require("@noble/curves/ed25519");
37
+ var import_sha22 = require("@noble/hashes/sha2");
38
+ var import_hkdf = require("@noble/hashes/hkdf");
39
+ var import_bip39 = require("@scure/bip39");
40
+ var import_english = require("@scure/bip39/wordlists/english");
41
+
42
+ // src/utils.ts
43
+ var import_sha2 = require("@noble/hashes/sha2");
44
+ var import_utils = require("@noble/hashes/utils");
45
+ function sha256Hash(data) {
46
+ return (0, import_sha2.sha256)(data);
47
+ }
48
+ function toHex(bytes) {
49
+ return (0, import_utils.bytesToHex)(bytes);
50
+ }
51
+ function fromHex(hex) {
52
+ return (0, import_utils.hexToBytes)(hex);
53
+ }
54
+ function encodeUtf8(str) {
55
+ return new TextEncoder().encode(str);
56
+ }
57
+ function computeBlockId(parents, payloadJson, nonce) {
58
+ const parentsStr = parents.sort().join(",");
59
+ const payloadStr = payloadJson ?? "";
60
+ const content = `${parentsStr}|${payloadStr}|${nonce}`;
61
+ const hash = sha256Hash(encodeUtf8(content));
62
+ return toHex(hash);
63
+ }
64
+ function parseAmount(amount) {
65
+ const [whole, frac = ""] = amount.split(".");
66
+ const fracPadded = frac.padEnd(8, "0").slice(0, 8);
67
+ return BigInt(whole) * 100000000n + BigInt(fracPadded);
68
+ }
69
+ function formatAmount(sats) {
70
+ const whole = sats / 100000000n;
71
+ const frac = sats % 100000000n;
72
+ const fracStr = frac.toString().padStart(8, "0");
73
+ return `${whole}.${fracStr}`;
74
+ }
75
+
76
+ // src/wallet.ts
77
+ var PmsWallet = class _PmsWallet {
78
+ /** Clé privée secp256k1 (32 bytes) - pour signatures */
79
+ _privateKey;
80
+ /** Clé publique secp256k1 non compressée (65 bytes: 04 + x + y) */
81
+ _publicKey;
82
+ /** Clé privée X25519 (32 bytes) - pour chiffrement */
83
+ _x25519PrivateKey;
84
+ /** Clé publique X25519 (32 bytes) */
85
+ _x25519PublicKey;
86
+ /** Phrase mnémonique (24 mots) si générée/importée */
87
+ _mnemonic;
88
+ /**
89
+ * Constructeur privé - utiliser les méthodes statiques.
90
+ */
91
+ constructor(privateKey, mnemonic) {
92
+ this._privateKey = privateKey;
93
+ this._publicKey = import_secp256k1.secp256k1.getPublicKey(privateKey, false);
94
+ this._x25519PrivateKey = (0, import_hkdf.hkdf)(
95
+ import_sha22.sha256,
96
+ privateKey,
97
+ new TextEncoder().encode("pms-x25519"),
98
+ // salt
99
+ new TextEncoder().encode("encryption"),
100
+ // info
101
+ 32
102
+ );
103
+ this._x25519PublicKey = import_ed25519.x25519.getPublicKey(this._x25519PrivateKey);
104
+ this._mnemonic = mnemonic;
105
+ }
106
+ // ═══════════════════════════════════════════════════════════════════════
107
+ // Méthodes statiques de création
108
+ // ═══════════════════════════════════════════════════════════════════════
109
+ /**
110
+ * Génère un nouveau wallet avec une phrase de 24 mots.
111
+ */
112
+ static generate() {
113
+ const mnemonic = (0, import_bip39.generateMnemonic)(import_english.wordlist, 256);
114
+ return _PmsWallet.fromMnemonic(mnemonic);
115
+ }
116
+ /**
117
+ * Crée un wallet à partir d'une phrase mnémonique (12, 15, 18, 21 ou 24 mots).
118
+ * @throws Error si la phrase est invalide
119
+ */
120
+ static fromMnemonic(mnemonic) {
121
+ const normalized = mnemonic.trim().toLowerCase();
122
+ if (!(0, import_bip39.validateMnemonic)(normalized, import_english.wordlist)) {
123
+ throw new Error("Invalid mnemonic phrase");
124
+ }
125
+ const seed = (0, import_bip39.mnemonicToSeedSync)(normalized);
126
+ const privateKey = seed.slice(0, 32);
127
+ return new _PmsWallet(privateKey, normalized);
128
+ }
129
+ /**
130
+ * Crée un wallet à partir d'une clé privée hexadécimale.
131
+ */
132
+ static fromPrivateKey(privateKeyHex) {
133
+ const privateKey = fromHex(privateKeyHex);
134
+ if (privateKey.length !== 32) {
135
+ throw new Error("Private key must be 32 bytes");
136
+ }
137
+ return new _PmsWallet(privateKey);
138
+ }
139
+ /**
140
+ * Crée un wallet à partir d'une seed (32 bytes).
141
+ */
142
+ static fromSeed(seed) {
143
+ if (seed.length !== 32) {
144
+ throw new Error("Seed must be 32 bytes");
145
+ }
146
+ return new _PmsWallet(seed);
147
+ }
148
+ // ═══════════════════════════════════════════════════════════════════════
149
+ // Propriétés publiques
150
+ // ═══════════════════════════════════════════════════════════════════════
151
+ /**
152
+ * Adresse du wallet (clé publique hex).
153
+ * Format: "04" + 64 bytes hex = 130 caractères
154
+ */
155
+ get address() {
156
+ return toHex(this._publicKey);
157
+ }
158
+ /**
159
+ * Clé publique en bytes.
160
+ */
161
+ get publicKey() {
162
+ return this._publicKey;
163
+ }
164
+ /**
165
+ * Clé publique en hex.
166
+ */
167
+ get publicKeyHex() {
168
+ return toHex(this._publicKey);
169
+ }
170
+ /**
171
+ * Phrase mnémonique (si disponible).
172
+ * @returns undefined si le wallet a été créé depuis une clé privée
173
+ */
174
+ get mnemonic() {
175
+ return this._mnemonic;
176
+ }
177
+ // ═══════════════════════════════════════════════════════════════════════
178
+ // Clés X25519 (pour chiffrement)
179
+ // ═══════════════════════════════════════════════════════════════════════
180
+ /**
181
+ * Clé publique X25519 en hex (pour chiffrement).
182
+ * Utiliser cette clé comme destinataire pour encryptPayload().
183
+ */
184
+ get x25519PublicKeyHex() {
185
+ return toHex(this._x25519PublicKey);
186
+ }
187
+ /**
188
+ * Clé privée X25519 en hex (pour déchiffrement).
189
+ * ⚠️ Ne pas exposer cette clé publiquement !
190
+ */
191
+ get x25519PrivateKeyHex() {
192
+ return toHex(this._x25519PrivateKey);
193
+ }
194
+ // ═══════════════════════════════════════════════════════════════════════
195
+ // Méthodes de signature
196
+ // ═══════════════════════════════════════════════════════════════════════
197
+ /**
198
+ * Signe un message avec la clé privée.
199
+ * @param message - Message à signer (sera hashé avec SHA256)
200
+ * @returns Signature DER encodée en hex
201
+ */
202
+ sign(message) {
203
+ const hash = (0, import_sha22.sha256)(message);
204
+ const sig = import_secp256k1.secp256k1.sign(hash, this._privateKey);
205
+ return sig.toDERHex();
206
+ }
207
+ /**
208
+ * Signe un message déjà hashé.
209
+ * @param hash - Hash 32 bytes du message
210
+ * @returns Signature DER encodée en hex
211
+ */
212
+ signHash(hash) {
213
+ if (hash.length !== 32) {
214
+ throw new Error("Hash must be 32 bytes");
215
+ }
216
+ const sig = import_secp256k1.secp256k1.sign(hash, this._privateKey);
217
+ return sig.toDERHex();
218
+ }
219
+ /**
220
+ * Exporte la clé privée en hex.
221
+ * ⚠️ À utiliser avec précaution !
222
+ */
223
+ exportPrivateKey() {
224
+ return toHex(this._privateKey);
225
+ }
226
+ // ═══════════════════════════════════════════════════════════════════════
227
+ // Méthodes statiques de vérification
228
+ // ═══════════════════════════════════════════════════════════════════════
229
+ /**
230
+ * Vérifie une signature.
231
+ * @param message - Message original
232
+ * @param signature - Signature DER hex
233
+ * @param publicKeyHex - Clé publique hex du signataire
234
+ */
235
+ static verify(message, signature, publicKeyHex) {
236
+ try {
237
+ const hash = (0, import_sha22.sha256)(message);
238
+ const pubKey = fromHex(publicKeyHex);
239
+ const sig = import_secp256k1.secp256k1.Signature.fromDER(signature);
240
+ return import_secp256k1.secp256k1.verify(sig.toCompactRawBytes(), hash, pubKey);
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+ };
246
+ function isValidMnemonic(mnemonic) {
247
+ return (0, import_bip39.validateMnemonic)(mnemonic.trim().toLowerCase(), import_english.wordlist);
248
+ }
249
+
250
+ // src/types.ts
251
+ var DEFAULT_CONFIG = {
252
+ networkId: "pms-mainnet",
253
+ protocolVersion: 1,
254
+ timeout: 3e4,
255
+ seedNodes: [],
256
+ enableRacing: true
257
+ };
258
+
259
+ // src/crypto.ts
260
+ var import_ed255192 = require("@noble/curves/ed25519");
261
+ var import_aes = require("@noble/ciphers/aes.js");
262
+ var import_hkdf2 = require("@noble/hashes/hkdf");
263
+ var import_sha23 = require("@noble/hashes/sha2");
264
+ var import_utils3 = require("@noble/hashes/utils");
265
+ var import_base = require("@scure/base");
266
+ var SCHEME = "x25519+aes256gcm";
267
+ var HKDF_SALT = new TextEncoder().encode("pms-dek-wrap");
268
+ var HKDF_INFO_KEK = new TextEncoder().encode("kek-v1");
269
+ var HKDF_INFO_KID = new TextEncoder().encode("kid-v1");
270
+ function fromBase64(str) {
271
+ return import_base.base64.decode(str);
272
+ }
273
+ function sha256Hex(data) {
274
+ return (0, import_utils3.bytesToHex)((0, import_sha23.sha256)(data));
275
+ }
276
+ function decryptPayload(encrypted, recipientPrivateKeyHex) {
277
+ if (encrypted.scheme !== SCHEME) {
278
+ throw new Error(`Sch\xE9ma non support\xE9: ${encrypted.scheme}`);
279
+ }
280
+ const recipientSk = (0, import_utils3.hexToBytes)(recipientPrivateKeyHex);
281
+ let dek = null;
282
+ for (const wrap of encrypted.recipients) {
283
+ const ephemeralPk = (0, import_utils3.hexToBytes)(wrap.ephem_pub);
284
+ const sharedSecret = import_ed255192.x25519.getSharedSecret(recipientSk, ephemeralPk);
285
+ const kek = (0, import_hkdf2.hkdf)(import_sha23.sha256, sharedSecret, HKDF_SALT, HKDF_INFO_KEK, 32);
286
+ const kidBytes = (0, import_hkdf2.hkdf)(import_sha23.sha256, sharedSecret, HKDF_SALT, HKDF_INFO_KID, 16);
287
+ const expectedKid = (0, import_utils3.bytesToHex)(kidBytes);
288
+ if (expectedKid !== wrap.kid) {
289
+ continue;
290
+ }
291
+ try {
292
+ const kwNonce = fromBase64(wrap.kw_nonce_b64);
293
+ const wrappedKey = fromBase64(wrap.wrapped_key_b64);
294
+ const kwCipher = (0, import_aes.gcm)(kek, kwNonce, new TextEncoder().encode(wrap.kid));
295
+ dek = kwCipher.decrypt(wrappedKey);
296
+ break;
297
+ } catch {
298
+ continue;
299
+ }
300
+ }
301
+ if (!dek) {
302
+ throw new Error("Aucun destinataire correspondant trouv\xE9 ou d\xE9ballage \xE9chou\xE9");
303
+ }
304
+ const nonce = fromBase64(encrypted.nonce_b64);
305
+ const ciphertext = fromBase64(encrypted.ciphertext_b64);
306
+ const aadBytes = new TextEncoder().encode(JSON.stringify(encrypted.aad));
307
+ const cipher = (0, import_aes.gcm)(dek, nonce, aadBytes);
308
+ const plaintext = cipher.decrypt(ciphertext);
309
+ const gotCommitment = sha256Hex(plaintext);
310
+ if (gotCommitment !== encrypted.commitment) {
311
+ throw new Error("Commitment mismatch - donn\xE9es corrompues");
312
+ }
313
+ return new TextDecoder().decode(plaintext);
314
+ }
315
+
316
+ // src/client.ts
317
+ var PmsClient = class {
318
+ config;
319
+ knownNodes = /* @__PURE__ */ new Set();
320
+ lastNodeRefresh = 0;
321
+ NODE_REFRESH_INTERVAL = 6e4;
322
+ // 1 min
323
+ configCache = null;
324
+ CONFIG_TTL = 3e5;
325
+ // 5 min
326
+ /**
327
+ * Crée un nouveau client PMS.
328
+ * @param config - Configuration du client
329
+ * @param config.nodeUrl - URL du nœud principal
330
+ * @param config.enableRacing - Activer le racing pattern
331
+ * @param config.seedNodes - Liste des nœuds de seed
332
+ */
333
+ constructor(config) {
334
+ this.config = {
335
+ ...DEFAULT_CONFIG,
336
+ seedNodes: config.seedNodes ?? [],
337
+ enableRacing: config.enableRacing ?? true,
338
+ ...config
339
+ };
340
+ this.addKnownNode(this.config.nodeUrl);
341
+ this.config.seedNodes.forEach((url) => this.addKnownNode(url));
342
+ }
343
+ /**
344
+ * @internal
345
+ * Ajoute un nœud à la liste des nœuds connus.
346
+ */
347
+ addKnownNode(url) {
348
+ const normalized = url.replace(/\/$/, "");
349
+ if (normalized.startsWith("http")) {
350
+ this.knownNodes.add(normalized);
351
+ }
352
+ }
353
+ // ═══════════════════════════════════════════════════════════════════════
354
+ // Méthodes de lecture (GET)
355
+ // ═══════════════════════════════════════════════════════════════════════
356
+ /**
357
+ * Récupère les tips actuels du DAG.
358
+ */
359
+ async getTips() {
360
+ const res = await this.fetch("/v1/dag/tips", {
361
+ method: "POST",
362
+ body: JSON.stringify({ limit: 10 })
363
+ });
364
+ return res;
365
+ }
366
+ /**
367
+ * Récupère les informations publiques du coordinateur (clés).
368
+ */
369
+ async getCoordinatorInfo() {
370
+ return this.fetch("/v1/coordinator/info");
371
+ }
372
+ /**
373
+ * Récupère un bloc par son ID.
374
+ */
375
+ async getBlock(blockId) {
376
+ const wb = await this.fetch(`/v1/blocks/${blockId}`);
377
+ return {
378
+ id: wb.id,
379
+ parents: wb.parents,
380
+ nonce: wb.nonce,
381
+ payload: wb.payload_json ? JSON.parse(wb.payload_json) : void 0
382
+ };
383
+ }
384
+ /**
385
+ * Récupère les informations de supply.
386
+ * @param assetId - Optionnel: ID du token (undefined = PMS natif)
387
+ */
388
+ async getSupply(assetId) {
389
+ const query = assetId ? `?asset_id=${encodeURIComponent(assetId)}` : "";
390
+ return this.fetch(`/v1/supply${query}`);
391
+ }
392
+ /**
393
+ * Liste tous les tokens enregistrés.
394
+ */
395
+ async listTokens() {
396
+ const res = await this.fetch("/v1/tokens");
397
+ return res.tokens ?? [];
398
+ }
399
+ /**
400
+ * Récupère les métadonnées d'un token par son asset_id.
401
+ */
402
+ async getToken(assetId) {
403
+ return this.fetch(`/v1/tokens/${encodeURIComponent(assetId)}`);
404
+ }
405
+ /**
406
+ * Récupère les UTXOs d'une adresse.
407
+ */
408
+ async getUtxos(address) {
409
+ const res = await this.fetch(`/v1/wallet/${address}/utxos`);
410
+ return res.utxos ?? [];
411
+ }
412
+ // ═══════════════════════════════════════════════════════════════════════
413
+ // Préparation de Transaction (Server-Side)
414
+ // ═══════════════════════════════════════════════════════════════════════
415
+ /**
416
+ * Prépare une transaction de transfert via le serveur.
417
+ * Le serveur sélectionne les UTXOs et calcule les frais.
418
+ * Le client doit ensuite signer le `tx_hash` retourné.
419
+ *
420
+ * @param params - Paramètres de la transaction
421
+ * @param params.from - Adresse Bech32 de l'expéditeur
422
+ * @param params.to - Adresse Bech32 du destinataire
423
+ * @param params.amount - Montant à envoyer (décimal, ex: "100.5")
424
+ * @returns Transaction non-signée avec hash à signer
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * // 1. Préparer la transaction
429
+ * const prepared = await client.prepareTx({
430
+ * from: wallet.address,
431
+ * to: "pms1recipient...",
432
+ * amount: "100.0"
433
+ * });
434
+ *
435
+ * // 2. Signer le hash
436
+ * const signature = wallet.sign(fromHex(prepared.tx_hash));
437
+ *
438
+ * // 3. Soumettre via /wallet/tx/send (à implémenter)
439
+ * ```
440
+ */
441
+ async prepareTx(params) {
442
+ return this.fetch("/v1/tx/prepare", {
443
+ method: "POST",
444
+ body: JSON.stringify(params)
445
+ });
446
+ }
447
+ /**
448
+ * Récupère la balance d'une adresse.
449
+ */
450
+ /**
451
+ * Récupère la balance d'une adresse.
452
+ */
453
+ async getBalance(address) {
454
+ const res = await this.fetch("/v1/balance", {
455
+ method: "POST",
456
+ body: JSON.stringify({ address })
457
+ });
458
+ return res.balance;
459
+ }
460
+ /**
461
+ * Récupère la balance complète avec les UTXOs.
462
+ */
463
+ async getBalanceInfo(address) {
464
+ const utxos = await this.getUtxos(address);
465
+ let total = 0n;
466
+ for (const utxo of utxos) {
467
+ total += parseAmount(utxo.amount);
468
+ }
469
+ return {
470
+ address,
471
+ balance: formatAmount(total),
472
+ utxos
473
+ };
474
+ }
475
+ /**
476
+ * Récupère la liste des NFTs appartenant à une adresse.
477
+ *
478
+ * @param address - Adresse publique (hex) du propriétaire
479
+ * @returns Liste des token_ids possédés par cette adresse
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * const myNfts = await client.getNfts(myWallet.address);
484
+ * console.log(`Vous possédez ${myNfts.length} NFT(s)`);
485
+ * for (const tokenId of myNfts) {
486
+ * console.log(`- ${tokenId}`);
487
+ * }
488
+ * ```
489
+ */
490
+ async getNfts(address) {
491
+ const res = await this.fetch(`/v1/wallet/${address}/nfts`);
492
+ return res.token_ids ?? [];
493
+ }
494
+ /**
495
+ * Récupère les informations complètes d'un NFT par son token_id.
496
+ *
497
+ * @param tokenId - Identifiant unique du NFT (64 caractères hex)
498
+ * @returns NftResponse avec owner, exists et metadata
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * const nft = await client.getNft("abc123def456...");
503
+ * if (nft.exists) {
504
+ * console.log(`Owner: ${nft.owner}`);
505
+ * console.log(`Name: ${nft.metadata?.name}`);
506
+ * }
507
+ * ```
508
+ */
509
+ async getNft(tokenId) {
510
+ return this.fetch(`/v1/nft/${tokenId}`);
511
+ }
512
+ /**
513
+ * Récupère l'historique des transactions d'un wallet.
514
+ * Supporte le déchiffrement des récompenses (EncryptedReward) côté client.
515
+ *
516
+ * @param address - Adresse du wallet (bech32)
517
+ * @param options - Options (limit, decryptionWallet)
518
+ * @returns Historique complet
519
+ */
520
+ async getHistory(address, options) {
521
+ const limit = options?.limit ?? 100;
522
+ const res = await this.fetch("/wallet/history", {
523
+ method: "POST",
524
+ body: JSON.stringify({ bech32_addr: address, limit })
525
+ });
526
+ if (options?.decryptionWallet) {
527
+ const wallet = options.decryptionWallet;
528
+ for (const item of res.items) {
529
+ if (item.payload_type === "EncryptedReward") {
530
+ const encPayload = item.payload;
531
+ const decryptedOutputs = [];
532
+ for (const eOut of encPayload.encrypted_outputs) {
533
+ try {
534
+ const plaintext = decryptPayload(
535
+ eOut.encrypted,
536
+ wallet.x25519PrivateKeyHex
537
+ );
538
+ const output = JSON.parse(plaintext);
539
+ if (output.address === address) {
540
+ decryptedOutputs.push(output);
541
+ }
542
+ } catch (e) {
543
+ }
544
+ }
545
+ if (decryptedOutputs.length > 0) {
546
+ item.payload_type = "Reward";
547
+ const plain = {
548
+ fee_outputs: [],
549
+ reward_outputs: decryptedOutputs,
550
+ burned: encPayload.burned,
551
+ tx_block_id: encPayload.tx_block_id
552
+ };
553
+ item.payload = plain;
554
+ }
555
+ }
556
+ }
557
+ }
558
+ return res;
559
+ }
560
+ // ═══════════════════════════════════════════════════════════════════════
561
+ // Méthodes d'écriture (POST)
562
+ // ═══════════════════════════════════════════════════════════════════════
563
+ /**
564
+ * Récupère la configuration publique du nœud (frais, PoW, recipient, etc.)
565
+ */
566
+ async getNetworkConfig() {
567
+ if (this.configCache && Date.now() < this.configCache.expires) {
568
+ return this.configCache.data;
569
+ }
570
+ const cfg = await this.fetch("/v1/config");
571
+ this.configCache = { data: cfg, expires: Date.now() + this.CONFIG_TTL };
572
+ return cfg;
573
+ }
574
+ /**
575
+ * Récupère la configuration runtime du nœud (frais, PoW, etc.)
576
+ * @deprecated Use getNetworkConfig instead
577
+ */
578
+ async getRuntimeConfig() {
579
+ return this.getNetworkConfig();
580
+ }
581
+ /**
582
+ * @internal
583
+ * Soumet un bloc au réseau.
584
+ * Utilise le racing pattern si activé pour envoyer à plusieurs noeuds.
585
+ *
586
+ * ⚠️ API interne - préférez utiliser `send()`, `mintNft()`, ou `burnNft()`.
587
+ */
588
+ async submitBlock(wireBlock) {
589
+ if (this.config.enableRacing) {
590
+ return this.submitBlockRacing(wireBlock);
591
+ }
592
+ return this.fetch("/submit/block", {
593
+ method: "POST",
594
+ body: JSON.stringify(wireBlock)
595
+ });
596
+ }
597
+ /**
598
+ * Discovery & Racing Pattern:
599
+ * 1. Refresh node list if stale
600
+ * 2. Send to all known nodes in parallel
601
+ * 3. Return first success
602
+ */
603
+ async submitBlockRacing(wireBlock) {
604
+ this.refreshNodeList().catch((err) => console.debug("Node refresh failed:", err));
605
+ const targets = Array.from(this.knownNodes);
606
+ if (targets.length === 0) {
607
+ targets.push(this.config.nodeUrl.replace(/\/$/, ""));
608
+ }
609
+ const body = JSON.stringify(wireBlock);
610
+ const controller = new AbortController();
611
+ const promises = targets.map(async (baseUrl) => {
612
+ try {
613
+ const res = await this.fetchUrl(baseUrl, "/submit/block", {
614
+ method: "POST",
615
+ body,
616
+ signal: controller.signal
617
+ });
618
+ if (!res.block_id) res.block_id = wireBlock.id;
619
+ if (!res.status) res.status = "inserted";
620
+ return res;
621
+ } catch (err) {
622
+ throw err;
623
+ }
624
+ });
625
+ try {
626
+ const result = await Promise.any(promises);
627
+ controller.abort();
628
+ return result;
629
+ } catch (error) {
630
+ const aggregateError = error;
631
+ const errors = aggregateError.errors || [];
632
+ const errorMessages = errors.map((e) => e.message || String(e)).join("; ");
633
+ throw new Error(`Submit failed on all ${targets.length} nodes. Errors: ${errorMessages}`);
634
+ }
635
+ }
636
+ /**
637
+ * Rafraîchit la liste des noeuds connus depuis le registre
638
+ */
639
+ async refreshNodeList() {
640
+ if (Date.now() - this.lastNodeRefresh < this.NODE_REFRESH_INTERVAL) {
641
+ return;
642
+ }
643
+ try {
644
+ const res = await this.fetch("/v1/nodes");
645
+ res.nodes.forEach((node) => {
646
+ if (node.api_url) this.addKnownNode(node.api_url);
647
+ });
648
+ this.lastNodeRefresh = Date.now();
649
+ } catch (e) {
650
+ }
651
+ }
652
+ /**
653
+ * Envoie des tokens à une adresse.
654
+ * Construit automatiquement la transaction, la signe et la soumet.
655
+ */
656
+ async send(params) {
657
+ const { to, amount, wallet, memo: _memo } = params;
658
+ const utxos = await this.getUtxos(wallet.address);
659
+ if (utxos.length === 0) {
660
+ throw new Error("No UTXOs available");
661
+ }
662
+ const netConfig = await this.getNetworkConfig();
663
+ const baseFeeSats = parseAmount(netConfig.base_fee || "0");
664
+ const feeRateBps = BigInt(netConfig.fee_rate_bps || 0);
665
+ const amountSats = parseAmount(amount);
666
+ const variableFee = amountSats * feeRateBps / 10000n;
667
+ const fee = baseFeeSats + variableFee;
668
+ const totalNeeded = amountSats + fee;
669
+ let selectedSats = 0n;
670
+ const selectedUtxos = [];
671
+ for (const utxo of utxos) {
672
+ selectedUtxos.push(utxo);
673
+ selectedSats += parseAmount(utxo.amount || "0");
674
+ if (selectedSats >= totalNeeded) break;
675
+ }
676
+ if (selectedSats < totalNeeded) {
677
+ throw new Error(
678
+ `Insufficient balance: have ${formatAmount(selectedSats)}, need ${formatAmount(totalNeeded)} (incl. fee ${formatAmount(fee)})`
679
+ );
680
+ }
681
+ const outputs = [
682
+ { address: to, amount: formatAmount(amountSats) }
683
+ ];
684
+ if (fee > 0n) {
685
+ outputs.push({
686
+ address: netConfig.fee_recipient,
687
+ amount: formatAmount(fee)
688
+ });
689
+ }
690
+ const change = selectedSats - amountSats - fee;
691
+ if (change > 0n) {
692
+ outputs.push({ address: wallet.address, amount: formatAmount(change) });
693
+ }
694
+ const inputs = selectedUtxos.map((utxo) => ({
695
+ out: {
696
+ txid: utxo.outpoint.txid,
697
+ index: utxo.outpoint.index
698
+ }
699
+ }));
700
+ const txCanonical = {
701
+ inputs,
702
+ outputs,
703
+ fee: formatAmount(fee)
704
+ };
705
+ const txMessage = JSON.stringify(txCanonical);
706
+ const txSignatureHex = wallet.sign(encodeUtf8(txMessage));
707
+ const txSigBytes = fromHex(txSignatureHex);
708
+ const txSigB64 = btoa(String.fromCharCode(...txSigBytes));
709
+ const unlocks = inputs.map(() => ({
710
+ pubkey_hex: wallet.publicKeyHex,
711
+ signature_b64: txSigB64
712
+ }));
713
+ const tx = {
714
+ inputs,
715
+ outputs,
716
+ fee: formatAmount(fee),
717
+ unlocks
718
+ };
719
+ const tips = await this.getTips();
720
+ const parents = tips.slice(0, 2);
721
+ const payload = { Plain: { TxUtxo: tx } };
722
+ const payloadJson = JSON.stringify(payload);
723
+ let nonce = 0;
724
+ let blockId = computeBlockId(parents, payloadJson, nonce);
725
+ const canonicalView = {
726
+ id: blockId,
727
+ parents,
728
+ payload_json: payloadJson,
729
+ nonce,
730
+ network_id: this.config.networkId,
731
+ protocol_version: this.config.protocolVersion,
732
+ signer_pk_hex: wallet.publicKeyHex
733
+ };
734
+ const messageToSign = JSON.stringify(canonicalView);
735
+ const signatureHex = wallet.sign(encodeUtf8(messageToSign));
736
+ const signatureBytes = fromHex(signatureHex);
737
+ const signatureB64 = btoa(String.fromCharCode(...signatureBytes));
738
+ const wireBlock = {
739
+ id: blockId,
740
+ parents,
741
+ payload_json: payloadJson,
742
+ nonce,
743
+ network_id: this.config.networkId,
744
+ protocol_version: this.config.protocolVersion,
745
+ signer_pk_hex: wallet.publicKeyHex,
746
+ signature_hex: signatureB64
747
+ };
748
+ return this.submitBlock(wireBlock);
749
+ }
750
+ // ═══════════════════════════════════════════════════════════════════════
751
+ // Méthodes NFT Cube (Burn et Mint spécialisé)
752
+ // ═══════════════════════════════════════════════════════════════════════
753
+ /**
754
+ * Transférer un NFT à un autre propriétaire.
755
+ * Prend en charge le re-chiffrement des métadonnées via le coordinateur.
756
+ *
757
+ * @param params - Paramètres du transfert
758
+ * @param params.tokenId - Identifiant du NFT
759
+ * @param params.to - Adresse du nouveau propriétaire
760
+ * @param params.wallet - Wallet du propriétaire actuel (signataire)
761
+ * @param params.newOwnerX25519 - Clé publique X25519 du nouveau owner (pour re-encryption)
762
+ */
763
+ async transferNft(params) {
764
+ const { tokenId, to, wallet, newOwnerX25519 } = params;
765
+ let action;
766
+ if (newOwnerX25519) {
767
+ const prepReq = {
768
+ token_id: tokenId,
769
+ to_address: to,
770
+ from_address: wallet.address,
771
+ new_owner_x25519_pubkey: newOwnerX25519
772
+ };
773
+ const prepRes = await this.fetch("/v1/nft/transfer/prepare", {
774
+ method: "POST",
775
+ body: JSON.stringify(prepReq)
776
+ });
777
+ action = prepRes.action;
778
+ } else {
779
+ action = {
780
+ Transfer: {
781
+ token_id: tokenId,
782
+ from: wallet.address,
783
+ to
784
+ }
785
+ };
786
+ }
787
+ const tips = await this.getTips();
788
+ const parents = tips.slice(0, 2);
789
+ const payload = { Plain: { Nft: action } };
790
+ const payloadJson = JSON.stringify(payload);
791
+ const nonce = 0;
792
+ const blockId = computeBlockId(parents, payloadJson, nonce);
793
+ const canonicalView = {
794
+ id: blockId,
795
+ parents,
796
+ payload_json: payloadJson,
797
+ nonce,
798
+ network_id: this.config.networkId,
799
+ protocol_version: this.config.protocolVersion,
800
+ signer_pk_hex: wallet.publicKeyHex
801
+ };
802
+ const messageToSign = JSON.stringify(canonicalView);
803
+ const signatureHex = wallet.sign(encodeUtf8(messageToSign));
804
+ const signatureBytes = fromHex(signatureHex);
805
+ const signatureB64 = btoa(String.fromCharCode(...signatureBytes));
806
+ const wireBlock = {
807
+ id: blockId,
808
+ parents,
809
+ payload_json: payloadJson,
810
+ nonce,
811
+ network_id: this.config.networkId,
812
+ protocol_version: this.config.protocolVersion,
813
+ signer_pk_hex: wallet.publicKeyHex,
814
+ signature_hex: signatureB64
815
+ };
816
+ return this.submitBlock(wireBlock);
817
+ }
818
+ /**
819
+ * Brûle (détruit) un NFT existant.
820
+ *
821
+ * Seul le propriétaire du NFT peut le brûler.
822
+ * Une fois brûlé, le NFT est supprimé définitivement.
823
+ *
824
+ * Pour les Cubes authentiques (avec signature Authority valide),
825
+ * un remboursement est calculé selon la formule:
826
+ * `(weight * size * density) / 10000` PMS
827
+ *
828
+ * @param params - Paramètres du burn
829
+ * @param params.tokenId - Identifiant du NFT à brûler
830
+ * @param params.wallet - Wallet PMS du propriétaire (doit être l'owner actuel)
831
+ * @returns BurnNftResponse avec refund preview si cube authentique
832
+ *
833
+ * @example
834
+ * ```typescript
835
+ * const result = await client.burnNft({
836
+ * tokenId: "abc123def456...",
837
+ * wallet: myWallet,
838
+ * });
839
+ *
840
+ * if (result.refund) {
841
+ * console.log(`Remboursement: ${result.refund.amount} PMS`);
842
+ * }
843
+ * ```
844
+ */
845
+ async burnNft(params) {
846
+ const { tokenId, wallet } = params;
847
+ const tips = await this.getTips();
848
+ const parents = tips.slice(0, 2);
849
+ const payload = {
850
+ Plain: {
851
+ Nft: {
852
+ Burn: {
853
+ token_id: tokenId,
854
+ burner: wallet.address
855
+ }
856
+ }
857
+ }
858
+ };
859
+ const payloadJson = JSON.stringify(payload);
860
+ const nonce = 0;
861
+ const blockId = computeBlockId(parents, payloadJson, nonce);
862
+ const canonicalView = {
863
+ id: blockId,
864
+ parents,
865
+ payload_json: payloadJson,
866
+ nonce,
867
+ network_id: this.config.networkId,
868
+ protocol_version: this.config.protocolVersion,
869
+ signer_pk_hex: wallet.publicKeyHex
870
+ };
871
+ const messageToSign = JSON.stringify(canonicalView);
872
+ const signatureHex = wallet.sign(encodeUtf8(messageToSign));
873
+ const signatureBytes = fromHex(signatureHex);
874
+ const signatureB64 = btoa(String.fromCharCode(...signatureBytes));
875
+ const burnRequest = {
876
+ id: blockId,
877
+ parents,
878
+ payload_json: payloadJson,
879
+ nonce,
880
+ network_id: this.config.networkId,
881
+ protocol_version: this.config.protocolVersion,
882
+ signer_pk_hex: wallet.publicKeyHex,
883
+ signature_hex: signatureB64
884
+ // base64 malgré le nom "hex"
885
+ };
886
+ return this.fetch("/v1/nft/burn", {
887
+ method: "POST",
888
+ body: JSON.stringify(burnRequest)
889
+ });
890
+ }
891
+ /**
892
+ * Brûle (détruit) plusieurs NFTs en une seule transaction.
893
+ *
894
+ * @param params - Paramètres du batch burn
895
+ * @param params.tokenIds - Liste des Identifiants des NFTs à brûler
896
+ * @param params.wallet - Wallet PMS du propriétaire
897
+ * @returns BurnNftResponse avec refund preview cumulé si applicable
898
+ */
899
+ async burnNfts(params) {
900
+ const { tokenIds, wallet } = params;
901
+ if (tokenIds.length === 0) {
902
+ throw new Error("No token IDs provided for batch burn");
903
+ }
904
+ const tips = await this.getTips();
905
+ const parents = tips.slice(0, 2);
906
+ const payload = {
907
+ Plain: {
908
+ Nft: {
909
+ BatchBurn: {
910
+ token_ids: tokenIds,
911
+ burner: wallet.address
912
+ }
913
+ }
914
+ }
915
+ };
916
+ const payloadJson = JSON.stringify(payload);
917
+ const nonce = 0;
918
+ const blockId = computeBlockId(parents, payloadJson, nonce);
919
+ const canonicalView = {
920
+ id: blockId,
921
+ parents,
922
+ payload_json: payloadJson,
923
+ nonce,
924
+ network_id: this.config.networkId,
925
+ protocol_version: this.config.protocolVersion,
926
+ signer_pk_hex: wallet.publicKeyHex
927
+ };
928
+ const messageToSign = JSON.stringify(canonicalView);
929
+ const signatureHex = wallet.sign(encodeUtf8(messageToSign));
930
+ const signatureBytes = fromHex(signatureHex);
931
+ const signatureB64 = btoa(String.fromCharCode(...signatureBytes));
932
+ const burnRequest = {
933
+ id: blockId,
934
+ parents,
935
+ payload_json: payloadJson,
936
+ nonce,
937
+ network_id: this.config.networkId,
938
+ protocol_version: this.config.protocolVersion,
939
+ signer_pk_hex: wallet.publicKeyHex,
940
+ signature_hex: signatureB64
941
+ };
942
+ return this.fetch("/v1/nft/burn", {
943
+ method: "POST",
944
+ body: JSON.stringify(burnRequest)
945
+ });
946
+ }
947
+ /**
948
+ * Mint un NFT via le Coordinateur (Server-Side Signing).
949
+ * Le client génère l'ID et les métadonnées, mais c'est le serveur qui signe et chiffre.
950
+ */
951
+ async mintNft(params) {
952
+ const { wallet, metadata } = params;
953
+ const tokenId = params.tokenId || this.generateRandomHex(32);
954
+ const response = await this.fetch("/v1/nft/mint", {
955
+ method: "POST",
956
+ body: JSON.stringify({
957
+ token_id: tokenId,
958
+ owner_address: wallet.address,
959
+ owner_x25519_pubkey: wallet.x25519PublicKeyHex,
960
+ metadata
961
+ })
962
+ });
963
+ return { ...response, token_id: tokenId };
964
+ }
965
+ /**
966
+ * Mint un Cube avec des attributs générés et signés par le backend Authority.
967
+ * @param params.wallet - Wallet PMS du propriétaire
968
+ * @param params.generatorUrl - URL du backend générateur de cubes (ex: "http://localhost:3000")
969
+ */
970
+ async mintCube(params) {
971
+ const { wallet, generatorUrl } = params;
972
+ const cubeResponse = await fetch(`${generatorUrl}/api/cube/generate`, {
973
+ method: "POST"
974
+ });
975
+ if (!cubeResponse.ok) {
976
+ const text = await cubeResponse.text();
977
+ throw new Error(`Cube generation failed: ${cubeResponse.status} - ${text}`);
978
+ }
979
+ const cubeData = await cubeResponse.json();
980
+ const tokenId = this.generateRandomHex(32);
981
+ const metadata = {
982
+ name: `Cube ${cubeData.rarity}`,
983
+ description: `A ${cubeData.rarity} cube with unique properties.`,
984
+ nft_type: "cube",
985
+ extra: JSON.stringify({
986
+ rarity: cubeData.rarity,
987
+ attributes: cubeData.attributes,
988
+ roll: cubeData.roll,
989
+ signature: cubeData.signature
990
+ // Authority signature
991
+ })
992
+ };
993
+ const submitResult = await this.mintNft({
994
+ wallet,
995
+ metadata,
996
+ tokenId
997
+ });
998
+ return {
999
+ ...submitResult,
1000
+ rarity: cubeData.rarity,
1001
+ roll: cubeData.roll,
1002
+ attributes: cubeData.attributes
1003
+ };
1004
+ }
1005
+ /**
1006
+ * Génère une chaîne hexadécimale aléatoire de la longueur spécifiée (en bytes).
1007
+ * Utilise crypto.getRandomValues pour la sécurité cryptographique.
1008
+ */
1009
+ generateRandomHex(bytes) {
1010
+ const array = new Uint8Array(bytes);
1011
+ crypto.getRandomValues(array);
1012
+ return Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
1013
+ }
1014
+ // ═══════════════════════════════════════════════════════════════════════
1015
+ // Helper HTTP
1016
+ // ═══════════════════════════════════════════════════════════════════════
1017
+ async fetch(path, init) {
1018
+ return this.fetchUrl(this.config.nodeUrl, path, init);
1019
+ }
1020
+ async fetchUrl(baseUrl, path, init) {
1021
+ const url = `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1022
+ const controller = new AbortController();
1023
+ const timeout = setTimeout(() => controller.abort(), this.config.timeout);
1024
+ const signal = init?.signal || controller.signal;
1025
+ try {
1026
+ const res = await fetch(url, {
1027
+ ...init,
1028
+ headers: {
1029
+ "Content-Type": "application/json",
1030
+ ...init?.headers
1031
+ },
1032
+ signal
1033
+ });
1034
+ if (!res.ok) {
1035
+ const text2 = await res.text();
1036
+ throw new Error(`HTTP ${res.status} (${url}): ${text2}`);
1037
+ }
1038
+ const text = await res.text();
1039
+ if (!text) {
1040
+ return {};
1041
+ }
1042
+ try {
1043
+ return JSON.parse(text);
1044
+ } catch (e) {
1045
+ throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
1046
+ }
1047
+ } finally {
1048
+ clearTimeout(timeout);
1049
+ }
1050
+ }
1051
+ };
1052
+ // Annotate the CommonJS export names for ESM import in node:
1053
+ 0 && (module.exports = {
1054
+ PmsClient,
1055
+ PmsWallet,
1056
+ decryptPayload,
1057
+ formatAmount,
1058
+ fromHex,
1059
+ isValidMnemonic,
1060
+ parseAmount,
1061
+ toHex
1062
+ });