@rougechain/sdk
v1.15.0
Published
Official SDK for RougeChain — post-quantum Layer 1 blockchain secured by ML-DSA-65
Maintainers
Readme
The official SDK for RougeChain — a post-quantum Layer 1 blockchain secured by ML-DSA-65 (CRYSTALS-Dilithium). All transaction signing happens client-side with NIST-approved post-quantum cryptography. Private keys never leave your application.
Works in the browser, Node.js 18+, and React Native.
Install
npm install @rougechain/sdk30-Second Quickstart
import { RougeChain, Wallet } from "@rougechain/sdk";
// `chainId` names the network you sign for; every signature commits to it, and the client
// refuses to sign if the node reports a different chain id.
const rc = new RougeChain("https://testnet.rougechain.io/api", { chainId: "rougechain-devnet-1" });
const wallet = Wallet.generate();
// Get testnet tokens
await rc.faucet(wallet);
// Send 100 XRGE
await rc.transfer(wallet, { to: recipientPubKey, amount: 100 });
// Check balance
const { balance } = await rc.getBalance(wallet.publicKey);Features
| Feature | Sub-client | Description |
|---------|-----------|-------------|
| Wallet | — | ML-DSA-65 keypair generation, import/export, client-side signing |
| Transfers | rc | Send XRGE or custom tokens, burn tokens |
| Token Creation | rc | Launch new tokens with on-chain logo support |
| Token Allowances | rc | ERC-20 style approve/transferFrom for DeFi composability |
| Staking | rc | Stake/unstake XRGE for validation |
| DEX | rc.dex | AMM pools, swaps with slippage protection, liquidity |
| NFTs | rc.nft | RC-721 collections, mint, batch mint, royalties, freeze |
| Shielded | rc.shielded | Private transfers with zk-STARK proofs, shield/unshield XRGE |
| Bridge | rc.bridge | ETH ↔ qETH, USDC ↔ qUSDC, XRGE bridge (Base mainnet; auto-claim deposits, withdrawal status + auto-refund) |
| Rollup | rc | zk-STARK batch proofs, rollup status, submit transfers |
| Social | rc.social | Posts, timeline feed, reposts, likes, follows, comments |
| Mail | rc.mail | On-chain encrypted email (@rouge.quant) |
| Messenger | rc.messenger | E2E encrypted messaging with self-destruct |
| Address Resolution | rc | O(1) rouge1↔pubkey resolution via on-chain index |
| Push Notifications | rc | PQC-signed push token registration (Expo) |
| Token Freeze | rc | Creator-only token freeze/pause |
| Mintable Tokens | rc | Ongoing token minting with supply cap enforcement |
| Dynamic Fees | rc | EIP-1559 base fee, priority tips, fee burning |
| Finality Proofs | rc | BFT finality certificates with ≥2/3 validator stake |
| WebSocket | rc | Real-time event streaming with topic subscriptions |
Wallet & Addresses
import { Wallet, pubkeyToAddress, isRougeAddress, formatAddress } from "@rougechain/sdk";
// Generate a new post-quantum keypair
const wallet = Wallet.generate();
// Get the compact rouge1... address (~63 chars vs 3904-char hex pubkey)
const address = await wallet.address();
// "rouge1q8f3x7k2m4n9p..."
// Restore from saved keys
const restored = Wallet.fromKeys(publicKey, privateKey);
// Export for storage
const keys = wallet.toJSON(); // { publicKey, privateKey }
// Verify keypair integrity
wallet.verify(); // true
// Address utilities
const addr = await pubkeyToAddress(someHexPubKey);
const display = formatAddress(addr); // "rouge1q8f3x7...k9m2"
isRougeAddress("rouge1q8f3x7k2m4..."); // trueTransfers & Tokens
// Send XRGE
await rc.transfer(wallet, { to: recipient, amount: 100 });
// Send custom token
await rc.transfer(wallet, { to: recipient, amount: 50, token: "MYTOKEN" });
// Create a new token (costs 100 XRGE)
await rc.createToken(wallet, {
name: "My Token",
symbol: "MTK",
totalSupply: 1_000_000,
image: "https://example.com/logo.png", // optional — URL or data:image/webp;base64,...
});
// Update token metadata (creator only)
await rc.updateTokenMetadata(wallet, {
symbol: "MTK",
image: "data:image/webp;base64,UklGR...", // base64 logos persist on-chain
description: "A community token",
website: "https://mytoken.io",
});
// Burn tokens
await rc.burn(wallet, 500);
// Mintable tokens (node TOKEN_MINTING upgrade — the node refuses these until it is active)
if (await rc.isTokenMintingActive()) {
// Create a mintable token with an optional supply cap (integer >= totalSupply)
await rc.createToken(wallet, {
name: "Inflation Token",
symbol: "INFT",
totalSupply: 100_000,
mintable: true,
maxSupply: 1_000_000, // omit for an uncapped mintable token
});
// Mint more (creator only, signed mint_tokens, 1 XRGE fee; initial + minted must stay <= maxSupply)
const res = await rc.mintTokens(wallet, { symbol: "INFT", amount: 50_000 });
if (!res.success) console.error(res.error); // e.g. "mint would exceed the max supply of INFT: ..."
// Supply info: getTokenMetadata → mintable, max_supply, total_minted, initial_supply
const meta = await rc.getTokenMetadata("INFT");
}Dynamic Fees (EIP-1559)
// Get current fee info — base fee adjusts per block
const fee = await rc.getFeeInfo();
console.log(`Base fee: ${fee.base_fee} XRGE`);
console.log(`Suggested total: ${fee.total_fee_suggestion} XRGE`);
console.log(`Total burned: ${fee.total_fees_burned} XRGE`);Finality Proofs
// Get a BFT finality proof for a block height
const { proof } = await rc.getFinalityProof(42);
if (proof) {
console.log(`Block ${proof.height} finalized with ${proof.voting_stake}/${proof.total_stake} stake`);
console.log(`${proof.precommit_votes.length} precommit signatures`);
}WebSocket Subscriptions
// Subscribe to specific topics for real-time events
const ws = rc.connectWebSocket(["blocks", `account:${wallet.publicKey}`]);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "new_block") {
console.log(`New block #${data.height}`);
}
};
// Available topics: "blocks", "transactions", "stats",
// "account:<pubkey>", "token:<symbol>"DEX (rc.dex)
// List all pools
const pools = await rc.dex.getPools();
// Get a single pool
const pool = await rc.dex.getPool("XRGE-MTK");
// Get price history (PriceSnapshot[]) — for building charts
const prices = await rc.dex.getPriceHistory("XRGE-MTK");
// Each snapshot: { pool_id, timestamp, block_height, reserve_a, reserve_b, price_a_in_b, price_b_in_a }
// Get pool stats (volume, trade count)
const stats = await rc.dex.getPoolStats("XRGE-MTK");
// Get pool events (swaps, adds, removes)
const events = await rc.dex.getPoolEvents("XRGE-MTK");
// Get a swap quote
const quote = await rc.dex.quote({
poolId: "XRGE-MTK",
tokenIn: "XRGE",
tokenOut: "MTK",
amountIn: 100,
});
console.log(`You'll receive ${quote.amount_out} MTK`);
// Execute swap with slippage protection
await rc.dex.swap(wallet, {
tokenIn: "XRGE",
tokenOut: "MTK",
amountIn: 100,
minAmountOut: quote.amount_out * 0.98, // 2% slippage
});
// Create a new liquidity pool
await rc.dex.createPool(wallet, {
tokenA: "XRGE",
tokenB: "MTK",
amountA: 10_000,
amountB: 5_000,
});
// Add / remove liquidity
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)
RC-721 standard with collections, royalties, freezing, and batch minting.
// Create a collection (5% royalty, max 10k supply)
await rc.nft.createCollection(wallet, {
symbol: "ART",
name: "My Art Collection",
royaltyBps: 500,
maxSupply: 10_000,
});
// Mint
await rc.nft.mint(wallet, {
collectionId: "abc123",
name: "Piece #1",
metadataUri: "https://example.com/nft/1.json",
attributes: { rarity: "legendary" },
});
// Batch mint (up to 50 at once); attributes: one entry per name (optional)
await rc.nft.batchMint(wallet, {
collectionId: "abc123",
names: ["#1", "#2", "#3"],
attributes: [{ rarity: "common" }, { rarity: "rare" }, { rarity: "legendary" }],
});
// Transfer with sale price (triggers royalty)
await rc.nft.transfer(wallet, {
collectionId: "abc123",
tokenId: 1,
to: buyerPubKey,
salePrice: 100,
});
// Query
const myNfts = await rc.nft.getByOwner(wallet.publicKey);Bridge (rc.bridge)
Bridge assets between Base mainnet and RougeChain L1. Supports ETH ↔ qETH, USDC ↔ qUSDC, and XRGE.
// Check bridge status & supported tokens
const config = await rc.bridge.getConfig();
// { enabled: true, supportedTokens: ["ETH", "USDC"], chainId: 8453 }
// Deposits are auto-claimed by the relayer's deposit watcher once your on-chain
// deposit confirms — no claim call needed. claim() remains as a manual fallback:
await rc.bridge.claim({
evmTxHash: "0x...",
evmAddress: "0x...",
evmSignature: "0x...",
recipientPubkey: wallet.publicKey,
token: "ETH", // or "USDC"
});
// Withdraw qETH → receive ETH on Base
await rc.bridge.withdraw(wallet, {
amount: 500_000,
evmAddress: "0xYourAddress",
tokenSymbol: "qETH",
});
// Track release status of your pending withdrawals
const pending = await rc.bridge.getWithdrawals();
// [{ txId, evmAddress, amountUnits, tokenSymbol: "qETH",
// status: "pending" | "failed" | "refunded", attempts, lastError, ownerPubkey }]
// A withdrawal the relayer cannot release is auto-refunded (status: "refunded").
// XRGE bridge
const xrgeConfig = await rc.bridge.getXrgeConfig();
await rc.bridge.withdrawXrge(wallet, {
amount: 1000,
evmAddress: "0xYourAddress",
});
const xrgePending = await rc.bridge.getXrgeWithdrawals();Rollup (zk-STARK Batch Proofs)
Submit transfers to the rollup accumulator for batched STARK proving. Transfers are collected into batches of up to 32 and proven with a single zk-STARK proof.
// Check rollup status
const status = await rc.getRollupStatus();
// { pending_transfers, completed_batches, current_state_root, ... }
// Submit a transfer to the rollup batch
const result = await rc.submitRollupTransfer({
sender: wallet.publicKey,
receiver: recipientPubKey,
amount: 100,
fee: 1,
});
// result.queued = true (waiting for batch) or result.batch_completed = true
// Get a completed batch result
const batch = await rc.getRollupBatch(1);
// { batch_id, transfer_count, proof_size_bytes, proof_time_ms, verified, ... }Social (rc.social)
On-chain social layer with posts, threaded replies, reposts, likes, follows, and comments. All write operations require a wallet parameter for ML-DSA-65 signed requests.
Posts & Timeline
// Create a post (max 4000 chars)
const { post } = await rc.social.createPost(wallet, "Hello RougeChain!");
// Reply to a post (threaded)
await rc.social.createPost(wallet, "Great point!", post.id);
// Delete your own post
await rc.social.deletePost(wallet, post.id);
// Get a single post with stats
const result = await rc.social.getPost(post.id, wallet.publicKey);
// result.post, result.stats { likes, reposts, replies, liked, reposted }
// Global timeline (all posts, newest first)
const timeline = await rc.social.getGlobalTimeline(50, 0);
// Following feed (posts from people you follow)
const feed = await rc.social.getFollowingFeed(wallet, 50, 0);
// User's posts
const { posts, total } = await rc.social.getUserPosts(userPubKey);
// Get replies to a post
const replies = await rc.social.getPostReplies(post.id);Likes, Reposts & Follows
// Like/unlike a post or track (toggle)
await rc.social.toggleLike(wallet, postOrTrackId);
// Repost/unrepost (toggle)
await rc.social.toggleRepost(wallet, post.id);
// Follow/unfollow (toggle)
await rc.social.toggleFollow(wallet, artistPubKey);
// Get stats
const stats = await rc.social.getPostStats(post.id, wallet.publicKey);
const artistStats = await rc.social.getArtistStats(pubkey, wallet.publicKey);Comments (Track-level)
// Comment on a track
const { comment } = await rc.social.postComment(wallet, trackId, "Fire track!");
// Get comments
const comments = await rc.social.getComments(trackId);
// Delete your comment
await rc.social.deleteComment(wallet, comment.id);Play Counts
// Record a play (debounce client-side, one per session per track)
await rc.social.recordPlay(wallet, trackId);
// Get track stats (plays, likes, comments)
const trackStats = await rc.social.getTrackStats(trackId, wallet.publicKey);Mail (rc.mail)
On-chain encrypted email with @rouge.quant / @qwalla.mail addresses. All write operations require a wallet parameter for ML-DSA-65 request signing — requests are authenticated via /api/v2/ endpoints with anti-replay nonce protection.
Name Registry
Register a mail name so other users can send you encrypted email. Third-party apps (QWALLA, qRougee, etc.) should call these on wallet creation.
// Step 1: Register wallet on the node (required for encryption keys)
await rc.messenger.registerWallet(wallet, {
id: wallet.publicKey,
displayName: "Alice",
signingPublicKey: wallet.publicKey,
encryptionPublicKey: encPubKey,
});
// Step 2: Register a mail name (signed request)
await rc.mail.registerName(wallet, "alice", wallet.publicKey);
// Resolve a name → wallet info (includes encryption key, public data only)
const resolved = await rc.mail.resolveName("alice");
// { entry: { name, wallet_id }, wallet: { id, signing_public_key, encryption_public_key } }
// Reverse lookup: wallet ID → name
const name = await rc.mail.reverseLookup(wallet.publicKey);
// "alice"
// Release a name (signed request)
await rc.mail.releaseName(wallet, "alice");Sending & Reading Mail
// Send an encrypted email (signed, multi-recipient CEK encryption)
await rc.mail.send(wallet, {
from: wallet.publicKey,
to: recipientPubKey,
subject: "Hello",
body: "This is a test",
encrypted_subject: encryptedSubject,
encrypted_body: encryptedBody,
});
// Read inbox (signed request)
const inbox = await rc.mail.getInbox(wallet);
// Move to trash (signed request)
await rc.mail.move(wallet, messageId, "trash");
// Mark as read (signed request)
await rc.mail.markRead(wallet, messageId);
// Delete (signed request)
await rc.mail.delete(wallet, messageId);Unread Counts
The SDK does not expose a dedicated unread-count endpoint. Derive unread totals client-side from inbox data:
// Mail: count unread inbox items
const inbox = await rc.mail.getInbox(wallet);
const unreadMail = inbox.filter((m: any) => {
const label = m.label ?? {};
return !(label.is_read ?? label.isRead ?? true);
}).length;
// Messenger: sum unread_count across conversations
const convos = await rc.messenger.getConversations(wallet);
const unreadChats = convos.reduce((sum: number, c: any) => {
return sum + (c.unread_count ?? c.unreadCount ?? 0);
}, 0);Messenger (rc.messenger)
End-to-end encrypted messaging with media and self-destruct support. All operations use ML-DSA-65 signed requests via /api/v2/ endpoints with nonce-based anti-replay protection.
// Register wallet for messaging (signed request)
await rc.messenger.registerWallet(wallet, {
id: wallet.publicKey,
displayName: "Alice",
signingPublicKey: wallet.publicKey,
encryptionPublicKey: encPubKey,
});
// Create a conversation (signed request)
const result = await rc.messenger.createConversation(wallet, [
wallet.publicKey,
recipientPubKey,
]);
// Fetch conversations (signed request)
const convos = await rc.messenger.getConversations(wallet);
// Send an encrypted message (signed, with optional self-destruct)
await rc.messenger.sendMessage(wallet, conversationId, encryptedContent, {
selfDestruct: true,
destructAfterSeconds: 30,
});
// Read messages (signed request)
const messages = await rc.messenger.getMessages(wallet, conversationId);
// Delete a message (signed request)
await rc.messenger.deleteMessage(wallet, messageId, conversationId);
// Delete a conversation (signed request)
await rc.messenger.deleteConversation(wallet, conversationId);Shielded Transactions (rc.shielded)
Private value transfers using zk-STARK proofs. Shield XRGE into private notes, transfer privately, and unshield back to public balance.
import { createShieldedNote, computeCommitment, computeNullifier } from "@rougechain/sdk";
// Shield 100 XRGE into a private note
const { note } = await rc.shielded.shield(wallet, { amount: 100 });
// ⚠️ Save `note` securely — losing it means losing the funds!
// note = { commitment, nullifier, value, randomness, ownerPubKey }
// Check pool stats
const stats = await rc.shielded.getStats();
// { commitment_count, nullifier_count, active_notes }
// Check if a nullifier has been spent
const { spent } = await rc.shielded.isNullifierSpent(note.nullifier);
// Private transfer (STARK proof generated by WASM prover in browser or Rust prover on node)
await rc.shielded.transfer(wallet, {
nullifiers: [note.nullifier],
outputCommitments: [recipientCommitment],
proof: starkProofHex,
});
// Unshield back to public balance
await rc.shielded.unshield(wallet, {
nullifiers: [note.nullifier],
amount: 100,
proof: starkProofHex,
});
// Client-side crypto primitives
const randomness = generateRandomness();
const commitment = computeCommitment(100, wallet.publicKey, randomness);
const nullifier = computeNullifier(randomness, commitment);Smart Contracts (rc.contracts)
WASM contracts with player-signed transactions: the signer is the caller the contract sees and pays the fee.
const pub = await rc.contracts.publish(wallet, wasmBytes); // 10 XRGE; pub.predictedAddress known up front
await rc.contracts.waitForReceipt(pub.txId!);
const q = await rc.contracts.query(addr, "get_score", { player: wallet.publicKey }); // free
const r = await rc.contracts.execute(wallet, addr, "move", { x: 1 }); // gas auto-sized from a query
const receipt = await rc.contracts.waitForReceipt(r.txId!);
if (receipt.status !== "Success") console.error("reverted:", receipt.status.Failed); // fee still charged
const mine = await rc.contracts.events(addr, { tx: r.txId }); // events emitted by that tx
const stop = rc.contracts.subscribe(addr, (e) => console.log(e.topic, e.data));
// Game helper
const game = rc.contracts.game(addr, wallet);
game.on("move", (e) => render(JSON.parse(e.data)));
await game.call("move", { x: 1, y: 2 });Fees: publish 10 XRGE flat; call gasLimit × 0.000001 XRGE (max gas 10,000,000). predictContractAddress(from, nonce, wasm) derives the address offline.
Payable calls (1.10.0, node from block 190)
A call can pay the contract. amount is an integer: quanta for XRGE (use xrgeToQuanta), raw units for tokens. The payment moves only if the call succeeds. A failing call keeps 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 quanta
const preview = await rc.contracts.query(addr, "roll", {}, wallet.publicKey, { attach }); // free
const r = await rc.contracts.execute(wallet, addr, "roll", {}, { attach }); // gas sized WITH the payment
await game.call("buy", { item: 3 }, { attach: { symbol: "GOLD", amount: 25 } }); // token units
quantaToXrge(500_000_000n); // "0.5"Amounts must be positive safe integers (≤ Number.MAX_SAFE_INTEGER). Anything else is rejected before signing. The node refuses the call if you can't cover the gas fee plus an XRGE payment, or the token amount.
Low-Level Signing
For advanced use cases:
import { signTransaction, verifyTransaction, generateNonce } from "@rougechain/sdk";
const payload = {
type: "transfer" as const,
from: wallet.publicKey,
to: recipient,
amount: 100,
fee: 1,
token: "XRGE",
timestamp: Date.now(),
nonce: generateNonce(),
};
const signedTx = signTransaction(payload, wallet.privateKey, wallet.publicKey);
const valid = verifyTransaction(signedTx); // trueMessage Signing & Sign-In (1.13.0)
Prove control of a wallet to a website (login, token gating) without sending a transaction.
import { signMessage, verifyMessage, createSignInMessage, verifySignIn, pubkeyToAddress } from "@rougechain/sdk";
// Arbitrary message
const signature = signMessage(wallet.privateKey, "hello");
verifyMessage(wallet.publicKey, "hello", signature); // true — false on anything else, never throws
// Sign-in message (canonical multi-line text, modelled on Sign-In with Ethereum)
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.",
nonce, // from your server, single use, >= 16 characters
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 10 * 60_000).toISOString(),
chainId: "rougechain-mainnet-1",
});
// Server side
const result = await verifySignIn({
message, signature, publicKey,
expectedDomain: "tickets.example.com",
expectedNonce: nonce,
expectedChainId: "rougechain-mainnet-1",
});
// { valid: true, address, publicKey, fields } | { valid: false, error }The signature is ML-DSA-65 over "\x19RougeChain Signed Message:\n" + decimal(byte length) + "\n" + message bytes. A transaction's signed bytes are always a JSON document starting with {, so a message signature can never be replayed as a transaction and a transaction signature never verifies as a message. In a browser, ask the wallet with window.rougechain.signMessage({ message }) (extension 1.8.0+; feature-detect). Full guide with a token-gating server: Wallet authentication.
Address Resolution
Resolve between compact rouge1… addresses and full hex public keys using the on-chain persistent index.
// rouge1… → public key
const { publicKey } = await rc.resolveAddress("rouge1q8f3x7k2m4...");
// public key → rouge1…
const { address } = await rc.resolveAddress(hexPubKey);
// Both return: { success, address, publicKey, balance }Push Notifications
Register Expo push tokens for real-time notifications. Requires PQC signature — only the wallet owner can register.
// Register (wallet signs the request with ML-DSA-65)
await rc.registerPushToken(wallet, expoPushToken);
// Unregister
await rc.unregisterPushToken(wallet);Nonce Management
// Get sequential nonce for an account
const { nonce, next_nonce } = await rc.getNonce(wallet.publicKey);Environment Support
| Environment | Notes |
|-------------|-------|
| Browser | Works with any bundler (Vite, webpack, etc.) |
| Node.js 18+ | Works out of the box |
| Node.js < 18 | Pass a fetch polyfill: new RougeChain(url, { fetch }) |
| React Native | Install react-native-get-random-values before importing |
TypeScript
Written in TypeScript with full type declarations shipped. All interfaces are exported:
import type {
Block, Transaction, TokenMetadata, NftCollection,
NftToken, LiquidityPool, BalanceResponse, Validator,
BridgeConfig, MailMessage, MessengerMessage, WalletKeys,
PriceSnapshot, PoolEvent, PoolStats, SwapQuote,
ShieldParams, ShieldedTransferParams, UnshieldParams, ShieldedStats,
ShieldedNote,
RollupStatus, RollupBatchResult, RollupSubmitParams, RollupSubmitResult,
FeeInfo, FinalityProof, MintTokenParams, VoteMessage, WsSubscribeMessage,
SocialPost, PostStats, TrackStats, ArtistStats, SocialComment,
} from "@rougechain/sdk";Security
- Post-quantum cryptography — All signatures use ML-DSA-65 (CRYSTALS-Dilithium), resistant to quantum computer attacks
- Client-side signing — Private keys never leave your application
- No key storage — The SDK does not store or transmit keys
- Signed API requests — All mail, messenger, and name registry operations use ML-DSA-65 signed requests with timestamp validation and nonce-based anti-replay protection
- Multi-recipient CEK encryption — Mail content is encrypted once with a random AES-256 key, KEM-wrapped individually per recipient via ML-KEM-768
- TOFU key verification — Public key fingerprints (SHA-256) are tracked for key change detection
Links
License
MIT © RougeChain
