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:
parent
960c25eb8a
commit
a6233f285d
5 changed files with 2254 additions and 0 deletions
271
docs/tutorials/01-getting-started.md
Normal file
271
docs/tutorials/01-getting-started.md
Normal 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)*
|
||||||
771
docs/tutorials/02-building-a-wallet.md
Normal file
771
docs/tutorials/02-building-a-wallet.md
Normal 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)*
|
||||||
517
docs/tutorials/03-smart-contracts.md
Normal file
517
docs/tutorials/03-smart-contracts.md
Normal 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)*
|
||||||
607
docs/tutorials/04-api-guide.md
Normal file
607
docs/tutorials/04-api-guide.md
Normal 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
88
docs/tutorials/README.md
Normal 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!*
|
||||||
Loading…
Add table
Reference in a new issue