SDK (@rougechain/sdk)

The official JavaScript/TypeScript SDK for building on RougeChain.

Installation

npm install @rougechain/sdk

Quick Start

import { RougeChain, Wallet } from '@rougechain/sdk';

const rc = new RougeChain('https://testnet.rougechain.io/api');

// Generate a new post-quantum wallet (24-word BIP-39 mnemonic + keypair)
const wallet = Wallet.generate();
console.log('Public key:', wallet.publicKey);
console.log('Recovery phrase:', wallet.mnemonic); // save this to restore the wallet

// Request testnet tokens
await rc.faucet(wallet);

// Check balance
const balance = await rc.getBalance(wallet.publicKey);
console.log('Balance:', balance);

// Transfer tokens
await rc.transfer(wallet, { to: recipientPubKey, amount: 100 });

Wallet

All signing happens client-side. Private keys never leave your application.

import { Wallet } from '@rougechain/sdk';

// Generate a new wallet WITH a 24-word BIP-39 recovery phrase (default, recommended)
const wallet = Wallet.generate();
console.log(wallet.mnemonic); // "word1 word2 … word24"

// 12-word phrase instead of 24
const wallet12 = Wallet.generate(128);

// Generate from pure entropy — no mnemonic, cannot be seed-restored
const random = Wallet.generateRandom();

// Restore from a BIP-39 recovery phrase (optionally with a 25th-word passphrase)
const fromSeed = Wallet.fromMnemonic('word1 word2 … word24');

// Restore from saved hex keys
const restored = Wallet.fromKeys(publicKey, privateKey);

// Derive the short Bech32m address (rouge1…) from the public key
const address = await wallet.address();

// Export for storage — includes `mnemonic` when the wallet has one
const keys = wallet.toJSON(); // { publicKey, privateKey, mnemonic? }

// Verify keypair integrity
const valid = wallet.verify(); // true

The mnemonic model is what the MCP server uses via ROUGECHAIN_MNEMONIC. Store the phrase securely — anyone with it controls the wallet.

Client

import { RougeChain } from '@rougechain/sdk';

// Basic connection
const rc = new RougeChain('https://testnet.rougechain.io/api');

// With API key
const rc = new RougeChain('https://testnet.rougechain.io/api', {
  apiKey: 'your-api-key',
});

Available Methods

Queries

await rc.getHealth();
await rc.getStats();
await rc.getBlocks({ limit: 10 });
await rc.getBalance(publicKey);
await rc.getValidators();
await rc.getTokens();
await rc.getBurnedTokens();

Transactions

await rc.transfer(wallet, { to: recipient, amount: 100 });
await rc.transfer(wallet, { to: recipient, amount: 50, token: 'MYTOKEN' });
await rc.createToken(wallet, { name: 'My Token', symbol: 'MTK', totalSupply: 1_000_000, image: 'https://example.com/logo.png' });
await rc.burn(wallet, 500, 1, 'XRGE');
await rc.faucet(wallet);

Mintable tokens (SDK 1.12.0, node TOKEN_MINTING upgrade)

Mintable tokens need the node's TOKEN_MINTING upgrade, active on mainnet since block 235 and on testnet since block 1360. On a network where it is not active the node refuses mintable / maxSupply and every mint with "token minting is not active yet". To check:

import { tokenMintingActive } from '@rougechain/sdk';

await rc.isTokenMintingActive();             // GET /api/stats → upgrade_schedule.token_minting
tokenMintingActive(await rc.getStats());     // the same check on stats you already have

// Creator can mint more later; maxSupply (optional) caps initial + minted supply
await rc.createToken(wallet, { name: 'My Token', symbol: 'MTK', totalSupply: 1_000_000, mintable: true, maxSupply: 5_000_000 });

// Signed mint_tokens → POST /api/v2/token/mint (creator only, 1 XRGE fee)
const r = await rc.mintTokens(wallet, { symbol: 'MTK', amount: 250_000 });
if (!r.success) console.error(r.error); // not creator / not mintable / exceeds max supply / not active

// Supply info
const meta = await rc.getTokenMetadata('MTK'); // mintable, max_supply, total_minted, initial_supply
  • mintable: true and max_supply are signed only when mintable is true, so a fixed-supply token's payload is the same as before. A maxSupply without mintable, a maxSupply below totalSupply, a non-integer value or anything above 2^53 − 1 returns { success: false, error } without sending anything. tokenMintFields(initialSupply, { mintable, maxSupply }) runs the same validation and returns the fields to sign.
  • Low-level builders: createSignedTokenCreation(wallet, name, symbol, supply, fee?, image?, { mintable, maxSupply, description }) and createSignedTokenMint(wallet, symbol, amount, fee = 1, accountNonce?).
  • Constants: TOKEN_MINT_MAX_AMOUNT (2^53 − 1), TOKEN_MINT_FEE_XRGE (1), TOKEN_CREATE_FEE_XRGE (100).
  • Before 1.12.0, mintTokens sent an unsigned request that the node always rejected, and createToken dropped mintable / maxSupply.

Staking

await rc.stake(wallet, { amount: 1000 });
await rc.unstake(wallet, { amount: 500 });

DEX (rc.dex)

// Pool queries
await rc.dex.getPools();
await rc.dex.getPool('XRGE-MTK');
await rc.dex.getPriceHistory('XRGE-MTK');  // PriceSnapshot[] — for building charts
await rc.dex.getPoolStats('XRGE-MTK');     // volume, swap counts (total + 24h)
await rc.dex.getPoolEvents('XRGE-MTK');    // swap/add/remove event history

// Quote & swap
await rc.dex.quote({ poolId: 'XRGE-MTK', tokenIn: 'XRGE', tokenOut: 'MTK', amountIn: 100 });
await rc.dex.swap(wallet, { tokenIn: 'XRGE', tokenOut: 'MTK', amountIn: 100, minAmountOut: 95 });

// Liquidity
await rc.dex.createPool(wallet, { tokenA: 'XRGE', tokenB: 'MTK', amountA: 10000, amountB: 5000 });
await rc.dex.addLiquidity(wallet, { poolId: 'XRGE-MTK', amountA: 1000, amountB: 500 });
await rc.dex.removeLiquidity(wallet, { poolId: 'XRGE-MTK', lpAmount: 100 });

NFTs (rc.nft)

await rc.nft.createCollection(wallet, { symbol: 'ART', name: 'My Art', royaltyBps: 500, maxSupply: 10000 });
// Route royalties to another wallet or a contract (defaults to the creator). Only use a contract with a tested XRGE payout path — see Contract XRGE custody.
await rc.nft.createCollection(wallet, { symbol: 'ART2', name: 'Split Art', royaltyBps: 1000, royaltyRecipient: payoutWalletPubKey });
await rc.nft.mint(wallet, { collectionId: 'abc123', name: 'Piece #1', metadataUri: '...' });
await rc.nft.batchMint(wallet, { collectionId: 'abc123', names: ['#1', '#2'], uris: ['...', '...'] });
await rc.nft.transfer(wallet, { collectionId: 'abc123', tokenId: 1, to: buyerPubKey, salePrice: 100 });
await rc.nft.getCollections();
await rc.nft.getByOwner(wallet.publicKey);

Bridge (rc.bridge)

await rc.bridge.getConfig();
await rc.bridge.withdraw(wallet, { amount: 500000, evmAddress: '0x...' });
await rc.bridge.claim({ evmTxHash: '0x...', evmAddress: '0x...', evmSignature: '0x...', recipientPubkey: wallet.publicKey });
await rc.bridge.getWithdrawals();

Mail Name Registry (rc.mail)

Register human-readable names and resolve recipients before sending encrypted mail. Third-party apps must call these for cross-app mail to work.

All write operations require a wallet parameter for ML-DSA-65 signed requests via /api/v2/ endpoints with anti-replay nonce protection.

// Register wallet on the node first (signed request)
await rc.messenger.registerWallet(wallet, {
  id: wallet.publicKey,
  displayName: "Alice",
  signingPublicKey: wallet.publicKey,
  encryptionPublicKey: encPubKey,
});

// Register a mail name (signed request)
await rc.mail.registerName(wallet, "alice", wallet.publicKey);

// Resolve a name → wallet info (public, no signing needed)
const resolved = await rc.mail.resolveName("bob");
// { entry: { name, wallet_id }, wallet: { id, encryption_public_key, ... } }

// Reverse lookup: wallet ID → name (public, no signing needed)
const name = await rc.mail.reverseLookup(wallet.publicKey); // "alice"

// Release a name (signed request)
await rc.mail.releaseName(wallet, "alice");

// Send mail (signed request, multi-recipient CEK encryption)
await rc.mail.send(wallet, { from, to, subject, body, encrypted_subject, encrypted_body });

// Read inbox / sent (signed requests)
const inbox = await rc.mail.getInbox(wallet);
const sent = await rc.mail.getSent(wallet);

// Manage mail (signed requests)
await rc.mail.markRead(wallet, messageId);
await rc.mail.move(wallet, messageId, "trash");
await rc.mail.delete(wallet, messageId);

Messenger (rc.messenger)

All operations use ML-DSA-65 signed requests with nonce-based anti-replay protection.

await rc.messenger.getWallets();
await rc.messenger.registerWallet(wallet, { id, displayName, signingPublicKey, encryptionPublicKey });
await rc.messenger.getConversations(wallet);
await rc.messenger.createConversation(wallet, [pubKeyA, pubKeyB]);
await rc.messenger.getMessages(wallet, conversationId);
await rc.messenger.sendMessage(wallet, conversationId, encryptedContent, { selfDestruct: true, destructAfterSeconds: 30 });
await rc.messenger.deleteMessage(wallet, messageId, conversationId);
await rc.messenger.deleteConversation(wallet, conversationId);

Social (rc.social)

Posts, replies, reposts, follows, likes, and track comments. These are node-hosted — stored in the node's own database, not in blocks. Writes are ML-DSA-65 signed; reads are public.

// Reads
await rc.social.getGlobalTimeline(50, 0);        // newest-first feed
await rc.social.getPost(postId, viewerPubKey);   // single post + stats
await rc.social.getUserPosts(pubKey);
await rc.social.getPostReplies(postId);
await rc.social.getTrackStats(trackId, viewerPubKey);
await rc.social.getArtistStats(artistPubKey, viewerPubKey);

// Writes (signed)
await rc.social.createPost(wallet, 'gm from RougeChain');
await rc.social.createPost(wallet, 'nice!', parentPostId); // reply
await rc.social.deletePost(wallet, postId);
await rc.social.toggleRepost(wallet, postId);
await rc.social.toggleFollow(wallet, artistPubKey);
await rc.social.toggleLike(wallet, trackId);
await rc.social.postComment(wallet, trackId, 'great track');

Push Notifications

// Register for push notifications (PQC-signed) — pass the wallet, not raw keys
await rc.registerPushToken(wallet, 'ExponentPushToken[xxx]');           // platform defaults to "expo"
await rc.registerPushToken(wallet, 'ExponentPushToken[xxx]', 'expo');

// Unregister
await rc.unregisterPushToken(wallet);

Address Resolution

// Resolve rouge1… address to public key, or vice versa
const result = await rc.resolveAddress('rouge1q8f3x...');
// → { address, publicKey, balance }

Account Nonce

const nonce = await rc.getNonce(publicKey);
// → { nonce, next_nonce }

Token Allowances (ERC-20 style approve / transferFrom)

There are no rc.approveAllowance / rc.transferFrom convenience methods. Build the signed transaction with the exported signer helpers, then submit it with rc.submitTx(endpoint, signedTx):

import {
  createSignedTokenApproval,
  createSignedTokenTransferFrom,
} from '@rougechain/sdk';

// Approve a spender to move up to `amount` of your MTK
await rc.submitTx(
  '/v2/token/approve',
  createSignedTokenApproval(wallet, spenderPubKey, 'MTK', 1000),
);

// Spend an existing allowance on the owner's behalf
await rc.submitTx(
  '/v2/token/transfer-from',
  createSignedTokenTransferFrom(wallet, ownerPubKey, recipientPubKey, 'MTK', 1000),
);

// Read current allowances (public)
await rc.get('/token/allowances');
await rc.get('/token/allowance');

Freeze/unfreeze a token you created is a creator-only signed request to /api/v2/token/freeze (payload { tokenSymbol, frozen }). There is no dedicated SDK helper — sign it with the generic signRequest(wallet, payload) and post it.

Message Signing and Sign-In (SDK 1.13.0)

Prove control of a wallet to a website without a transaction. Full guide, including the exact signed bytes and a token-gating server: Wallet Authentication.

import {
  signMessage, verifyMessage,
  createSignInMessage, parseSignInMessage, verifySignIn,
  pubkeyToAddress,
} from "@rougechain/sdk";

// Any message (string or Uint8Array)
const signature = signMessage(wallet.privateKey, "hello");
verifyMessage(wallet.publicKey, "hello", signature);   // true; false on anything else, never throws

// Sign-in message
const message = createSignInMessage({
  domain: "tickets.example.com",
  address: await pubkeyToAddress(wallet.publicKey),
  uri: "https://tickets.example.com/login",
  statement: "Sign in to see your tickets.",          // optional
  nonce,                                               // server-issued, single use, >= 16 characters
  issuedAt: new Date().toISOString(),
  expirationTime: new Date(Date.now() + 10 * 60_000).toISOString(),   // optional
  chainId: "rougechain-mainnet-1",
  resources: ["https://tickets.example.com/events/42"],               // optional
});

const result = await verifySignIn({
  message, signature: signMessage(wallet.privateKey, message), publicKey: wallet.publicKey,
  expectedDomain: "tickets.example.com",
  expectedNonce: nonce,
  expectedChainId: "rougechain-mainnet-1",
});
// { valid: true, address, publicKey, fields }  or  { valid: false, error }
FunctionDescription
signMessage(privateKeyHex, message)ML-DSA-65 signature (hex) over "\x19RougeChain Signed Message:\n" + decimal(byte length) + "\n" + message bytes.
verifyMessage(publicKeyHex, message, signatureHex)boolean. Never throws.
messageSigningBytes(message)The exact signed bytes.
createSignInMessage(fields) / parseSignInMessage(text)Build / strictly parse the canonical sign-in text.
verifySignIn(params)Signature, address-of-key, domain, nonce, chain id, issued-at and expiry checks. Resolves to a result object, never rejects.

In a browser the private key stays in the wallet: call window.rougechain.signMessage({ message }) (extension 1.8.0+, Qwalla after its next update) and verify the result on your server. A message signature is never a transaction signature and signTransaction / verifyTransaction / signRequest are unchanged.

Verifying Finality (SDK 1.14.0)

Check that a transaction is in a finalized block without trusting a node's "success" answer. Full guide, including what it cannot prove: Verifying Finality.

import { verifyTxFinalized } from '@rougechain/sdk';

const r = await verifyTxFinalized({ txHash, nodes: [nodeA, nodeB, nodeC], chainId: 'rougechain-mainnet-1', trustedValidatorSet });
r.status;   // "finalized" | "included-not-finalized" | "not-found" | "invalid"
r.checks;   // every check, pass/fail
r.outcome;  // { source: "node receipt", verified: false, value } — success is NOT proven

Multi-Sig Wallets

Multi-sig queries are available today:

const wallets   = await rc.get('/multisig/wallets');
const myWallets = await rc.get(`/multisig/wallets/${myPubKey}`);
const wallet_   = await rc.get(`/multisig/wallet/${walletId}`);
const proposals = await rc.get(`/multisig/wallet/${walletId}/proposals`);

Note: creating a multi-sig wallet and submitting/approving proposals (multisig_create / multisig_submit / multisig_approve) are on-chain transaction types, but they are not yet exposed through a stable SDK method or public submit endpoint — the node currently serves the multi-sig read routes above only. Do not call rc.sendTransaction(...); that method does not exist. Multi-sig write support is tracked for a future release.

Smart Contracts (rc.contracts)

Since 1.9.0. Contract transactions are signed by your wallet: the signer is the caller the contract sees and pays the fee. See WASM Smart Contracts for the host functions and the raw API.

// Publish WASM (Uint8Array, ArrayBuffer or base64). Fee: 10 XRGE flat.
const pub = await rc.contracts.publish(wallet, wasmBytes);
// → { success, txId, address, predictedAddress, nonce, fee }
await rc.contracts.waitForReceipt(pub.txId!);        // resolves once the tx is in a block

// The address is fixed by what you sign; compute it offline:
rc.contracts.predictContractAddress(wallet.publicKey, pub.nonce, wasmBytes);

// Read-only call (free, no signature)
const q = await rc.contracts.query(addr, 'get_score', { player: wallet.publicKey });
// → { success, returnData, gasUsed, events, error? }

// Signed call. Fee = gasLimit × 0.000001 XRGE.
// Without gasLimit the SDK queries first and signs min(ceil(gasUsed × 1.5) + 1000, 10M).
const r = await rc.contracts.execute(wallet, addr, 'move', { x: 1, y: 2 });
const r2 = await rc.contracts.execute(wallet, addr, 'move', { x: 3 }, { gasLimit: 80_000 });
// → { success, txId, fee, gasLimit, preview: { returnData, gasUsed, events } }
// The node dry-runs first; a call that would fail returns { success: false, error } and costs nothing.
// The block run is authoritative: a call that reverts there is still included and charged,
// and its receipt says so.
const receipt = await rc.contracts.waitForReceipt(r.txId!);
// receipt.status === "Success", or { Failed: "<error>" } when the call reverted in the block

// Metadata, storage, events, list
const meta   = await rc.contracts.get(addr);                    // null if none
const state  = await rc.contracts.state(addr);                  // whole storage
const one    = await rc.contracts.state(addr, 'score');         // hex key, else UTF-8
const events = await rc.contracts.events(addr, { limit: 50, before: 12_000 });
const byTx   = await rc.contracts.events(addr, { tx: r.txId });  // events of one tx
const all    = await rc.contracts.list();

// Live events after each accepted block (shared socket, auto-reconnect + resubscribe)
const stop = rc.contracts.subscribe(addr, (e) => console.log(e.topic, e.data, e.block_height));
stop();

Game helper — a handle bound to one contract (and optionally a wallet):

const game = rc.contracts.game(addr, wallet);
const off = game.on('move', (e) => render(JSON.parse(e.data)));  // '*' = every topic
await game.call('move', { x: 1, y: 2 });          // signed; needs the wallet
const board = await game.query('board');          // free; caller = wallet key
const s = await game.state();
off();

Payable calls (SDK 1.10.0; the node accepts them from the payable-calls upgrade, block 190). A call can pay the contract in XRGE or a token. The amount is an integer: quanta for XRGE (1 XRGE = 1,000,000,000 quanta), raw units for tokens. The payment moves only if the call succeeds. A failing or trapping call leaves it with you, but the gas fee is still charged.

import { xrgeToQuanta, quantaToXrge } from '@rougechain/sdk';

const attach = { symbol: 'XRGE', amount: xrgeToQuanta('0.5') };   // 500_000_000n (exact, no floats)

// Free preview of the paid call. The contract sees the payment. Needs a caller.
const p = await rc.contracts.query(addr, 'roll', {}, wallet.publicKey, { attach });

// Signed, paid call. Without gasLimit, gas is sized from a preview that includes the payment.
const r = await rc.contracts.execute(wallet, addr, 'roll', {}, { attach });
// → { success, txId, fee, gasLimit, attach: { symbol: 'XRGE', amount: 500000000 } }

await game.call('buy', { item: 3 }, { attach: { symbol: 'GOLD', amount: 25 } });  // token units
quantaToXrge(1_500_000_000n);  // "1.5"

amount may be a bigint, a safe-integer number or a digit string. The SDK throws, or returns { success: false, error }, for a non-integer, zero, negative or > Number.MAX_SAFE_INTEGER amount, because the node reads it as an exact JSON integer. The node refuses the call if you can't cover fee + XRGE payment, or the token amount.

Lower-level builders are exported too: createSignedContractCall, createSignedContractPublish, predictContractAddress, suggestGasLimit, contractCallFee, normalizeContractAttach, xrgeToQuanta, quantaToXrge. On Node versions without a global WebSocket, pass one: new RougeChain(url, { WebSocket: (await import('ws')).default }).

The old rc.shielded.deployContract / callContract helpers used node-signed endpoints that are retired (deploy returns 410; call is a dry run only). They are deprecated; use rc.contracts.

EIP-1559 Fee Info

const feeInfo = await rc.getFeeInfo();
// → { base_fee, total_fee_suggestion, total_fees_burned, block_height }

Environment Support

EnvironmentRequirements
BrowserAny bundler (Vite, webpack)
Node.js 18+Works out of the box
Node.js < 18Provide node-fetch polyfill
React NativeInstall react-native-get-random-values

TypeScript

Full type declarations are included:

import type {
  Block, Transaction, Validator, LiquidityPool, NftCollection,
  NameEntry, ResolvedName, MailMessage, SendMailParams,
  PriceSnapshot, PoolStats, SwapQuote,
} from '@rougechain/sdk';

Security

  • All signatures use ML-DSA-65 (FIPS 204), resistant to quantum attacks
  • Private keys never leave your application
  • The SDK does not store keys — persistence is your responsibility
  • All mail, messenger, and name registry operations use ML-DSA-65 signed requests with timestamp validation and nonce-based anti-replay protection
  • Mail uses a CEK pattern for efficient multi-recipient encryption via ML-KEM-768
  • TOFU key fingerprint tracking for contact key-change detection

Source

The SDK source code is in the sdk/ directory of the quantum-vault repository.