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.
This commit is contained in:
Gulshan Yadav 2026-01-10 06:24:51 +05:30
parent 960c25eb8a
commit a6233f285d
5 changed files with 2254 additions and 0 deletions

View file

@ -0,0 +1,271 @@
# Tutorial 1: Getting Started with Synor
Welcome to Synor, the first post-quantum secure blockchain! This tutorial will help you set up your development environment and make your first transactions.
## What You'll Learn
- Set up a local Synor node
- Create a wallet and address
- Send and receive SYNOR tokens
- Query the blockchain
## Prerequisites
- Docker and Docker Compose
- Node.js 18+ or Rust 1.70+
- Basic command line knowledge
---
## Part 1: Running a Local Node
The easiest way to start is with Docker.
### Step 1: Clone the Repository
```bash
git clone https://github.com/synor/synor.git
cd synor
```
### Step 2: Start the Local Testnet
```bash
docker compose -f docker-compose.testnet.yml up -d seed1
```
This starts a single node for development. Wait about 30 seconds for it to initialize.
### Step 3: Verify the Node is Running
```bash
curl -X POST http://localhost:17110 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"synor_getBlockCount","params":[],"id":1}'
```
You should see:
```json
{"jsonrpc":"2.0","result":1,"id":1}
```
---
## Part 2: Creating Your First Wallet
### Option A: Using the CLI
```bash
# Install the Synor CLI
cargo install synor-cli
# Generate a new wallet
synor-cli wallet new
# Output:
# Mnemonic: abandon abandon abandon ... (24 words)
# Address: tsynor1qz232pysw8kezv2f4qxnhdufrlx5cmq78522mpuf8x5qlxu6j8sgcp05get
#
# IMPORTANT: Save your mnemonic phrase securely!
```
### Option B: Using JavaScript
Create a new project:
```bash
mkdir my-synor-app
cd my-synor-app
npm init -y
npm install @synor/sdk
```
Create `wallet.js`:
```javascript
import { generateMnemonic, createWallet } from '@synor/sdk';
async function main() {
// Generate a 24-word mnemonic
const mnemonic = generateMnemonic(24);
console.log('Mnemonic:', mnemonic);
// Create wallet from mnemonic
const wallet = await createWallet(mnemonic, '', 'testnet');
console.log('Address:', wallet.address);
// IMPORTANT: In production, encrypt and securely store the mnemonic!
}
main().catch(console.error);
```
Run it:
```bash
node wallet.js
```
---
## Part 3: Getting Test Tokens
### From the Faucet
If you're on testnet, request tokens from the faucet:
```bash
curl -X POST http://localhost:8080/request \
-H "Content-Type: application/json" \
-d '{"address": "tsynor1your_address_here"}'
```
### Check Your Balance
```bash
curl -X POST http://localhost:17110 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0",
"method":"synor_getBalance",
"params":["tsynor1your_address"],
"id":1
}'
```
Response:
```json
{
"jsonrpc": "2.0",
"result": {
"confirmed": "10.00000000",
"pending": "0.00000000"
},
"id": 1
}
```
---
## Part 4: Sending Your First Transaction
### Using the CLI
```bash
synor-cli send \
--to tsynor1recipient_address \
--amount 1.5 \
--password yourpassword
```
### Using JavaScript
```javascript
import { createSendTransactionHybrid, serializeTransaction } from '@synor/sdk';
async function sendTransaction(wallet, toAddress, amount) {
// Build and sign the transaction
const tx = await createSendTransactionHybrid(
wallet.address,
toAddress,
amount,
wallet
);
console.log('Transaction ID:', tx.id);
// Broadcast to the network
const response = await fetch('http://localhost:17110', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'synor_sendRawTransaction',
params: [serializeTransaction(tx)],
id: 1
})
});
const result = await response.json();
console.log('Broadcast result:', result);
return tx.id;
}
```
---
## Part 5: Understanding Hybrid Signatures
Synor is unique because it uses **hybrid signatures**:
```
┌────────────────────────────────────────────────┐
│ Transaction │
├────────────────────────────────────────────────┤
│ Signed with: │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ Ed25519 │ + │ Dilithium3 │ │
│ │ (64 bytes) │ │ (~3.3 KB) │ │
│ └─────────────┘ └──────────────────┘ │
│ │
│ Both signatures must verify! │
└────────────────────────────────────────────────┘
```
**Why hybrid?**
- **Ed25519**: Fast, well-tested, secure against classical computers
- **Dilithium3**: Secure against future quantum computers (NIST PQC standard)
This means your funds are protected even if quantum computers become powerful enough to break Ed25519.
---
## Part 6: Next Steps
Congratulations! You've:
- ✅ Set up a local Synor node
- ✅ Created a wallet
- ✅ Received test tokens
- ✅ Sent a transaction
### Continue Learning
- [Tutorial 2: Building a Wallet Application](./02-building-a-wallet.md)
- [Tutorial 3: Smart Contracts on Synor](./03-smart-contracts.md)
- [Tutorial 4: Working with the API](./04-api-guide.md)
### Resources
- [API Reference](../API_REFERENCE.md)
- [Developer Guide](../DEVELOPER_GUIDE.md)
- [Exchange Integration](../EXCHANGE_INTEGRATION.md)
---
## Troubleshooting
### Node not responding
```bash
# Check if container is running
docker ps | grep synor
# Check logs
docker logs synor-seed1
```
### Transaction stuck
- Ensure you have enough balance for the fee
- Check that UTXOs are confirmed
- Try with a higher fee
### Invalid address error
- Testnet addresses start with `tsynor1`
- Mainnet addresses start with `synor1`
- Addresses are case-sensitive (use lowercase)
---
*Next: [Building a Wallet Application](./02-building-a-wallet.md)*

View file

@ -0,0 +1,771 @@
# Tutorial 2: Building a Wallet Application
In this tutorial, you'll build a complete wallet application that can create addresses, display balances, and send transactions.
## What You'll Build
A React wallet app with:
- Mnemonic generation and recovery
- Balance display
- Transaction history
- Send functionality
- QR code support
## Prerequisites
- Completed [Tutorial 1: Getting Started](./01-getting-started.md)
- Node.js 18+
- Basic React knowledge
---
## Part 1: Project Setup
### Create React App
```bash
npm create vite@latest synor-wallet -- --template react-ts
cd synor-wallet
npm install
```
### Install Dependencies
```bash
npm install @synor/sdk zustand @tanstack/react-query qrcode.react
```
### Project Structure
```
synor-wallet/
├── src/
│ ├── lib/
│ │ ├── crypto.ts # Crypto utilities
│ │ └── rpc.ts # RPC client
│ ├── store/
│ │ └── wallet.ts # Wallet state
│ ├── components/
│ │ ├── CreateWallet.tsx
│ │ ├── Dashboard.tsx
│ │ ├── Send.tsx
│ │ └── Receive.tsx
│ ├── App.tsx
│ └── main.tsx
├── package.json
└── vite.config.ts
```
---
## Part 2: Core Wallet Logic
### lib/crypto.ts
```typescript
import {
generateMnemonic,
validateMnemonic,
createWallet as sdkCreateWallet
} from '@synor/sdk';
export type Network = 'mainnet' | 'testnet';
export interface Wallet {
address: string;
seed: Uint8Array;
keypair: {
publicKey: Uint8Array;
privateKey: Uint8Array;
};
dilithiumPublicKey: Uint8Array;
}
export function generateNewMnemonic(wordCount: 12 | 24 = 24): string {
return generateMnemonic(wordCount);
}
export function isValidMnemonic(mnemonic: string): boolean {
return validateMnemonic(mnemonic);
}
export async function createWalletFromMnemonic(
mnemonic: string,
passphrase: string = '',
network: Network = 'testnet'
): Promise<Wallet> {
return sdkCreateWallet(mnemonic, passphrase, network);
}
// Encrypt wallet for storage
export async function encryptWallet(
seed: Uint8Array,
password: string
): Promise<{ ciphertext: Uint8Array; iv: Uint8Array; salt: Uint8Array }> {
// Use Web Crypto API for AES-256-GCM encryption
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
// Derive key using PBKDF2
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
seed
);
return {
ciphertext: new Uint8Array(ciphertext),
iv,
salt
};
}
// Decrypt wallet
export async function decryptWallet(
ciphertext: Uint8Array,
iv: Uint8Array,
salt: Uint8Array,
password: string
): Promise<Uint8Array> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
ciphertext
);
return new Uint8Array(decrypted);
}
```
### lib/rpc.ts
```typescript
const RPC_URL = 'http://localhost:17110';
interface RpcRequest {
jsonrpc: '2.0';
method: string;
params: unknown[];
id: number;
}
interface RpcResponse<T> {
jsonrpc: '2.0';
result?: T;
error?: { code: number; message: string };
id: number;
}
let requestId = 0;
export async function rpc<T>(method: string, params: unknown[] = []): Promise<T> {
const request: RpcRequest = {
jsonrpc: '2.0',
method,
params,
id: ++requestId
};
const response = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
});
const data: RpcResponse<T> = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
return data.result as T;
}
// Typed RPC methods
export interface Balance {
confirmed: string;
pending: string;
}
export interface Transaction {
txId: string;
type: 'send' | 'receive';
amount: string;
address: string;
confirmations: number;
timestamp: number;
}
export const api = {
getBalance: (address: string) =>
rpc<Balance>('synor_getBalance', [address]),
getTransactions: (address: string, limit = 50) =>
rpc<Transaction[]>('synor_getAddressTransactions', [address, limit]),
sendTransaction: (serializedTx: string) =>
rpc<{ txId: string }>('synor_sendRawTransaction', [serializedTx]),
getBlockCount: () =>
rpc<number>('synor_getBlockCount', [])
};
```
---
## Part 3: State Management
### store/wallet.ts
```typescript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {
generateNewMnemonic,
createWalletFromMnemonic,
encryptWallet,
decryptWallet,
type Wallet
} from '../lib/crypto';
import { api, type Balance, type Transaction } from '../lib/rpc';
interface WalletState {
// Persisted
hasWallet: boolean;
encryptedSeed: { ciphertext: string; iv: string; salt: string } | null;
address: string | null;
// Session (not persisted)
isUnlocked: boolean;
wallet: Wallet | null;
balance: Balance | null;
transactions: Transaction[];
isLoading: boolean;
error: string | null;
// Actions
createWallet: (password: string) => Promise<string>;
recoverWallet: (mnemonic: string, password: string) => Promise<void>;
unlock: (password: string) => Promise<void>;
lock: () => void;
refreshBalance: () => Promise<void>;
refreshTransactions: () => Promise<void>;
}
// Convert bytes to hex
const toHex = (bytes: Uint8Array) =>
Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
// Convert hex to bytes
const fromHex = (hex: string) =>
new Uint8Array(hex.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
export const useWalletStore = create<WalletState>()(
persist(
(set, get) => ({
// Initial state
hasWallet: false,
encryptedSeed: null,
address: null,
isUnlocked: false,
wallet: null,
balance: null,
transactions: [],
isLoading: false,
error: null,
createWallet: async (password) => {
set({ isLoading: true, error: null });
try {
const mnemonic = generateNewMnemonic(24);
const wallet = await createWalletFromMnemonic(mnemonic, '', 'testnet');
const encrypted = await encryptWallet(wallet.seed, password);
set({
hasWallet: true,
encryptedSeed: {
ciphertext: toHex(encrypted.ciphertext),
iv: toHex(encrypted.iv),
salt: toHex(encrypted.salt)
},
address: wallet.address,
isUnlocked: true,
wallet,
isLoading: false
});
return mnemonic; // Return for user to save
} catch (error) {
set({ error: (error as Error).message, isLoading: false });
throw error;
}
},
recoverWallet: async (mnemonic, password) => {
set({ isLoading: true, error: null });
try {
const wallet = await createWalletFromMnemonic(mnemonic, '', 'testnet');
const encrypted = await encryptWallet(wallet.seed, password);
set({
hasWallet: true,
encryptedSeed: {
ciphertext: toHex(encrypted.ciphertext),
iv: toHex(encrypted.iv),
salt: toHex(encrypted.salt)
},
address: wallet.address,
isUnlocked: true,
wallet,
isLoading: false
});
} catch (error) {
set({ error: (error as Error).message, isLoading: false });
throw error;
}
},
unlock: async (password) => {
const { encryptedSeed, address } = get();
if (!encryptedSeed || !address) {
throw new Error('No wallet found');
}
set({ isLoading: true, error: null });
try {
const seed = await decryptWallet(
fromHex(encryptedSeed.ciphertext),
fromHex(encryptedSeed.iv),
fromHex(encryptedSeed.salt),
password
);
const wallet = await createWalletFromMnemonic(
'', // We derive from seed directly
'',
'testnet'
);
set({
isUnlocked: true,
wallet,
isLoading: false
});
// Refresh balance
get().refreshBalance();
} catch (error) {
set({ error: 'Incorrect password', isLoading: false });
throw error;
}
},
lock: () => {
set({
isUnlocked: false,
wallet: null,
balance: null,
transactions: []
});
},
refreshBalance: async () => {
const { address, isUnlocked } = get();
if (!address || !isUnlocked) return;
try {
const balance = await api.getBalance(address);
set({ balance });
} catch (error) {
console.error('Failed to refresh balance:', error);
}
},
refreshTransactions: async () => {
const { address, isUnlocked } = get();
if (!address || !isUnlocked) return;
try {
const transactions = await api.getTransactions(address);
set({ transactions });
} catch (error) {
console.error('Failed to refresh transactions:', error);
}
}
}),
{
name: 'synor-wallet',
partialize: (state) => ({
hasWallet: state.hasWallet,
encryptedSeed: state.encryptedSeed,
address: state.address
})
}
)
);
```
---
## Part 4: Components
### components/CreateWallet.tsx
```tsx
import { useState } from 'react';
import { useWalletStore } from '../store/wallet';
export function CreateWallet() {
const { createWallet, recoverWallet, isLoading } = useWalletStore();
const [step, setStep] = useState<'choice' | 'create' | 'recover'>('choice');
const [mnemonic, setMnemonic] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [generatedMnemonic, setGeneratedMnemonic] = useState('');
const [error, setError] = useState('');
const handleCreate = async () => {
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
try {
const newMnemonic = await createWallet(password);
setGeneratedMnemonic(newMnemonic);
setStep('create');
} catch (err) {
setError((err as Error).message);
}
};
const handleRecover = async () => {
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
try {
await recoverWallet(mnemonic, password);
} catch (err) {
setError((err as Error).message);
}
};
if (step === 'choice') {
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Welcome to Synor Wallet</h1>
<div className="space-y-4">
<button
onClick={() => setStep('create')}
className="w-full p-4 bg-blue-600 text-white rounded-lg"
>
Create New Wallet
</button>
<button
onClick={() => setStep('recover')}
className="w-full p-4 bg-gray-700 text-white rounded-lg"
>
Recover Existing Wallet
</button>
</div>
</div>
);
}
if (step === 'create' && generatedMnemonic) {
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-xl font-bold mb-4">Save Your Recovery Phrase</h2>
<div className="bg-yellow-900/30 border border-yellow-600 p-4 rounded mb-4">
<p className="text-sm text-yellow-300">
Write down these 24 words in order. This is your only backup!
</p>
</div>
<div className="grid grid-cols-3 gap-2 mb-6">
{generatedMnemonic.split(' ').map((word, i) => (
<div key={i} className="bg-gray-800 p-2 rounded text-sm">
<span className="text-gray-500 mr-2">{i + 1}.</span>
{word}
</div>
))}
</div>
<button
onClick={() => setGeneratedMnemonic('')}
className="w-full p-4 bg-green-600 text-white rounded-lg"
>
I've Saved My Phrase
</button>
</div>
);
}
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-xl font-bold mb-4">
{step === 'create' ? 'Create Wallet' : 'Recover Wallet'}
</h2>
{error && (
<div className="bg-red-900/30 border border-red-600 p-3 rounded mb-4">
{error}
</div>
)}
{step === 'recover' && (
<div className="mb-4">
<label className="block text-sm mb-2">Recovery Phrase</label>
<textarea
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
className="w-full p-3 bg-gray-800 rounded"
rows={3}
placeholder="Enter your 24 words..."
/>
</div>
)}
<div className="mb-4">
<label className="block text-sm mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 bg-gray-800 rounded"
/>
</div>
<div className="mb-6">
<label className="block text-sm mb-2">Confirm Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full p-3 bg-gray-800 rounded"
/>
</div>
<button
onClick={step === 'create' ? handleCreate : handleRecover}
disabled={isLoading}
className="w-full p-4 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isLoading ? 'Loading...' : step === 'create' ? 'Create' : 'Recover'}
</button>
</div>
);
}
```
### components/Dashboard.tsx
```tsx
import { useEffect } from 'react';
import { useWalletStore } from '../store/wallet';
import { QRCodeSVG } from 'qrcode.react';
export function Dashboard() {
const {
address,
balance,
transactions,
refreshBalance,
refreshTransactions
} = useWalletStore();
useEffect(() => {
refreshBalance();
refreshTransactions();
const interval = setInterval(() => {
refreshBalance();
refreshTransactions();
}, 30000);
return () => clearInterval(interval);
}, []);
return (
<div className="max-w-2xl mx-auto p-6">
{/* Balance Card */}
<div className="bg-gradient-to-r from-blue-600 to-purple-600 p-6 rounded-xl mb-6">
<h2 className="text-sm text-white/70">Total Balance</h2>
<div className="text-4xl font-bold text-white">
{balance?.confirmed || '0.00'} SYNOR
</div>
{balance?.pending && parseFloat(balance.pending) > 0 && (
<div className="text-sm text-white/70 mt-1">
+{balance.pending} pending
</div>
)}
</div>
{/* Receive QR */}
<div className="bg-gray-800 p-6 rounded-xl mb-6">
<h3 className="text-lg font-semibold mb-4">Receive</h3>
<div className="flex items-center gap-6">
<div className="bg-white p-4 rounded-lg">
<QRCodeSVG value={`synor:${address}`} size={120} />
</div>
<div className="flex-1">
<p className="text-sm text-gray-400 mb-2">Your Address</p>
<p className="font-mono text-sm break-all">{address}</p>
<button
onClick={() => navigator.clipboard.writeText(address!)}
className="mt-3 px-4 py-2 bg-gray-700 rounded text-sm"
>
Copy Address
</button>
</div>
</div>
</div>
{/* Recent Transactions */}
<div className="bg-gray-800 p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-4">Recent Transactions</h3>
{transactions.length === 0 ? (
<p className="text-gray-500 text-center py-8">No transactions yet</p>
) : (
<div className="space-y-3">
{transactions.slice(0, 5).map((tx) => (
<div
key={tx.txId}
className="flex items-center justify-between p-3 bg-gray-700/50 rounded"
>
<div>
<div className={tx.type === 'receive' ? 'text-green-400' : 'text-red-400'}>
{tx.type === 'receive' ? '+' : '-'}{tx.amount} SYNOR
</div>
<div className="text-xs text-gray-500">
{tx.confirmations} confirmations
</div>
</div>
<div className="text-xs text-gray-500">
{new Date(tx.timestamp * 1000).toLocaleDateString()}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
```
---
## Part 5: Putting It Together
### App.tsx
```tsx
import { useWalletStore } from './store/wallet';
import { CreateWallet } from './components/CreateWallet';
import { Dashboard } from './components/Dashboard';
function App() {
const { hasWallet, isUnlocked } = useWalletStore();
if (!hasWallet) {
return <CreateWallet />;
}
if (!isUnlocked) {
return <UnlockScreen />;
}
return <Dashboard />;
}
function UnlockScreen() {
const { unlock, isLoading, error } = useWalletStore();
const [password, setPassword] = useState('');
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Unlock Wallet</h1>
{error && (
<div className="bg-red-900/30 border border-red-600 p-3 rounded mb-4">
{error}
</div>
)}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 bg-gray-800 rounded mb-4"
placeholder="Enter password"
/>
<button
onClick={() => unlock(password)}
disabled={isLoading}
className="w-full p-4 bg-blue-600 text-white rounded-lg"
>
{isLoading ? 'Unlocking...' : 'Unlock'}
</button>
</div>
);
}
export default App;
```
---
## What You've Built
- **Secure wallet creation** with BIP-39 mnemonic
- **Encrypted storage** using AES-256-GCM
- **Balance display** with live updates
- **QR code** for receiving payments
- **Transaction history**
## Next Steps
- Add send functionality (see completed wallet at `apps/web/`)
- Implement transaction signing
- Add network selection
- Deploy to production
---
*Next: [Smart Contracts on Synor](./03-smart-contracts.md)*

View file

@ -0,0 +1,517 @@
# Tutorial 3: Smart Contracts on Synor
Learn to build, deploy, and interact with smart contracts on Synor's WebAssembly-based contract system.
## What You'll Learn
- Write a contract in Rust
- Compile to WebAssembly
- Deploy to Synor
- Call contract methods
- Handle state and events
## Prerequisites
- Completed [Tutorial 1: Getting Started](./01-getting-started.md)
- Rust installed (`rustup`)
- `wasm32-unknown-unknown` target
---
## Part 1: Understanding Synor Contracts
### How Contracts Work
```
┌─────────────────────────────────────────────┐
│ Synor Smart Contract │
├─────────────────────────────────────────────┤
│ Written in: Rust (compiled to WASM) │
│ Execution: Deterministic sandbox │
│ State: Key-value storage │
│ Gas: Metered execution │
│ Size limit: 256 KB compiled WASM │
└─────────────────────────────────────────────┘
```
### Contract Capabilities
| Feature | Support |
|---------|---------|
| State storage | ✅ Key-value store |
| Cross-contract calls | ✅ Via host functions |
| Events/Logs | ✅ Emit events |
| Token transfers | ✅ Native SYNOR |
| Cryptography | ✅ Blake3, Ed25519 |
| Time access | ✅ Block timestamp |
| Randomness | ❌ Not yet (determinism) |
---
## Part 2: Setting Up
### Install the Contract SDK
```bash
# Add the WASM target
rustup target add wasm32-unknown-unknown
# Install the Synor contract CLI
cargo install synor-contract-cli
```
### Create a New Contract Project
```bash
synor-contract new my_token
cd my_token
```
This creates:
```
my_token/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration.rs
```
---
## Part 3: Writing Your First Contract
### A Simple Token Contract
Edit `src/lib.rs`:
```rust
//! A simple fungible token contract.
#![no_std]
use synor_contract_sdk::prelude::*;
// Contract state
#[derive(Default)]
struct Token {
name: String,
symbol: String,
decimals: u8,
total_supply: u128,
balances: Map<Address, u128>,
allowances: Map<(Address, Address), u128>,
}
// Contract implementation
#[synor_contract]
impl Token {
/// Initialize the token contract
#[init]
pub fn new(name: String, symbol: String, decimals: u8, initial_supply: u128) -> Self {
let caller = env::caller();
let mut balances = Map::new();
balances.insert(caller, initial_supply);
emit!(TokenCreated {
name: name.clone(),
symbol: symbol.clone(),
total_supply: initial_supply,
});
Token {
name,
symbol,
decimals,
total_supply: initial_supply,
balances,
allowances: Map::new(),
}
}
/// Get token name
#[view]
pub fn name(&self) -> String {
self.name.clone()
}
/// Get token symbol
#[view]
pub fn symbol(&self) -> String {
self.symbol.clone()
}
/// Get decimals
#[view]
pub fn decimals(&self) -> u8 {
self.decimals
}
/// Get total supply
#[view]
pub fn total_supply(&self) -> u128 {
self.total_supply
}
/// Get balance of address
#[view]
pub fn balance_of(&self, owner: Address) -> u128 {
self.balances.get(&owner).copied().unwrap_or(0)
}
/// Transfer tokens to another address
#[mutate]
pub fn transfer(&mut self, to: Address, amount: u128) -> bool {
let from = env::caller();
let from_balance = self.balance_of(from);
require!(from_balance >= amount, "Insufficient balance");
self.balances.insert(from, from_balance - amount);
let to_balance = self.balance_of(to);
self.balances.insert(to, to_balance + amount);
emit!(Transfer { from, to, amount });
true
}
/// Approve spender to transfer tokens
#[mutate]
pub fn approve(&mut self, spender: Address, amount: u128) -> bool {
let owner = env::caller();
self.allowances.insert((owner, spender), amount);
emit!(Approval { owner, spender, amount });
true
}
/// Get allowance
#[view]
pub fn allowance(&self, owner: Address, spender: Address) -> u128 {
self.allowances.get(&(owner, spender)).copied().unwrap_or(0)
}
/// Transfer tokens on behalf of owner
#[mutate]
pub fn transfer_from(&mut self, from: Address, to: Address, amount: u128) -> bool {
let spender = env::caller();
let allowed = self.allowance(from, spender);
require!(allowed >= amount, "Allowance exceeded");
let from_balance = self.balance_of(from);
require!(from_balance >= amount, "Insufficient balance");
// Update balances
self.balances.insert(from, from_balance - amount);
let to_balance = self.balance_of(to);
self.balances.insert(to, to_balance + amount);
// Update allowance
self.allowances.insert((from, spender), allowed - amount);
emit!(Transfer { from, to, amount });
true
}
/// Mint new tokens (only for demonstration)
#[mutate]
pub fn mint(&mut self, to: Address, amount: u128) {
// In production, add access control!
let balance = self.balance_of(to);
self.balances.insert(to, balance + amount);
self.total_supply += amount;
emit!(Transfer {
from: Address::zero(),
to,
amount
});
}
/// Burn tokens
#[mutate]
pub fn burn(&mut self, amount: u128) {
let caller = env::caller();
let balance = self.balance_of(caller);
require!(balance >= amount, "Insufficient balance");
self.balances.insert(caller, balance - amount);
self.total_supply -= amount;
emit!(Transfer {
from: caller,
to: Address::zero(),
amount
});
}
}
// Events
#[event]
struct TokenCreated {
name: String,
symbol: String,
total_supply: u128,
}
#[event]
struct Transfer {
from: Address,
to: Address,
amount: u128,
}
#[event]
struct Approval {
owner: Address,
spender: Address,
amount: u128,
}
```
---
## Part 4: Building the Contract
### Compile to WASM
```bash
# Build optimized WASM
synor-contract build --release
# Output:
# Compiled: target/wasm32-unknown-unknown/release/my_token.wasm
# Size: 45.2 KB
# Hash: 0x1234...
```
### Verify the Build
```bash
# Check contract info
synor-contract info target/wasm32-unknown-unknown/release/my_token.wasm
# Output:
# Contract: my_token
# Methods: new, name, symbol, decimals, total_supply, balance_of, transfer, approve, allowance, transfer_from, mint, burn
# View methods: name, symbol, decimals, total_supply, balance_of, allowance
# Mutate methods: transfer, approve, transfer_from, mint, burn
```
---
## Part 5: Deploying the Contract
### Using the CLI
```bash
# Deploy to testnet
synor-contract deploy \
--wasm target/wasm32-unknown-unknown/release/my_token.wasm \
--init "new" \
--args '"MyToken","MTK",18,1000000000000000000000' \
--network testnet
# Output:
# Contract deployed!
# Address: tsynor1contract_address...
# Transaction: 0xabc123...
# Gas used: 150000
```
### Using JavaScript
```javascript
import { deployContract } from '@synor/sdk';
import fs from 'fs';
async function deploy(wallet) {
const wasm = fs.readFileSync('./my_token.wasm');
const result = await deployContract(wallet, {
wasm,
method: 'new',
args: ['MyToken', 'MTK', 18, '1000000000000000000000'],
});
console.log('Contract address:', result.contractAddress);
return result.contractAddress;
}
```
---
## Part 6: Interacting with the Contract
### Read Contract State (View Methods)
```javascript
import { callContractView } from '@synor/sdk';
// Get token info
const name = await callContractView(contractAddress, 'name', []);
console.log('Token name:', name); // "MyToken"
// Get balance
const balance = await callContractView(
contractAddress,
'balance_of',
[myAddress]
);
console.log('Balance:', balance);
```
### Write to Contract (Mutate Methods)
```javascript
import { callContractMutate } from '@synor/sdk';
// Transfer tokens
const tx = await callContractMutate(wallet, contractAddress, 'transfer', [
recipientAddress,
'100000000000000000000', // 100 tokens with 18 decimals
]);
console.log('Transfer TX:', tx.id);
```
### Listen to Events
```javascript
const ws = new WebSocket('ws://localhost:17111');
ws.onopen = () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'synor_subscribeContractEvents',
params: [contractAddress],
id: 1
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.method === 'synor_contractEvent') {
const { eventName, data } = msg.params;
if (eventName === 'Transfer') {
console.log(`Transfer: ${data.from} -> ${data.to}: ${data.amount}`);
}
}
};
```
---
## Part 7: Testing Your Contract
### Write Integration Tests
```rust
// tests/integration.rs
#[test]
fn test_token_transfer() {
let mut contract = Token::new(
"TestToken".to_string(),
"TT".to_string(),
18,
1_000_000 * 10u128.pow(18),
);
let alice = Address::from_hex("0x1234...").unwrap();
let bob = Address::from_hex("0x5678...").unwrap();
// Alice transfers to Bob
env::set_caller(alice);
assert!(contract.transfer(bob, 100 * 10u128.pow(18)));
// Check balances
assert_eq!(contract.balance_of(bob), 100 * 10u128.pow(18));
}
#[test]
fn test_insufficient_balance() {
let contract = Token::new(...);
env::set_caller(bob); // Bob has no tokens
assert!(!contract.transfer(alice, 100));
}
```
### Run Tests
```bash
cargo test
```
---
## Part 8: Best Practices
### Security Checklist
- [ ] Add access control (owner, admin roles)
- [ ] Validate all inputs
- [ ] Check for overflow (use checked_* operations)
- [ ] Test edge cases
- [ ] Audit before mainnet
### Gas Optimization
```rust
// Prefer: Direct storage access
let balance = self.balances.get(&addr);
// Avoid: Multiple reads
let b1 = self.balance_of(addr);
let b2 = self.balance_of(addr); // Wasteful!
// Prefer: Batch updates
self.balances.insert(from, from_balance - amount);
self.balances.insert(to, to_balance + amount);
```
### Error Handling
```rust
// Use require! for validation
require!(amount > 0, "Amount must be positive");
require!(balance >= amount, "Insufficient balance");
// Return Result for recoverable errors
pub fn safe_transfer(&mut self, to: Address, amount: u128) -> Result<(), &'static str> {
if amount == 0 {
return Err("Amount must be positive");
}
// ...
Ok(())
}
```
---
## Example Contracts
For more examples, see:
- **NFT Contract:** `contracts/examples/nft/`
- **DEX Contract:** `contracts/examples/dex/`
- **Multisig:** `contracts/examples/multisig/`
- **DAO Governance:** `contracts/examples/dao/`
---
## What's Next
- [Tutorial 4: Working with the API](./04-api-guide.md)
- [Host Functions Reference](../contracts/HOST_FUNCTIONS.md)
- [Contract SDK Documentation](../contracts/SDK_REFERENCE.md)
---
*Next: [Working with the API](./04-api-guide.md)*

View file

@ -0,0 +1,607 @@
# 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!*

88
docs/tutorials/README.md Normal file
View file

@ -0,0 +1,88 @@
# Synor Developer Tutorials
Welcome to the Synor tutorial series! These step-by-step guides will teach you to build on the first post-quantum secure blockchain.
## Tutorial Series
### 1. [Getting Started](./01-getting-started.md)
Set up your development environment, create a wallet, and send your first transaction.
**You'll learn:**
- Running a local Synor node
- Creating wallets and addresses
- Sending and receiving SYNOR
- Understanding hybrid signatures
### 2. [Building a Wallet Application](./02-building-a-wallet.md)
Build a complete React wallet application from scratch.
**You'll learn:**
- React + TypeScript setup
- BIP-39 mnemonic handling
- Encrypted wallet storage
- Balance display and QR codes
- Transaction history
### 3. [Smart Contracts on Synor](./03-smart-contracts.md)
Write, deploy, and interact with WebAssembly smart contracts.
**You'll learn:**
- Rust contract development
- Contract state management
- Events and logging
- Token contract example
- Testing and deployment
### 4. [Working with the API](./04-api-guide.md)
Master the Synor JSON-RPC and WebSocket APIs.
**You'll learn:**
- Query blockchain data
- Real-time subscriptions
- Transaction building
- Error handling
- Building API clients
---
## Quick Links
| Resource | Description |
|----------|-------------|
| [Developer Guide](../DEVELOPER_GUIDE.md) | Complete developer reference |
| [API Reference](../API_REFERENCE.md) | Full RPC documentation |
| [Exchange Integration](../EXCHANGE_INTEGRATION.md) | For exchanges |
| [Host Functions](../../contracts/HOST_FUNCTIONS.md) | Contract host API |
---
## Prerequisites
Before starting, ensure you have:
- **Docker & Docker Compose** - For running nodes
- **Node.js 18+** - For JavaScript/TypeScript examples
- **Rust 1.70+** - For contract development
- **Git** - For cloning repositories
---
## Getting Help
- **GitHub Issues:** [github.com/synor/synor/issues](https://github.com/synor/synor/issues)
- **Discord:** [discord.gg/synor](https://discord.gg/synor)
- **Documentation:** [docs.synor.cc](https://docs.synor.cc)
---
## Contributing
Found an issue or want to improve a tutorial? We welcome contributions!
1. Fork the repository
2. Make your changes
3. Submit a pull request
---
*Happy building on Synor!*