diff --git a/README.md b/README.md index fe47c56..e13455d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,202 @@ -# PVQ +# PVQ: PolkaVM Query System for Polkadot -PolkaVM Query for Polkadot + -## Getting Started +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) +[![Rust](https://github.com/open-web3-stack/PVQ/workflows/Rust/badge.svg)](https://github.com/open-web3-stack/PVQ/actions) + +A powerful and secure query system for Polkadot parachains that enables developers to write expressive, sandboxed guest programs using PolkaVM. PVQ provides a safe and efficient alternative to custom RPC endpoints for runtime data queries. + +## ✨ Features + +- **πŸ”’ Secure Execution**: Sandboxed PolkaVM environment for safe query execution +- **🧩 Modular Extensions**: Extensible system for exposing runtime functionalities +- **⚑ High Performance**: Efficient RISC-V execution with minimal overhead +- **πŸ› οΈ Developer Friendly**: Rust-first development experience with procedural macros +- **🌐 Runtime Integration**: Seamless integration with Substrate runtimes +- **πŸ” Rich Querying**: Support for complex queries involving multiple runtime components + +## πŸ—οΈ Architecture + +The PVQ system consists of several interconnected components: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ PVQ Program │───▢│ PVQ Executor │───▢│ Substrate β”‚ +β”‚ (Guest Code) β”‚ β”‚ (Host Side) β”‚ β”‚ Runtime β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + └─────────────▢│ PVQ Extensions β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ (Modules) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Core Components + +| Component | Description | +|-----------|-------------| +| **[PVQ Program](pvq-program/)** | Guest programs written in Rust that compile to RISC-V | +| **[PVQ Executor](pvq-executor/)** | Host-side component managing PolkaVM instances and runtime interaction | +| **[PVQ Extensions](pvq-extension/)** | Modular system exposing runtime functionalities to guest programs | +| **[PVQ Runtime API](pvq-runtime-api/)** | Substrate runtime API for external query submission | +| **[PVQ Primitives](pvq-primitives/)** | Common types and utilities shared across components | + +### Available Extensions + +- **[Core Extension](pvq-extension-core/)**: Fundamental functionalities and extension discovery +- **[Fungibles Extension](pvq-extension-fungibles/)**: Asset querying, balances, and metadata +- **[Swap Extension](pvq-extension-swap/)**: DEX interactions, liquidity pools, and price quotes + +## πŸš€ Getting Started ### Prerequisites -- Pull vendored `polkavm`: `git submodule update --init --recursive`. -- Install `polkatool` (for relinking the standard RV32E ELF to a PolkaVM blob) and `chain-spec-builder` (for building chainspec from a wasm): `make tools` +Ensure you have the following installed: + +- **Rust** (latest stable version) +- **Git** with submodule support + +### Installation + +1. **Clone the repository with submodules:** + ```bash + git clone --recursive https://github.com/open-web3-stack/PVQ.git + cd PVQ + ``` + +2. **Install required tools:** + ```bash + make tools + ``` + This installs `polkatool` for ELF to PolkaVM blob conversion and `chain-spec-builder`. + +3. **Build the project:** + ```bash + cargo build --release + ``` + +### Quick Start + +#### Running Example Programs + +1. **Build guest programs:** + ```bash + make guests + ``` + +2. **Run a test program:** + ```bash + cargo run -p pvq-test-runner -- --program output/guest-sum-balance + ``` + +#### Available Example Programs + +| Program | Description | +|---------|-------------| +| `guest-sum-balance` | Sum balances of multiple accounts | +| `guest-total-supply` | Get total supply of an asset | +| `guest-sum-balance-percent` | Calculate percentage of total supply for account balances | +| `guest-swap-info` | Query DEX/swap information | + +### Runtime Integration + +#### Testing with PoC Runtime + +1. **Start local test chain:** + ```bash + make run + ``` + +2. **Build and test programs:** + ```bash + make guests + cargo run -p pvq-test-runner -- --program output/guest-total-supply + ``` + +3. **Use with Polkadot JS UI:** + - Copy the hex-encoded `args` from test runner logs + - Upload program and arguments through the PJS UI + +## πŸ“– Documentation + +### Writing Your First PVQ Program + +```rust +use pvq_program::program; +use pvq_extension_fungibles::ExtensionFungibles; + +#[program] +fn query_balance(account: [u8; 32], asset_id: u32) -> u128 { + ExtensionFungibles::balance(asset_id, &account) +} +``` + +### Creating Custom Extensions + +```rust +use pvq_extension::extension_decl; + +#[extension_decl] +pub trait MyCustomExtension { + fn my_query() -> String; +} +``` + +### Component Documentation + +- πŸ“š **[Development Guide](docs/development.md)** - Comprehensive development setup +- πŸ”§ **[Extension Development](docs/extensions.md)** - Creating custom extensions +- πŸ—οΈ **[Architecture Deep Dive](docs/architecture.md)** - System design and internals +- πŸ“‹ **[API Reference](docs/api.md)** - Complete API documentation + +## πŸ› οΈ Development + +### Project Structure + +``` +PVQ/ +β”œβ”€β”€ poc/runtime/ # PoC Substrate runtime +β”œβ”€β”€ pvq-program/ # Program macro and utilities +β”œβ”€β”€ pvq-executor/ # Core execution engine +β”œβ”€β”€ pvq-extension*/ # Extension system and implementations +β”œβ”€β”€ pvq-runtime-api/ # Runtime API definition +β”œβ”€β”€ pvq-test-runner/ # Testing utilities +β”œβ”€β”€ guest-examples/ # Example programs +└── vendor/polkavm/ # Vendored PolkaVM dependency +``` + +### Building from Source + +```bash +# Development build +cargo build + +# Release build with optimizations +cargo build --release + +# Build guest examples +make guests -### Run Examples +# Run all tests +cargo test --all -`guest-examples` contains several guest programs to test the PVQ. +# Run clippy lints +cargo clippy --all --all-targets +``` -1. Build guest program: `make guests` -2. Run test runner: `cargo run -p pvq-test-runner -- --program output/` +### Testing -Available PoC guest programs: +```bash +# Run unit tests +cargo test -- `guest-sum-balance`: sum the balances of multiple accounts -- `guest-total-supply`: get the total supply of an asset -- `guest-sum-balance-percent`: sum the balances of multiple accounts and calculate the percentage of the total supply +# Run integration tests +cargo test --test integration -### RuntimeAPI PoC +# Test specific component +cargo test -p pvq-executor -1. Use chopsticks to start a local chain with the RuntimeAPI enabled: `make run` -2. Build guest programs: `make guests` -3. Run test runner to display hex-encoded `args` in tracing logs: `cargo run -p pvq-test-runner -- --program output/` -4. Upload `program` and `args` in PJS UI. +# Run example programs +make test-guests +``` diff --git a/guest-examples/sum-balance-percent/src/main.rs b/guest-examples/sum-balance-percent/src/main.rs index 1468c9c..a04dab1 100644 --- a/guest-examples/sum-balance-percent/src/main.rs +++ b/guest-examples/sum-balance-percent/src/main.rs @@ -1,16 +1,37 @@ +//! A guest program that sums the balances of a set of accounts for a given asset and returns the percentage of the total supply. #![no_std] #![no_main] +/// The primary module for the sum-balance-percent guest program. +/// +/// This module defines the necessary types, extension functions, and the main entrypoint +/// for calculating the percentage of total supply represented by the sum of balances +/// for a given set of accounts. #[pvq_program::program] mod sum_balance_percent { + /// A type alias for the asset identifier. type AssetId = u32; + /// A type alias for the account identifier. type AccountId = [u8; 32]; + /// A type alias for the balance of an account. type Balance = u64; + + /// Retrieves the balance of a specific account for a given asset. + /// + /// This is an extension function that calls into the runtime. #[program::extension_fn(extension_id = 1248491991627109725u64, fn_index = 6)] fn balance(asset: AssetId, who: AccountId) -> Balance {} + + /// Retrieves the total supply of a given asset. + /// + /// This is an extension function that calls into the runtime. #[program::extension_fn(extension_id = 1248491991627109725u64, fn_index = 5)] fn total_supply(asset: AssetId) -> Balance {} + /// The entrypoint of the program. + /// + /// It takes an asset ID and a vector of account IDs, calculates the sum of their balances, + /// and returns the percentage of this sum with respect to the total supply of the asset. #[program::entrypoint] fn sum_balance(asset: AssetId, accounts: alloc::vec::Vec) -> Balance { let mut sum_balance = 0; diff --git a/guest-examples/sum-balance/src/main.rs b/guest-examples/sum-balance/src/main.rs index 2780a58..737c1a0 100644 --- a/guest-examples/sum-balance/src/main.rs +++ b/guest-examples/sum-balance/src/main.rs @@ -1,28 +1,59 @@ +//! A guest program that sums the balance of a given asset for a list of accounts. #![no_std] #![no_main] +/// A guest program that sums the balance of a given asset for a list of accounts. #[pvq_program::program] mod sum_balance { cfg_if::cfg_if! { if #[cfg(feature = "option_version_1")] { + /// Represents a unique identifier for an account. type AccountId = [u8; 64]; + /// Represents a unique identifier for an asset. type AssetId = u64; + /// Represents the balance of an asset. type Balance = u128; } else if #[cfg(feature = "option_version_2")] { + /// Represents a unique identifier for an account. type AccountId = [u8; 32]; + /// Represents a unique identifier for an asset. type AssetId = u32; + /// Represents the balance of an asset. type Balance = u64; } else { + /// Represents a unique identifier for an account. type AccountId = [u8; 32]; + /// Represents a unique identifier for an asset. type AssetId = u32; + /// Represents the balance of an asset. type Balance = u64; } } + /// Get the balance of a given asset for a specific account. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// * `who`: The account identifier. + /// + /// # Returns + /// + /// The balance of the asset for the specified account. #[program::extension_fn(extension_id = 1248491991627109725u64, fn_index = 6)] fn balance(asset: AssetId, who: AccountId) -> Balance {} + /// Sums the balance of a given asset for a list of accounts. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// * `accounts`: A list of account identifiers. + /// + /// # Returns + /// + /// The total balance of the asset for all the specified accounts. #[program::entrypoint] fn sum_balance(asset: AssetId, accounts: alloc::vec::Vec) -> Balance { let mut sum = 0; diff --git a/guest-examples/swap-info/src/main.rs b/guest-examples/swap-info/src/main.rs index 10538d9..4689878 100644 --- a/guest-examples/swap-info/src/main.rs +++ b/guest-examples/swap-info/src/main.rs @@ -1,21 +1,42 @@ +//! A guest program that provides information about asset swaps. #![no_std] #![no_main] +/// A guest program that provides information about asset swaps. #[pvq_program::program] mod swap_info { + /// Represents a unique identifier for an asset. type AssetId = alloc::vec::Vec; + /// Represents the balance of an asset. type Balance = u128; + /// Represents information about an asset. #[derive( Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo, )] pub struct AssetInfo { + /// The unique identifier of the asset. pub asset_id: AssetId, + /// The name of the asset. pub name: alloc::vec::Vec, + /// The symbol of the asset. pub symbol: alloc::vec::Vec, + /// The number of decimals the asset has. pub decimals: u8, } + /// Get the quote price of `asset1` in terms of `asset2` for a given amount of `asset2`. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the asset to be quoted. + /// * `asset2`: The identifier of the asset to quote against. + /// * `amount`: The amount of `asset2`. + /// * `include_fee`: Whether to include the fee in the quote. + /// + /// # Returns + /// + /// The quote price of `asset1` in terms of `asset2`, or `None` if the quote is not available. #[program::extension_fn(extension_id = 15900548380266538526u64, fn_index = 0)] fn quote_price_tokens_for_exact_tokens( asset1: AssetId, @@ -25,6 +46,18 @@ mod swap_info { ) -> Option { } + /// Get the quote price of `asset2` in terms of `asset1` for a given amount of `asset1`. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the asset to quote against. + /// * `asset2`: The identifier of the asset to be quoted. + /// * `amount`: The amount of `asset1`. + /// * `include_fee`: Whether to include the fee in the quote. + /// + /// # Returns + /// + /// The quote price of `asset2` in terms of `asset1`, or `None` if the quote is not available. #[program::extension_fn(extension_id = 15900548380266538526u64, fn_index = 1)] fn quote_price_exact_tokens_for_tokens( asset1: AssetId, @@ -34,18 +67,58 @@ mod swap_info { ) -> Option { } + /// Get the liquidity pool of two assets. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the first asset. + /// * `asset2`: The identifier of the second asset. + /// + /// # Returns + /// + /// A tuple containing the balance of each asset in the liquidity pool, or `None` if the pool does not exist. #[program::extension_fn(extension_id = 15900548380266538526u64, fn_index = 2)] fn get_liquidity_pool(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)> {} + /// List all available liquidity pools. + /// + /// # Returns + /// + /// A list of tuples, where each tuple represents a liquidity pool and contains the identifiers of the two assets in the pool. #[program::extension_fn(extension_id = 15900548380266538526u64, fn_index = 3)] fn list_pools() -> alloc::vec::Vec<(AssetId, AssetId)> {} + /// Get information about a specific asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// Information about the asset, or `None` if the asset does not exist. #[program::extension_fn(extension_id = 15900548380266538526u64, fn_index = 4)] fn asset_info(asset: AssetId) -> Option {} + /// Get information about all assets. + /// + /// # Returns + /// + /// A map of asset identifiers to asset information. #[program::extension_fn(extension_id = 15900548380266538526u64, fn_index = 5)] fn assets_info() -> alloc::collections::BTreeMap {} + /// Get the quote price of `asset2` in terms of `asset1` for a given amount of `asset1`. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the asset to quote against. + /// * `asset2`: The identifier of the asset to be quoted. + /// * `amount`: The amount of `asset1`. + /// + /// # Returns + /// + /// The quote price of `asset2` in terms of `asset1`, or `None` if the quote is not available. #[program::entrypoint] fn entrypoint_quote_price_exact_tokens_for_tokens( asset1: AssetId, @@ -55,6 +128,17 @@ mod swap_info { quote_price_exact_tokens_for_tokens(asset1, asset2, amount, true) } + /// Get the quote price of `asset1` in terms of `asset2` for a given amount of `asset2`. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the asset to be quoted. + /// * `asset2`: The identifier of the asset to quote against. + /// * `amount`: The amount of `asset2`. + /// + /// # Returns + /// + /// The quote price of `asset1` in terms of `asset2`, or `None` if the quote is not available. #[program::entrypoint] fn entrypoint_quote_price_tokens_for_exact_tokens( asset1: AssetId, @@ -64,11 +148,26 @@ mod swap_info { quote_price_tokens_for_exact_tokens(asset1, asset2, amount, true) } + /// Get the liquidity pool of two assets. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the first asset. + /// * `asset2`: The identifier of the second asset. + /// + /// # Returns + /// + /// A tuple containing the balance of each asset in the liquidity pool, or `None` if the pool does not exist. #[program::entrypoint] fn entrypoint_get_liquidity_pool(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)> { get_liquidity_pool(asset1, asset2) } + /// List all available liquidity pools with their asset information. + /// + /// # Returns + /// + /// A list of tuples, where each tuple represents a liquidity pool and contains the information of the two assets in the pool. #[program::entrypoint] fn entrypoint_list_pools() -> alloc::vec::Vec<(AssetInfo, AssetInfo)> { let pools = list_pools(); diff --git a/guest-examples/total-supply/src/main.rs b/guest-examples/total-supply/src/main.rs index cdda439..56d095f 100644 --- a/guest-examples/total-supply/src/main.rs +++ b/guest-examples/total-supply/src/main.rs @@ -1,11 +1,31 @@ +//! A guest program that queries the total supply of a given asset. #![no_std] #![no_main] +/// A guest program that queries the total supply of a given asset. #[pvq_program::program] mod query_total_supply { + /// Get the total supply of a given asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The total supply of the asset. #[program::extension_fn(extension_id = 4071833530116166512u64, fn_index = 0)] fn total_supply(asset: u32) -> u64 {} + /// Get the total supply of a given asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The total supply of the asset. #[program::entrypoint] fn get_total_supply(asset: u32) -> u64 { total_supply(asset) diff --git a/poc/runtime/README.md b/poc/runtime/README.md new file mode 100644 index 0000000..8168391 --- /dev/null +++ b/poc/runtime/README.md @@ -0,0 +1,20 @@ +# PVQ PoC Runtime + +FRAME runtime demonstrating PVQ integration used in this repository. + +### What it exposes + +- Runtime API `PvqApi` with two methods: + - `execute_query(...)`: runs a PVQ program with input bytes and an optional gas limit; if not provided, a default ~2s limit is applied. + - `metadata()`: returns serialized extension metadata bytes (portable type registry + extension functions). + +### How it’s wired + +- Uses the generated `extensions` module to connect extension traits to runtime pallets. +- Executes programs via `ExtensionsExecutor` with invocation source set to runtime API. + +### Build + +```bash +cargo build -p poc-runtime --release +``` diff --git a/pvq-executor/README.md b/pvq-executor/README.md new file mode 100644 index 0000000..8aa08f0 --- /dev/null +++ b/pvq-executor/README.md @@ -0,0 +1,338 @@ +# PVQ Executor + + + +The core execution engine for PVQ (PolkaVM Query) programs. This crate provides a secure, sandboxed environment for running guest programs compiled to RISC-V, with controlled access to runtime functionality through extensions. + +## Overview + +The PVQ Executor serves as the bridge between compiled guest programs and the Substrate runtime. It manages PolkaVM instances, handles program lifecycle, and provides secure execution context for queries. + +## Features + +- πŸ”’ **Secure Execution**: Sandboxed PolkaVM environment with resource limits +- ⚑ **High Performance**: Optimized for fast query execution +- πŸ›‘οΈ **Memory Safety**: Controlled memory allocation and access +- πŸ”Œ **Extension Integration**: Seamless interaction with PVQ extensions +- ⏱️ **Timeout Protection**: Configurable execution time limits +- πŸ“Š **Resource Monitoring**: Track memory usage and execution metrics + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ PVQ Executor β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PvqExecutor β”‚ β”‚ ExecutorContext β”‚ β”‚ PolkaVM β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Instance β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Lifecycle β”‚ β”‚ β”œβ”€ Extensions β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Resource β”‚ β”‚ β”œβ”€ Runtime β”‚ β”‚ β”œβ”€ Memory β”‚ β”‚ +β”‚ β”‚ β”‚ Management β”‚ β”‚ β”‚ Access β”‚ β”‚ β”œβ”€ Execution β”‚ β”‚ +β”‚ β”‚ └─ Error β”‚ β”‚ └─ Permissions β”‚ β”‚ └─ Sandbox β”‚ β”‚ +β”‚ β”‚ Handling β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Core Components + +### PvqExecutor + +The main executor that manages the complete lifecycle of PVQ program execution. + +**Responsibilities:** +- Program loading and validation +- PolkaVM instance creation and management +- Execution context setup +- Resource limit enforcement +- Error handling and recovery + +### PvqExecutorContext + +Provides the execution context and environment for guest programs. + +**Responsibilities:** +- Extension registry management +- Runtime state access +- Permission control +- Inter-component communication + +### Error Handling + +Comprehensive error types covering all execution scenarios. + +**Error Categories:** +- Program loading errors +- Execution timeout/resource exhaustion +- Extension call failures +- Memory access violations + +## Usage + +### Basic Execution + +```rust +use pvq_executor::{PvqExecutor, PvqExecutorContext}; +use pvq_extension::ExtensionsExecutor; + +// Create executor context +let extensions = ExtensionsExecutor::new(); +let context = PvqExecutorContext::new(extensions); + +// Create and configure executor +let executor = PvqExecutor::new(context); + +// Load and execute program +let program_blob = std::fs::read("program.polkavm")?; +let args = vec![1, 2, 3, 4]; // Serialized arguments + +let result = executor.execute(&program_blob, &args)?; +println!("Execution result: {:?}", result); +``` + +### Advanced Configuration + +```rust +use pvq_executor::{PvqExecutor, PvqExecutorConfig, PvqExecutorContext}; +use std::time::Duration; + +// Configure execution parameters +let config = PvqExecutorConfig { + max_memory: 32 * 1024 * 1024, // 32MB + max_execution_time: Duration::from_secs(30), + enable_debugging: false, + stack_size: 1024 * 1024, // 1MB +}; + +// Create executor with custom config +let context = PvqExecutorContext::new(extensions); +let executor = PvqExecutor::with_config(context, config); +``` + +### Error Handling + +```rust +use pvq_executor::{PvqExecutor, PvqExecutorError}; + +match executor.execute(&program_blob, &args) { + Ok(result) => { + println!("Success: {:?}", result); + } + Err(PvqExecutorError::Timeout) => { + eprintln!("Program execution timed out"); + } + Err(PvqExecutorError::OutOfMemory) => { + eprintln!("Program exceeded memory limits"); + } + Err(PvqExecutorError::ExtensionError(e)) => { + eprintln!("Extension call failed: {}", e); + } + Err(e) => { + eprintln!("Execution failed: {}", e); + } +} +``` + +## Configuration + +### Execution Limits + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `max_memory` | 16MB | Maximum memory allocation | +| `max_execution_time` | 10s | Maximum execution duration | +| `stack_size` | 512KB | Program stack size | +| `max_call_depth` | 1024 | Maximum call stack depth | + +### Security Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `enable_debugging` | false | Enable debug information | +| `strict_memory` | true | Strict memory access checking | +| `extension_whitelist` | None | Allowed extensions (None = all) | + +### Performance Tuning + +```rust +let config = PvqExecutorConfig { + // Optimize for throughput + max_memory: 64 * 1024 * 1024, + precompile_programs: true, + enable_jit: true, + + // Or optimize for safety + max_execution_time: Duration::from_secs(5), + strict_memory: true, + enable_debugging: false, +}; +``` + +## Integration + +### With Substrate Runtime + +```rust +use pvq_executor::{PvqExecutor, PvqExecutorContext}; +use pvq_runtime_api::PvqApi; +use sp_api::ProvideRuntimeApi; + +impl PvqApi for Runtime { + fn execute_query(&self, program: Vec, args: Vec) -> Vec { + let extensions = self.create_extensions_executor(); + let context = PvqExecutorContext::new(extensions); + let executor = PvqExecutor::new(context); + + executor.execute(&program, &args) + .unwrap_or_else(|e| format!("Error: {}", e).into_bytes()) + } +} +``` + +### With Extension System + +```rust +use pvq_executor::PvqExecutorContext; +use pvq_extension::ExtensionsExecutor; +use pvq_extension_fungibles::ExtensionFungibles; + +// Register extensions +let mut extensions = ExtensionsExecutor::new(); +extensions.register::(); + +// Create context with extensions +let context = PvqExecutorContext::new(extensions); +``` + +## Development + +### Building + +```bash +# Build the executor +cargo build -p pvq-executor + +# Build with all features +cargo build -p pvq-executor --all-features + +# Run tests +cargo test -p pvq-executor +``` + +### Testing + +```bash +# Run unit tests +cargo test -p pvq-executor + +# Run integration tests +cargo test -p pvq-executor --test integration + +# Test with different configurations +cargo test -p pvq-executor --test config_tests + +# Benchmark performance +cargo bench -p pvq-executor +``` + +### Debugging + +Enable debug logging: + +```rust +use log::debug; + +// In executor implementation +debug!("Executing program with {} bytes", program.len()); +debug!("Memory usage: {} bytes", memory_usage); +``` + +Run with debug output: + +```bash +RUST_LOG=pvq_executor=debug cargo test +``` + +## Error Reference + +### PvqExecutorError Types + +| Error Type | Description | Recovery | +|------------|-------------|----------| +| `InvalidProgram` | Malformed program blob | Validate program before execution | +| `Timeout` | Execution time limit exceeded | Increase timeout or optimize program | +| `OutOfMemory` | Memory limit exceeded | Increase memory limit or optimize program | +| `ExtensionError` | Extension call failed | Check extension implementation | +| `RuntimeError` | Runtime execution error | Debug program logic | + +### Common Issues + +**Program won't load:** +```bash +# Validate program format +polkatool validate program.polkavm + +# Check file size and format +file program.polkavm +``` + +**Execution timeouts:** +```rust +// Increase timeout for complex queries +let config = PvqExecutorConfig { + max_execution_time: Duration::from_secs(60), + ..Default::default() +}; +``` + +**Memory errors:** +```rust +// Monitor memory usage +let config = PvqExecutorConfig { + max_memory: 64 * 1024 * 1024, // Increase limit + enable_debugging: true, // Enable memory tracking + ..Default::default() +}; +``` + +## Performance + +### Optimization Tips + +1. **Program Size**: Keep programs small for faster loading +2. **Memory Access**: Use sequential memory access patterns +3. **Extension Calls**: Minimize expensive extension operations +4. **Data Structures**: Use efficient serialization formats + +### Benchmarking + +```bash +# Run performance benchmarks +cargo bench -p pvq-executor + +# Profile memory usage +cargo run --example memory_profile --release + +# Measure execution time +cargo run --example timing_analysis --release +``` + +## Security Considerations + +- Programs execute in a sandboxed environment +- Memory access is controlled and limited +- Execution time is bounded to prevent DoS +- Extension calls go through permission checks +- No direct system call access from guest programs + +## Related Components + +- [PVQ Extension System](../pvq-extension/) - Extension framework +- [PVQ Runtime API](../pvq-runtime-api/) - Runtime integration +- [PVQ Program](../pvq-program/) - Program development tools +- [PVQ Test Runner](../pvq-test-runner/) - Testing utilities + +--- + +*The PVQ Executor provides the secure foundation for all PVQ program execution.* \ No newline at end of file diff --git a/pvq-executor/src/context.rs b/pvq-executor/src/context.rs index 8c37c94..aba7713 100644 --- a/pvq-executor/src/context.rs +++ b/pvq-executor/src/context.rs @@ -1,8 +1,13 @@ use polkavm::Linker; +/// The context for the PVQ executor. pub trait PvqExecutorContext { + /// The user data. type UserData; + /// The user error. type UserError; + /// Registers the host functions. fn register_host_functions(&mut self, linker: &mut Linker); + /// Returns a mutable reference to the user data. fn data(&mut self) -> &mut Self::UserData; } diff --git a/pvq-executor/src/error.rs b/pvq-executor/src/error.rs index 8aba798..d1cb633 100644 --- a/pvq-executor/src/error.rs +++ b/pvq-executor/src/error.rs @@ -1,20 +1,23 @@ use pvq_primitives::PvqError; +/// The error type for the PVQ executor. #[derive(Debug, thiserror::Error)] pub enum PvqExecutorError { + /// The program format is invalid. #[error("Invalid PVQ program format")] InvalidProgramFormat, + /// A memory access error occurred. #[error("Memory access error: {0}")] MemoryAccessError(polkavm::MemoryAccessError), - // Extract from the PVM CallError + /// A trap occurred during execution. #[error("Trap")] Trap, - // Extract from the PVM CallError + /// Not enough gas to execute the program. #[error("Not enough gas")] NotEnoughGas, - // Usually a custom error type from the extension system definition + /// A user-defined error occurred. #[error("User error: {0}")] User(UserError), - // Other errors directly from the PVM + /// An other error from the PolkaVM occurred. #[error("Other PVM error: {0}")] OtherPvmError(polkavm::Error), } diff --git a/pvq-executor/src/executor.rs b/pvq-executor/src/executor.rs index d2f1862..6c6a605 100644 --- a/pvq-executor/src/executor.rs +++ b/pvq-executor/src/executor.rs @@ -4,9 +4,12 @@ use polkavm::{Config, Engine, Linker, Module, ModuleConfig, ProgramBlob}; use crate::context::PvqExecutorContext; use crate::error::PvqExecutorError; +/// The result of a PVQ execution. type PvqExecutorResult = Result, PvqExecutorError>; +/// The gas limit for a PVQ execution. type GasLimit = Option; +/// The PVQ executor. pub struct PvqExecutor { engine: Engine, linker: Linker, @@ -14,6 +17,12 @@ pub struct PvqExecutor { } impl PvqExecutor { + /// Creates a new PVQ executor. + /// + /// # Arguments + /// + /// * `config`: The PolkaVM configuration. + /// * `context`: The PVQ executor context. pub fn new(config: Config, mut context: Ctx) -> Self { let engine = Engine::new(&config).unwrap(); let mut linker = Linker::::new(); @@ -26,6 +35,17 @@ impl PvqExecutor { } } + /// Executes a PVQ program. + /// + /// # Arguments + /// + /// * `program`: The PVQ program to execute. + /// * `args`: The arguments to pass to the program. + /// * `gas_limit`: The gas limit for the execution. + /// + /// # Returns + /// + /// A tuple containing the result of the execution and the remaining gas. pub fn execute( &mut self, program: &[u8], diff --git a/pvq-executor/src/lib.rs b/pvq-executor/src/lib.rs index 8092ad9..f938e62 100644 --- a/pvq-executor/src/lib.rs +++ b/pvq-executor/src/lib.rs @@ -1,3 +1,4 @@ +//! The PVQ executor. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; diff --git a/pvq-extension-core/src/lib.rs b/pvq-extension-core/src/lib.rs index 4a5c039..75b58be 100644 --- a/pvq-extension-core/src/lib.rs +++ b/pvq-extension-core/src/lib.rs @@ -1,12 +1,25 @@ +//! The core PVQ extension. #![cfg_attr(not(feature = "std"), no_std)] use pvq_extension::extension_decl; +/// The core PVQ extension. #[extension_decl] pub mod extension { + /// The core PVQ extension trait. #[extension_decl::extension] pub trait ExtensionCore { + /// The extension identifier type. type ExtensionId; + /// Check if an extension is enabled. + /// + /// # Arguments + /// + /// * `id`: The identifier of the extension. + /// + /// # Returns + /// + /// `true` if the extension is enabled, `false` otherwise. fn has_extension(id: Self::ExtensionId) -> bool; // crypto functions // fn blake2_64(data: Vec) -> [u8; 8]; diff --git a/pvq-extension-fungibles/src/lib.rs b/pvq-extension-fungibles/src/lib.rs index 7e104ca..2686d51 100644 --- a/pvq-extension-fungibles/src/lib.rs +++ b/pvq-extension-fungibles/src/lib.rs @@ -1,20 +1,90 @@ +//! The fungibles PVQ extension. #![cfg_attr(not(feature = "std"), no_std)] use pvq_extension::extension_decl; +/// The fungibles PVQ extension. #[extension_decl] pub mod extension { use scale_info::prelude::vec::Vec; + /// The fungibles PVQ extension trait. #[extension_decl::extension] pub trait ExtensionFungibles { + /// The asset identifier type. type AssetId; + /// The balance type. type Balance; + /// The account identifier type. type AccountId; + /// Check if an asset exists. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// `true` if the asset exists, `false` otherwise. fn asset_exists(asset: Self::AssetId) -> bool; + /// Get the name of an asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The name of the asset. fn name(asset: Self::AssetId) -> Vec; + /// Get the symbol of an asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The symbol of the asset. fn symbol(asset: Self::AssetId) -> Vec; + /// Get the decimals of an asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The decimals of the asset. fn decimals(asset: Self::AssetId) -> u8; + /// Get the minimum balance of an asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The minimum balance of the asset. fn minimum_balance(asset: Self::AssetId) -> Self::Balance; + /// Get the total supply of an asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// The total supply of the asset. fn total_supply(asset: Self::AssetId) -> Self::Balance; + /// Get the balance of an asset for a specific account. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// * `who`: The account identifier. + /// + /// # Returns + /// + /// The balance of the asset for the specified account. fn balance(asset: Self::AssetId, who: Self::AccountId) -> Self::Balance; } } diff --git a/pvq-extension-swap/README.md b/pvq-extension-swap/README.md new file mode 100644 index 0000000..f935888 --- /dev/null +++ b/pvq-extension-swap/README.md @@ -0,0 +1,589 @@ +# PVQ Extension Swap + + + +A specialized extension for the PVQ (PolkaVM Query) system that enables guest programs to interact with decentralized exchange (DEX) and liquidity pool functionality. This extension provides comprehensive access to swap information, price quotes, and liquidity data. + +## Overview + +The Swap Extension integrates with DEX pallets and liquidity protocols to provide PVQ guest programs with read-only access to trading and liquidity information. It supports various AMM (Automated Market Maker) models and liquidity pool configurations. + +## Features + +- 🏊 **Pool Discovery**: List and query available liquidity pools +- πŸ’± **Price Quotes**: Get accurate swap price estimates +- πŸ“Š **Liquidity Information**: Query pool reserves and liquidity depth +- πŸ“ˆ **Trading Analytics**: Calculate slippage, fees, and trading metrics +- πŸ” **Route Finding**: Discover optimal trading paths +- ⚑ **Multi-DEX Support**: Support for various DEX implementations + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ExtensionSwap β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Pools β”‚ β”‚ Pricing β”‚ β”‚ Analytics β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Pool List β”‚ β”‚ β”œβ”€ Quote β”‚ β”‚ β”œβ”€ Volume β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Reserves β”‚ β”‚ β”‚ Calculation β”‚ β”‚ β”œβ”€ Slippage β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Pool Info β”‚ β”‚ β”œβ”€ Price Impact β”‚ β”‚ β”œβ”€ Fees β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Liquidity β”‚ β”‚ β”œβ”€ Route β”‚ β”‚ β”œβ”€ APR/APY β”‚ β”‚ +β”‚ β”‚ β”‚ Depth β”‚ β”‚ β”‚ Discovery β”‚ β”‚ └─ Historicalβ”‚ β”‚ +β”‚ β”‚ └─ Pool Status β”‚ β”‚ └─ Multi-hop β”‚ β”‚ Data β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ Routing β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## API Reference + +### Pool Discovery + +#### `list_pools() -> Vec` + +Get a list of all available liquidity pools. + +```rust +use pvq_extension_swap::ExtensionSwap; + +#[pvq_program::program] +fn available_pools() -> Vec { + ExtensionSwap::list_pools() +} +``` + +#### `pool_exists(pool_id: PoolId) -> bool` + +Check if a specific pool exists. + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +let pool_id = PoolId::new(1, 2); // Token pair (1, 2) +if ExtensionSwap::pool_exists(pool_id) { + // Pool exists +} +``` + +#### `pools_for_asset(asset_id: u32) -> Vec` + +Find all pools containing a specific asset. + +```rust +use pvq_extension_swap::ExtensionSwap; + +let pools = ExtensionSwap::pools_for_asset(asset_id); +println!("Found {} pools with asset {}", pools.len(), asset_id); +``` + +### Pool Information + +#### `pool_info(pool_id: PoolId) -> Option` + +Get comprehensive information about a pool. + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +if let Some(info) = ExtensionSwap::pool_info(pool_id) { + println!("Pool reserves: {} / {}", info.reserve_0, info.reserve_1); + println!("Total liquidity: {}", info.total_liquidity); +} +``` + +#### `pool_reserves(pool_id: PoolId) -> Option<(u128, u128)>` + +Get current pool reserves for both assets. + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +if let Some((reserve_a, reserve_b)) = ExtensionSwap::pool_reserves(pool_id) { + println!("Reserve A: {}, Reserve B: {}", reserve_a, reserve_b); +} +``` + +#### `pool_total_liquidity(pool_id: PoolId) -> Option` + +Get the total liquidity in a pool. + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +if let Some(liquidity) = ExtensionSwap::pool_total_liquidity(pool_id) { + println!("Total liquidity: {}", liquidity); +} +``` + +### Price Quotation + +#### `quote_exact_in(asset_in: u32, asset_out: u32, amount_in: u128) -> Option` + +Get a quote for swapping an exact input amount. + +```rust +use pvq_extension_swap::ExtensionSwap; + +#[pvq_program::program] +fn get_swap_quote(asset_in: u32, asset_out: u32, amount_in: u128) -> String { + match ExtensionSwap::quote_exact_in(asset_in, asset_out, amount_in) { + Some(quote) => { + format!( + "Output: {}, Price Impact: {:.2}%, Fee: {}", + quote.amount_out, quote.price_impact, quote.fee + ) + } + None => "No route available".to_string(), + } +} +``` + +#### `quote_exact_out(asset_in: u32, asset_out: u32, amount_out: u128) -> Option` + +Get a quote for receiving an exact output amount. + +```rust +use pvq_extension_swap::ExtensionSwap; + +if let Some(quote) = ExtensionSwap::quote_exact_out(asset_in, asset_out, amount_out) { + println!("Required input: {}", quote.amount_in); +} +``` + +#### `get_price(asset_in: u32, asset_out: u32) -> Option` + +Get the current price ratio between two assets. + +```rust +use pvq_extension_swap::ExtensionSwap; + +if let Some(price) = ExtensionSwap::get_price(asset_in, asset_out) { + println!("Price: 1 token {} = {} token {}", asset_in, price, asset_out); +} +``` + +### Trading Analytics + +#### `calculate_slippage(pool_id: PoolId, amount_in: u128) -> Option` + +Calculate expected slippage for a trade. + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +if let Some(slippage) = ExtensionSwap::calculate_slippage(pool_id, amount_in) { + println!("Expected slippage: {:.2}%", slippage); +} +``` + +#### `pool_fee_rate(pool_id: PoolId) -> Option` + +Get the fee rate for a pool (in basis points). + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +if let Some(fee) = ExtensionSwap::pool_fee_rate(pool_id) { + println!("Pool fee: {:.2}%", fee as f64 / 100.0); +} +``` + +#### `pool_volume_24h(pool_id: PoolId) -> Option` + +Get 24-hour trading volume for a pool. + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +if let Some(volume) = ExtensionSwap::pool_volume_24h(pool_id) { + println!("24h volume: {}", volume); +} +``` + +### Route Discovery + +#### `find_route(asset_in: u32, asset_out: u32) -> Option>` + +Find the optimal trading route between two assets. + +```rust +use pvq_extension_swap::ExtensionSwap; + +if let Some(route) = ExtensionSwap::find_route(asset_in, asset_out) { + println!("Route found with {} hops", route.len()); +} else { + println!("No route available"); +} +``` + +#### `multi_hop_quote(route: Vec, amount_in: u128) -> Option` + +Get a quote for a multi-hop swap route. + +```rust +use pvq_extension_swap::ExtensionSwap; + +let route = ExtensionSwap::find_route(asset_in, asset_out)?; +if let Some(quote) = ExtensionSwap::multi_hop_quote(route, amount_in) { + println!("Multi-hop output: {}", quote.amount_out); +} +``` + +## Data Types + +### PoolId + +Unique identifier for a liquidity pool: + +```rust +pub struct PoolId { + pub asset_0: u32, + pub asset_1: u32, +} + +impl PoolId { + pub fn new(asset_0: u32, asset_1: u32) -> Self { + // Ensures asset_0 < asset_1 for consistency + if asset_0 < asset_1 { + Self { asset_0, asset_1 } + } else { + Self { asset_0: asset_1, asset_1: asset_0 } + } + } +} +``` + +### PoolInfo + +Comprehensive pool information: + +```rust +pub struct PoolInfo { + pub pool_id: PoolId, + pub reserve_0: u128, + pub reserve_1: u128, + pub total_liquidity: u128, + pub fee_rate: u32, // In basis points + pub is_active: bool, +} +``` + +### SwapQuote + +Detailed swap quotation: + +```rust +pub struct SwapQuote { + pub amount_in: u128, + pub amount_out: u128, + pub price_impact: f64, // Percentage + pub fee: u128, + pub route: Vec, + pub slippage_tolerance: f64, +} +``` + +## Usage Examples + +### Basic Pool Information + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +#[pvq_program::program] +fn pool_summary(asset_a: u32, asset_b: u32) -> String { + let pool_id = PoolId::new(asset_a, asset_b); + + match ExtensionSwap::pool_info(pool_id) { + Some(info) => { + format!( + "Pool {}-{}: Reserves: {} / {}, Liquidity: {}, Fee: {:.2}%", + asset_a, asset_b, + info.reserve_0, info.reserve_1, + info.total_liquidity, + info.fee_rate as f64 / 100.0 + ) + } + None => "Pool not found".to_string(), + } +} +``` + +### Price Comparison Across Pools + +```rust +use pvq_extension_swap::ExtensionSwap; + +#[pvq_program::program] +fn compare_prices(asset_in: u32, asset_out: u32) -> Vec<(u32, f64)> { + let pools = ExtensionSwap::pools_for_asset(asset_in); + let mut prices = Vec::new(); + + for pool in pools { + if pool.asset_0 == asset_out || pool.asset_1 == asset_out { + if let Some(price) = ExtensionSwap::get_price(asset_in, asset_out) { + prices.push((pool.asset_0.max(pool.asset_1), price)); + } + } + } + + prices +} +``` + +### Arbitrage Opportunity Scanner + +```rust +use pvq_extension_swap::{ExtensionSwap, PoolId}; + +#[pvq_program::program] +fn find_arbitrage(asset_a: u32, asset_b: u32, amount: u128) -> f64 { + let pools = ExtensionSwap::list_pools(); + let mut best_profit = 0.0; + + for pool in pools { + if (pool.asset_0 == asset_a && pool.asset_1 == asset_b) || + (pool.asset_0 == asset_b && pool.asset_1 == asset_a) { + + // Check price in this pool vs others + if let Some(quote) = ExtensionSwap::quote_exact_in(asset_a, asset_b, amount) { + let return_quote = ExtensionSwap::quote_exact_in(asset_b, asset_a, quote.amount_out); + + if let Some(return_quote) = return_quote { + let profit = return_quote.amount_out as f64 - amount as f64; + let profit_pct = (profit / amount as f64) * 100.0; + + if profit_pct > best_profit { + best_profit = profit_pct; + } + } + } + } + } + + best_profit +} +``` + +### Liquidity Analysis + +```rust +use pvq_extension_swap::ExtensionSwap; + +#[pvq_program::program] +fn liquidity_analysis(asset_id: u32) -> String { + let pools = ExtensionSwap::pools_for_asset(asset_id); + let mut total_liquidity = 0u128; + let mut active_pools = 0u32; + + for pool_id in pools { + if let Some(info) = ExtensionSwap::pool_info(pool_id) { + if info.is_active { + total_liquidity += info.total_liquidity; + active_pools += 1; + } + } + } + + format!( + "Asset {}: {} active pools, Total liquidity: {}", + asset_id, active_pools, total_liquidity + ) +} +``` + +### Best Route Finder + +```rust +use pvq_extension_swap::ExtensionSwap; + +#[pvq_program::program] +fn best_swap_route(asset_in: u32, asset_out: u32, amount: u128) -> String { + match ExtensionSwap::find_route(asset_in, asset_out) { + Some(route) => { + match ExtensionSwap::multi_hop_quote(route.clone(), amount) { + Some(quote) => { + format!( + "Best route: {} hops, Output: {}, Price Impact: {:.2}%", + route.len(), quote.amount_out, quote.price_impact + ) + } + None => "Route found but quote failed".to_string(), + } + } + None => "No route available".to_string(), + } +} +``` + +## Integration + +### With Runtime + +Configure the swap extension in your runtime: + +```rust +// In runtime configuration +impl pvq_extension_swap::Config for Runtime { + type AssetId = u32; + type Balance = u128; + type DexPallet = Dex; + type PoolId = PoolIdOf; +} +``` + +### With Extension Executor + +Register the swap extension: + +```rust +use pvq_extension::ExtensionsExecutor; +use pvq_extension_swap::ExtensionSwap; + +let mut executor = ExtensionsExecutor::new(); +executor.register::(); +``` + +## Configuration + +### DEX Integration + +Configure supported DEX types: + +```rust +impl pvq_extension_swap::Config for Runtime { + type SupportedDexes = ( + UniswapV2Pallet, + CurveStablePallet, + BalancerPallet, + ); +} +``` + +### Fee Configuration + +Set fee calculation parameters: + +```rust +impl pvq_extension_swap::Config for Runtime { + type DefaultFeeRate = ConstU32<30>; // 0.30% + type MaxSlippageTolerance = ConstU32<500>; // 5.00% +} +``` + +## Development + +### Building + +```bash +# Build swap extension +cargo build -p pvq-extension-swap + +# Run tests +cargo test -p pvq-extension-swap + +# Generate documentation +cargo doc -p pvq-extension-swap --open +``` + +### Testing + +```bash +# Unit tests +cargo test -p pvq-extension-swap + +# Integration tests with DEX pallets +cargo test -p pvq-extension-swap --test dex_integration + +# Test with guest programs +cargo run -p pvq-test-runner -- --program test-swap +``` + +### Benchmarking + +```bash +# Run performance benchmarks +cargo bench -p pvq-extension-swap + +# Profile quote calculations +cargo run --example profile_quotes --release +``` + +## Performance Considerations + +### Optimization Tips + +1. **Cache Pool Data**: Pool information changes infrequently +2. **Batch Route Discovery**: Find multiple routes in single queries +3. **Limit Route Depth**: Set maximum hops for complex routes +4. **Filter Inactive Pools**: Only query active pools + +### Query Patterns + +```rust +// Efficient: Batch multiple quotes +#[pvq_program::program] +fn batch_quotes(pairs: Vec<(u32, u32)>, amount: u128) -> Vec> { + pairs.iter() + .map(|(asset_in, asset_out)| { + ExtensionSwap::quote_exact_in(*asset_in, *asset_out, amount) + .map(|quote| quote.amount_out) + }) + .collect() +} +``` + +## Security Considerations + +- All functions are read-only and cannot execute trades +- Price quotes are estimates and may differ from actual execution +- Pool data reflects current state, which can change rapidly +- Route calculations consider current liquidity conditions + +## Common Use Cases + +### DEX Aggregation + +```rust +// Find best price across multiple DEXes +#[pvq_program::program] +fn best_price_aggregation(asset_in: u32, asset_out: u32, amount: u128) -> SwapQuote { + let all_routes = ExtensionSwap::find_all_routes(asset_in, asset_out); + + all_routes.into_iter() + .filter_map(|route| ExtensionSwap::multi_hop_quote(route, amount)) + .max_by_key(|quote| quote.amount_out) + .unwrap_or_default() +} +``` + +### Yield Farming Analysis + +```rust +// Calculate potential LP returns +#[pvq_program::program] +fn lp_yield_analysis(pool_id: PoolId) -> f64 { + let volume = ExtensionSwap::pool_volume_24h(pool_id).unwrap_or(0); + let liquidity = ExtensionSwap::pool_total_liquidity(pool_id).unwrap_or(1); + let fee_rate = ExtensionSwap::pool_fee_rate(pool_id).unwrap_or(0); + + // Simple APR calculation based on fees + let daily_fees = (volume * fee_rate as u128) / 10000; + let apr = (daily_fees as f64 / liquidity as f64) * 365.0 * 100.0; + + apr +} +``` + +## Related Components + +- [PVQ Extension System](../pvq-extension/) - Extension framework +- [PVQ Extension Core](../pvq-extension-core/) - Core functionality +- [PVQ Extension Fungibles](../pvq-extension-fungibles/) - Asset functionality +- [PVQ Runtime API](../pvq-runtime-api/) - Runtime integration + +--- + +*The Swap Extension provides comprehensive DEX and liquidity pool access for PVQ programs.* \ No newline at end of file diff --git a/pvq-extension-swap/src/lib.rs b/pvq-extension-swap/src/lib.rs index 1b46977..28cf030 100644 --- a/pvq-extension-swap/src/lib.rs +++ b/pvq-extension-swap/src/lib.rs @@ -1,19 +1,37 @@ +//! The swap PVQ extension. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use pvq_extension::extension_decl; +/// The swap PVQ extension. #[extension_decl] pub mod extension { use alloc::collections::BTreeMap; use alloc::vec::Vec; + /// The swap PVQ extension trait. #[extension_decl::extension] pub trait ExtensionSwap { + /// The asset identifier type. type AssetId; + /// The balance type. type Balance; + /// The asset info type. type AssetInfo; + /// Get the quote price of `asset1` in terms of `asset2` for a given amount of `asset2`. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the asset to be quoted. + /// * `asset2`: The identifier of the asset to quote against. + /// * `amount`: The amount of `asset2`. + /// * `include_fee`: Whether to include the fee in the quote. + /// + /// # Returns + /// + /// The quote price of `asset1` in terms of `asset2`, or `None` if the quote is not available. fn quote_price_tokens_for_exact_tokens( asset1: Self::AssetId, asset2: Self::AssetId, @@ -21,6 +39,18 @@ pub mod extension { include_fee: bool, ) -> Option; + /// Get the quote price of `asset2` in terms of `asset1` for a given amount of `asset1`. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the asset to quote against. + /// * `asset2`: The identifier of the asset to be quoted. + /// * `amount`: The amount of `asset1`. + /// * `include_fee`: Whether to include the fee in the quote. + /// + /// # Returns + /// + /// The quote price of `asset2` in terms of `asset1`, or `None` if the quote is not available. fn quote_price_exact_tokens_for_tokens( asset1: Self::AssetId, asset2: Self::AssetId, @@ -28,12 +58,41 @@ pub mod extension { include_fee: bool, ) -> Option; + /// Get the liquidity pool of two assets. + /// + /// # Arguments + /// + /// * `asset1`: The identifier of the first asset. + /// * `asset2`: The identifier of the second asset. + /// + /// # Returns + /// + /// A tuple containing the balance of each asset in the liquidity pool, or `None` if the pool does not exist. fn get_liquidity_pool(asset1: Self::AssetId, asset2: Self::AssetId) -> Option<(Self::Balance, Self::Balance)>; + /// List all available liquidity pools. + /// + /// # Returns + /// + /// A list of tuples, where each tuple represents a liquidity pool and contains the identifiers of the two assets in the pool. fn list_pools() -> Vec<(Self::AssetId, Self::AssetId)>; + /// Get information about a specific asset. + /// + /// # Arguments + /// + /// * `asset`: The identifier of the asset. + /// + /// # Returns + /// + /// Information about the asset, or `None` if the asset does not exist. fn asset_info(asset: Self::AssetId) -> Option; + /// Get information about all assets. + /// + /// # Returns + /// + /// A map of asset identifiers to asset information. fn assets_info() -> BTreeMap; } } diff --git a/pvq-extension/README.md b/pvq-extension/README.md new file mode 100644 index 0000000..abc11cd --- /dev/null +++ b/pvq-extension/README.md @@ -0,0 +1,27 @@ +# PVQ Extension + +Extension framework for PVQ with declarative interfaces, metadata generation, execution, and permissions. + +### What it provides + +- Declarative extension traits and implementations via macros +- Generated dispatch and metadata for extensions +- Execution wrapper to run PVQ programs against your extensions +- Pluggable permission controller (allow‑all by default) + +### Typical use + +1. Declare an extension trait and implement it for your runtime. +2. Generate an `extensions` module (via the macros) and expose its `metadata()`. +3. Initialize `ExtensionsExecutor` with your generated `extensions` and an invocation source. +4. Optionally implement a custom `PermissionController`. + +### Metadata + +- A portable type registry plus per‑extension function signatures (names, inputs, output). Useful to serve from your runtime’s `metadata()`. + +### Build + +```bash +cargo build -p pvq-extension +``` diff --git a/pvq-extension/procedural/src/lib.rs b/pvq-extension/procedural/src/lib.rs index 9bc82dd..a5a13b9 100644 --- a/pvq-extension/procedural/src/lib.rs +++ b/pvq-extension/procedural/src/lib.rs @@ -1,13 +1,16 @@ +//! This crate provides procedural macros for the PVQ extension system. use proc_macro::TokenStream; mod extension_decl; mod extensions_impl; pub(crate) mod utils; +/// A procedural macro for declaring an extension. #[proc_macro_attribute] pub fn extension_decl(attr: TokenStream, item: TokenStream) -> TokenStream { extension_decl::extension_decl(attr, item) } +/// A procedural macro for implementing extensions. #[proc_macro_attribute] pub fn extensions_impl(attr: TokenStream, item: TokenStream) -> TokenStream { extensions_impl::extensions_impl(attr, item) diff --git a/pvq-extension/src/calldata.rs b/pvq-extension/src/calldata.rs index 3810f25..6e73c3c 100644 --- a/pvq-extension/src/calldata.rs +++ b/pvq-extension/src/calldata.rs @@ -1,31 +1,35 @@ +//! This module defines the traits and types for handling extension call data. use parity_scale_codec::Decode; use scale_info::prelude::vec::Vec; -/// Type for extension IDs +/// The type for extension identifiers. pub type ExtensionIdTy = u64; -/// Trait for identifying extensions +/// A trait for identifying extensions. pub trait ExtensionId { + /// The unique identifier of the extension. const EXTENSION_ID: ExtensionIdTy; } -/// Trait for dispatching extension calls +/// A trait for dispatching extension calls. pub trait Dispatchable { + /// Dispatches the extension call. fn dispatch(self) -> Result, DispatchError>; } -/// Error type for dispatch operations +/// The error type for dispatch operations. #[derive(Debug, thiserror::Error)] pub enum DispatchError { + /// A phantom data error. #[error("PhantomData")] PhantomData, } -/// Trait for extension call data +/// A trait for extension call data. /// /// This trait combines several traits that are required for extension call data: -/// - `Dispatchable`: Allows dispatching calls to the extension functions -/// - `ExtensionId`: Identifies the extension -/// - `Decode`: Allows decoding the call data +/// - `Dispatchable`: Allows dispatching calls to the extension functions. +/// - `ExtensionId`: Identifies the extension. +/// - `Decode`: Allows decoding the call data. pub trait CallData: Dispatchable + ExtensionId + Decode {} impl CallData for T where T: Dispatchable + ExtensionId + Decode {} diff --git a/pvq-extension/src/context.rs b/pvq-extension/src/context.rs index 976c0c6..4e0d22d 100644 --- a/pvq-extension/src/context.rs +++ b/pvq-extension/src/context.rs @@ -1,3 +1,4 @@ +//! This module defines the execution context for extensions. use pvq_executor::{Caller, Linker, PvqExecutorContext}; use scale_info::prelude::marker::PhantomData; @@ -7,25 +8,25 @@ use crate::{ CallDataTuple, }; -/// Execution context for extensions +/// The execution context for extensions. /// /// This struct provides the context for executing extensions. /// It includes the invoke source and user data. pub struct Context { - /// The source of the invocation + /// The source of the invocation. invoke_source: InvokeSource, - /// User data for the context + /// User data for the context. user_data: (), - /// Marker for the generic parameters + /// A marker for the generic parameters. _marker: PhantomData<(C, P)>, } impl Context { - /// Create a new context + /// Creates a new context. /// /// # Arguments /// - /// * `invoke_source` - The source of the invocation + /// * `invoke_source`: The source of the invocation. pub fn new(invoke_source: InvokeSource) -> Self { Self { invoke_source, diff --git a/pvq-extension/src/error.rs b/pvq-extension/src/error.rs index d3ac67f..89401f1 100644 --- a/pvq-extension/src/error.rs +++ b/pvq-extension/src/error.rs @@ -1,32 +1,33 @@ +//! This module defines the error types for the extension system. // TODO: contain source error use crate::DispatchError; use parity_scale_codec::Error as CodecError; -/// Errors that can occur when working with extensions +/// The error type for the extension system. // Typically will be used as a UserError #[derive(Debug, thiserror::Error)] pub enum ExtensionError { - /// Permission denied for the requested operation + /// Permission to perform the requested operation was denied. #[error("Permission denied")] PermissionError, - /// Failed to allocate memory + /// Failed to allocate memory. #[error("Failed to allocate memory")] MemoryAllocationError, - /// Error accessing memory + /// An error occurred while accessing memory. #[error("Memory access error: {0}")] MemoryAccessError(polkavm::MemoryAccessError), - /// Error decoding data + /// An error occurred while decoding data. #[error("Decode error: {0}")] DecodeError(CodecError), - /// Error dispatching a call + /// An error occurred while dispatching a call. #[error("Dispatch error: {0:?}")] DispatchError(#[from] DispatchError), - /// The requested extension is not supported + /// The requested extension is not supported. #[error("Unsupported extension")] UnsupportedExtension, } diff --git a/pvq-extension/src/executor.rs b/pvq-extension/src/executor.rs index f83fd03..3d68829 100644 --- a/pvq-extension/src/executor.rs +++ b/pvq-extension/src/executor.rs @@ -1,3 +1,4 @@ +//! This module defines the executor for extensions. use pvq_executor::PvqExecutor; use pvq_primitives::{PvqError, PvqResult}; @@ -6,36 +7,38 @@ use crate::{ CallDataTuple, Context, }; -/// Executor for extensions +/// The executor for extensions. /// /// This struct provides an executor for running extension code. -/// It wraps a PvqExecutor with a Context for extensions. +/// It wraps a `PvqExecutor` with a `Context` for extensions. pub struct ExtensionsExecutor { - /// The underlying PVQ executor + /// The underlying PVQ executor. executor: PvqExecutor>, } impl ExtensionsExecutor { - /// Create a new extensions executor + /// Creates a new extensions executor. /// /// # Arguments /// - /// * `source` - The source of the invocation + /// * `source`: The source of the invocation. pub fn new(source: InvokeSource) -> Self { let context = Context::::new(source); let executor = PvqExecutor::new(Default::default(), context); Self { executor } } + /// Executes a program with the given arguments and gas limit. /// /// # Arguments /// - /// * `program` - The program data - /// * `args` - The input data + /// * `program`: The program to execute. + /// * `args`: The arguments to pass to the program. + /// * `gas_limit`: The gas limit for the execution. /// /// # Returns /// - /// The result of the execution or an error + /// A tuple containing the result of the execution and the remaining gas. pub fn execute(&mut self, program: &[u8], args: &[u8], gas_limit: Option) -> (PvqResult, Option) { let (result, gas_remaining) = self.executor.execute(program, args, gas_limit); tracing::info!("result: {:?}", result); diff --git a/pvq-extension/src/macros.rs b/pvq-extension/src/macros.rs index 447640a..b4dff2d 100644 --- a/pvq-extension/src/macros.rs +++ b/pvq-extension/src/macros.rs @@ -1,19 +1,20 @@ +//! This module defines the `CallDataTuple` trait, which is used to dispatch calls to extensions. use crate::{CallData, ExtensionError, ExtensionIdTy}; use fortuples::fortuples; use scale_info::prelude::vec::Vec; -/// Trait for a tuple of extension call data types +/// A trait for a tuple of extension call data types. pub trait CallDataTuple { - /// Dispatch a call to an extension + /// Dispatches a call to an extension. /// /// # Arguments /// - /// * `extension_id` - The ID of the extension to call - /// * `data` - The encoded call data + /// * `extension_id`: The identifier of the extension to call. + /// * `data`: The encoded call data. /// /// # Returns /// - /// The encoded response data or an error + /// The encoded response data or an error. fn dispatch(extension_id: ExtensionIdTy, data: &[u8]) -> Result, ExtensionError>; } diff --git a/pvq-extension/src/metadata.rs b/pvq-extension/src/metadata.rs index 3237302..036dd05 100644 --- a/pvq-extension/src/metadata.rs +++ b/pvq-extension/src/metadata.rs @@ -1,8 +1,10 @@ +//! This module defines the metadata structures for extensions. use crate::ExtensionIdTy; use scale_info::prelude::string::{String, ToString}; -// This trait is for ExtensionImpl +/// A trait for retrieving extension implementation metadata. pub trait ExtensionImplMetadata { + /// Returns the metadata for a given extension. fn extension_metadata(extension_id: ExtensionIdTy) -> ExtensionMetadata; } @@ -14,15 +16,17 @@ use scale_info::{ IntoPortable, PortableRegistry, Registry, }; use serde::Serialize; -/// Metadata of extensions +/// The metadata of all extensions. #[derive(Clone, PartialEq, Eq, Encode, Debug, Serialize)] pub struct Metadata { + /// The portable type registry. pub types: PortableRegistry, - // Use String to prevent loss of precision in frontend codes + /// A map of extension identifiers to their metadata. pub extensions: BTreeMap>, } impl Metadata { + /// Creates a new `Metadata` instance. pub fn new(extensions: BTreeMap) -> Self { let mut registry = Registry::new(); let extensions = extensions @@ -36,10 +40,12 @@ impl Metadata { } } -/// Metadata of an extension. +/// The metadata of an extension. #[derive(Clone, PartialEq, Eq, Encode, Debug)] pub struct ExtensionMetadata { + /// The name of the extension. pub name: T::String, + /// The functions of the extension. pub functions: Vec>, } @@ -67,14 +73,14 @@ impl Serialize for ExtensionMetadata { } } -/// Metadata of a runtime function. +/// The metadata of a runtime function. #[derive(Clone, PartialEq, Eq, Encode, Debug)] pub struct FunctionMetadata { - /// Method name. + /// The name of the function. pub name: T::String, - /// Method parameters. + /// The parameters of the function. pub inputs: Vec>, - /// Method output. + /// The output of the function. pub output: T::Type, } @@ -104,12 +110,12 @@ impl Serialize for FunctionMetadata { } } -/// Metadata of a runtime method parameter. +/// The metadata of a runtime function parameter. #[derive(Clone, PartialEq, Eq, Encode, Debug)] pub struct FunctionParamMetadata { - /// Parameter name. + /// The name of the parameter. pub name: T::String, - /// Parameter type. + /// The type of the parameter. pub ty: T::Type, } diff --git a/pvq-extension/src/perm_controller.rs b/pvq-extension/src/perm_controller.rs index 8a98ef4..5a4f3ab 100644 --- a/pvq-extension/src/perm_controller.rs +++ b/pvq-extension/src/perm_controller.rs @@ -1,41 +1,42 @@ +//! This module defines the permission controller for extensions. use crate::ExtensionIdTy; -/// Source of an extension invocation +/// The source of an extension invocation. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum InvokeSource { - /// Invoked from a runtime API + /// The invocation is from a runtime API. RuntimeAPI, - /// Invoked from XCM (Cross-Consensus Message) + /// The invocation is from an XCM (Cross-Consensus Message). XCM, - /// Invoked from an extrinsic + /// The invocation is from an extrinsic. Extrinsic, - /// Invoked from the runtime inside + /// The invocation is from the runtime itself. Runtime, } -/// Controller for extension permissions +/// A controller for extension permissions. /// /// This trait is used to control access to extensions based on the extension ID, /// call data, and invocation source. pub trait PermissionController { - /// Check if a call to an extension is allowed + /// Checks if a call to an extension is allowed. /// /// # Arguments /// - /// * `extension_id` - The ID of the extension - /// * `call` - The encoded call data - /// * `source` - The source of the invocation + /// * `extension_id`: The identifier of the extension. + /// * `call`: The encoded call data. + /// * `source`: The source of the invocation. /// /// # Returns /// - /// `true` if the call is allowed, `false` otherwise + /// `true` if the call is allowed, `false` otherwise. fn is_allowed(extension_id: ExtensionIdTy, call: &[u8], source: InvokeSource) -> bool; } -/// Default permission controller that allows everything +/// A default permission controller that allows all calls. impl PermissionController for () { fn is_allowed(_extension_id: ExtensionIdTy, _call: &[u8], _source: InvokeSource) -> bool { true diff --git a/pvq-primitives/README.md b/pvq-primitives/README.md new file mode 100644 index 0000000..4203475 --- /dev/null +++ b/pvq-primitives/README.md @@ -0,0 +1,17 @@ +# PVQ Primitives + +Lightweight types shared across PVQ crates. + +- PvqResponse: raw bytes returned from a PVQ program +- PvqResult: success bytes or error +- PvqError: in `std` it is a string message; in `no_std` it is a compact enum + +### Behavior + +- No helpers or codecs are provided here; this crate only defines the basic types used by executors, extensions, and runtime APIs. + +### Build + +```bash +cargo build -p pvq-primitives +``` diff --git a/pvq-primitives/src/lib.rs b/pvq-primitives/src/lib.rs index 7f4f93d..e1aaf10 100644 --- a/pvq-primitives/src/lib.rs +++ b/pvq-primitives/src/lib.rs @@ -1,3 +1,4 @@ +//! This crate defines the primitive types for the PVQ system. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -8,21 +9,32 @@ use parity_scale_codec::{Decode, Encode}; #[cfg(not(feature = "std"))] use scale_info::TypeInfo; +/// The result of a PVQ query. pub type PvqResult = Result; +/// The response of a PVQ query. pub type PvqResponse = Vec; +/// The error of a PVQ query. #[cfg(feature = "std")] pub type PvqError = String; +/// The error of a PVQ query. #[cfg(not(feature = "std"))] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub enum PvqError { + /// Failed to decode the query. FailedToDecode, + /// The PVQ program format is invalid. InvalidPvqProgramFormat, + /// The query exceeds the weight limit. QueryExceedsWeightLimit, + /// A trap occurred during execution. Trap, + /// A memory access error occurred. MemoryAccessError, + /// A host call error occurred. HostCallError, + /// An other error occurred. Other, } diff --git a/pvq-program-metadata-gen/src/lib.rs b/pvq-program-metadata-gen/src/lib.rs index 82961fe..6be6c4d 100644 --- a/pvq-program-metadata-gen/src/lib.rs +++ b/pvq-program-metadata-gen/src/lib.rs @@ -1,3 +1,4 @@ +//! This crate provides a tool for generating metadata for PVQ programs. pub type ExtensionId = u64; pub type FnIndex = u8; mod features; diff --git a/pvq-program/README.md b/pvq-program/README.md new file mode 100644 index 0000000..119f03f --- /dev/null +++ b/pvq-program/README.md @@ -0,0 +1,113 @@ +# PVQ Program + +Procedural macro for declaring PVQ (PolkaVM Query) guest programs. This crate exposes one attribute, `#[pvq_program::program]`, which turns an inline Rust module into a PolkaVM guest with entrypoints and host extension calls. + +The README reflects the real implementation in this crate and the working examples in `guest-examples/`. + +## Usage overview + +- Annotate an inline module with `#[pvq_program::program]`. +- Mark extension functions with `#[program::extension_fn(extension_id = , fn_index = )]`. +- Mark one or more entrypoints with `#[program::entrypoint]`. +- Use SCALE-encodable types for all arguments and return values. + +## Simple example + +A minimal module with one extension function and one entrypoint: + +```rust +#![no_std] +#![no_main] + +#[pvq_program::program] +mod my_program { + type AssetId = u32; + type Balance = u64; + + // Replace extension_id/fn_index with values that match your runtime + #[program::extension_fn(extension_id = 123u64, fn_index = 0)] + fn total_supply(asset: AssetId) -> Balance {} + + #[program::entrypoint] + fn query(asset: AssetId) -> Balance { + total_supply(asset) + } +} +``` + +## Authoring rules + +- Use on an inline module only: `#[pvq_program::program] mod my_program { ... }`. +- Mark extension functions with `#[program::extension_fn(extension_id = , fn_index = )]`. + - Both attributes are required. + - Functions must be free functions (no `self`/receiver). +- Mark one or more entrypoints with `#[program::entrypoint]`. + - At least one entrypoint is required. + - Entrypoints must be free functions (no receivers). + - Entrypoints’ arguments and return types must be SCALE `Encode`/`Decode`. + +## Types and encoding + +All inputs/outputs are SCALE encoded. For custom structs/enums, always derive +`parity_scale_codec::{Encode, Decode}` (for host/guest serialization) and +`scale_info::TypeInfo` (for metadata generation). + +Example: + +```rust +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode, scale_info::TypeInfo)] +pub struct MyData { pub x: u32, pub y: u64 } + +#[program::entrypoint] +fn do_something(d: MyData) -> Option { Some(d.x as u64 + d.y) } +``` + +## Building and metadata + +Guest programs are `no_std` PolkaVM binaries. Typical Cargo dependencies (see `guest-examples/*/Cargo.toml`): + +```toml +[dependencies] +polkavm-derive = { workspace = true } +pvq-program = { workspace = true } +parity-scale-codec = { workspace = true } +# For richer metadata types: +scale-info = { workspace = true, optional = true } +``` + +To emit program metadata during build (as used in all examples), use a `build.rs` +that runs the generator and writes to a directory provided by the environment: + +```rust +use std::{env, path::PathBuf, process::Command}; + +fn main() { + println!("cargo:rerun-if-changed=src/main.rs"); + let current_dir = env::current_dir().expect("Failed to get current directory"); + let output_dir = PathBuf::from(env::var("METADATA_OUTPUT_DIR").expect("METADATA_OUTPUT_DIR is not set")); + + let status = Command::new("pvq-program-metadata-gen") + .arg("--crate-path").arg(¤t_dir) + .arg("--output-dir").arg(&output_dir) + .env("RUST_LOG", "info") + .status() + .expect("Failed to execute pvq-program-metadata-gen"); + if !status.success() { panic!("Failed to generate program metadata"); } +} +``` + +## Not supported (by design) + +- Per-function macro options such as optimization levels, timeouts, custom stack/memory, or built-in testing/debug/profiling helpers are not part of this crate. +- Methods with `self`/`&self` receivers cannot be entrypoints or extension functions. + +## See also + +- `guest-examples/sum-balance` +- `guest-examples/sum-balance-percent` +- `guest-examples/total-supply` +- `guest-examples/swap-info` + +--- + +See `pvq-program/src/lib.rs` for the re-export and `pvq-program/procedural` for internals. diff --git a/pvq-program/procedural/src/lib.rs b/pvq-program/procedural/src/lib.rs index 37babc0..c617b2e 100644 --- a/pvq-program/procedural/src/lib.rs +++ b/pvq-program/procedural/src/lib.rs @@ -1,4 +1,7 @@ -/// Declare the calls used in PVQ program +//! This crate provides the `program` procedural macro for creating PVQ programs. +/// +/// # Usage +/// /// ```ignore /// #[program] /// mod query_fungibles { @@ -20,10 +23,10 @@ /// } /// } /// ``` -/// mod program; use proc_macro::TokenStream; +/// A procedural macro for creating PVQ programs. #[proc_macro_attribute] pub fn program(attr: TokenStream, item: TokenStream) -> TokenStream { program::program(attr, item) diff --git a/pvq-program/src/lib.rs b/pvq-program/src/lib.rs index dd2d335..cae8057 100644 --- a/pvq-program/src/lib.rs +++ b/pvq-program/src/lib.rs @@ -1,2 +1,3 @@ +//! This crate provides the `program` macro for creating PVQ programs. #![cfg_attr(not(feature = "std"), no_std)] pub use pvq_program_procedural::program; diff --git a/pvq-runtime-api/README.md b/pvq-runtime-api/README.md new file mode 100644 index 0000000..16040f9 --- /dev/null +++ b/pvq-runtime-api/README.md @@ -0,0 +1,86 @@ +# PVQ Runtime API + +Substrate runtime API definition for PVQ (PolkaVM Query). This crate exposes a single runtime API trait that runtimes implement to execute PVQ programs and expose PVQ-related metadata. + +## API definition + +The trait exactly as defined in this crate: + +```rust +use alloc::vec::Vec; +use pvq_primitives::PvqResult; +use sp_api::decl_runtime_apis; + +decl_runtime_apis! { + /// The runtime API for the PVQ module. + pub trait PvqApi { + /// Execute a PVQ program with arguments. + /// + /// - `program`: PolkaVM bytecode of the guest program + /// - `args`: SCALE-encoded call data + /// - `gas_limit`: Optional execution gas limit; `None` means use the default time boundary + fn execute_query(program: Vec, args: Vec, gas_limit: Option) -> PvqResult; + + /// Return PVQ extensions metadata as an opaque byte blob. + fn metadata() -> Vec; + } +} +``` + +Notes + +- `PvqResult` is defined in `pvq-primitives` as `Result` where `PvqResponse = Vec`. +- The `args` buffer must match the PVQ guest ABI (see `pvq-program`): first byte is the entrypoint index, followed by SCALE-encoded arguments. + +## Minimal runtime implementation + +```rust +use sp_api::impl_runtime_apis; +use pvq_runtime_api::PvqApi; +use pvq_primitives::PvqResult; + +impl_runtime_apis! { + impl PvqApi for Runtime { + fn execute_query(program: Vec, args: Vec, gas_limit: Option) -> PvqResult { + // Integrate with your PVQ executor here, e.g. pvq-executor + // Ok(result_bytes) or Err(pvq_error) + unimplemented!() + } + + fn metadata() -> Vec { + // Return extension metadata bytes (format decided by the runtime) + Vec::new() + } + } +} +``` + +## Metadata + +`metadata()` returns a byte vector. The encoding and schema are defined by the runtime. Recommended format is either SCALE or JSON of the structure defined in `pvq-extension` (`pvq-extension/src/metadata.rs`). + +Shape + +```json +{ + "types": { /* scale-info PortableRegistry */ }, + "extensions": { + "": { + "name": "", + "functions": [ + { + "name": "", + "inputs": [ { "name": "", "ty": } ], + "output": + } + ] + } + } +} +``` + +Notes + +- `types` is a `scale-info` PortableRegistry describing all referenced types. +- `ty` and `output` are numeric type IDs that reference entries inside `types`. +- The `extensions` object is keyed by stringified extension IDs (u64 -> string) and maps to per-extension metadata: name and function signatures. diff --git a/pvq-runtime-api/src/lib.rs b/pvq-runtime-api/src/lib.rs index af4110d..7a69861 100644 --- a/pvq-runtime-api/src/lib.rs +++ b/pvq-runtime-api/src/lib.rs @@ -1,3 +1,4 @@ +//! This crate defines the runtime API for the PVQ module. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -6,13 +7,22 @@ use alloc::vec::Vec; use pvq_primitives::PvqResult; use sp_api::decl_runtime_apis; -// The runtime API for the PVQ module. -// - `program`: PVQ binary. -// - `args`: Query arguments that is SCALE-encoded. -// - `gas_limit`: Optional gas limit for query execution. When set to `None`, execution is constrained by the default time boundary. decl_runtime_apis! { + /// The runtime API for the PVQ module. pub trait PvqApi { + /// Executes a PVQ query. + /// + /// # Arguments + /// + /// * `program`: The PVQ program binary. + /// * `args`: The SCALE-encoded query arguments. + /// * `gas_limit`: An optional gas limit for the query execution. If `None`, the execution is constrained by the default time boundary. + /// + /// # Returns + /// + /// The result of the PVQ query. fn execute_query(program: Vec, args: Vec, gas_limit: Option) -> PvqResult; + /// Returns the metadata of the PVQ extensions. fn metadata() -> Vec; } } diff --git a/pvq-test-runner/README.md b/pvq-test-runner/README.md new file mode 100644 index 0000000..4c39b7f --- /dev/null +++ b/pvq-test-runner/README.md @@ -0,0 +1,24 @@ +# PVQ Test Runner + +CLI to execute PVQ guest programs against the extensions and executor used in this repository. + +### Use cases + +- Prepare input bytes for bundled example programs +- Execute a program and print the raw result +- Inspect generated extension metadata + +### CLI usage + +```bash +cargo run -p pvq-test-runner -- --program path/to/program.polkavm --chain poc --entrypoint-idx 0 + +# Print only prepared input/expected result +cargo run -p pvq-test-runner -- --program path/to/program.polkavm --chain poc --entrypoint-idx 0 --print-data +``` + +### Build + +```bash +cargo build -p pvq-test-runner --release +``` diff --git a/pvq-test-runner/src/lib.rs b/pvq-test-runner/src/lib.rs index 106de3a..c7743d3 100644 --- a/pvq-test-runner/src/lib.rs +++ b/pvq-test-runner/src/lib.rs @@ -1,3 +1,4 @@ +//! This crate provides a test runner for PVQ programs. use parity_scale_codec::Encode; use pvq_extension::{extensions_impl, ExtensionsExecutor, InvokeSource}; use sp_core::crypto::{AccountId32, Ss58Codec}; @@ -5,22 +6,27 @@ use sp_core::hexdisplay::HexDisplay; use xcm::v5::Junction::{GeneralIndex, PalletInstance}; use xcm::v5::Location; +/// The functions of the fungibles extension. #[derive(Encode)] #[allow(non_camel_case_types)] #[allow(dead_code)] pub enum ExtensionFungiblesFunctions { + /// The `total_supply` function. #[codec(index = 0)] total_supply { asset: u32 }, + /// The `balance` function. #[codec(index = 1)] balance { asset: u32, who: [u8; 32] }, } +/// The extensions implementation for the test runner. #[extensions_impl] pub mod extensions { use std::collections::BTreeMap; use parity_scale_codec::Decode; + /// The extensions implementation struct. #[extensions_impl::impl_struct] pub struct ExtensionsImpl; @@ -102,17 +108,20 @@ pub mod extensions { } } +/// The test runner for PVQ programs. pub struct TestRunner { executor: ExtensionsExecutor, } impl TestRunner { + /// Creates a new test runner. pub fn new() -> Self { Self { executor: ExtensionsExecutor::new(InvokeSource::RuntimeAPI), } } + /// Prepares the input data for a test. pub fn prepare_input_data(program_path: &str, chain: &str) -> Vec { let mut input_data = Vec::new(); @@ -146,6 +155,7 @@ impl TestRunner { input_data } + /// Returns the expected result of a test. pub fn expected_result(program_path: &str, chain: &str, entrypoint_idx: u8) -> Vec { // TODO: add more entrypoints if program_path.contains("sum-balance") && chain == "poc" && entrypoint_idx == 0 { @@ -159,6 +169,7 @@ impl TestRunner { Vec::new() } + /// Executes a PVQ program. pub fn execute_program(&mut self, program_blob: &[u8], input_data: &[u8]) -> pvq_primitives::PvqResult { let (result, _) = self.executor.execute(program_blob, input_data, None); result