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.
517 lines
11 KiB
Markdown
517 lines
11 KiB
Markdown
# 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)*
|