synor/docs/tutorials/04-api-guide.md
Gulshan Yadav a6233f285d docs: add developer tutorial series
Create comprehensive step-by-step tutorials:
- Tutorial 1: Getting Started - node setup, wallet creation, transactions
- Tutorial 2: Building a Wallet - React app with state management, encryption
- Tutorial 3: Smart Contracts - Rust WASM contracts, token example, testing
- Tutorial 4: API Guide - JSON-RPC, WebSocket subscriptions, client building

Each tutorial includes working code examples and best practices.
2026-01-10 06:24:51 +05:30

13 KiB

Tutorial 4: Working with the Synor API

Learn to interact with Synor nodes via JSON-RPC, WebSocket subscriptions, and the public API gateway.

What You'll Learn

  • Query blockchain data
  • Subscribe to real-time events
  • Build transactions programmatically
  • Use the rate-limited public API

Prerequisites


Part 1: API Overview

Connection Options

Method URL Use Case
HTTP RPC http://localhost:17110 Query, send tx
WebSocket ws://localhost:17111 Real-time subscriptions
Public API https://api.synor.cc/rpc Rate-limited access

Authentication (Public API)

# Anonymous (100 req/min)
curl https://api.synor.cc/rpc ...

# With API key (1000 req/min)
curl https://api.synor.cc/rpc \
  -H "X-API-Key: sk_developer_abc123..."

Part 2: Basic Queries

Get Block Count

async function getBlockCount() {
  const response = await fetch('http://localhost:17110', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'synor_getBlockCount',
      params: [],
      id: 1
    })
  });

  const { result } = await response.json();
  return result; // e.g., 1234567
}

Get Block by Hash

async function getBlock(hash) {
  const response = await rpc('synor_getBlock', [hash]);
  return response;
}

// Response:
// {
//   hash: "0x...",
//   height: 1234567,
//   timestamp: 1704067200,
//   transactions: [...],
//   parents: ["0x...", "0x..."],  // DAG parents
//   difficulty: "1234567890",
//   nonce: 12345
// }

Get Transaction

async function getTransaction(txId) {
  return rpc('synor_getTransaction', [txId]);
}

// Response:
// {
//   txId: "0x...",
//   version: 1,
//   inputs: [...],
//   outputs: [...],
//   confirmations: 50,
//   blockHash: "0x...",
//   isHybrid: true
// }

Part 3: Address Operations

Get Balance

async function getBalance(address) {
  const balance = await rpc('synor_getBalance', [address]);

  return {
    confirmed: parseFloat(balance.confirmed),
    pending: parseFloat(balance.pending),
    total: parseFloat(balance.confirmed) + parseFloat(balance.pending)
  };
}

Get UTXOs

async function getUtxos(address) {
  const utxos = await rpc('synor_getUtxos', [address]);

  return utxos.map(utxo => ({
    txId: utxo.txId,
    outputIndex: utxo.outputIndex,
    amount: parseFloat(utxo.amount),
    confirmations: utxo.confirmations
  }));
}

Get Transaction History

async function getTransactionHistory(address, options = {}) {
  const { limit = 50, offset = 0 } = options;

  const transactions = await rpc('synor_getAddressTransactions', [
    address,
    { limit, offset }
  ]);

  return transactions.map(tx => ({
    ...tx,
    type: tx.outputs.some(o => o.address === address) ? 'receive' : 'send',
    date: new Date(tx.timestamp * 1000)
  }));
}

Part 4: Building Transactions

Simple Transfer

import { buildTransaction, signTransactionHybrid, serializeTransaction } from '@synor/sdk';

async function sendTokens(wallet, toAddress, amount) {
  // 1. Get UTXOs
  const utxos = await rpc('synor_getUtxos', [wallet.address]);

  // 2. Select UTXOs
  const selectedUtxos = selectUtxos(utxos, amount);

  // 3. Build transaction
  const tx = buildTransaction({
    inputs: selectedUtxos,
    outputs: [{ address: toAddress, amount }],
    changeAddress: wallet.address
  });

  // 4. Sign with hybrid signatures
  const signedTx = await signTransactionHybrid(
    tx,
    wallet.seed,
    wallet.keypair.publicKey,
    wallet.dilithiumPublicKey
  );

  // 5. Broadcast
  const result = await rpc('synor_sendRawTransaction', [
    serializeTransaction(signedTx)
  ]);

  return result.txId;
}

// UTXO selection helper
function selectUtxos(utxos, targetAmount) {
  const sorted = [...utxos].sort((a, b) => b.amount - a.amount);
  const selected = [];
  let total = 0;

  for (const utxo of sorted) {
    selected.push(utxo);
    total += utxo.amount;
    if (total >= targetAmount + 0.0001) { // Include fee estimate
      break;
    }
  }

  if (total < targetAmount) {
    throw new Error('Insufficient funds');
  }

  return selected;
}

Fee Estimation

async function estimateFee(fromAddress, toAddress, amount) {
  const estimate = await rpc('synor_estimateFee', [{
    from: fromAddress,
    to: toAddress,
    amount: amount.toString()
  }]);

  return {
    fee: parseFloat(estimate.fee),
    feePerByte: estimate.feePerByte,
    estimatedSize: estimate.size, // Bytes (larger for hybrid signatures)
    priority: estimate.priority // 'low', 'medium', 'high'
  };
}

Part 5: WebSocket Subscriptions

Connect to WebSocket

class SynorWebSocket {
  constructor(url = 'ws://localhost:17111') {
    this.url = url;
    this.ws = null;
    this.handlers = new Map();
    this.reconnectAttempts = 0;
  }

  connect() {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.url);

      this.ws.onopen = () => {
        this.reconnectAttempts = 0;
        resolve();
      };

      this.ws.onerror = reject;

      this.ws.onmessage = (event) => {
        const msg = JSON.parse(event.data);
        this.handleMessage(msg);
      };

      this.ws.onclose = () => {
        this.scheduleReconnect();
      };
    });
  }

  send(method, params) {
    const id = Date.now();
    this.ws.send(JSON.stringify({
      jsonrpc: '2.0',
      method,
      params,
      id
    }));
    return id;
  }

  handleMessage(msg) {
    if (msg.method) {
      // Subscription notification
      const handler = this.handlers.get(msg.method);
      if (handler) handler(msg.params);
    }
  }

  on(event, handler) {
    this.handlers.set(event, handler);
  }

  scheduleReconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;
    setTimeout(() => this.connect(), delay);
  }
}

Subscribe to New Blocks

const ws = new SynorWebSocket();
await ws.connect();

// Subscribe
ws.send('synor_subscribeBlocks', []);

// Handle new blocks
ws.on('synor_newBlock', (block) => {
  console.log('New block:', block.hash);
  console.log('Height:', block.height);
  console.log('Transactions:', block.transactions.length);
});

Subscribe to Address Transactions

const myAddress = 'tsynor1...';

// Subscribe to address
ws.send('synor_subscribeAddress', [myAddress]);

// Handle transactions
ws.on('synor_addressNotification', (notification) => {
  const { txId, type, amount, confirmations } = notification;

  if (type === 'receive') {
    console.log(`Received ${amount} SYNOR!`);

    if (confirmations >= 10) {
      console.log('Transaction confirmed!');
    }
  }
});

Subscribe to Mempool

ws.send('synor_subscribeMempool', []);

ws.on('synor_mempoolTransaction', (tx) => {
  console.log('New pending tx:', tx.txId);
  console.log('Value:', tx.totalValue);
});

Part 6: Advanced Queries

Get DAG Information

async function getDAGInfo() {
  const info = await rpc('synor_getDAGInfo', []);

  return {
    tips: info.tips,           // Current DAG tips
    blueScore: info.blueScore, // SPECTRE blue score
    virtualParent: info.virtualParent,
    pruningPoint: info.pruningPoint
  };
}

Get Network Stats

async function getNetworkStats() {
  const stats = await rpc('synor_getNetworkStats', []);

  return {
    hashrate: stats.hashrate,
    difficulty: stats.difficulty,
    blockRate: stats.blocksPerSecond,
    peers: stats.connectedPeers,
    mempoolSize: stats.mempoolSize
  };
}

Search Transactions

async function searchTransactions(query) {
  // Search by txId prefix
  if (query.length === 64) {
    return rpc('synor_getTransaction', [query]);
  }

  // Search by address
  if (query.startsWith('synor1') || query.startsWith('tsynor1')) {
    return rpc('synor_getAddressTransactions', [query, 10]);
  }

  // Search by block height
  if (/^\d+$/.test(query)) {
    const hash = await rpc('synor_getBlockHash', [parseInt(query)]);
    return rpc('synor_getBlock', [hash]);
  }

  throw new Error('Invalid search query');
}

Part 7: Error Handling

Common Error Codes

Code Meaning Action
-32600 Invalid Request Check JSON format
-32601 Method not found Check method name
-32602 Invalid params Verify parameters
-32603 Internal error Retry or report
-32001 Invalid API key Check authentication
-32005 Rate limit exceeded Wait and retry
-32010 Transaction rejected Check validation

Retry Logic

async function rpcWithRetry(method, params, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await rpc(method, params);
    } catch (error) {
      if (error.code === -32005) {
        // Rate limited - wait and retry
        const retryAfter = error.data?.retryAfter || 60;
        await sleep(retryAfter * 1000);
        continue;
      }

      if (error.code === -32603 && i < maxRetries - 1) {
        // Internal error - retry with backoff
        await sleep(1000 * Math.pow(2, i));
        continue;
      }

      throw error;
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Part 8: Building an API Client

Complete TypeScript Client

// synor-client.ts

interface RpcResponse<T> {
  jsonrpc: '2.0';
  result?: T;
  error?: { code: number; message: string; data?: any };
  id: number;
}

export class SynorClient {
  private baseUrl: string;
  private apiKey?: string;

  constructor(options: { url?: string; apiKey?: string } = {}) {
    this.baseUrl = options.url || 'http://localhost:17110';
    this.apiKey = options.apiKey;
  }

  private async call<T>(method: string, params: unknown[] = []): Promise<T> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json'
    };

    if (this.apiKey) {
      headers['X-API-Key'] = this.apiKey;
    }

    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        jsonrpc: '2.0',
        method,
        params,
        id: Date.now()
      })
    });

    const data: RpcResponse<T> = await response.json();

    if (data.error) {
      const error = new Error(data.error.message);
      (error as any).code = data.error.code;
      (error as any).data = data.error.data;
      throw error;
    }

    return data.result as T;
  }

  // Chain methods
  async getBlockCount(): Promise<number> {
    return this.call('synor_getBlockCount');
  }

  async getBlock(hash: string): Promise<Block> {
    return this.call('synor_getBlock', [hash]);
  }

  async getBlockHash(height: number): Promise<string> {
    return this.call('synor_getBlockHash', [height]);
  }

  // Transaction methods
  async getTransaction(txId: string): Promise<Transaction> {
    return this.call('synor_getTransaction', [txId]);
  }

  async sendRawTransaction(serializedTx: string): Promise<{ txId: string }> {
    return this.call('synor_sendRawTransaction', [serializedTx]);
  }

  // Address methods
  async getBalance(address: string): Promise<Balance> {
    return this.call('synor_getBalance', [address]);
  }

  async getUtxos(address: string): Promise<Utxo[]> {
    return this.call('synor_getUtxos', [address]);
  }

  async getAddressTransactions(address: string, limit = 50): Promise<Transaction[]> {
    return this.call('synor_getAddressTransactions', [address, limit]);
  }

  // Network methods
  async getNetworkStats(): Promise<NetworkStats> {
    return this.call('synor_getNetworkStats');
  }

  async estimateFee(params: FeeEstimateParams): Promise<FeeEstimate> {
    return this.call('synor_estimateFee', [params]);
  }
}

// Usage
const client = new SynorClient({
  url: 'https://api.synor.cc/rpc',
  apiKey: 'sk_developer_abc123...'
});

const balance = await client.getBalance('tsynor1...');
console.log(balance);

API Reference Summary

Chain Methods

  • synor_getBlockCount - Current block height
  • synor_getBlockHash - Hash by height
  • synor_getBlock - Block details
  • synor_getDAGInfo - DAG structure

Transaction Methods

  • synor_sendRawTransaction - Broadcast tx
  • synor_getTransaction - TX details
  • synor_getMempool - Pending transactions
  • synor_estimateFee - Fee estimation

Address Methods

  • synor_getBalance - Address balance
  • synor_getUtxos - Unspent outputs
  • synor_getAddressTransactions - TX history

Contract Methods

  • synor_deployContract - Deploy WASM
  • synor_callContract - Call method
  • synor_getContractState - Read storage

Complete Documentation


Congratulations! You've completed the Synor Developer Tutorial Series!