Architecture Overview

The Mythical SDK uses a provider pattern for secure communication between your game and the Mythical Beings embedded wallet.

Three Packages
  • @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.

⚠️ Important

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 NQT
  • 0.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

  1. Your game requests a signed token from the wallet
  2. User confirms in wallet UI
  3. Wallet signs a random token with private key
  4. Your game sends token + signature to your backend
  5. 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 
    })
  });
}