@empereur-rouge/pms-sdk
v0.14.0
Published
TypeScript SDK for PMS (Planetary Monetary System) — wallet management, transactions, NFTs, and DAG interactions
Maintainers
Readme
@empereur-rouge/pms-sdk
SDK TypeScript officiel pour interagir avec le réseau PMS (Planetary Monetary System).
Installation
npm install @empereur-rouge/pms-sdk
# ou
yarn add @empereur-rouge/pms-sdk
# ou
pnpm add @empereur-rouge/pms-sdkStructure de l'API
Le SDK est organisé en deux niveaux :
| Import | Usage | Contenu |
|--------|-------|--------|
| @empereur-rouge/pms-sdk | 99% des cas | PmsWallet, PmsClient, utils simples |
| @empereur-rouge/pms-sdk/src/advanced | Power users | computeBlockId, crypto, types bas niveau |
Démarrage Rapide
import { PmsWallet, PmsClient } from "@empereur-rouge/pms-sdk";
// 1. Créer un wallet
const wallet = PmsWallet.generate();
console.log("Mnemonic:", wallet.mnemonic);
console.log("Address:", wallet.address);
// 2. Connecter au réseau (clé API obligatoire)
const client = new PmsClient({
nodeUrl: "https://node.pms.network",
apiKey: "pk_live_votre_cle_ici",
});
// 3. Consulter la balance
const balance = await client.getBalance(wallet.address);
console.log("Balance:", balance);
// 4. Envoyer des tokens
const result = await client.send({
to: "04abc123...",
amount: "10.0",
wallet,
});
console.log("Transaction:", result.block_id);client.help() — les règles du réseau
Un DAG PMS n'a pas les mêmes règles partout : les frais, le kill-switch de mint et
surtout le mode restreint vivent dans une config gouvernée qui change par
propose/enact. Le SDK ne peut pas les connaître à la compilation.
console.log(await client.help());╔══════════════════════════════════════════════════════════════════════════╗
║ ⚠ ATTENTION — LE DAG EST EN MODE RESTREINT (BOUCLE FERMÉE) ║
║ ║
║ Les transferts pair-à-pair sont REFUSÉS au consensus. ║
║ Un paiement ne peut viser qu'un ACCEPTEUR INSCRIT. ║
╚══════════════════════════════════════════════════════════════════════════╝
Règles en vigueur sur le DAG
Mode
État RESTREINT (boucle fermée)
Surface custodiale FERMÉE — 12 endpoints refusent (code 3072)
Voie à utiliser POST /v1/tx/prepare → signature locale → POST /wallet/tx/send
Transferts UNIQUEMENT vers un accepteur inscrit
Bridge FERMÉ, y compris à l'opérateur
Levée du mode 45 jours de préavis public (palier Constitution)
Accepteurs
Inscrits 1
Plafond gouverné 50
Changements annoncés (pas encore en vigueur)
SetRestrictedMode(enabled=false) [Constitution] → dans 44 j
9f3c1a7e
…La bannière rouge encadre le rapport — en tête ET en pied — pour rester visible dans un terminal qui a défilé. En mode ouvert, aucune alerte n'est affichée : une bannière montrée à tort userait le signal.
Pourquoi ce n'est pas qu'un avertissement
Un intégrateur qui appelle send-simple sur un réseau fermé reçoit un 403 code
3072 sans contexte. Le rapport donne la cause et la voie de remplacement —
/v1/tx/prepare puis /wallet/tx/send, ce que client.send() fait déjà.
La section « Changements annoncés » est souvent la plus utile : savoir qu'une règle bascule dans 44 jours vaut plus que connaître la règle actuelle.
Version structurée
const rules = await client.getRules(); // NetworkRules
if (rules.restricted && rules.acceptors.count === 0) {
throw new Error("réseau fermé sans accepteur : aucun paiement possible");
}formatRules(rules, { color }) est pure — utile pour rendre des règles
obtenues autrement, ou pour forcer/interdire les couleurs (auto par défaut :
TTY Node uniquement, jamais si NO_COLOR est défini).
Méthodes d'appoint : getAcceptors() (liste publique + état du mode) et
getVersions().
API Reference
PmsWallet - Gestion des Wallets
Le wallet gère les clés cryptographiques (secp256k1) et permet de signer des transactions.
Création de Wallet
// Générer un nouveau wallet (24 mots)
const wallet = PmsWallet.generate();
// Restaurer depuis un mnemonic
const restored = PmsWallet.fromMnemonic("word1 word2 ... word24");
// Importer depuis une clé privée (hex)
const imported = PmsWallet.fromPrivateKey("abc123...");
// Créer depuis une seed (32 bytes)
const seeded = PmsWallet.fromSeed(new Uint8Array(32));Propriétés
| Propriété | Type | Description |
|-----------|------|-------------|
| address | string | Adresse publique (clé publique hex non compressée, commence par 04) |
| publicKeyHex | string | Clé publique en hexadécimal (identique à address) |
| x25519PublicKeyHex | string | Clé publique X25519 pour le chiffrement |
| mnemonic | string \| undefined | Phrase mnémonique (24 mots) si disponible |
Méthodes
// Signer un message
const signature: string = wallet.sign(messageBytes);
// Exporter la clé privée (hex)
const privateKey: string = wallet.exportPrivateKey();
// Vérifier une signature (statique)
const isValid: boolean = PmsWallet.verify(message, signature, publicKeyHex);Validation de Mnemonic
import { isValidMnemonic } from "@empereur-rouge/pms-sdk";
if (isValidMnemonic(userInput)) {
const wallet = PmsWallet.fromMnemonic(userInput);
}PmsClient - Client API
Le client permet d'interagir avec l'API REST des nœuds PMS.
Configuration
const client = new PmsClient({
nodeUrl: "https://node.pms.network", // URL du nœud principal
apiKey: "pk_live_votre_cle_ici", // Clé API (obligatoire)
adminToken: "admin_token_ici", // Optionnel: requis pour les méthodes admin (gouvernance, setConfig)
seedNodes: [ // Optionnel: nœuds de secours
"https://node2.pms.network",
"https://node3.pms.network",
],
networkId: "pms-mainnet", // Défaut: "pms-mainnet"
protocolVersion: 1, // Défaut: 1
timeout: 30000, // Timeout en ms (défaut: 30s)
enableRacing: true, // Racing pattern (défaut: true)
});[!IMPORTANT] La clé API est obligatoire. Obtenez-la depuis votre dashboard PMS ou auprès de l'administrateur du réseau.
[!WARNING]
adminTokenn'est requis que pour les méthodes admin (gouvernancepropose/enact/cancel,setConfig). Il est envoyé enAuthorization: Bearer <adminToken>, uniquement vers le nœud principal (jamais vers les seeds lors du racing). Ne jamais l'exposer côté navigateur ni le committer. Les lectures publiques de gouvernance n'en ont pas besoin.
Wallet (Custodial — Server-Side)
Ces méthodes créent/restaurent des wallets côté serveur. L'adresse retournée est au format Bech32 (ex: 8e1a...).
// Créer un nouveau wallet (serveur génère les clés)
const wallet = await client.createWallet();
// Réponse (WalletResponse):
// {
// address: "8e1ahpltzjauwev6lf0jql...",
// private_key_b64: "2lM0rVdFXX...",
// private_key_hex: "da5334ad57455d74...",
// public_key_hex: "04593037fa9d4f6ea2...",
// x25519_pub_hex: "266bcd33ab2d20d2...",
// mnemonic_words: ["gain", "space", "color", ...]
// }// Restaurer un wallet depuis un mnemonic (24 mots BIP39)
const restored = await client.restoreFromMnemonic(
"gain space color filter buzz bind side before sauce twist slam history chief patch desk chunk way oblige output turtle purchase scare token rapid"
);
console.log(restored.address); // "8e1ahpltzjauwev6lf0jql..."// Restaurer un wallet depuis une clé privée hex
const imported = await client.restoreFromPrivateKey(
"da5334ad57455d74e5150e4eb06398ce9cf27762b6f29eac7f82f178bee07406"
);
console.log(imported.address); // même adresse que ci-dessus[!NOTE] Le mnemonic et la clé privée produisent exactement la même adresse — la dérivation est déterministe.
Méthodes de Lecture
// Lister les ledgers actifs (avec block_count et symbol par ledger)
const ledgers: LedgerInfo[] = await client.listLedgers();
// Réponse:
// [
// { id: "main", network_id: "mainnet", prefix: "", protocol_version: 1, block_count: 12345, symbol: "PMS" },
// { id: "gaming", network_id: "gaming-net", prefix: "gam_", protocol_version: 1, block_count: 678, symbol: "GAME" }
// ]
// Filtrer par préfixe de nom (starts_with, case-insensitive sur id, network_id ou symbol)
const gaming = await client.listLedgers({ search: "gam" });
// → [{ id: "gaming", ... }]// Récupérer les tips du DAG (blocs les plus récents)
const tips: string[] = await client.getTips();
// Réponse: ["4aa21b8f570005b4088c53149c9afedc528d7a47127e915744a1811c11d5956c", "..."]// Récupérer un bloc par son ID
const block: Block = await client.getBlock(blockId);
// Réponse:
// {
// id: "4aa21b8f570005b4088c53149c9afedc528d7a47127e915744a1811c11d5956c",
// parents: ["abc123...", "def456..."],
// payload: { Plain: { TxUtxo: { ... } } },
// nonce: 12345
// }// Récupérer le supply total
const supply: SupplyInfo = await client.getSupply();
// Réponse:
// {
// circulating: "1000000.00000000",
// utxo_count: 42567
// }// Récupérer les UTXOs d'une adresse
const utxos: Utxo[] = await client.getUtxos(address);
// Réponse:
// [
// {
// address: "04abc123...",
// amount: "50.00000000",
// outpoint: { txid: "tx123...", index: 0 }
// },
// {
// address: "04abc123...",
// amount: "25.50000000",
// outpoint: { txid: "tx456...", index: 1 }
// }
// ]// Récupérer la balance d'une adresse
const balance: string = await client.getBalance(address);
// Réponse: "75.50000000"// Récupérer balance + UTXOs détaillés
const info: BalanceInfo = await client.getBalanceInfo(address);
// Réponse:
// {
// address: "04abc123...",
// balance: "75.50000000",
// utxos: [{ address: "...", amount: "...", outpoint: {...} }, ...]
// }// Récupérer l'historique (Transactions, Mints, Rewards)
const history = await client.getHistory(address, { limit: 50 });
// Réponse:
// {
// address: "04abc123...",
// count: 50,
// items: [
// { block_id: "...", ts_ms: 1700000000000, payload_type: "Reward", payload: { ... } },
// { block_id: "...", ts_ms: 1700000050000, payload_type: "TxUtxo", payload: { ... } }
// ]
// }
// Récupérer l'historique avec déchiffrement automatique des rewards
// (Nécessite le wallet pour déchiffrer les EncryptedRewards)
const historyWithDecrypt = await client.getHistory(address, {
limit: 50,
decryptionWallet: myWallet
});
// Les items "EncryptedReward" déchiffrés apparaîtront comme des "Reward" standardsActivity API
L'Activity API classifie chaque transaction avec un type semantique (fee_received, transfer_in, nft_mint, freeze, etc.), une direction et un montant net. Elle remplace /wallet/history (deprecie).
// Toute l'activite d'un wallet
const activity = await client.getActivity("8eabc...");
// Reponse: { address, items: ActivityItem[], count, has_more, next_after_ts?, next_after_id? }
// Filtrer par type
const fees = await client.getActivity("8eabc...", {
type: ["fee_received", "reward"],
limit: 20,
});
// Pagination
const page2 = await client.getActivity("8eabc...", {
after_ts: activity.next_after_ts,
after_id: activity.next_after_id,
});
// Filtrer par asset
const edeniteActivity = await client.getActivity("8eabc...", {
asset_id: "edenite",
});
// Avec dechiffrement des payloads chiffres (EncryptedReward, Encrypted)
// IMPORTANT: ne passer la cle privee que sur TLS
const decrypted = await client.getActivity("8eabc...", {
x25519_sk_hex: "votre_cle_privee_x25519_hex",
});Streaming temps reel (SSE) :
const stop = client.streamActivity("8eabc...", {
type: ["transfer_in", "fee_received"],
x25519_sk_hex: "votre_cle_privee_x25519_hex", // Optionnel: dechiffrement en temps reel
onActivity: (item) => {
console.log(`${item.activity_type}: ${item.amount} (${item.direction})`);
},
onError: (err) => console.error("SSE error:", err),
onClose: () => console.log("Stream closed"),
});
// Plus tard: fermer la connexion
stop();Methodes d'Ecriture (Transactions)
// Envoyer des tokens
const result = await client.send({
to: "04destinataire...",
amount: "100.0",
wallet: myWallet,
memo: "Paiement optionnel", // Optionnel
});
// Réponse:
// {
// status: "inserted", // ou "already_exists"
// block_id: "7f3a2b1c..."
// }NFT - Non-Fungible Tokens
Le SDK supporte les opérations NFT natives du réseau PMS.
Mint NFT (Standard)
La création de NFT est sécurisée par le Coordinateur.
// Le server (Coordinateur) signe et chiffre le NFT pour vous.
const result = await client.mintNft({
tokenId: "unique-nft-001",
metadata: {
name: "Mon NFT",
description: "Description du NFT",
uri: "ipfs://QmXxx...",
nft_type: "art",
},
wallet: myWallet, // Votre wallet personnel
});
// Réponse:
// {
// status: "inserted",
// token_id: "unique-nft-001",
// block_id: "a1b2c3d4e5f6..."
// }[!NOTE] Les métadonnées sont automatiquement chiffrées par le serveur pour le propriétaire et le Coordinateur.
Mint Cube (NFT avec Rareté)
Les Cubes sont des NFTs spéciaux avec rareté et attributs générés par un backend Authority.
// Mint un cube pour un utilisateur
const result = await client.mintCube({
wallet: myWallet,
generatorUrl: "https://cube-generator.example.com", // Backend Authority
});
// Réponse (MintCubeResponse):
// {
// status: "inserted",
// block_id: "9f8e7d6c5b4a...",
// token_id: "a1b2c3d4e5f6...64chars...",
// rarity: "Legendary",
// roll: 75,
// attributes: {
// weight: 50.5,
// size: 30.2,
// density: 2.5
// }
// }Raretés possibles :
| Rareté | Probabilité | Cubes sur 10M | |--------|-------------|---------------| | Unique | 1/1,000,000 | 10 | | Legendary | 1/100,000 | 100 | | Rare | 1/10,000 | 1,000 | | Uncommon | 1/1,000 | 10,000 | | Common | 1/100 | 100,000 | | Basic | ~99% | ~9,888,890 |
Burn NFT
// Détruit un NFT (seul le propriétaire peut brûler)
const result = await client.burnNft({
tokenId: "nft-to-destroy",
wallet: ownerWallet,
});
// Réponse:
// {
// status: "inserted",
// block_id: "1a2b3c4d5e6f...",
// refund: { // Uniquement pour les Cubes authentiques
// amount: "1.23456789",
// recipient: "04abc123..."
// }
// }[!TIP] Les Cubes avec une signature Authority valide génèrent un remboursement automatique calculé selon leurs attributs.
Batch Burn (Destruction Multiple)
Pour détruire plusieurs NFTs en une seule transaction (économie de frais) :
const result = await client.burnNfts({
tokenIds: ["token-1", "token-2", "token-3"],
wallet: ownerWallet,
});
// Réponse:
// {
// status: "burned",
// token_id: "batch",
// token_ids: ["token-1", "token-2", "token-3"],
// refund: { ... } // Remboursement cumulé
// }Burn Token (actif fongible — token OU classe SFT)
burnToken détruit n'importe quel actif fongible, identifié par son assetId : un token custom ("edenite") OU une classe SFT ("{collection_id}:{class_id}"). Les deux circulent sur le même chemin UTXO — il n'y a donc pas de méthode de burn SFT dédiée. (Pour un NFT, identifié par token_id, utiliser burnNft.)
import { fromHex } from "@empereur-rouge/pms-sdk";
// La route attend la clé privée en base64. Le wallet l'exporte en hex :
const privateKeyB64 = Buffer.from(fromHex(wallet.exportPrivateKey())).toString("base64");
// Brûler un token fongible
const result = await client.burnToken({
privateKeyB64, // clé privée du propriétaire (base64)
assetId: "edenite", // null/omis = PMS natif
amount: "10.5",
});
// Réponse:
// {
// status: "ok",
// block_id: "1a2b3c4d...",
// burned: "10.5",
// asset_id: "edenite",
// owner: "8eabc...",
// converted_pms: "0", // > 0 si un contrat de conversion token→PMS (voie B) s'applique
// mint_block_id: null // ID du bloc de mint de conversion, sinon null
// }
// Brûler des unités d'une classe SFT — MÊME méthode, asset_id = "collection:class"
await client.burnToken({
privateKeyB64,
assetId: "edenite-game:iron-sword",
amount: "3",
});[!NOTE]
burnTokenest une route API-key (X-API-Key), commesend()— pas admin. Le serveur forge et signe le bloc de burn (single-writer).
Gouvernance & Émission
La gouvernance est ancrée dans le DAG : un changement de config peut être proposé (avec un palier et un timelock), puis enacté une fois le timelock écoulé, ou annulé. Les lectures sont publiques ; les écritures requièrent adminToken.
Lectures publiques (aucun adminToken requis)
// Propositions en attente (statut "pending")
const pending = await client.getGovernancePending();
// [{ proposal_id, tier, status: "pending", reason, announced_at_ms, enact_after_ms, update, proposal_block_id, enact_block_id, cancel_block_id }]
// Propositions terminées (enactées ou annulées)
const history = await client.getGovernanceHistory();
// Blocs de gouvernance ancrés dans le DAG
const { count, blocks } = await client.getGovernanceBlocks();
// blocks: [{ block_id, kind: "proposal" | "enact" | "cancel", proposal_id, tier, status, update, reason, announced_at_ms, enact_after_ms }]Actions admin (requièrent adminToken)
// Proposer un changement de config (ici, le couloir d'émission)
const proposal = await client.proposeGovernance({
update: { SetEmissionCorridor: { ceiling_bps: 1500, floor_bps: 0, target_bps: 1000, epoch_duration_sec: 86400 } },
tier: "constitution", // "operator" | "policy" | "constitution" (minuscules)
reason: "raise emission ceiling for Q3",
});
// { status: "ok", proposal_id, block_id, tier, announced_at_ms, enact_after_ms }
// Enacter une proposition dont le timelock est écoulé
await client.enactProposal(proposal.proposal_id, "timelock elapsed");
// { status: "ok", proposal_id, block_id }
// Annuler une proposition en attente
await client.cancelProposal(proposal.proposal_id);setConfig — 200 (appliqué) vs 202 (timelocké)
Un durcissement (ex: réduire un plafond, désactiver le mint) est appliqué instantanément ; un assouplissement est placé sous timelock de gouvernance. Le SDK expose les deux cas via une union discriminée sur applied — un 202 n'est jamais traité comme un succès appliqué :
const r = await client.setConfig({ SetMintEnabled: { enabled: false } });
if (r.applied) {
// HTTP 200 — en vigueur immédiatement
console.log("appliqué:", r.updateApplied, "bloc:", r.enactBlockId);
} else {
// HTTP 202 — proposé, PAS encore en vigueur
console.log("timelocké, enact après", new Date(r.enactAfterMs), "via", r.proposalId);
}Codes d'erreur stables
Le moteur renvoie ses erreurs au format { "code": NNNN, "message": "..." }. Branchez sur le code numérique (stable), pas sur le message :
import { ErrorCode, HttpError } from "@empereur-rouge/pms-sdk";
try {
await client.enactProposal(proposalId);
} catch (e) {
if (e instanceof HttpError && e.code === ErrorCode.GovernanceRejected) {
// 3071 — timelock non écoulé, proposition non en attente, ou id inconnu
}
}
// Codes notables : GovernanceRejected (3071), MintDisabled (5031), MissingAuth (1001), InvalidAuth (1002).
// Sponsorisation (protocole 2.9) : SponsorPoolEmpty (5011, HTTP 503), SponsorQuotaExceeded (5012, HTTP 429),
// SponsorNotEligible (5013, HTTP 422).[!IMPORTANT]
SponsorPoolEmpty(5011) arrive avec un HTTP 503 mais c'est un état, pas une panne transitoire : une réserve vide le restera tant que l'émetteur n'a pas réapprovisionné. Le SDK l'exclut du retry automatique (contrairement aux autres 503) — quand vous le recevez, il faut agir : approvisionner la réserve (sponsorDeposit) ou payer le gaz soi-même.
SFT - Jetons Semi-Fongibles
Un SFT (Semi-Fungible Token) combine une collection (collection_id) avec une classe fongible interne (class_id). Toutes les unités d'une même classe sont interchangeables (comme un token fongible), mais chaque classe reste distincte au sein de sa collection (comme un NFT). L'identifiant d'asset unique est "{collection_id}:{class_id}" — c'est ce asset_id qui sert de filtre partout (supply, balance, transfert). Les lectures sont publiques ; la création et le mint requièrent adminToken.
Lectures publiques (aucun adminToken requis)
// Toutes les classes SFT
const classes = await client.getSftClasses();
// [{ asset_id, collection_id, class_id, name, uri, attributes, decimals, max_supply, creator, mint_authority }]
// Une classe par son asset_id ("collection:class")
const sword = await client.getSftClass("edenite-game:iron-sword");
// Toutes les classes d'une collection
const { collection, count, classes: gameClasses } = await client.getSftCollection("edenite-game");Actions admin (requièrent adminToken)
// Créer une classe SFT — asset_id = "edenite-game:iron-sword"
const created = await client.createSftClass({
collection_id: "edenite-game",
class_id: "iron-sword",
name: "Iron Sword",
uri: "ipfs://Qm...",
decimals: 0,
max_supply: "10000",
});
// { status: "ok", asset_id: "edenite-game:iron-sword", collection_id, class_id, block_id }
// Mint des unités vers une adresse
const minted = await client.mintSft({
asset_id: created.asset_id,
to: "8eabc...",
amount: "100",
});
// { status: "ok", asset_id, amount: "100", to, block_id }[!NOTE] Transférer un SFT réutilise
send()— un SFT circule sur le même chemin UTXO qu'un token fongible. Passez simplement sonasset_id:await client.send({ to: "8edef...", amount: "5", wallet: myWallet, assetId: "edenite-game:iron-sword" });Il n'y a donc PAS de méthode de transfert SFT dédiée.
[!TIP] Un mint dépassant
max_supplyest rejeté avec le code stableInvalidField(2030, "would exceed max_supply ...") ; unasset_idde classe inconnu renvoie 404.
Une classe peut porter des royalties (royalty_bps / royalty_beneficiary) prélevées à chaque vente atomique. Elles sont posées à la création (createSftClass({..., royalty_bps, royalty_beneficiary})) et modifiables ensuite via les endpoints royalty (voir ci-dessous). La lecture d'une classe renvoie royalty_bps: number|null, royalty_beneficiary: string|null et royalty_version: number (compteur anti-rejeu).
Marketplace & Royalties
Vente / revente atomique (marketSettle)
Règle une vente atomiquement : le vendeur cède l'actif, l'acheteur paie, le moteur prélève les royalties dues au bénéficiaire de la classe et les frais, crédite le net au vendeur, puis forge et signe l'unique bloc (single-writer). Route API-key (X-API-Key), comme send().
const res = await client.marketSettle({
seller: aliceWallet,
buyer: bobWallet,
assetSold: "edenite-game:iron-sword",
quantity: "1",
price: "50.0",
// priceAsset: "edenite", // optionnel — omis = PMS natif
});
// { block_id, royalty, royalty_beneficiary, net_to_seller, fee }
console.log(res.net_to_seller, "au vendeur, royalty", res.royalty);Mise à jour des royalties d'une classe
Deux voies. Custodial (le coordinateur signe avec la clé de l'autorité) :
await client.royaltyUpdateCustodial({
assetId: "studio:ticket",
authorizer: creatorWallet, // mint_authority / créateur
royaltyBps: 250, // 2.5 %
royaltyBeneficiary: "8ebeneficiary...",
// clearRoyalty: true, clearBeneficiary: true, // suppression explicite
});
// { status: "ok", asset_id, royalty_bps, royalty_beneficiary, block_id }Non-custodial (recommandé — la clé privée ne quitte jamais le client) : le SDK prépare le message canonique, le revérifie localement (royaltyUpdateSigningMessage, refus si le message_hex du serveur diverge), signe, puis soumet la signature détachée :
await client.royaltyUpdateSigned({
assetId: "studio:ticket",
wallet: creatorWallet,
royaltyBps: 250,
royaltyBeneficiary: "8ebeneficiary...",
});[!NOTE]
royaltyUpdateSigningMessage(networkId, assetId, royaltyBps|null, royaltyBeneficiary|null, currentVersion)est exporté : il reproduit byte-pour-byte le message signé par le moteur (hex(SHA256(JSON compact, ordre de clés figé,nullexplicites))). LecurrentVersion(lu viaroyaltyPrepare) entre dans le message — signer une version obsolète est rejeté (anti-rejeu).
Provisionnement custodial d'assets (protocole 2.8)
Une instance creator-studio (custodiale : elle détient les clés de SES créateurs) crée et minte ses éditions capées + royalty sans le token admin du DAG partagé : l'autorisation vient de la clé du créateur. Routes API-key (X-API-Key), PAS admin. Le coordinateur forge/signe les blocs mais ne peut ni créer sous la collection d'un autre ni minter sans la clé du mint_authority ; le consensus re-vérifie signature + cap max_supply + anti-replay.
Créer une classe SFT ou un token (creator = mint_authority, dérivé de la clé du wallet) :
const cls = await client.createSftClassCustodial({
collectionId: "studio-x",
classId: "ticket",
name: "VIP Ticket",
creator: creatorWallet,
maxSupply: "1000",
royaltyBps: 500,
royaltyBeneficiary: creatorWallet.address,
});
// { status, asset_id: "studio-x:ticket", collection_id, class_id, creator, mint_authority, block_id }
const tok = await client.createTokenCustodial({
assetId: "vipcoin", // 1-32 chars [a-z0-9_], sans ":"
symbol: "VIP", name: "VIP Coin", decimals: 8,
creator: creatorWallet,
maxSupply: "1000000",
});
// { status, asset_id, creator, mint_authority, block_id }Minter — deux modes. Custodial (la clé transite, le serveur signe) :
await client.mintSftCustodial({
assetId: "studio-x:ticket",
to: "8erecipient...",
amount: "100",
mintAuthority: creatorWallet,
// lockedUntil: 1893456000000, // time-lock optionnel (UNIX ms)
});
// { status, asset_id, to, amount, mint_nonce, block_id }
// mintTokenCustodial(...) : idem pour un token fongible.Non-custodial / pré-signé (recommandé — la clé ne quitte jamais le client) : le SDK prépare le message, le revérifie localement (custodialMintSigningMessage, refus si le message_hex du serveur diverge), signe, puis soumet la signature détachée :
await client.mintSftSigned({
assetId: "studio-x:ticket",
to: "8erecipient...",
amount: "100",
wallet: creatorWallet, // DOIT être le mint_authority
});
// mintTokenSigned(...) : idem pour un token.
// prepareSftMint / prepareTokenMint exposent l'étape de prépa seule (message_hex + mint_nonce).[!NOTE]
custodialMintSigningMessage(networkId, assetId, outputs, mintNonce)est exporté : il reproduit byte-pour-byte le message signé par le moteur (hex(SHA256(JSON compact {domain:"pms-custodial-mint-v1", network_id, asset_id, outputs, mint_nonce}))). Chaque output est sérialisé comme unTxOutput(optionnels absents omis) etcreated_atest toujours omis (assigné par le système). Lemint_nonce(consommé one-shot au consensus) et lenetwork_idempêchent le rejeu.
Sponsorisation du gaz (protocole 2.9)
Un asset custom (token ou classe SFT) peut déclarer qui paie son gaz : l'utilisateur, ou une réserve approvisionnée par son émetteur. La réserve vit à une adresse spool1… dérivée déterministiquement de l'asset_id, dont aucune clé privée n'existe — elle n'est dépensable que par les payloads dont le consensus re-dérive lui-même le montant légal. Cible « UX sans crypto visible » : l'utilisateur d'un token d'entreprise ne touche jamais de PMS.
L'autorité de toutes les écritures est la signature du creator (jamais le token admin, jamais le mint_authority : c'est l'argent de celui qui approvisionne). Routes API-key (X-API-Key) ; les lectures sont publiques.
Lectures publiques (aucun adminToken requis) :
const pol = await client.getSponsorPolicy("edenite");
// { asset_id, creator, gas_sponsor_mode: "Disabled"|"Preferred"|"Required",
// gas_rate, sponsor_limits, sponsor_version }
const pool = await client.getSponsorPool("edenite");
// { asset_id, reserve_address: "spool1…", deposited, franchise, consumed, settled,
// available, withdrawable, settleable, window_spent, window_start_ms, version }
const al = await client.getSponsorAllowlist("edenite");
// { asset_id, allowlist_only, count, identities }withdrawable exclut la franchise d'amorçage (sinon créer un token puis retirer serait un faucet) et se calcule sur consumed, jamais sur settled — sinon le créateur retirerait, entre deux règlements, du PMS que ses utilisateurs ont déjà dépensé.
Approvisionner la réserve (n'importe qui peut abonder la réserve d'autrui — c'est un cadeau non reprenable ; seul le creator retire) :
await client.sponsorDeposit({ assetId: "edenite", amount: "500", depositor: creatorWallet });
// { status, asset_id, reserve_address, deposited, fee, block_id }
// ⚠️ Relire getSponsorPool() pour l'état à jour : la persistance est en tâche de fond.Politique, retrait, allowlist — trois flux prepare → sign → apply. Les wrappers *Signed recalculent le message localement et refusent de signer si le message_hex du serveur diverge (sans ça, un coordinateur compromis fait signer n'importe quoi au détenteur de la clé) :
// Politique : qui paie, à quel prix, avec quels garde-fous.
await client.sponsorPolicyUpdateSigned({
assetId: "edenite",
wallet: creatorWallet, // DOIT être le creator
gasSponsorMode: "Required",
gasRate: "0.00040000",
sponsorLimits: { daily_cap_pms: "50", per_address_daily_tx: 20, allowlist_only: true },
// clearGasRate / clearLimits : effacement EXPLICITE (les deltas omis sont CONSERVÉS)
});
// Retrait : borné par `withdrawable`, jamais par le solde on-chain de la réserve.
await client.sponsorWithdrawSigned({ assetId: "edenite", amount: "12.5", wallet: creatorWallet });
// `to` optionnel — défaut : le creator lui-même.
// Allowlist : ne mord que si sponsor_limits.allowlist_only === true.
await client.sponsorAllowlistUpdateSigned({
assetId: "edenite",
wallet: creatorWallet,
add: ["8e1alice", "8e1bob"],
remove: ["8e1carol"],
});
// sponsorPolicyPrepare / sponsorWithdrawPrepare / sponsorAllowlistPrepare exposent
// l'étape de prépa seule (message_hex + version courante).Les garde-fous ne sont pas un confort : sans eux, un attaquant détenant une seule unité du token spamme des transferts de poussière et brûle les PMS de l'émetteur (le problème classique des paymasters ERC-4337). Un delta omis est conservé — effacer exige clearGasRate / clearLimits, pour qu'un oubli d'appelant n'ouvre jamais la réserve au spam.
[!NOTE]
sponsorPolicySigningMessage,sponsorAllowlistSigningMessageetsponsorWithdrawSigningMessagesont exportés : ils reproduisent byte-pour-byte les messages signés par le moteur (hex(SHA256(JSON compact, ordre de clés figé)), domainespms-sponsor-policy-v1/-allowlist-v1/-withdraw-v1). Deux subtilités critiques :
- Message de politique : au niveau racine,
gas_sponsor_mode,gas_rateetsponsor_limitssortent ennullexplicite quand absents ; mais à l'intérieur desponsor_limits, les champs absents sont omis etallowlist_onlyest toujours présent. Les deux règles coexistent dans le même message.- Message d'allowlist :
addetremovesont liés verbatim, dans l'ordre soumis — jamais triés. Permuter deux adresses change le hash, ce qui empêche un intermédiaire de réordonner ou d'allongeraddsur une autorisation par ailleurs valide.La version courante (
sponsor_versionpour la politique et l'allowlist, version du pool pour le retrait) entre dans le message : signer une version obsolète est rejeté (anti-rejeu).
API Avancée
Pour les cas d'usage avancés (construction manuelle de blocs, chiffrement custom), importez depuis le module advanced :
import {
computeBlockId,
checkPowBits,
encryptPayload,
decryptPayload,
generateX25519Keypair,
deriveX25519PublicKey,
} from "@empereur-rouge/pms-sdk/src/advanced";
import type {
WireBlock,
PayloadEnvelope,
TxUtxo,
} from "@empereur-rouge/pms-sdk/src/advanced";[!WARNING] L'API avancée peut changer sans préavis. Préférez l'API publique pour la stabilité.
Utilitaires (API Publique)
import {
parseAmount,
formatAmount,
toHex,
fromHex,
} from "@empereur-rouge/pms-sdk";
// Convertir les montants
const sats = parseAmount("10.5"); // -> 1050000000n
const formatted = formatAmount(sats); // -> "10.50000000"
// Conversions hex
const hex = toHex(new Uint8Array([1, 2, 3]));
const bytes = fromHex("010203");Types TypeScript
Tous les types sont exportés et documentés :
import type {
// Configuration
PmsClientConfig,
// Réponses API
SubmitResponse,
BalanceInfo,
SupplyInfo,
WalletResponse,
// NFT
NftMetadata,
MintCubeResponse,
BurnNftResponse,
CubeAttributes,
// Blocs & Transactions (lecture)
Block,
Utxo,
// Historique
WalletHistoryResp,
HistoryItem,
// Activity
ActivityItem,
ActivityResp,
ActivityOptions,
StreamActivityOptions,
ActivityType,
ActivityDirection,
// Ledger
LedgerInfo,
// SFT — Jetons Semi-Fongibles
SftClass,
CreateSftClassRequest,
CreateSftClassResult,
MintSftRequest,
MintSftResult,
// Marketplace & Royalties
MarketSettleResult,
RoyaltyDeltas,
RoyaltyUpdateParams,
RoyaltyPrepareResult,
RoyaltyUpdateResult,
// Provisionnement custodial d'assets (protocole 2.8)
CreateSftClassCustodialResult,
CreateTokenCustodialResult,
CustodialMintResult,
CustodialMintPrepareResult,
// Sponsorisation du gaz (protocole 2.9)
GasSponsorMode,
SponsorLimits,
SponsorPolicy,
SponsorPool,
SponsorAllowlist,
SponsorAuthorization,
SponsorPolicyDeltas,
SponsorPolicyPrepareResult,
SponsorPolicyUpdateResult,
SponsorDepositResult,
SponsorWithdrawPrepareResult,
SponsorWithdrawResult,
SponsorAllowlistPrepareResult,
SponsorAllowlistUpdateResult,
} from "@empereur-rouge/pms-sdk";
// Types bas niveau (API avancée)
import type {
WireBlock,
PayloadEnvelope,
TxUtxo,
OutputRef,
TxOutput,
} from "@empereur-rouge/pms-sdk/src/advanced";Sécurité
Coordinateur et Minting
En production, seul le Coordinateur peut créer de nouveaux NFTs (y compris les Cubes). Cette restriction est appliquée au niveau du backend via la clé coordinator_pk.
Les NFTs authentiques sont identifiés par le creator qui correspond à la clé publique du Coordinateur.
Chiffrement
Le SDK utilise :
- secp256k1 pour les signatures (ECDSA, DER-encoded)
- X25519 pour l'échange de clés
- AES-256-GCM pour le chiffrement symétrique
Bonnes Pratiques
// ✅ Stocker le mnemonic de façon sécurisée
const wallet = PmsWallet.generate();
// Sauvegarder wallet.mnemonic dans un stockage sécurisé
// ✅ Ne jamais exposer la clé privée
const privateKey = wallet.exportPrivateKey();
// Ne pas logger ou transmettre cette valeur
// ✅ Vérifier les signatures avant d'accepter des données
const isValid = PmsWallet.verify(message, signature, senderPubKey);Racing Pattern
Le SDK utilise un "racing pattern" pour la soumission des transactions :
- Le client maintient une liste de nœuds connus
- Lors de
submitBlock(), la transaction est envoyée à tous les nœuds en parallèle - La première réponse positive est retournée
- Les autres requêtes sont annulées
Cela améliore :
- La latence (premier nœud qui répond)
- La fiabilité (tolérance aux pannes)
- La propagation (le bloc atteint plusieurs nœuds rapidement)
// Désactiver si non souhaité
const client = new PmsClient({
nodeUrl: "...",
apiKey: "pk_live_...",
enableRacing: false,
});Tests
# Lancer les tests
npm test
# Mode watch
npm run test:watch
# Couverture
npm run test:coverageLicence
MIT © PMS Team
