Architecture Overview
The Mythical SDK uses a provider pattern for secure communication between your game and the Mythical Beings embedded wallet.
- @mythicalb/ardor-provider - Used by your game (dApp)
- @mythicalb/ardor-host - Used by the wallet host
- @mythicalb/ardor-core - Shared utilities and types
Communication Flow
┌─────────────────┐ postMessage ┌─────────────────┐
│ │ ──────────────────► │ │
│ Your Game │ │ Wallet Host │
│ (iframe) │ ◄────────────────── │ (parent) │
│ │ postMessage │ │
└─────────────────┘ └─────────────────┘
│ │
│ MythicalProvider │ MythicalHost
│ - connect() │ - onRequest()
│ - sendIgnis() │ - wallet signing
│ - etc. │ - tx broadcast
└────────────────────────────────────────┘
Security Model
- Origin validation - Communication only with trusted origins
- Permission-based access - Actions require explicit user approval
- User confirmation - Transactions require user confirmation in the wallet UI
- No private key exposure - Keys never leave the wallet
Decimals & Precision
Understanding decimal handling is critical for accurate transactions on Ardor.
IGNIS uses 8 decimal places. Always use the conversion functions to avoid precision errors.
NQT (Nano Quantity)
All amounts on Ardor are stored as integers in the smallest unit (NQT):
1 IGNIS = 100,000,000 NQT0.001 IGNIS = 100,000 NQT
Conversion Functions
import {
parseIgnisToNQT,
formatNQTToIgnis
} from '@mythicalb/ardor-core';
// Human-readable → NQT (for sending)
const nqt = parseIgnisToNQT('1.5'); // "150000000"
const nqt2 = parseIgnisToNQT('0.001'); // "100000"
// NQT → Human-readable (for display)
const ignis = formatNQTToIgnis('150000000'); // "1.5"
const ignis2 = formatNQTToIgnis('100000'); // "0.001"
// Trailing zeros are trimmed
formatNQTToIgnis('100000000'); // "1" (not "1.00000000")
Asset Decimals
Different assets can have different decimal precisions (0-8):
import {
parseAssetQuantity,
formatAssetQuantity
} from '@mythicalb/ardor-core';
// For an asset with 2 decimals:
parseAssetQuantity('1.5', 2); // "150"
formatAssetQuantity('150', 2); // "1.5"
// For an asset with 0 decimals (whole units only):
parseAssetQuantity('5', 0); // "5"
formatAssetQuantity('5', 0); // "5"
Best Practices
- Always use string amounts to avoid JavaScript number precision issues
- Convert to NQT before sending transactions
- Convert from NQT when displaying to users
- Store amounts as NQT strings in your database
Permissions System
The SDK uses a permission system to control what actions your game can perform.
Available Permissions
| Permission | Description | Risk Level |
|---|---|---|
READ_ACCOUNT |
Read account address and public key | 🟢 Low |
READ_BALANCES |
Read IGNIS and asset balances | 🟢 Low |
TX_SEND_IGNIS |
Send IGNIS transactions | 🟡 Medium |
TX_TRANSFER_ASSET |
Transfer assets/NFTs | 🟡 Medium |
TX_SEND_MESSAGE |
Send on-chain messages | 🟡 Medium |
TX_PLACE_ORDER |
Place marketplace orders | 🟡 Medium |
TX_CANCEL_ORDER |
Cancel marketplace orders | 🟢 Low |
SIGN_TOKEN |
Sign authentication tokens | 🟢 Low |
Requesting Permissions
// Request only what you need
const session = await provider.connect({
appName: 'My Game',
permissions: [
'READ_ACCOUNT',
'READ_BALANCES',
'TX_SEND_IGNIS'
]
});
// Check granted permissions
if (session.permissions.includes('TX_SEND_IGNIS')) {
// Can send IGNIS
}
Permission Errors
try {
await provider.placeAskOrder({ ... });
} catch (error) {
if (error.code === 'PERMISSION_DENIED') {
// Request additional permissions
await provider.connect({
appName: 'My Game',
permissions: [...currentPermissions, 'TX_PLACE_ORDER']
});
}
}
Messaging System
Send on-chain messages to other Ardor accounts, optionally encrypted.
Plain Messages
// Public message (visible to everyone)
await provider.sendMessage({
recipientRS: 'ARDOR-XXXX-XXXX-XXXX-XXXXX',
message: 'Hello from the game!'
});
// Message with fee bumping for priority
await provider.sendMessage({
recipientRS: 'ARDOR-XXXX-XXXX-XXXX-XXXXX',
message: 'Urgent message',
feeNQT: '200000000' // 2 IGNIS fee
});
Encrypted Messages
// Private message (only sender and recipient can read)
await provider.sendEncryptedMessage({
recipientRS: 'ARDOR-XXXX-XXXX-XXXX-XXXXX',
message: 'This is a secret message'
});
// Encrypted with custom expiration
await provider.sendEncryptedMessage({
recipientRS: 'ARDOR-XXXX-XXXX-XXXX-XXXXX',
message: 'Secret trade offer',
deadline: 60 // Expires in 60 minutes if not confirmed
});
Use Cases
- Trade negotiations - Private messages between players
- Game events - Broadcast announcements
- NFT metadata - Attach data to transfers
- Proof of action - Immutable game records
Marketplace Trading
The Ardor Asset Exchange allows trading of game assets with no centralized order book.
Order Book Concepts
- Ask Order - Sell order: "I'm selling X assets at Y price"
- Bid Order - Buy order: "I'm buying X assets at Y price"
- Orders are matched automatically when prices overlap
- Partial fills are supported
Price Calculation
// Price is in NQT per QNT (smallest unit)
// For an asset with 2 decimals:
// - 1 unit = 100 QNT
// - Price of 1 IGNIS per unit = 1,000,000 NQT per QNT
const assetDecimals = 2;
const pricePerUnit = '1'; // 1 IGNIS per unit
// Convert to NQT per QNT
const nqtPerUnit = parseIgnisToNQT(pricePerUnit); // "100000000"
const qntPerUnit = Math.pow(10, assetDecimals); // 100
const priceNQTPerQNT = BigInt(nqtPerUnit) / BigInt(qntPerUnit); // 1000000
Place Sell Order
await provider.placeAskOrder({
assetId: '123456789',
quantityQNT: '100', // Sell 1 unit (2 decimals)
priceNQTPerShare: '1000000' // At 1 IGNIS per unit
});
Place Buy Order
await provider.placeBidOrder({
assetId: '123456789',
quantityQNT: '100', // Buy 1 unit
priceNQTPerShare: '1000000' // At 1 IGNIS per unit
});
Cancel Orders
// Cancel a sell order
await provider.cancelAskOrder({ orderId: '987654321' });
// Cancel a buy order
await provider.cancelBidOrder({ orderId: '123456789' });
Token Authentication
Use signed tokens for secure backend authentication without exposing private keys.
How It Works
- Your game requests a signed token from the wallet
- User confirms in wallet UI
- Wallet signs a random token with private key
- Your game sends token + signature to your backend
- Backend verifies signature proves account ownership
Client-Side
async function login() {
// 1. Get signed token from wallet
const { token, signature, accountRS, timestamp } =
await provider.signToken();
// 2. Send to backend for verification
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, signature, accountRS, timestamp })
});
// 3. Store session token
const { sessionToken } = await response.json();
localStorage.setItem('session', sessionToken);
}
Server-Side Verification
// Using Ardor API to verify
async function verifySignature(token, signature, accountRS, timestamp) {
// 1. Check token isn't expired (e.g., 5 min max)
if (Date.now() - timestamp > 5 * 60 * 1000) {
throw new Error('Token expired');
}
// 2. Verify signature using Ardor API
const response = await fetch('https://ardor.jelurida.com/nxt', {
method: 'POST',
body: new URLSearchParams({
requestType: 'verifyTokenSignature',
signature,
data: token,
account: accountRS
})
});
const result = await response.json();
return result.verify === true;
}
Error Handling
Proper error handling ensures a good user experience.
Error Categories
try {
await provider.sendIgnis({ ... });
} catch (error) {
switch (error.code) {
// User actions
case 4001:
console.log('User rejected the request');
break;
// Permission issues
case 'PERMISSION_DENIED':
console.log('Permission not granted');
break;
// Connection issues
case 'NOT_CONNECTED':
console.log('Wallet not connected');
await provider.connect({ ... });
break;
// Insufficient funds
case 'INSUFFICIENT_BALANCE':
console.log('Not enough IGNIS');
break;
// Network issues
case 'NETWORK_ERROR':
console.log('Network problem, retry later');
break;
default:
console.error('Unknown error:', error);
}
}
User-Friendly Messages
const errorMessages = {
4001: 'Transaction cancelled',
'NOT_CONNECTED': 'Please connect your wallet first',
'PERMISSION_DENIED': 'Additional permissions required',
'INSUFFICIENT_BALANCE': 'Not enough IGNIS in your wallet',
'INVALID_RECIPIENT': 'Invalid recipient address',
'TIMEOUT': 'Request timed out, please try again'
};
function showError(error) {
const message = errorMessages[error.code] || 'An error occurred';
showNotification(message, 'error');
}
Fee Management
Understanding and managing transaction fees on Ardor.
Fee Structure
- Minimum fee: 0.1 IGNIS (most transactions)
- Fees are paid in IGNIS (child chain currency)
- Higher fees = faster confirmation
- Fees are burned (deflationary)
Default Fees
// Most transactions use minimum fee automatically
await provider.sendIgnis({
recipientRS: '...',
amount: { ignis: '1' }
}); // Uses ~0.1 IGNIS fee
Custom Fees
// Bump fee for priority
await provider.sendIgnis({
recipientRS: '...',
amount: { ignis: '1' },
feeNQT: '50000000' // 0.5 IGNIS fee
});
// Large transactions may need higher fees
await provider.transferAsset({
recipientRS: '...',
assetId: '...',
quantityQNT: '1000000',
feeNQT: '100000000' // 1 IGNIS fee
});
Fee Estimation
// For complex operations, estimate first
const estimatedFee = await estimateTransactionFee({
type: 'sendIgnis',
amount: '100',
hasMessage: true,
messageLength: 500
});
console.log(`Estimated fee: ${formatNQTToIgnis(estimatedFee)} IGNIS`);
Security Guidelines
Best practices for secure SDK integration.
Origin Validation
// Always specify the exact host origin
const provider = new MythicalProvider('https://store.mythicalbeings.io');
// Never use wildcards or accept any origin
Input Validation
// Validate addresses before use
function isValidArdorAddress(address) {
return /^ARDOR-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{5}$/.test(address);
}
// Validate amounts
function isValidAmount(amount) {
const num = parseFloat(amount);
return !isNaN(num) && num > 0 && num <= 1000000000;
}
// Use in transactions
async function sendPayment(recipient, amount) {
if (!isValidArdorAddress(recipient)) {
throw new Error('Invalid recipient address');
}
if (!isValidAmount(amount)) {
throw new Error('Invalid amount');
}
await provider.sendIgnis({
recipientRS: recipient,
amount: { ignis: amount }
});
}
Rate Limiting
// Prevent spam/abuse
const rateLimiter = {
lastRequest: 0,
minInterval: 1000, // 1 second between requests
async throttle(fn) {
const now = Date.now();
const wait = this.minInterval - (now - this.lastRequest);
if (wait > 0) {
await new Promise(r => setTimeout(r, wait));
}
this.lastRequest = Date.now();
return fn();
}
};
// Use with SDK calls
await rateLimiter.throttle(() => provider.getBalances());
Session Management
// Check session validity regularly
setInterval(async () => {
const session = await provider.getSession();
if (!session) {
// Session expired, re-authenticate
handleSessionExpired();
}
}, 60000); // Check every minute
// Handle wallet lock/unlock
provider.on('locked', () => {
showLockScreen();
});
provider.on('unlocked', async () => {
hideLockScreen();
await refreshBalances();
});
Never Trust Client Data
// BAD: Trusting client-reported balances
async function purchaseItem(item) {
const balances = await provider.getBalances();
if (balances.ignisNQT >= item.priceNQT) {
// ❌ Don't trust this! User could manipulate
grantItemToUser(item);
}
}
// GOOD: Verify on backend after transaction
async function purchaseItem(item) {
const result = await provider.sendIgnis({
recipientRS: SHOP_WALLET,
amount: { ignis: item.price }
});
// ✅ Backend verifies transaction on blockchain
await fetch('/api/verify-purchase', {
method: 'POST',
body: JSON.stringify({
txHash: result.fullHash,
itemId: item.id
})
});
}