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.
13 KiB
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
- Completed Tutorial 1: Getting Started
- 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)
# 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 heightsynor_getBlockHash- Hash by heightsynor_getBlock- Block detailssynor_getDAGInfo- DAG structure
Transaction Methods
synor_sendRawTransaction- Broadcast txsynor_getTransaction- TX detailssynor_getMempool- Pending transactionssynor_estimateFee- Fee estimation
Address Methods
synor_getBalance- Address balancesynor_getUtxos- Unspent outputssynor_getAddressTransactions- TX history
Contract Methods
synor_deployContract- Deploy WASMsynor_callContract- Call methodsynor_getContractState- Read storage
Complete Documentation
Congratulations! You've completed the Synor Developer Tutorial Series!