# 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 - Completed [Tutorial 1: Getting Started](./01-getting-started.md) - Running Synor node (local or testnet) --- ## 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) ```bash # 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```typescript // synor-client.ts interface RpcResponse { 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(method: string, params: unknown[] = []): Promise { const headers: Record = { '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 = 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 { return this.call('synor_getBlockCount'); } async getBlock(hash: string): Promise { return this.call('synor_getBlock', [hash]); } async getBlockHash(height: number): Promise { return this.call('synor_getBlockHash', [height]); } // Transaction methods async getTransaction(txId: string): Promise { 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 { return this.call('synor_getBalance', [address]); } async getUtxos(address: string): Promise { return this.call('synor_getUtxos', [address]); } async getAddressTransactions(address: string, limit = 50): Promise { return this.call('synor_getAddressTransactions', [address, limit]); } // Network methods async getNetworkStats(): Promise { return this.call('synor_getNetworkStats'); } async estimateFee(params: FeeEstimateParams): Promise { 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 - [Full API Reference](../API_REFERENCE.md) - [WebSocket Subscriptions](../WEBSOCKET_GUIDE.md) - [Exchange Integration](../EXCHANGE_INTEGRATION.md) --- *Congratulations! You've completed the Synor Developer Tutorial Series!*