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.
607 lines
13 KiB
Markdown
607 lines
13 KiB
Markdown
# 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<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
|
|
|
|
- [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!*
|