# Evolve > Modular Rust SDK for building blockchain applications with Ethereum compatibility ## Collections Type-safe storage primitives for module state. ### Overview | Collection | Use Case | Iteration | | -------------------- | ---------------- | --------------------- | | `Item` | Single value | N/A | | `Map` | Key-value lookup | Deterministic | | `Vector` | Ordered list | Index-based | | `Queue` | FIFO processing | Front-to-back | | `UnorderedMap` | Large datasets | Session-deterministic | ### Item Single value storage: ```rust use evolve_collections::Item; static CONFIG: Item = Item::new(b"config"); // Operations CONFIG.set(env, value)?; let val = CONFIG.get(env)?; let exists = CONFIG.exists(env)?; CONFIG.remove(env)?; ``` ### Map Key-value mapping: ```rust use evolve_collections::Map; static BALANCES: Map = Map::new(b"bal"); // Basic operations BALANCES.set(env, key, value)?; let val = BALANCES.get(env, key)?; BALANCES.remove(env, key)?; // Atomic update BALANCES.update(env, key, |current| { Ok(current.unwrap_or(0) + amount) })?; // Iteration (deterministic order) for (key, value) in BALANCES.iter(env)? { // ... } ``` ### Vector Ordered dynamic array: ```rust use evolve_collections::Vector; static HISTORY: Vector = Vector::new(b"hist"); // Operations HISTORY.push(env, event)?; let event = HISTORY.get(env, index)?; let last = HISTORY.pop(env)?; let len = HISTORY.len(env)?; // Iteration for event in HISTORY.iter(env)? { // ... } ``` ### Queue FIFO queue: ```rust use evolve_collections::Queue; static PENDING: Queue = Queue::new(b"pending"); // Operations PENDING.enqueue(env, task)?; let task = PENDING.dequeue(env)?; let next = PENDING.peek(env)?; let empty = PENDING.is_empty(env)?; ``` ### UnorderedMap Optimized for large datasets: ```rust use evolve_collections::UnorderedMap; static METADATA: UnorderedMap = UnorderedMap::new(b"meta"); // Same API as Map // Better performance for: // - Large datasets (100k+ entries) // - Random access patterns // - When iteration order doesn't matter ``` ### Composite Keys Use tuples or custom types: ```rust // Tuple key static ALLOWANCES: Map<(AccountId, AccountId), u128> = Map::new(b"allow"); ALLOWANCES.set(env, (owner, spender), amount)?; // Custom key type #[derive(BorshSerialize, BorshDeserialize)] pub struct PositionKey { pub user: AccountId, pub pool: AccountId, } static POSITIONS: Map = Map::new(b"pos"); ``` ### Nested Collections ```rust // Map of vectors (conceptually) static USER_ORDERS: Map> = Map::new(b"orders"); // Better: separate collections with composite key static ORDERS: Map<(AccountId, u64), Order> = Map::new(b"orders"); static ORDER_COUNT: Map = Map::new(b"order_cnt"); ``` ### Size Limits | Limit | Value | | ------------------- | --------- | | Max key size | 254 bytes | | Max value size | 1 MB | | Max overlay entries | 100,000 | ### Serialization All types must implement Borsh: ```rust use borsh::{BorshSerialize, BorshDeserialize}; #[derive(BorshSerialize, BorshDeserialize)] pub struct MyData { pub field1: u64, pub field2: String, } ``` ## Macros Evolve provides procedural macros to eliminate boilerplate in module development. ### account\_impl The main macro for defining account modules: ```rust #[account_impl(MyAccount)] pub mod my_account { #[init] fn initialize(env: &impl Env, value: u128) -> SdkResult<()> { ... } #[exec] fn do_something(env: &impl Env, arg: String) -> SdkResult<()> { ... } #[query] fn get_value(env: &impl Env) -> SdkResult { ... } } ``` This generates: * `MyAccount` struct implementing `AccountCode` * Message types for each function * Function ID constants (SHA-256 of function name) * Dispatch logic in `execute` and `query` ### init Marks a one-time initialization function: ```rust #[init] fn initialize(env: &impl Env, admin: AccountId, config: Config) -> SdkResult<()> { ADMIN.set(env, admin)?; CONFIG.set(env, config)?; Ok(()) } ``` Rules: * Called exactly once when account is created * Must return `SdkResult<()>` * First parameter must be `env: &impl Env` ### exec Marks state-mutating functions: ```rust #[exec] fn transfer(env: &impl Env, to: AccountId, amount: u128) -> SdkResult<()> { // State changes are allowed BALANCES.set(env, env.sender(), new_balance)?; Ok(()) } #[exec] fn mint(env: &impl Env, amount: u128) -> SdkResult { // Can return data Ok(MintResult { new_supply }) } ``` Rules: * Can modify state * Can emit events * First parameter must be `env: &impl Env` * Returns `SdkResult` where T is serializable ### query Marks read-only query functions: ```rust #[query] fn balance_of(env: &impl Env, account: AccountId) -> SdkResult { Ok(BALANCES.get(env, account)?.unwrap_or(0)) } #[query] fn get_config(env: &impl Env) -> SdkResult { CONFIG.get(env)?.ok_or(ERR_NOT_INITIALIZED) } ``` Rules: * Cannot modify state (compile error if attempted) * First parameter must be `env: &impl Env` * Returns `SdkResult` where T is serializable ### Generated Code For a function like: ```rust #[exec] fn transfer(env: &impl Env, to: AccountId, amount: u128) -> SdkResult<()> ``` The macro generates: ```rust // Message struct #[derive(BorshSerialize, BorshDeserialize)] pub struct TransferMsg { pub to: AccountId, pub amount: u128, } impl TransferMsg { // Function ID from SHA-256("transfer") pub const ID: [u8; 4] = [0x12, 0x34, 0x56, 0x78]; } ``` ### Calling Generated Functions From other accounts: ```rust use my_module::TransferMsg; // Build and send message let msg = TransferMsg { to: recipient, amount: 100 }; env.do_exec(token_account_id, &msg, funds)?; ``` ### Best Practices 1. **Keep functions focused** - One responsibility per function 2. **Use descriptive names** - Function names become message types 3. **Document parameters** - Add doc comments above functions 4. **Validate early** - Check inputs at function start 5. **Return meaningful errors** - Use specific error codes ## Pre-built Modules Evolve includes ready-to-use modules for common functionality. ### Token Module Full-featured fungible token with mint/burn/transfer: ```rust use evolve_x::token::{Token, TokenConfig}; // Register in STF let stf = StfBuilder::new() .register_account::() .build(); // Genesis configuration let token_genesis = TokenConfig { name: "My Token".to_string(), symbol: "MTK".to_string(), decimals: 18, initial_supply: 1_000_000_000, admin: admin_account_id, }; ``` #### Token Features | Feature | Description | | --------------- | ------------------------------ | | `transfer` | Move tokens between accounts | | `transfer_from` | Move tokens with allowance | | `approve` | Set spending allowance | | `mint` | Create new tokens (admin only) | | `burn` | Destroy tokens | | `balance_of` | Query account balance | | `total_supply` | Query total supply | | `allowance` | Query spending allowance | #### Token Events ```rust // Emitted on transfers Event::Transfer { from, to, amount } // Emitted on approvals Event::Approval { owner, spender, amount } // Emitted on mint Event::Mint { to, amount } // Emitted on burn Event::Burn { from, amount } ``` ### Scheduler Module Block-level scheduling for begin/end block hooks: ```rust use evolve_x::scheduler::{Scheduler, ScheduledTask}; // Register in STF let stf = StfBuilder::new() .register_account::() .with_begin_blocker::() .with_end_blocker::() .build(); ``` #### Scheduler Features | Feature | Description | | ------------- | -------------------------------- | | `schedule` | Schedule a task for future block | | `cancel` | Cancel a scheduled task | | `get_pending` | Query pending tasks | #### Usage Example ```rust // Schedule a task for block 1000 let task = ScheduledTask { target: my_account_id, message: my_message, run_at_block: 1000, }; env.call(scheduler_id, &ScheduleMsg { task }, funds)?; ``` ### Gas Service Gas configuration and metering: ```rust use evolve_x::gas::{GasService, GasConfig}; // Configuration let gas_config = GasConfig { storage_read_per_byte: 10, storage_write_per_byte: 100, storage_remove_per_byte: 50, compute_per_unit: 1, }; ``` ### Using Pre-built Modules 1. Add dependency: ```toml [dependencies] evolve_x = { path = "crates/app/sdk/x" } ``` 2. Register in STF: ```rust use evolve_x::token::Token; use evolve_x::scheduler::Scheduler; let stf = StfBuilder::new() .register_account::() .register_account::() .with_begin_blocker::() .build(); ``` 3. Configure in genesis: ```rust let genesis = Genesis { accounts: vec![ (TOKEN_ID, token_config), (SCHEDULER_ID, scheduler_config), ], }; ``` ### Extending Pre-built Modules Wrap or compose with your own logic: ```rust #[account_impl(WrappedToken)] pub mod wrapped_token { fn transfer_with_fee( env: &impl Env, to: AccountId, amount: u128, ) -> SdkResult<()> { let fee = amount / 100; // 1% fee // Transfer to recipient env.call( TOKEN_ID, &TransferMsg { to, amount: amount - fee }, vec![], )?; // Transfer fee to treasury env.call( TOKEN_ID, &TransferMsg { to: TREASURY_ID, amount: fee }, vec![], )?; Ok(()) } } ``` ## Standards Evolve provides interface standards for common patterns. ### Fungible Asset The standard interface for fungible tokens: ```rust pub trait FungibleAsset { /// Get total supply fn total_supply(&self, env: &impl Env) -> SdkResult; /// Get balance of account fn balance_of(&self, env: &impl Env, account: AccountId) -> SdkResult; /// Transfer tokens fn transfer( &self, env: &impl Env, to: AccountId, amount: u128, ) -> SdkResult<()>; /// Transfer from another account (with allowance) fn transfer_from( &self, env: &impl Env, from: AccountId, to: AccountId, amount: u128, ) -> SdkResult<()>; /// Approve spender fn approve( &self, env: &impl Env, spender: AccountId, amount: u128, ) -> SdkResult<()>; /// Get allowance fn allowance( &self, env: &impl Env, owner: AccountId, spender: AccountId, ) -> SdkResult; } ``` ### Authentication The standard interface for account authentication: ```rust pub trait Authentication { /// Verify a signature fn verify( &self, env: &impl Env, message: &[u8], signature: &[u8], ) -> SdkResult; /// Get the public key fn public_key(&self, env: &impl Env) -> SdkResult>; } ``` ### Implementing Standards ```rust use evolve_standards::FungibleAsset; #[account_impl(MyToken)] pub mod my_token { // Implement standard methods #[query] fn total_supply(env: &impl Env) -> SdkResult { TOTAL_SUPPLY.get(env)?.ok_or(ERR_NOT_INITIALIZED) } #[query] fn balance_of(env: &impl Env, account: AccountId) -> SdkResult { Ok(BALANCES.get(env, account)?.unwrap_or(0)) } #[exec] fn transfer(env: &impl Env, to: AccountId, amount: u128) -> SdkResult<()> { // Implementation } // ... other methods } ``` ### Querying Standard Interfaces ```rust // Call any token that implements FungibleAsset let balance: u128 = env.do_query( token_id, &BalanceOfMsg { account: user }, )?; ``` ### Benefits 1. **Interoperability** - Modules can interact without knowing implementations 2. **Composability** - Build complex systems from standard components 3. **Tooling** - Wallets and explorers can work with any compliant token 4. **Testing** - Mock implementations for testing ## Production Deployment Guidelines for running Evolve in production. ### Hardware Requirements #### Minimum | Resource | Specification | | -------- | ------------- | | CPU | 4 cores | | RAM | 16 GB | | Storage | 500 GB SSD | | Network | 100 Mbps | #### Recommended | Resource | Specification | | -------- | -------------- | | CPU | 8+ cores | | RAM | 32+ GB | | Storage | 1+ TB NVMe SSD | | Network | 1 Gbps | ### Configuration #### Production config.yaml ```yaml chain: chain_id: 1 storage: path: "/var/lib/evolve/data" cache_size: 8GB write_buffer_size: 256MB rpc: enabled: true http_addr: "0.0.0.0:8545" ws_addr: "0.0.0.0:8546" max_connections: 1000 request_timeout_secs: 60 operations: shutdown_timeout_secs: 60 startup_checks: true min_disk_space_mb: 10240 observability: log_level: "info" log_format: "json" metrics_enabled: true metrics_addr: "0.0.0.0:9090" ``` ### Systemd Service Create `/etc/systemd/system/evolve.service`: ```ini [Unit] Description=Evolve Node After=network.target [Service] Type=simple User=evolve Group=evolve WorkingDirectory=/var/lib/evolve ExecStart=/usr/local/bin/evolve run --config /etc/evolve/config.yaml Restart=on-failure RestartSec=10 LimitNOFILE=65535 [Install] WantedBy=multi-user.target ``` Enable and start: ```bash sudo systemctl daemon-reload sudo systemctl enable evolve sudo systemctl start evolve ``` ### Security #### Firewall ```bash # Allow RPC (if needed externally) sudo ufw allow 8545/tcp # Allow metrics (internal only) sudo ufw allow from 10.0.0.0/8 to any port 9090 ``` #### Reverse Proxy (nginx) ```nginx upstream evolve { server 127.0.0.1:8545; } server { listen 443 ssl http2; server_name rpc.example.com; ssl_certificate /etc/letsencrypt/live/rpc.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/rpc.example.com/privkey.pem; location / { proxy_pass http://evolve; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } ``` ### Monitoring #### Prometheus Scrape config: ```yaml scrape_configs: - job_name: 'evolve' static_configs: - targets: ['localhost:9090'] ``` #### Key Metrics | Metric | Description | | ---------------------------- | --------------------- | | `evolve_block_height` | Current block height | | `evolve_tx_count` | Transaction count | | `evolve_storage_size_bytes` | Database size | | `evolve_rpc_requests_total` | RPC request count | | `evolve_rpc_latency_seconds` | RPC latency histogram | #### Alerts ```yaml groups: - name: evolve rules: - alert: EvolveStopped expr: increase(evolve_block_height[5m]) == 0 for: 5m labels: severity: critical - alert: EvolveHighLatency expr: histogram_quantile(0.99, evolve_rpc_latency_seconds_bucket) > 1 for: 5m labels: severity: warning ``` ### Backup #### Database Backup ```bash # Stop node first for consistent backup sudo systemctl stop evolve # Backup data directory tar -czf evolve-backup-$(date +%Y%m%d).tar.gz /var/lib/evolve/data # Restart node sudo systemctl start evolve ``` #### Automated Backups ```bash #!/bin/bash # /etc/cron.daily/evolve-backup BACKUP_DIR="/var/backups/evolve" DATA_DIR="/var/lib/evolve/data" RETENTION_DAYS=7 # Create backup tar -czf "$BACKUP_DIR/evolve-$(date +%Y%m%d).tar.gz" "$DATA_DIR" # Cleanup old backups find "$BACKUP_DIR" -name "evolve-*.tar.gz" -mtime +$RETENTION_DAYS -delete ``` ### Troubleshooting #### Check Logs ```bash journalctl -u evolve -f ``` #### Check Status ```bash curl http://localhost:8545 \ -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` #### Common Issues | Issue | Solution | | ------------------ | ----------------------------------- | | Out of disk space | Increase storage, enable pruning | | Connection refused | Check firewall, verify bind address | | High memory usage | Reduce cache\_size | | Slow sync | Check network, increase resources | ## Server Configuration Configure and run an Evolve node. ### Configuration File Create `config.yaml`: ```yaml chain: chain_id: 1 gas: storage_get_charge: 10 storage_set_charge: 100 storage_remove_charge: 50 storage: path: "./data" cache_size: 1GB write_buffer_size: 64MB rpc: enabled: true http_addr: "127.0.0.1:8545" ws_addr: "127.0.0.1:8546" client_version: "evolve/0.1.0" max_connections: 100 request_timeout_secs: 30 grpc: enabled: false addr: "127.0.0.1:9545" max_message_size: 4MB operations: shutdown_timeout_secs: 30 startup_checks: true min_disk_space_mb: 1024 observability: log_level: "info" log_format: "json" metrics_enabled: true metrics_addr: "127.0.0.1:9090" ``` ### Configuration Sections #### Chain | Field | Description | Default | | --------------------------- | ------------------- | ------- | | `chain_id` | Network identifier | 1 | | `gas.storage_get_charge` | Gas per byte read | 10 | | `gas.storage_set_charge` | Gas per byte write | 100 | | `gas.storage_remove_charge` | Gas per byte delete | 50 | #### Storage | Field | Description | Default | | ------------------- | ------------------ | -------- | | `path` | Data directory | "./data" | | `cache_size` | RocksDB cache size | 1GB | | `write_buffer_size` | Write buffer size | 64MB | #### RPC | Field | Description | Default | | ---------------------- | -------------------------- | ---------------- | | `enabled` | Enable JSON-RPC | true | | `http_addr` | HTTP endpoint | "127.0.0.1:8545" | | `ws_addr` | WebSocket endpoint | "127.0.0.1:8546" | | `client_version` | Version string | "evolve/0.1.0" | | `max_connections` | Max concurrent connections | 100 | | `request_timeout_secs` | Request timeout | 30 | #### Operations | Field | Description | Default | | ----------------------- | ------------------------- | ------- | | `shutdown_timeout_secs` | Graceful shutdown timeout | 30 | | `startup_checks` | Run startup validation | true | | `min_disk_space_mb` | Minimum free disk space | 1024 | #### Observability | Field | Description | Default | | ----------------- | --------------------------------------- | ---------------- | | `log_level` | Log level (trace/debug/info/warn/error) | "info" | | `log_format` | Log format (json/text) | "json" | | `metrics_enabled` | Enable Prometheus metrics | true | | `metrics_addr` | Metrics endpoint | "127.0.0.1:9090" | ### Running the Node ```bash # Development mode cargo run --release -- run --config config.yaml # With specific genesis cargo run --release -- run --config config.yaml --genesis genesis.json ``` ### Environment Variables Override config with environment variables: ```bash EVOLVE_CHAIN_ID=42 \ EVOLVE_RPC_HTTP_ADDR="0.0.0.0:8545" \ EVOLVE_LOG_LEVEL="debug" \ cargo run --release -- run ``` ### Startup Checks When `startup_checks: true`, the node validates: * Sufficient disk space * Valid configuration * Database integrity * Genesis file (if specified) ### Graceful Shutdown On SIGTERM/SIGINT: 1. Stop accepting new requests 2. Complete in-flight requests 3. Flush pending writes 4. Close database connections Timeout controlled by `shutdown_timeout_secs`. ## Module Architecture The Evolve module system uses an account-centric model where every piece of application logic is an account. ### Core Components ``` ┌─────────────────────────────────────────────────────────────┐ │ State Transition Function (STF) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ ExecutionState │ │ │ │ - Overlay (write cache) │ │ │ │ - Undo log (for rollback) │ │ │ │ - Events │ │ │ └─────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Invoker │ │ │ │ - Implements Environment/EnvironmentQuery │ │ │ │ - Manages call stack (max depth: 64) │ │ │ │ - Handles fund transfers │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Lifecycle Hooks Modules can implement lifecycle traits: ```rust pub trait BeginBlocker { fn begin_block(&self, block: &B, env: &mut dyn Environment); } pub trait EndBlocker { fn end_block(&self, env: &mut dyn Environment); } pub trait TxValidator { fn validate_tx(&self, tx: &Tx, env: &mut dyn Environment) -> SdkResult<()>; } pub trait PostTxExecution { fn after_tx_executed( tx: &Tx, gas_consumed: u64, tx_result: Result<...>, env: &mut dyn Environment, ) -> SdkResult<()>; } ``` ### Resource Limits | Limit | Value | Purpose | | ------------------- | --------- | ---------------------- | | Max call depth | 64 | Prevent stack overflow | | Max overlay entries | 100,000 | Memory bound | | Max events | 10,000 | Memory bound | | Max key size | 254 bytes | Storage efficiency | | Max value size | 1 MB | Storage efficiency | ### State Management #### Checkpoint/Restore Every `do_exec` call creates a checkpoint: ```rust let checkpoint = state.checkpoint(); match execute_call() { Ok(result) => result, Err(e) => { state.restore(checkpoint)?; // Rollback all changes return Err(e); } } ``` #### Storage Isolation Each account's storage is prefixed by its AccountId: ``` [AccountId][FieldPrefix][Key] => Value ``` ## Creating Modules This guide walks through creating a complete module from scratch. ### Module Structure A typical module consists of: ```rust // 1. Storage definitions static BALANCE: Map = Map::new(b"balance"); static TOTAL_SUPPLY: Item = Item::new(b"supply"); // 2. Account implementation #[account_impl(MyToken)] pub mod my_token { use super::*; #[init] fn initialize(env: &impl Env, initial_supply: u128) -> SdkResult<()> { // ... } #[exec] fn transfer(env: &impl Env, to: AccountId, amount: u128) -> SdkResult<()> { // ... } #[query] fn balance_of(env: &impl Env, account: AccountId) -> SdkResult { // ... } } ``` ### Step 1: Define Storage Choose the appropriate collection type: | Type | Use Case | | -------------------- | --------------------------------------- | | `Item` | Single value (config, counter) | | `Map` | Key-value lookup (balances, allowances) | | `Vector` | Ordered list (history, logs) | | `Queue` | FIFO processing | | `UnorderedMap` | Large datasets | ```rust use evolve_collections::{Item, Map, Vector}; // Each field needs a unique prefix static CONFIG: Item = Item::new(b"config"); static BALANCES: Map = Map::new(b"bal"); static HISTORY: Vector = Vector::new(b"hist"); ``` ### Step 2: Define Types Types must implement serialization: ```rust use borsh::{BorshSerialize, BorshDeserialize}; #[derive(BorshSerialize, BorshDeserialize)] pub struct Config { pub admin: AccountId, pub paused: bool, } #[derive(BorshSerialize, BorshDeserialize)] pub struct Transfer { pub from: AccountId, pub to: AccountId, pub amount: u128, } ``` ### Step 3: Implement Functions #### Initialization (`#[init]`) Called once when the account is created: ```rust #[init] fn initialize(env: &impl Env, admin: AccountId) -> SdkResult<()> { CONFIG.set(env, Config { admin, paused: false })?; Ok(()) } ``` #### Execution (`#[exec]`) State-mutating operations: ```rust #[exec] fn transfer(env: &impl Env, to: AccountId, amount: u128) -> SdkResult<()> { let sender = env.sender(); // Read and validate let sender_balance = BALANCES.get(env, sender)?.unwrap_or(0); if sender_balance < amount { return Err(SdkError::insufficient_funds()); } // Update state BALANCES.set(env, sender, sender_balance - amount)?; let to_balance = BALANCES.get(env, to)?.unwrap_or(0); BALANCES.set(env, to, to_balance + amount)?; // Emit event env.emit_event("transfer", &Transfer { from: sender, to, amount })?; Ok(()) } ``` #### Queries (`#[query]`) Read-only operations: ```rust #[query] fn balance_of(env: &impl Env, account: AccountId) -> SdkResult { Ok(BALANCES.get(env, account)?.unwrap_or(0)) } ``` ### Step 4: Register the Module Register your account code in the STF: ```rust use evolve_stf::StfBuilder; let stf = StfBuilder::new() .register_account::() .build(); ``` ### Step 5: Test Use MockEnv for unit tests: ```rust #[test] fn test_transfer() { let env = MockEnv::builder() .with_caller(AccountId::new(1)) .build(); // Initialize my_token::initialize(&env, AccountId::new(1)).unwrap(); // Set initial balance BALANCES.set(&env, AccountId::new(1), 1000).unwrap(); // Transfer my_token::transfer(&env, AccountId::new(2), 100).unwrap(); // Verify assert_eq!(my_token::balance_of(&env, AccountId::new(1)).unwrap(), 900); assert_eq!(my_token::balance_of(&env, AccountId::new(2)).unwrap(), 100); } ``` ## Error Handling Evolve uses error codes for compact, deterministic error handling. ### Defining Errors Use the `define_error!` macro: ```rust use evolve_core::define_error; define_error!(ERR_NOT_ENOUGH_BALANCE, 0x01, "not enough balance"); define_error!(ERR_UNAUTHORIZED, 0x02, "unauthorized"); define_error!(ERR_OVERFLOW, 0x03, "arithmetic overflow"); ``` ### Error Code Namespacing Prevent collisions between modules with namespaced codes: ```rust // Each module gets a range const MODULE_BASE: u16 = 0x0100; define_error!(ERR_MY_ERROR, MODULE_BASE | 0x01, "my error"); define_error!(ERR_ANOTHER, MODULE_BASE | 0x02, "another error"); ``` #### Reserved Ranges | Range | Module | | --------------- | ---------------- | | `0x0000-0x00FF` | Core SDK errors | | `0x0100-0x01FF` | Token module | | `0x0200-0x02FF` | Gas module | | `0x0300-0x03FF` | Scheduler module | | `0x0400+` | Custom modules | ### Core SDK Errors | Error | Code | Description | | -------------------------- | ------ | ------------------------------ | | `ERR_ENCODING` | `0x01` | Borsh encoding/decoding failed | | `ERR_UNKNOWN_FUNCTION` | `0x02` | Function ID not found | | `ERR_ACCOUNT_NOT_INIT` | `0x03` | Account not initialized | | `ERR_UNAUTHORIZED` | `0x04` | Sender not authorized | | `ERR_NOT_PAYABLE` | `0x05` | Function doesn't accept funds | | `ERR_ONE_COIN` | `0x06` | Expected exactly one coin | | `ERR_INCOMPATIBLE_FA` | `0x07` | Incompatible fungible asset | | `ERR_INSUFFICIENT_BALANCE` | `0x08` | Insufficient balance | | `ERR_OVERFLOW` | `0x09` | Arithmetic overflow | ### Using ensure! For conditional error returns: ```rust use evolve_core::ensure; fn transfer(&self, to: AccountId, amount: u128, env: &mut dyn Environment) -> SdkResult<()> { let balance = self.balances.get(&env.sender(), env)?; // Returns Err(ERR_INSUFFICIENT_BALANCE) if false ensure!(balance >= amount, ERR_INSUFFICIENT_BALANCE); // ... proceed with transfer Ok(()) } ``` ### Error Propagation Use `?` for clean propagation: ```rust fn complex_operation(&self, env: &mut dyn Environment) -> SdkResult<()> { let value = self.storage.get(env)?; let result = self.other_account.query(env)?; self.storage.set(&new_value, env)?; Ok(()) } ``` ### Handling Optional Values ```rust // BAD - panics if not found let value = self.data.may_get(env)?.unwrap(); // GOOD - returns error if not found let value = self.data.may_get(env)?.ok_or(ERR_NOT_FOUND)?; // GOOD - use default let value = self.data.may_get(env)?.unwrap_or_default(); ``` ### Transaction Atomicity Errors automatically trigger rollback: ```rust fn multi_step_operation(&self, env: &mut dyn Environment) -> SdkResult<()> { self.step_one(env)?; // Succeeds self.step_two(env)?; // Fails - step_one is rolled back Ok(()) } ``` ### Error Decoding (Development) Enable human-readable errors: ```toml [dependencies] evolve_core = { workspace = true, features = ["error-decode"] } ``` ### Best Practices 1. **Use namespaced error codes** - Prevent collisions 2. **Prefer ensure!** - Cleaner than manual if/return 3. **Never use unwrap()** - Use `ok_or(ERR_...)` or `unwrap_or_default()` 4. **Document error conditions** - Add doc comments 5. **Keep error messages short** - They're stored in the binary ## Storage Evolve provides type-safe, collision-free storage primitives for module state. ### Storage Patterns There are two patterns for defining storage: #### Static Storage (Simple) For straightforward modules, use static storage with byte prefixes: ```rust use evolve_collections::{Item, Map}; static CONFIG: Item = Item::new(b"config"); static BALANCES: Map = Map::new(b"bal"); ``` #### AccountState Derive (Recommended) For compile-time validation and struct-based organization: ```rust #[derive(evolve_core::AccountState)] pub struct MyModule { #[storage(0)] pub counter: Item, #[storage(1)] pub balances: Map, #[storage(2)] pub metadata: Item, } ``` Benefits of AccountState: * **Compile-time validation** - Duplicate prefixes are caught at build time * **Refactor safety** - Reordering fields doesn't change storage layout * **Migration safety** - Adding new fields doesn't corrupt existing data #### Multi-Prefix Collections Some collections require multiple prefixes: | Collection | Prefixes | Example | | ------------------- | -------- | ------------------------------- | | `Item` | 1 | `Item::new(0)` | | `Map` | 1 | `Map::new(0)` | | `Vector` | 2 | `Vector::new(0, 1)` | | `Queue` | 2 | `Queue::new(0, 1)` | | `UnorderedMap` | 4 | `UnorderedMap::new(0, 1, 2, 3)` | For multi-prefix collections, use manual initialization: ```rust pub struct ComplexModule { validators: UnorderedMap, changes: Vector, } impl ComplexModule { pub const fn new() -> Self { Self { validators: UnorderedMap::new(0, 1, 2, 3), changes: Vector::new(4, 5), } } } ``` ### Collection Types #### Item Single value storage: ```rust use evolve_collections::Item; static CONFIG: Item = Item::new(b"config"); // Set value CONFIG.set(env, Config { admin, paused: false })?; // Get value let config = CONFIG.get(env)?.unwrap_or_default(); // Check existence if CONFIG.exists(env)? { // ... } // Remove value CONFIG.remove(env)?; ``` #### Map Key-value mapping with deterministic iteration: ```rust use evolve_collections::Map; static BALANCES: Map = Map::new(b"bal"); // Set value BALANCES.set(env, account, 1000)?; // Get value let balance = BALANCES.get(env, account)?.unwrap_or(0); // Update atomically BALANCES.update(env, account, |balance| { Ok(balance.unwrap_or(0) + amount) })?; // Remove entry BALANCES.remove(env, account)?; // Iterate (deterministic order) for (key, value) in BALANCES.iter(env)? { // ... } ``` #### Vector Dynamic ordered array: ```rust use evolve_collections::Vector; static HISTORY: Vector = Vector::new(b"hist"); // Push to end HISTORY.push(env, event)?; // Get by index let event = HISTORY.get(env, 0)?; // Get length let len = HISTORY.len(env)?; // Pop from end let last = HISTORY.pop(env)?; // Iterate for event in HISTORY.iter(env)? { // ... } ``` #### Queue FIFO queue for processing: ```rust use evolve_collections::Queue; static PENDING: Queue = Queue::new(b"pending"); // Enqueue (add to back) PENDING.enqueue(env, task)?; // Dequeue (remove from front) let task = PENDING.dequeue(env)?; // Peek at front let next = PENDING.peek(env)?; // Check if empty if PENDING.is_empty(env)? { // ... } ``` #### UnorderedMap Hash-based mapping for large datasets: ```rust use evolve_collections::UnorderedMap; static METADATA: UnorderedMap = UnorderedMap::new(b"meta"); // Same API as Map, but optimized for: // - Large datasets // - Random access patterns // - When iteration order doesn't matter ``` ### Storage Prefixes Each collection needs a unique prefix to avoid collisions: ```rust // Good: unique prefixes static BALANCES: Map = Map::new(b"bal"); static ALLOWANCES: Map<(AccountId, AccountId), u128> = Map::new(b"allow"); // Bad: collision! static A: Map = Map::new(b"data"); static B: Map = Map::new(b"data"); // Same prefix! ``` ### Serialization Types must implement Borsh serialization: ```rust use borsh::{BorshSerialize, BorshDeserialize}; #[derive(BorshSerialize, BorshDeserialize)] pub struct Config { pub admin: AccountId, pub fee_rate: u64, } ``` ### Best Practices #### Use Typed Keys ```rust // Good: type-safe key #[derive(BorshSerialize, BorshDeserialize)] pub struct AllowanceKey { owner: AccountId, spender: AccountId, } static ALLOWANCES: Map = Map::new(b"allow"); // Also good: tuple key static ALLOWANCES: Map<(AccountId, AccountId), u128> = Map::new(b"allow"); ``` #### Batch Updates ```rust // Bad: multiple writes for account in accounts { BALANCES.set(env, account, amount)?; } // Better: use update for atomic operations BALANCES.update(env, account, |bal| Ok(bal.unwrap_or(0) + amount))?; ``` #### Lazy Loading ```rust // Only load what you need let balance = BALANCES.get(env, specific_account)?; // Avoid loading everything // let all = BALANCES.iter(env)?.collect::>(); ``` ### Helper Fields For stateless helper types (like `EventsEmitter`), use `#[skip_storage]`: ```rust #[derive(evolve_core::AccountState)] pub struct MyModule { #[storage(0)] pub data: Item, #[skip_storage] pub events: EventsEmitter, // Initialized with Type::new() } ``` ### Size Limits | Limit | Value | | ------------------- | ----------------------- | | Max key size | 254 bytes | | Max value size | 1 MB | | Max overlay entries | 100,000 per transaction | | Max events | 10,000 per execution | ## Testing Evolve provides multiple testing levels: unit tests with MockEnv, integration tests with TestApp, and simulation tests with SimTestApp. ### Unit Testing with MockEnv ```rust use evolve_core::{AccountId, Environment}; use evolve_testing::MockEnv; #[test] fn test_basic_operation() { let contract_id = AccountId::new(1); let sender_id = AccountId::new(100); let mut env = MockEnv::new(contract_id, sender_id); let module = MyModule::default(); module.initialize(42, &mut env).expect("init failed"); let result = module.increment(&mut env).expect("increment failed"); assert_eq!(result, 43); } ``` ### MockEnv Builder ```rust let mut env = MockEnv::new(contract_id, sender_id) .with_sender(new_sender) .with_funds(vec![fungible_asset]) .with_account_balance(account, token, amount); ``` ### Testing Authorization ```rust #[test] fn test_unauthorized_access() { let contract_id = AccountId::new(1); let owner_id = AccountId::new(100); let attacker_id = AccountId::new(999); let mut env = MockEnv::new(contract_id, owner_id); let module = MyModule::default(); module.initialize(&mut env).unwrap(); // Try as attacker env = env.with_sender(attacker_id); let result = module.admin_only_function(&mut env); assert!(matches!(result, Err(e) if e == ERR_UNAUTHORIZED)); } ``` ### Testing Payable Functions ```rust #[test] fn test_deposit() { let mut env = MockEnv::new(contract_id, depositor_id) .with_funds(vec![FungibleAsset { asset_id: token_id, amount: 1000, }]); let module = MyModule::default(); module.deposit(&mut env).expect("deposit failed"); assert_eq!(env.funds().len(), 0); // Funds consumed } ``` ### Testing Error Conditions ```rust #[test] fn test_insufficient_balance() { let mut env = setup_env(); let module = MyModule::default(); module.initialize(vec![(sender, 100)], &mut env).unwrap(); let result = module.transfer(recipient, 1000, &mut env); assert!(matches!(result, Err(e) if e == ERR_NOT_ENOUGH_BALANCE)); } ``` ### Property-Based Testing Use proptest for exhaustive testing: ```rust use proptest::prelude::*; proptest! { #[test] fn test_transfer_preserves_total( balance1 in 0u128..1_000_000, balance2 in 0u128..1_000_000, transfer_amount in 0u128..1_000_000, ) { let mut env = setup_env(); let module = MyModule::default(); module.initialize( vec![(account1, balance1), (account2, balance2)], &mut env ).unwrap(); let total_before = balance1 + balance2; // Attempt transfer (may fail) let _ = module.transfer(account1, account2, transfer_amount, &mut env); // Total preserved regardless let b1 = module.get_balance(account1, &env).unwrap().unwrap_or(0); let b2 = module.get_balance(account2, &env).unwrap().unwrap_or(0); prop_assert_eq!(b1 + b2, total_before); } } ``` ### Integration Testing with TestApp ```rust use evolve_testapp::TestApp; #[test] fn test_full_block_execution() { let app = TestApp::new(); let result = app.execute_block(vec![ transaction1, transaction2, ]); assert!(result.is_ok()); assert_eq!(result.tx_results.len(), 2); } ``` ### Simulation Testing For deterministic testing with fault injection: ```rust use evolve_testing::SimTestApp; #[test] fn test_with_simulation() { let sim = SimTestApp::builder() .with_seed(42) // Deterministic .with_fault_injection(true) .build(); // Run simulation sim.run_for_blocks(100); // Check invariants assert!(sim.check_invariants()); } ``` ### Testing Determinism Run the same test multiple times and verify identical results: ```rust #[test] fn test_determinism() { for _ in 0..100 { let result = execute_module_logic(); assert_eq!(result, EXPECTED_VALUE); } } ``` ### Testing Overflow Protection ```rust #[test] fn test_overflow_protection() { let mut env = setup_env(); let module = MyModule::default(); // Set balance near u128::MAX module.set_balance(u128::MAX - 10, &mut env).unwrap(); // Adding 100 should overflow let result = module.add_balance(100, &mut env); assert!(matches!(result, Err(e) if e == ERR_OVERFLOW)); } ``` ### Best Practices 1. **Test both success and failure paths** 2. **Use property-based testing for arithmetic** 3. **Test authorization for all privileged functions** 4. **Test edge cases** (zero, max values, empty collections) 5. **Test determinism** by running tests multiple times 6. **Test overflow protection** for all arithmetic operations ## Installation ### Prerequisites * **Rust 1.86.0** or later * **Cargo** (comes with Rust) ### Install Rust If you don't have Rust installed: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Verify your installation: ```bash rustc --version # Should be 1.86.0 or later cargo --version ``` ### Clone Evolve ```bash git clone https://github.com/evolvesdk/evolve.git cd evolve ``` ### Build ```bash # Build the project cargo build # Run tests cargo test # Format code cargo fmt # Run lints cargo clippy ``` ### Add to Your Project Add the SDK crates to your `Cargo.toml`: ```toml [dependencies] evolve_core = { path = "crates/app/sdk/core" } evolve_macros = { path = "crates/app/sdk/macros" } evolve_collections = { path = "crates/app/sdk/collections" } ``` Or if using a published version: ```toml [dependencies] evolve_core = "0.1" evolve_macros = "0.1" evolve_collections = "0.1" ``` ## Quickstart Build your first account module in 5 minutes. ### 1. Define Storage Every account has isolated storage. Define your state using typed collections: ```rust use evolve_collections::Item; // Single value storage static VALUE: Item = Item::new(b"value"); ``` ### 2. Create Your Account Use the `#[account_impl]` macro to define your account module: ```rust use evolve_core::prelude::*; use evolve_macros::account_impl; use evolve_collections::Item; static VALUE: Item = Item::new(b"value"); #[account_impl(Counter)] pub mod counter { use super::*; #[init] fn initialize(env: &impl Env) -> SdkResult<()> { VALUE.set(env, 0)?; Ok(()) } #[exec] fn increment(env: &impl Env) -> SdkResult<()> { let current = VALUE.get(env)?.unwrap_or(0); VALUE.set(env, current + 1)?; Ok(()) } #[query] fn get_value(env: &impl Env) -> SdkResult { Ok(VALUE.get(env)?.unwrap_or(0)) } } ``` #### Function Types | Macro | Purpose | State Changes | Returns | | ---------- | ------------------------ | ------------- | --------------- | | `#[init]` | One-time initialization | Yes | `SdkResult<()>` | | `#[exec]` | State-mutating execution | Yes | `SdkResult` | | `#[query]` | Read-only queries | No | `SdkResult` | ### 3. Write Tests Use `MockEnv` for unit testing: ```rust use evolve_testing::MockEnv; #[test] fn test_counter() { let env = MockEnv::builder() .with_caller(AccountId::new(1)) .build(); counter::initialize(&env).unwrap(); counter::increment(&env).unwrap(); assert_eq!(counter::get_value(&env).unwrap(), 1); } ``` ### 4. Register Your Account Register your account code in the State Transition Function: ```rust use evolve_stf::StfBuilder; let stf = StfBuilder::new() .register_account::() .build(); ``` ### 5. Run ```bash cargo test cargo run ``` ### Next Steps * [Storage Collections](/modules/storage) - Learn about Maps, Vectors, and Queues * [Error Handling](/modules/errors) - Proper error handling patterns * [Testing](/modules/testing) - Advanced testing with TestApp and Simulator ## Running the Testapp The `testapp` crate is a complete example application demonstrating all Evolve features. ### Run the Dev Node ```bash cd crates/app/testapp cargo run -- run ``` This starts: * JSON-RPC server at `http://localhost:8545` * Block production with configurable interval * In-memory or persistent storage ### Configuration Create a `config.yaml`: ```yaml chain: chain_id: 1 gas: storage_get_charge: 10 storage_set_charge: 10 storage_remove_charge: 10 storage: path: "./data" cache_size: 1GB write_buffer_size: 64MB rpc: enabled: true http_addr: "127.0.0.1:8545" client_version: "evolve/0.1.0" grpc: enabled: false addr: "127.0.0.1:9545" max_message_size: 4MB operations: shutdown_timeout_secs: 30 startup_checks: true min_disk_space_mb: 1024 observability: log_level: "info" log_format: "json" metrics_enabled: true ``` ### Connect a Wallet 1. Open MetaMask or any Ethereum wallet 2. Add a custom network: * **RPC URL**: `http://localhost:8545` * **Chain ID**: `1` (or your configured chain\_id) * **Currency Symbol**: `ETH` ### Interact via JSON-RPC ```bash # Get block number curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' # Send transaction curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x..."],"id":1}' ``` ### What's Included The testapp demonstrates: | Feature | Description | | ----------------------- | -------------------------------------- | | Token module | Fungible token with mint/burn/transfer | | Scheduler | Begin/end block hooks | | Gas metering | EIP-1559 gas pricing | | Transaction validation | ECDSA signature verification | | WebSocket subscriptions | `newHeads` and log filtering | ### Next Steps * [Core Concepts](/concepts/accounts) - Understand the account model * [Building Modules](/modules/architecture) - Create your own modules ## Transactions Evolve supports Ethereum-compatible transactions with EIP-2718 typed transactions and EIP-1559 gas pricing. ### Transaction Types #### Legacy Transactions (Type 0) ```rust LegacyTransaction { nonce: u64, gas_price: u128, gas_limit: u64, to: Option
, value: u128, data: Vec, v: u64, r: [u8; 32], s: [u8; 32], } ``` #### EIP-1559 Transactions (Type 2) ```rust Eip1559Transaction { chain_id: u64, nonce: u64, max_priority_fee_per_gas: u128, max_fee_per_gas: u128, gas_limit: u64, to: Option
, value: u128, data: Vec, access_list: Vec, v: u64, r: [u8; 32], s: [u8; 32], } ``` ### Sending Transactions #### Using ethers.js ```javascript const tx = await wallet.sendTransaction({ to: recipientAddress, value: ethers.parseEther("1.0"), maxFeePerGas: ethers.parseGwei("20"), maxPriorityFeePerGas: ethers.parseGwei("2"), }); await tx.wait(); ``` #### Using curl ```bash curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_sendRawTransaction", "params": ["0x...signed_tx_hex..."], "id": 1 }' ``` ### Transaction Lifecycle ``` 1. User signs transaction │ ▼ 2. Submit via JSON-RPC (eth_sendRawTransaction) │ ▼ 3. Mempool validation - Signature verification - Nonce check - Balance check │ ▼ 4. Block inclusion - Ordered by gas price │ ▼ 5. STF processing - Validate - Execute - Post-tx handler (fees) │ ▼ 6. State committed ``` ### Gas Model #### EIP-1559 Parameters | Parameter | Description | | -------------------------- | -------------------------------------------- | | `base_fee` | Protocol-set minimum fee (adjusts per block) | | `max_fee_per_gas` | Maximum total fee user will pay | | `max_priority_fee_per_gas` | Tip to block producer | #### Effective Gas Price ``` effective_gas_price = min(max_fee_per_gas, base_fee + max_priority_fee_per_gas) ``` #### Gas Costs | Operation | Cost | | ------------------------- | ------- | | Storage read (per byte) | 10 gas | | Storage write (per byte) | 100 gas | | Storage delete (per byte) | 50 gas | | Compute (per unit) | 1 gas | ### ECDSA Signatures Evolve uses standard Ethereum ECDSA signatures: ```rust use evolve_tx::ecdsa::{recover_signer, sign_message}; // Recover signer from signature let signer = recover_signer(&message_hash, &signature)?; // Sign a message let signature = sign_message(&private_key, &message_hash)?; ``` ### RLP Encoding Transactions are RLP encoded per Ethereum standards: ```rust use evolve_tx::rlp::{encode, decode}; // Encode transaction let encoded = encode(&transaction)?; // Decode transaction let decoded: Transaction = decode(&bytes)?; ``` ### Account/Address Mapping Evolve maps between AccountId (u128) and Ethereum Address (20 bytes): ```rust // AccountId to Address let address = Address::from_account_id(account_id); // Address to AccountId let account_id = address.to_account_id(); ``` ## Wallet Integration Evolve is compatible with standard Ethereum wallets. ### MetaMask Setup 1. Open MetaMask 2. Click network dropdown → "Add Network" 3. Enter network details: | Field | Value | | --------------- | ---------------------------------------------- | | Network Name | Evolve Local | | RPC URL | [http://localhost:8545](http://localhost:8545) | | Chain ID | 1 (or your configured chain\_id) | | Currency Symbol | ETH | 4. Click "Save" ### Using ethers.js ```javascript import { ethers } from 'ethers'; // Connect to local node const provider = new ethers.JsonRpcProvider('http://localhost:8545'); // Create wallet const wallet = new ethers.Wallet(privateKey, provider); // Get balance const balance = await provider.getBalance(wallet.address); console.log('Balance:', ethers.formatEther(balance)); // Send transaction const tx = await wallet.sendTransaction({ to: recipientAddress, value: ethers.parseEther('1.0'), }); await tx.wait(); ``` ### Using viem ```typescript import { createPublicClient, createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; // Create clients const publicClient = createPublicClient({ transport: http('http://localhost:8545'), }); const account = privateKeyToAccount('0x...'); const walletClient = createWalletClient({ account, transport: http('http://localhost:8545'), }); // Get balance const balance = await publicClient.getBalance({ address: account.address, }); // Send transaction const hash = await walletClient.sendTransaction({ to: recipientAddress, value: parseEther('1.0'), }); ``` ### Using wagmi (React) ```typescript import { WagmiConfig, createConfig, http } from 'wagmi'; import { defineChain } from 'viem'; // Define custom chain const evolveLocal = defineChain({ id: 1, name: 'Evolve Local', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['http://localhost:8545'] }, }, }); // Create config const config = createConfig({ chains: [evolveLocal], transports: { [evolveLocal.id]: http(), }, }); // Use in app function App() { return ( {/* Your app */} ); } ``` ### Address Format Evolve uses standard Ethereum addresses (20 bytes, hex-encoded with 0x prefix): ``` 0x742d35Cc6634C0532925a3b844Bc9e7595f1Ef67 ``` #### Address to AccountId Mapping ```rust // Ethereum address (20 bytes) maps to AccountId (u128) // Lower 16 bytes of address become the AccountId let account_id = AccountId::from_address(address); let address = account_id.to_address(); ``` ### Transaction Signing All standard Ethereum signing methods work: ```javascript // Personal sign const signature = await wallet.signMessage('Hello'); // Typed data (EIP-712) const signature = await wallet._signTypedData(domain, types, value); // Transaction signing (automatic with sendTransaction) ``` ### Supported Features | Feature | Status | | ------------------- | --------- | | EOA transactions | Supported | | EIP-1559 gas | Supported | | Legacy gas | Supported | | Contract calls | Supported | | Event logs | Supported | | Block subscriptions | Supported | | EIP-712 signing | Supported | ## Accounts Everything in Evolve is an account. This account-centric model is the foundation of the entire system. ### What is an Account? An account has three components: | Component | Description | | ------------ | ----------------------------------------- | | **Identity** | Unique `AccountId` (u128) | | **Code** | Implementation of the `AccountCode` trait | | **State** | Isolated storage namespace | ### The AccountCode Trait Every account implements the `AccountCode` trait: ```rust pub trait AccountCode: Send + Sync { fn execute( &self, env: &mut dyn Environment, request: InvokeRequest, ) -> SdkResult>; fn query( &self, env: &dyn EnvironmentQuery, request: InvokeRequest, ) -> SdkResult>; } ``` The `#[account_impl]` macro generates this implementation for you. ### System Accounts Reserved AccountId values with special behavior: | ID | Name | Purpose | | -- | --------------------------- | ----------------------------- | | 0 | `RUNTIME_ACCOUNT_ID` | Account creation, migrations | | 1 | `STORAGE_ACCOUNT_ID` | Storage read/write operations | | 2 | `EVENT_HANDLER_ACCOUNT_ID` | Event emission | | 5 | `UNIQUE_HANDLER_ACCOUNT_ID` | Unique ID generation | System accounts have hardcoded handlers in the invoker. ### Inter-Account Calls Accounts communicate through message passing: ```rust // Account A calls Account B env.do_exec(account_b_id, &message, funds)?; // Internally: // 1. Invoker creates checkpoint // 2. Funds are transferred (if any) // 3. Account B's code executes // 4. On error: checkpoint restored // 5. On success: changes committed ``` ### Message Dispatch The `#[account_impl]` macro generates: 1. **Message structs** for each function 2. **Function IDs** from SHA-256 of function name 3. **Dispatch logic** in `AccountCode::execute/query` ### Storage Isolation Each account's storage is prefixed by its AccountId: ``` [AccountId][FieldPrefix][Key] => Value Example: [0x0A][0x01][alice] => 1000 // Account 10, field 1, key "alice" ``` This prevents accounts from accidentally (or maliciously) accessing each other's state. ### No Sudo Model There is no privileged escalation in Evolve. All execution is account-driven: * No "admin" accounts with special powers * No way to bypass account code * All state changes go through the account's execute method * System accounts are explicitly defined and limited in scope ## Concurrency Model Evolve uses a hybrid sync/async model optimized for deterministic execution while maximizing performance where possible. ### Overview The STF is single-threaded and synchronous for determinism. Parallelism is used only where it doesn't affect execution order: ``` ┌──────────────────────────────────────────────────────────────┐ │ PARALLEL (Pre-consensus) │ │ Signature verification, cache warming, network I/O │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ SEQUENTIAL (Block execution) │ │ Transaction processing, state transitions, event emission │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ PARALLEL (Post-commit) │ │ Indexing, notifications, cache updates │ └──────────────────────────────────────────────────────────────┘ ``` ### Why Single-Threaded Execution? | Requirement | Why Sequential | | ----------------- | ---------------------------------------- | | Determinism | Same order = same result on all nodes | | Atomicity | Checkpoint/restore assumes single writer | | Simplicity | No complex concurrency bugs | | State consistency | No race conditions on storage | ### Where Parallelism is Used #### 1. Signature Verification Before block execution, signatures can be verified in parallel: ```rust // Done before STF receives the block let valid_sigs: Vec = transactions .par_iter() .map(|tx| verify_signature(tx)) .collect(); ``` #### 2. Storage Cache The storage layer uses sharded locking for concurrent reads: ```rust // 256 shards for cache access struct ShardedCache { shards: [RwLock>; 256], } impl ShardedCache { fn shard_for_key(&self, key: &Key) -> usize { hash(key) % 256 } } ``` #### 3. Database I/O Async I/O for disk operations that don't affect execution order: ```rust // Background commit doesn't block next block tokio::spawn(async move { storage.commit_to_disk(changes).await; }); ``` ### Send/Sync Analysis | Type | Send | Sync | Reason | | ------------------- | ---- | ---- | ------------------------- | | `AccountCode` | Yes | Yes | Stored in shared registry | | `ExecutionState` | No | No | Contains \&mut references | | `Invoker` | No | No | Contains \&mut references | | `CommonwareStorage` | Yes | Yes | Needs cross-thread access | ### Best Practices #### Do * Use Evolve collections (deterministic by design) * Pass data through the `Environment` trait * Use block time instead of system time * Derive randomness from chain state #### Don't * Spawn threads in module code * Use async in module code * Access external state (files, network, env vars) * Use HashMap/HashSet for iteration ### Performance Characteristics | Operation | Time Complexity | Notes | | --------------------- | --------------- | ----------------------------- | | Storage read (cached) | O(1) | Sharded lock, no contention | | Storage read (miss) | O(log n) | RocksDB lookup | | Storage write | O(1) | Overlay only during execution | | Checkpoint | O(1) | Just captures indices | | Restore | O(k) | k = changes since checkpoint | | Commit | O(n) | n = total overlay size | ## Determinism All module code must be deterministic. Given identical inputs, execution must produce identical outputs on all nodes. Violating determinism causes consensus failures. ### Banned Patterns #### 1. Non-Deterministic Collections **NEVER use `HashMap` or `HashSet`** - iteration order varies by platform/run. ```rust // BAD - iteration order is non-deterministic use std::collections::HashMap; let map = HashMap::new(); for (k, v) in &map { /* order varies! */ } // GOOD - use BTreeMap for deterministic ordering use std::collections::BTreeMap; let map = BTreeMap::new(); for (k, v) in &map { /* order is consistent */ } // BEST - use Evolve's storage collections use evolve_collections::map::Map; let map: Map = Map::new(0); ``` #### 2. System Time **NEVER use `std::time`** - system clocks vary between nodes. ```rust // BAD - non-deterministic use std::time::{Instant, SystemTime}; let now = SystemTime::now(); // GOOD - use block time from environment let block_time = env.block_time_ms(); ``` #### 3. Random Number Generation **NEVER use `rand` or any RNG** without deterministic seeding from chain state. ```rust // BAD - non-deterministic use rand::Rng; let random = rand::thread_rng().gen::(); // GOOD - derive randomness from block hash or chain state let seed = block_hash.as_bytes(); ``` #### 4. Floating Point Arithmetic **NEVER use `f32` or `f64`** - floating point operations can vary across platforms. ```rust // BAD - platform-dependent results let result = 0.1 + 0.2; // might not equal 0.3 exactly // GOOD - use fixed-point arithmetic use evolve_math::FixedPoint; let result = FixedPoint::from_decimal(1, 1) + FixedPoint::from_decimal(2, 1); ``` #### 5. External I/O **NEVER perform I/O operations** - file access, network calls, or environment variables. ```rust // BAD - all of these std::fs::read("file.txt"); std::net::TcpStream::connect("..."); std::env::var("SECRET"); ``` #### 6. Threading and Async **NEVER spawn threads or use async** - execution order is non-deterministic. ```rust // BAD std::thread::spawn(|| { /* ... */ }); // Modules execute synchronously within the STF ``` ### Compile-Time Protection Configure clippy to catch common issues. Add to `.clippy.toml`: ```toml disallowed-types = [ { path = "std::collections::HashMap", reason = "Non-deterministic iteration order" }, { path = "std::collections::HashSet", reason = "Non-deterministic iteration order" }, { path = "std::time::Instant", reason = "Non-deterministic - use env.block_time_ms()" }, { path = "std::time::SystemTime", reason = "Non-deterministic - use env.block_time_ms()" }, ] ``` ### Safe Patterns #### Using Evolve Collections All Evolve collections are deterministic by design: | Collection | Description | | -------------------- | -------------------------------------- | | `Item` | Single value storage | | `Map` | Key-value with deterministic iteration | | `Vector` | Ordered sequence | | `Queue` | FIFO queue | | `UnorderedMap` | Deterministic within a session | #### Deriving Values from Chain State When you need "randomness" or unique values: ```rust // Use a counter stored in your module for unique IDs let unique_id = self.counter.update(|c| Ok(c + 1), env)?; // Use block info from environment for time-based logic let block_height = env.block_height(); let block_time = env.block_time_ms(); ``` ### Testing for Determinism Run the same test multiple times and verify identical results: ```rust #[test] fn test_determinism() { for _ in 0..100 { let result = execute_module_logic(); assert_eq!(result, EXPECTED_VALUE); } } ``` ### Debugging Consensus Failures If nodes diverge: 1. Check for `HashMap`/`HashSet` usage 2. Look for floating point arithmetic 3. Verify no time-dependent logic 4. Check for uninitialized memory access 5. Run with `RUST_BACKTRACE=1` to find the divergence point ## State Transition Function The STF is the core execution engine. It processes blocks of transactions deterministically, transforming state from one consistent snapshot to the next. ### Design Principles * **Sequential, deterministic execution** - No transaction parallelism * **Atomic transactions** - Checkpoint/rollback for atomicity * **Single-threaded block processing** - Guarantees determinism * **Lazy system configuration** - Config loaded at block start ### Block Processing Flow ``` BeginBlock │ ├── BeginBlocker accounts execute │ └── e.g., Scheduler, PoA validator updates │ ▼ For each Transaction: │ ├── 1. Validate (TxValidator) │ └── Check signature, nonce │ ├── 2. Execute (AccountCode.execute) │ └── State changes in overlay │ ├── 3. Post-TX Handler │ └── Fee collection, logging │ └── 4. Commit or Rollback └── Based on success/failure │ ▼ EndBlock │ └── EndBlocker accounts execute ``` ### Component Relationships #### ExecutionState Holds all mutable state during block execution: ```rust ExecutionState { base_storage: &S, // Immutable reference to committed state overlay: HashMap, // Write cache undo_log: Vec, // For checkpoint/restore events: Vec, // Collected events unique_objects: u64, // Counter for unique IDs metrics: ExecutionMetrics, // Performance tracking } ``` #### Invoker Implements the `Environment` trait for account code: ```rust Invoker { whoami: AccountId, // Current executing account sender: AccountId, // Message sender funds: Coins, // Attached funds account_codes: &Registry, storage: &mut ExecutionState, gas_counter: GasCounter, scope: ExecutionScope, call_depth: u32, } ``` #### GasCounter Tracks gas consumption: ```rust enum GasCounter { Infinite, // For system operations Finite { limit, used, cfg } // For user transactions } ``` ### Checkpoint/Restore Every `do_exec` call creates a checkpoint for atomicity: ```rust let checkpoint = state.checkpoint(); match execute_call() { Ok(result) => result, Err(e) => { state.restore(checkpoint)?; // Rollback all changes return Err(e); } } ``` A checkpoint captures: * `undo_log` index * `events` index * `unique_objects` counter ### Threading Model #### Single-Threaded (Determinism Required) | Component | Reason | | --------------------- | ------------------------ | | Block execution | Sequential tx processing | | Transaction execution | Shared ExecutionState | | Checkpoint/restore | Single-writer undo log | | Event collection | Sequential append | #### Can Be Parallel | Component | Where | Mechanism | | ---------------------- | ------------- | ------------------- | | Signature verification | Pre-consensus | `rayon::par_iter()` | | Cache reads | Storage layer | 256-shard RwLock | | ADB access | Storage layer | `tokio::RwLock` | ### Resource Limits | Limit | Value | Purpose | | ------------------- | --------- | ------------------------- | | Max overlay entries | 100,000 | Prevent memory exhaustion | | Max events | 10,000 | Bound event log size | | Max key size | 254 bytes | Limit key storage | | Max value size | 1 MB | Limit value storage | | Max call depth | 64 | Prevent stack overflow | ### Gas Model * **Infinite gas**: BeginBlock, EndBlock, system operations * **Finite gas**: User transactions * **Config loaded**: From GasService account at block start * **Charges**: Per-byte for reads, writes, and deletes ### Storage Layer ``` ExecutionState.overlay │ │ into_changes() ▼ Vec - Set { key, value } - Remove { key } │ │ apply to storage ▼ CommonwareStorage.adb │ │ commit ▼ Merkle Root (state hash) ``` ## gRPC API Evolve exposes a gRPC API for high-performance programmatic access. The gRPC interface is ideal for: * **Consensus layer integration** - Connect execution clients to consensus nodes * **High-throughput indexers** - Stream blocks and events efficiently * **Backend services** - Low-latency queries from application servers * **Schema introspection** - Discover module APIs programmatically ### Service Definitions #### ExecutionService The primary service for block execution and queries. ```protobuf service ExecutionService { // Block execution rpc ExecuteBlock(ExecuteBlockRequest) returns (ExecuteBlockResponse); rpc QueryState(QueryStateRequest) returns (QueryStateResponse); // Schema introspection rpc ListModules(ListModulesRequest) returns (ListModulesResponse); rpc GetModuleSchema(GetModuleSchemaRequest) returns (GetModuleSchemaResponse); rpc GetAllSchemas(GetAllSchemasRequest) returns (GetAllSchemasResponse); } ``` ### Schema Introspection RPCs #### ListModules Returns all registered module identifiers. ```protobuf message ListModulesRequest {} message ListModulesResponse { repeated string identifiers = 1; } ``` #### GetModuleSchema Returns the schema for a specific module. ```protobuf message GetModuleSchemaRequest { string identifier = 1; } message GetModuleSchemaResponse { optional AccountSchema schema = 1; } ``` #### GetAllSchemas Returns schemas for all registered modules. ```protobuf message GetAllSchemasRequest {} message GetAllSchemasResponse { repeated AccountSchema schemas = 1; } ``` ### Schema Message Types ```protobuf message TypeSchema { oneof kind { string primitive = 1; // "u128", "bool", "String" TypeSchema array_element = 2; TypeSchema optional_inner = 3; TupleSchema tuple = 4; StructSchema struct_type = 5; EnumSchema enum_type = 6; bool account_id = 7; bool unit = 8; string opaque = 9; // Rust type name for complex types } } message FieldSchema { string name = 1; TypeSchema ty = 2; } message TupleSchema { repeated TypeSchema elements = 1; } message StructSchema { string name = 1; repeated FieldSchema fields = 2; } message EnumSchema { string name = 1; repeated VariantSchema variants = 2; } message VariantSchema { string name = 1; repeated FieldSchema fields = 2; } message FunctionSchema { string name = 1; uint64 function_id = 2; FunctionKind kind = 3; // INIT, EXEC, QUERY repeated FieldSchema params = 4; TypeSchema return_type = 5; bool payable = 6; } message AccountSchema { string name = 1; string identifier = 2; optional FunctionSchema init = 3; repeated FunctionSchema exec_functions = 4; repeated FunctionSchema query_functions = 5; } enum FunctionKind { FUNCTION_KIND_UNSPECIFIED = 0; FUNCTION_KIND_INIT = 1; FUNCTION_KIND_EXEC = 2; FUNCTION_KIND_QUERY = 3; } ``` ### Client Examples #### grpcurl ```bash # List all modules grpcurl -plaintext localhost:9545 evolve.v1.ExecutionService/ListModules # Get Token schema grpcurl -plaintext -d '{"identifier": "Token"}' \ localhost:9545 evolve.v1.ExecutionService/GetModuleSchema # Get all schemas grpcurl -plaintext localhost:9545 evolve.v1.ExecutionService/GetAllSchemas ``` #### Rust Client ```rust use evolve_grpc::proto::evolve::v1::{ execution_service_client::ExecutionServiceClient, ListModulesRequest, GetModuleSchemaRequest, GetAllSchemasRequest, }; async fn discover_modules() -> Result<(), Box> { let mut client = ExecutionServiceClient::connect("http://localhost:9545").await?; // List all modules let response = client.list_modules(ListModulesRequest {}).await?; println!("Modules: {:?}", response.into_inner().identifiers); // Get Token schema let response = client .get_module_schema(GetModuleSchemaRequest { identifier: "Token".to_string(), }) .await?; if let Some(schema) = response.into_inner().schema { println!("Token has {} exec functions", schema.exec_functions.len()); println!("Token has {} query functions", schema.query_functions.len()); } Ok(()) } ``` #### Go Client ```go package main import ( "context" "log" pb "github.com/evolvesdk/evolve/proto/evolve/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func main() { conn, err := grpc.NewClient("localhost:9545", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } defer conn.Close() client := pb.NewExecutionServiceClient(conn) // List modules resp, err := client.ListModules(context.Background(), &pb.ListModulesRequest{}) if err != nil { log.Fatal(err) } for _, id := range resp.Identifiers { log.Printf("Module: %s", id) } } ``` ### Server Configuration #### Enabling gRPC Configure the gRPC server in your node configuration: ```toml [grpc] enabled = true listen_addr = "0.0.0.0:9545" # Optional TLS [grpc.tls] cert_path = "/path/to/cert.pem" key_path = "/path/to/key.pem" ``` #### Programmatic Setup ```rust use evolve_grpc::GrpcServer; let grpc_server = GrpcServer::builder() .with_execution_service(execution_service) .listen_addr("0.0.0.0:9545".parse()?) .build()?; grpc_server.serve().await?; ``` ### Port Conventions | Port | Protocol | Purpose | | ----- | -------- | -------------------------------- | | 8545 | HTTP | JSON-RPC (Ethereum compatible) | | 9545 | gRPC | Native gRPC API | | 26657 | HTTP | CometBFT RPC (if using CometBFT) | ### Performance Considerations gRPC offers several advantages over JSON-RPC: * **Binary encoding** - Protobuf is more compact than JSON * **Streaming** - Support for server-side and bidirectional streaming * **HTTP/2** - Multiplexing, header compression, connection reuse * **Code generation** - Strongly typed clients in multiple languages For high-throughput scenarios (indexers, analytics), prefer gRPC over JSON-RPC. ## JSON-RPC Evolve provides a full Ethereum-compatible JSON-RPC 2.0 API. ### Endpoint Default: `http://localhost:8545` ### Supported Methods #### eth\_ Namespace | Method | Description | | --------------------------- | ---------------------------- | | `eth_chainId` | Returns the chain ID | | `eth_blockNumber` | Returns current block number | | `eth_getBlockByNumber` | Get block by number | | `eth_getBlockByHash` | Get block by hash | | `eth_getTransactionByHash` | Get transaction by hash | | `eth_getTransactionReceipt` | Get transaction receipt | | `eth_sendRawTransaction` | Submit signed transaction | | `eth_call` | Call contract (read-only) | | `eth_estimateGas` | Estimate gas for transaction | | `eth_getBalance` | Get account balance | | `eth_getTransactionCount` | Get account nonce | | `eth_getCode` | Get contract code | | `eth_getStorageAt` | Get storage value | | `eth_gasPrice` | Get current gas price | | `eth_feeHistory` | Get fee history | | `eth_getLogs` | Get event logs | #### net\_ Namespace | Method | Description | | --------------- | ----------------- | | `net_version` | Network version | | `net_listening` | Is node listening | | `net_peerCount` | Number of peers | #### web3\_ Namespace | Method | Description | | -------------------- | --------------------- | | `web3_clientVersion` | Client version string | | `web3_sha3` | Keccak-256 hash | ### Examples #### Get Block Number ```bash curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` Response: ```json { "jsonrpc": "2.0", "id": 1, "result": "0x64" } ``` #### Get Balance ```bash curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x742d35Cc6634C0532925a3b844Bc9e7595f1Ef67", "latest"], "id": 1 }' ``` #### Send Transaction ```bash curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_sendRawTransaction", "params": ["0xf86c...signed_tx..."], "id": 1 }' ``` #### Call Contract ```bash curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_call", "params": [{ "to": "0x...", "data": "0x..." }, "latest"], "id": 1 }' ``` ### WebSocket Subscriptions Connect to `ws://localhost:8545` for real-time updates. #### Subscribe to New Blocks ```javascript const ws = new WebSocket('ws://localhost:8545'); ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'eth_subscribe', params: ['newHeads'], id: 1 })); ws.onmessage = (event) => { const data = JSON.parse(event.data); console.log('New block:', data.params.result); }; ``` #### Subscribe to Logs ```javascript ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'eth_subscribe', params: ['logs', { address: '0x...', topics: ['0x...'] }], id: 2 })); ``` ### Configuration ```yaml rpc: enabled: true http_addr: "127.0.0.1:8545" ws_addr: "127.0.0.1:8546" client_version: "evolve/0.1.0" max_connections: 100 request_timeout_secs: 30 ``` ### Error Codes | Code | Message | | ------ | ---------------- | | -32700 | Parse error | | -32600 | Invalid request | | -32601 | Method not found | | -32602 | Invalid params | | -32603 | Internal error | | -32000 | Server error | ## Schema Introspection The schema introspection API allows RPC clients to discover all available modules and their query/exec functions at runtime. ### Overview Evolve provides a reflection system that exposes module schemas via RPC. This enables: * **Client SDK generation** - Automatically generate typed clients from schemas * **Documentation** - Generate API documentation from live modules * **Tooling** - Build explorers, debuggers, and development tools * **Validation** - Validate requests against known schemas before sending ### Schema Structure Each registered module exposes an `AccountSchema` containing: ```json { "name": "Token", "identifier": "Token", "init": { /* FunctionSchema */ }, "exec_functions": [ /* FunctionSchema[] */ ], "query_functions": [ /* FunctionSchema[] */ ] } ``` #### FunctionSchema ```json { "name": "transfer", "function_id": 1234567890, "kind": "exec", "params": [ { "name": "to", "ty": { "kind": "account_id" } }, { "name": "amount", "ty": { "kind": "primitive", "name": "u128" } } ], "return_type": { "kind": "unit" }, "payable": false } ``` #### TypeSchema Variants | Kind | JSON Example | Description | | ------------ | ---------------------------------------------------- | ----------------------------------- | | `primitive` | `{"kind": "primitive", "name": "u128"}` | Basic types (u8-u128, bool, String) | | `account_id` | `{"kind": "account_id"}` | Account identifier | | `unit` | `{"kind": "unit"}` | Empty/void return | | `array` | `{"kind": "array", "element": {...}}` | Vec\ | | `optional` | `{"kind": "optional", "inner": {...}}` | Option\ | | `tuple` | `{"kind": "tuple", "elements": [...]}` | (A, B, C) | | `struct` | `{"kind": "struct", "name": "...", "fields": [...]}` | Named struct | | `enum` | `{"kind": "enum", "name": "...", "variants": [...]}` | Enum type | | `opaque` | `{"kind": "opaque", "rust_type": "..."}` | Unknown/complex type | ### JSON-RPC API The schema introspection endpoints are in the `evolve` namespace. #### evolve\_listModules Returns all registered module identifiers. ```json // Request { "jsonrpc": "2.0", "method": "evolve_listModules", "params": [], "id": 1 } // Response { "jsonrpc": "2.0", "result": ["Token", "Scheduler", "EoaAccount"], "id": 1 } ``` #### evolve\_getModuleSchema Returns the schema for a specific module. ```json // Request { "jsonrpc": "2.0", "method": "evolve_getModuleSchema", "params": ["Token"], "id": 1 } // Response { "jsonrpc": "2.0", "result": { "name": "Token", "identifier": "Token", "init": { "name": "initialize", "function_id": 9876543210, "kind": "init", "params": [ {"name": "metadata", "ty": {"kind": "opaque", "rust_type": "FungibleAssetMetadata"}}, {"name": "balances", "ty": {"kind": "array", "element": {"kind": "tuple", "elements": [{"kind": "account_id"}, {"kind": "primitive", "name": "u128"}]}}}, {"name": "supply_manager", "ty": {"kind": "optional", "inner": {"kind": "account_id"}}} ], "return_type": {"kind": "unit"}, "payable": false }, "exec_functions": [...], "query_functions": [...] }, "id": 1 } ``` #### evolve\_getAllSchemas Returns schemas for all registered modules. ```json // Request { "jsonrpc": "2.0", "method": "evolve_getAllSchemas", "params": [], "id": 1 } // Response { "jsonrpc": "2.0", "result": [ { "name": "Token", "identifier": "Token", ... }, { "name": "Scheduler", "identifier": "Scheduler", ... } ], "id": 1 } ``` #### curl Examples ```bash # List all modules curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"evolve_listModules","params":[],"id":1}' # Get Token schema curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"evolve_getModuleSchema","params":["Token"],"id":1}' # Get all schemas curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"evolve_getAllSchemas","params":[],"id":1}' ``` ### Server Setup #### Enabling Schema Introspection To enable schema introspection, provide an `AccountsCodeStorage` implementation when creating the `ChainStateProvider`. ```rust use evolve_chain_index::{ChainStateProvider, ChainStateProviderConfig}; use evolve_stf_traits::AccountsCodeStorage; // Your AccountsCodeStorage implementation that holds registered modules let account_codes: Arc = /* ... */; let provider = ChainStateProvider::with_account_codes( Arc::new(index), ChainStateProviderConfig { chain_id: 1 }, account_codes, ); ``` #### Implementing AccountsCodeStorage ```rust use std::collections::HashMap; use std::sync::Arc; use evolve_core::{AccountCode, ErrorCode}; use evolve_stf_traits::AccountsCodeStorage; pub struct MyAccountCodes { codes: HashMap>, } impl AccountsCodeStorage for MyAccountCodes { fn with_code(&self, identifier: &str, f: F) -> Result where F: FnOnce(Option<&dyn AccountCode>) -> R, { Ok(f(self.codes.get(identifier).map(|c| c.as_ref()))) } fn list_identifiers(&self) -> Vec { self.codes.keys().cloned().collect() } } ``` ### Use Cases #### Generate TypeScript Client ```typescript const response = await fetch('http://localhost:8545', { method: 'POST', body: JSON.stringify({ jsonrpc: '2.0', method: 'evolve_getAllSchemas', params: [], id: 1 }) }); const { result: schemas } = await response.json(); for (const schema of schemas) { console.log(`Module: ${schema.name}`); for (const query of schema.query_functions) { console.log(` Query: ${query.name}(${query.params.map(p => p.name).join(', ')})`); } } ``` #### Build CLI Tool ```bash #!/bin/bash MODULE=$1 SCHEMA=$(curl -s http://localhost:8545 \ -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"evolve_getModuleSchema\",\"params\":[\"$MODULE\"],\"id\":1}" \ | jq '.result') echo "Queries for $MODULE:" echo "$SCHEMA" | jq -r '.query_functions[] | " \(.name): \(.params | map(.name) | join(", "))"' ``` #### Validate Requests ```rust fn validate_query(schema: &AccountSchema, function_name: &str, params: &[Value]) -> Result<(), String> { let func = schema.query_functions .iter() .find(|f| f.name == function_name) .ok_or_else(|| format!("Unknown query: {}", function_name))?; if params.len() != func.params.len() { return Err(format!( "Expected {} params, got {}", func.params.len(), params.len() )); } Ok(()) } ``` ### Function IDs Each function has a deterministic `function_id` computed as the first 8 bytes of `SHA-256(function_name)`. This ID is used for message dispatch at runtime. ```rust use sha2::{Sha256, Digest}; fn compute_function_id(name: &str) -> u64 { let mut hasher = Sha256::new(); hasher.update(name.as_bytes()); let hash = hasher.finalize(); u64::from_le_bytes(hash[..8].try_into().unwrap()) } ``` The same function name always produces the same ID, enabling clients to cache and reuse IDs.