diff --git a/Cargo.lock b/Cargo.lock index b1b1d32..c3371f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,10 +134,12 @@ version = "0.1.3" dependencies = [ "ash_sdk", "async-std", + "chrono", "clap 4.2.7", "colored", "enum-display-derive", "exitcode", + "hex", "indent", "indoc", "rust_decimal", @@ -150,6 +152,7 @@ version = "0.1.3" dependencies = [ "async-std", "avalanche-types", + "chrono", "config", "enum-display-derive", "ethers", @@ -158,7 +161,10 @@ dependencies = [ "reqwest", "serde", "serde-aux", + "serde_json", "serde_yaml", + "serial_test", + "strum", "tempfile", "thiserror", "ureq", @@ -1246,6 +1252,19 @@ dependencies = [ "syn 2.0.16", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.7", +] + [[package]] name = "data-encoding" version = "2.3.3" @@ -4362,6 +4381,31 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.1", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + [[package]] name = "sha-1" version = "0.10.1" diff --git a/README.md b/README.md index f897d59..cadc8d8 100644 --- a/README.md +++ b/README.md @@ -7,40 +7,19 @@ This project provides Rust crates to interact with: ## Crates -- [ash_sdk](crates/ash_sdk): Ash Rust SDK -- [ash_cli](crates/ash_cli): Ash CLI +- [ash_sdk](crates/ash_sdk): Ash Rust SDK + [crates.io](https://crates.io/crates/ash_sdk) + [docs.rs](https://docs.rs/ash_sdk) +- [ash_cli](crates/ash_cli): Ash CLI + [crates.io](https://crates.io/crates/ash_cli) ## Ash CLI Installation -```sh -git clone https://github.com/AshAvalanche/ash-rs.git -cd ash-rs - -cargo install --path crates/ash_cli - -# The CLI is then available globally -ash --help -``` - -See [Available commands](crates/ash_cli/README.md#available-commands). +See the [Installation](https://ash.center/docs/toolkit/ash-cli/installation) section of the documentation. ## Configuration -A YAML configuration file can be generated using the `ash conf init` command, enriched and then reused in commands with the `--config` flag. - -This allows to query custom networks with the CLI: - -```yaml -avalancheNetworks: - - name: local - subnets: - - id: 11111111111111111111111111111111LpoYY - blockchains: - - id: yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp - name: C-Chain - vmType: EVM - rpcUrl: https://localhost:9650/ext/bc/C/rpc -``` +See the [Custom Configuration tutorial](https://ash.center/docs/toolkit/ash-cli/tutorials/custom-configuration) section of the documentation. ## Development @@ -108,6 +87,6 @@ ASH_TEST_AVAX_CONFIG="$PWD/target/ash-test-avax-conf.yml" cargo test - [x] Get Subnets and blockchains information from the Avalanche P-Chain - [x] Get nodes information from the Avalanche P-Chain - [x] Get Subnet validators information from the Avalanche P-Chain -- [ ] Subnet creation -- [ ] Blockchain creation +- [x] Subnet creation +- [x] Blockchain creation - [ ] WASM integration (to allow the library to be used from JavaScript) diff --git a/clippy.toml b/clippy.toml index e0ac3f3..7ccdb7b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,2 +1,2 @@ large-error-threshold = 256 -too-many-arguments-threshold = 10 +too-many-arguments-threshold = 16 diff --git a/crates/ash_cli/Cargo.toml b/crates/ash_cli/Cargo.toml index 813dd0e..c7f8fbd 100644 --- a/crates/ash_cli/Cargo.toml +++ b/crates/ash_cli/Cargo.toml @@ -25,6 +25,8 @@ serde_json = "1.0.91" async-std = { version = "1.10.0", features = ["attributes", "tokio1"] } enum-display-derive = "0.1.1" rust_decimal = "1.29.1" +hex = "0.4.3" +chrono = "0.4.24" [[bin]] name = "ash" diff --git a/crates/ash_cli/README.md b/crates/ash_cli/README.md index 5b9b0d6..119069a 100644 --- a/crates/ash_cli/README.md +++ b/crates/ash_cli/README.md @@ -11,19 +11,42 @@ ash avalanche subnet list --network mainnet # Show detailed information about one of the mainnet Subnets # The output can be set to JSON and piped to jq for maximum flexibility -ash avalanche subnet info --id Vn3aX6hNRstj5VHHm63TCgPNaeGnRSqCYXQqemSqDd2TQH4qJ --json | jq '.blockchains' +ash avalanche subnet info Vn3aX6hNRstj5VHHm63TCgPNaeGnRSqCYXQqemSqDd2TQH4qJ --json | jq '.blockchains' # Show detailed information about a validator of the mainnet Subnet -ash avalanche validator info --network fuji --id NodeID-FhFWdWodxktJYq884nrJjWD8faLTk9jmp +ash avalanche validator info --network fuji NodeID-FhFWdWodxktJYq884nrJjWD8faLTk9jmp ``` ## Available commands -- `ash conf`: Manipulate the Ash lib configuration -- `ash avalanche network`: Interact with Avalanche networks -- `ash avalanche node`: Interact with Avalanche nodes -- `ash avalanche subnet`: Interact with Avalanche Subnets -- `ash avalanche validator`: Interact with Avalanche validators +- `ash conf` + + ```bash + Interact with Ash configuration files + + Usage: ash conf [OPTIONS] + + Commands: + init Initialize an Ash config file + ``` + +- `ash avalanche` + + ```bash + Interact with Avalanche Subnets, blockchains and nodes + + Usage: ash avalanche [OPTIONS] + + Commands: + blockchain Interact with Avalanche blockchains + network Interact with Avalanche networks + node Interact with Avalanche nodes + subnet Interact with Avalanche Subnets + validator Interact with Avalanche validators + vm Interact with Avalanche VMs + wallet Interact with Avalanche wallets + x Interact with Avalanche X-Chain + ``` ## Tutorials diff --git a/crates/ash_cli/src/avalanche.rs b/crates/ash_cli/src/avalanche.rs index 2f0bab3..bbde167 100644 --- a/crates/ash_cli/src/avalanche.rs +++ b/crates/ash_cli/src/avalanche.rs @@ -1,16 +1,18 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2023, E36 Knots +mod blockchain; mod network; mod node; mod subnet; mod validator; +mod vm; mod wallet; mod x; // Module that contains the avalanche subcommand parser -use crate::utils::error::CliError; +use crate::utils::{error::CliError, parsing::*}; use ash_sdk::avalanche::AvalancheNetwork; use clap::{Parser, Subcommand}; @@ -23,12 +25,14 @@ pub(crate) struct AvalancheCommand { #[derive(Subcommand)] enum AvalancheSubcommands { + Blockchain(blockchain::BlockchainCommand), Network(network::NetworkCommand), Node(node::NodeCommand), Subnet(subnet::SubnetCommand), Validator(validator::ValidatorCommand), - X(x::XCommand), + Vm(vm::VmCommand), Wallet(wallet::WalletCommand), + X(x::XCommand), } // Load the network configuation @@ -55,7 +59,7 @@ fn update_subnet_validators( subnet_id: &str, ) -> Result<(), CliError> { network - .update_subnet_validators(subnet_id) + .update_subnet_validators(parse_id(subnet_id)?) .map_err(|e| CliError::dataerr(format!("Error updating validators: {e}")))?; Ok(()) } @@ -67,10 +71,12 @@ pub(crate) fn parse( json: bool, ) -> Result<(), CliError> { match avalanche.command { + AvalancheSubcommands::Blockchain(blockchain) => blockchain::parse(blockchain, config, json), AvalancheSubcommands::Network(network) => network::parse(network, config, json), AvalancheSubcommands::Node(node) => node::parse(node, json), AvalancheSubcommands::Subnet(subnet) => subnet::parse(subnet, config, json), AvalancheSubcommands::Validator(validator) => validator::parse(validator, config, json), + AvalancheSubcommands::Vm(vm) => vm::parse(vm, json), AvalancheSubcommands::X(x) => x::parse(x, config, json), AvalancheSubcommands::Wallet(wallet) => wallet::parse(wallet, config, json), } diff --git a/crates/ash_cli/src/avalanche/blockchain.rs b/crates/ash_cli/src/avalanche/blockchain.rs new file mode 100644 index 0000000..42e75a0 --- /dev/null +++ b/crates/ash_cli/src/avalanche/blockchain.rs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, E36 Knots + +// Module that contains the blockchain subcommand parser + +use crate::{ + avalanche::{wallet::*, *}, + utils::{error::CliError, parsing::*, templating::*}, +}; +use ash_sdk::avalanche::{ + blockchains::AvalancheBlockchain, + vms::{subnet_evm::AVAX_SUBNET_EVM_ID, AvalancheVmType}, +}; +use async_std::task; +use clap::{Parser, Subcommand}; + +/// Interact with Avalanche blockchains +#[derive(Parser)] +#[command()] +pub(crate) struct BlockchainCommand { + #[command(subcommand)] + command: BlockchainSubcommands, + /// Avalanche network + #[arg( + long, + short = 'n', + default_value = "mainnet", + global = true, + env = "AVALANCHE_NETWORK" + )] + network: String, +} + +#[derive(Subcommand)] +enum BlockchainSubcommands { + /// Create a new blockchain + #[command()] + Create { + /// Blockchain name + name: String, + /// Blockchain VM type + #[arg(long, short = 't', default_value = "SubnetEVM")] + vm_type: AvalancheVmType, + /// Blockchain VM ID + #[arg( + long, + short = 'i', + default_value = AVAX_SUBNET_EVM_ID, + )] + vm_id: String, + /// Blockchain genesis data string (hex encoded) + #[arg(long, short = 'g', group = "genesis")] + genesis_str: Option, + /// Path to a JSON file containing the blockchain genesis data (generated with `ash avalanche vm encode-genesis`) + #[arg(long, short = 'f', group = "genesis")] + genesis_file: Option, + /// Subnet ID to create the blockchain on + #[arg(long, short = 's')] + subnet_id: String, + /// Private key to sign the transaction with (must be a control key) + #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] + private_key: String, + /// Private key format + #[arg( + long, + short = 'e', + default_value = "cb58", + env = "AVALANCHE_KEY_ENCODING" + )] + key_encoding: PrivateKeyEncoding, + /// Whether to wait for transaction acceptance + #[arg(long, short = 'w')] + wait: bool, + }, +} + +fn create( + network_name: &str, + subnet_id: &str, + name: &str, + vm_type: AvalancheVmType, + vm_id: &str, + genesis_data: Option, + genesis_file: Option, + private_key: &str, + key_encoding: PrivateKeyEncoding, + wait: bool, + config: Option<&str>, + json: bool, +) -> Result<(), CliError> { + // Check how genesis data is provided + // If a file is provided, load it and parse the genesis data + let genesis_hex = match genesis_file { + Some(path) => { + let genesis_json = std::fs::read_to_string(path) + .map_err(|e| CliError::dataerr(format!("Error reading genesis file: {e}")))?; + let genesis_obj: serde_json::Value = serde_json::from_str(&genesis_json) + .map_err(|e| CliError::dataerr(format!("Error parsing genesis file: {e}")))?; + genesis_obj + .get("genesisBytes") + .ok_or_else(|| { + CliError::dataerr( + "Error parsing genesis file: it should contain a 'genesisBytes' field" + .to_string(), + ) + })? + .as_str() + .ok_or_else(|| { + CliError::dataerr( + "Error parsing genesis file: the 'genesisBytes' field should be a string" + .to_string(), + ) + })? + .to_string() + } + None => match genesis_data { + Some(data) => data, + None => { + return Err(CliError::dataerr( + "Error when parsing arguments: either 'genesis-str' or a 'genesis-file' must be provided".to_string(), + )) + } + }, + }; + + let network = load_network(network_name, config)?; + let wallet = create_wallet(&network, private_key, key_encoding)?; + let subnet_id_parsed = parse_id(subnet_id)?; + let vm_id_parsed = parse_id(vm_id)?; + let genesis_bytes = hex::decode(genesis_hex.trim_start_matches("0x")) + .map_err(|e| CliError::dataerr(format!("Error decoding genesis data: {e}")))?; + + if wait { + eprintln!("Waiting for transaction to be accepted..."); + } + + let blockchain = task::block_on(async { + AvalancheBlockchain::create( + &wallet, + subnet_id_parsed, + name, + vm_type, + vm_id_parsed, + genesis_bytes, + wait, + ) + .await + }) + .map_err(|e| CliError::dataerr(format!("Error creating blockchain: {e}")))?; + + if json { + println!("{}", serde_json::to_string(&blockchain).unwrap()); + return Ok(()); + } + + println!("{}", template_blockchain_creation(&blockchain, wait)); + + Ok(()) +} + +// Parse blockchain subcommand +pub(crate) fn parse( + subnet: BlockchainCommand, + config: Option<&str>, + json: bool, +) -> Result<(), CliError> { + match subnet.command { + BlockchainSubcommands::Create { + name, + vm_type, + vm_id, + genesis_str, + genesis_file, + subnet_id, + private_key, + key_encoding, + wait, + } => create( + &subnet.network, + &subnet_id, + &name, + vm_type, + &vm_id, + genesis_str, + genesis_file, + &private_key, + key_encoding, + wait, + config, + json, + ), + } +} diff --git a/crates/ash_cli/src/avalanche/network.rs b/crates/ash_cli/src/avalanche/network.rs index ef13c4a..05679a6 100644 --- a/crates/ash_cli/src/avalanche/network.rs +++ b/crates/ash_cli/src/avalanche/network.rs @@ -3,7 +3,7 @@ // Module that contains the network subcommand parser -use crate::utils::{error::CliError, templating::type_colorize}; +use crate::utils::{error::CliError, templating::*}; use ash_sdk::conf::AshConfig; use clap::{Parser, Subcommand}; diff --git a/crates/ash_cli/src/avalanche/subnet.rs b/crates/ash_cli/src/avalanche/subnet.rs index bc4c94c..2e182b9 100644 --- a/crates/ash_cli/src/avalanche/subnet.rs +++ b/crates/ash_cli/src/avalanche/subnet.rs @@ -3,8 +3,12 @@ // Module that contains the subnet subcommand parser -use crate::avalanche::*; -use crate::utils::{error::CliError, templating::*}; +use crate::{ + avalanche::{wallet::*, *}, + utils::{error::CliError, parsing::*, templating::*}, +}; +use ash_sdk::avalanche::subnets::AvalancheSubnet; +use async_std::task; use clap::{Parser, Subcommand}; /// Interact with Avalanche Subnets @@ -34,6 +38,27 @@ enum SubnetSubcommands { Info { /// Subnet ID id: String, + /// Whether to show extended information (here about validators) + #[arg(long, short = 'e')] + extended: bool, + }, + /// Create a new Subnet + #[command()] + Create { + /// Private key to sign the transaction with + #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] + private_key: String, + /// Private key format + #[arg( + long, + short = 'e', + default_value = "cb58", + env = "AVALANCHE_KEY_ENCODING" + )] + key_encoding: PrivateKeyEncoding, + /// Whether to wait for transaction acceptance + #[arg(long, short = 'w')] + wait: bool, }, } @@ -53,19 +78,25 @@ fn list(network_name: &str, config: Option<&str>, json: bool) -> Result<(), CliE type_colorize(&network.name) ); for subnet in network.subnets.iter() { - println!("{}", template_subnet_info(subnet, true, 0)); + println!("{}", template_subnet_info(subnet, true, false, 0)); } Ok(()) } -fn info(network_name: &str, id: &str, config: Option<&str>, json: bool) -> Result<(), CliError> { +fn info( + network_name: &str, + id: &str, + extended: bool, + config: Option<&str>, + json: bool, +) -> Result<(), CliError> { let mut network = load_network(network_name, config)?; update_network_subnets(&mut network)?; update_subnet_validators(&mut network, id)?; let subnet = network - .get_subnet(id) + .get_subnet(parse_id(id)?) .map_err(|e| CliError::dataerr(format!("Error loading Subnet info: {e}")))?; if json { @@ -73,7 +104,35 @@ fn info(network_name: &str, id: &str, config: Option<&str>, json: bool) -> Resul return Ok(()); } - println!("{}", template_subnet_info(subnet, false, 0)); + println!("{}", template_subnet_info(subnet, false, extended, 0)); + + Ok(()) +} + +fn create( + network_name: &str, + private_key: &str, + key_encoding: PrivateKeyEncoding, + wait: bool, + config: Option<&str>, + json: bool, +) -> Result<(), CliError> { + let network = load_network(network_name, config)?; + let wallet = create_wallet(&network, private_key, key_encoding)?; + + if wait { + eprintln!("Waiting for transaction to be accepted..."); + } + + let subnet = task::block_on(async { AvalancheSubnet::create(&wallet, wait).await }) + .map_err(|e| CliError::dataerr(format!("Error creating Subnet: {e}")))?; + + if json { + println!("{}", serde_json::to_string(&subnet).unwrap()); + return Ok(()); + } + + println!("{}", template_subnet_creation(&subnet, wait)); Ok(()) } @@ -85,7 +144,21 @@ pub(crate) fn parse( json: bool, ) -> Result<(), CliError> { match subnet.command { - SubnetSubcommands::Info { id } => info(&subnet.network, &id, config, json), + SubnetSubcommands::Info { id, extended } => { + info(&subnet.network, &id, extended, config, json) + } SubnetSubcommands::List => list(&subnet.network, config, json), + SubnetSubcommands::Create { + private_key, + key_encoding, + wait, + } => create( + &subnet.network, + &private_key, + key_encoding, + wait, + config, + json, + ), } } diff --git a/crates/ash_cli/src/avalanche/validator.rs b/crates/ash_cli/src/avalanche/validator.rs index 50064a7..9a4a023 100644 --- a/crates/ash_cli/src/avalanche/validator.rs +++ b/crates/ash_cli/src/avalanche/validator.rs @@ -3,9 +3,12 @@ // Module that contains the validator subcommand parser -use crate::avalanche::*; -use crate::utils::{error::CliError, templating::*}; -use ash_sdk::avalanche::AVAX_PRIMARY_NETWORK_ID; +use crate::{ + avalanche::{wallet::*, *}, + utils::{error::CliError, parsing::*, templating::*}, +}; +use ash_sdk::avalanche::{subnets::AvalancheSubnetType, AVAX_PRIMARY_NETWORK_ID}; +use async_std::task; use clap::{Parser, Subcommand}; /// Interact with Avalanche validators @@ -35,6 +38,37 @@ pub(crate) struct ValidatorCommand { #[derive(Subcommand)] enum ValidatorSubcommands { + /// Add a validator to a Subnet + #[command()] + Add { + /// Validator NodeID + id: String, + /// Validator weight (permissioned Subnet) or stake in AVAX (elastic Subnets) + stake_or_weight: u64, + /// Start time of the validation (YYYY-MM-DDTHH:MM:SSZ) + #[arg(long, short = 'S')] + start_time: String, + /// End time of the validation (YYYY-MM-DDTHH:MM:SSZ) + #[arg(long, short = 'E')] + end_time: String, + /// Delegation fee (percentage) + #[arg(long, short = 'f', default_value = "2")] + delegation_fee: u32, + /// Private key to sign the transaction with + #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] + private_key: String, + /// Private key format + #[arg( + long, + short = 'e', + default_value = "cb58", + env = "AVALANCHE_KEY_ENCODING" + )] + key_encoding: PrivateKeyEncoding, + /// Whether to wait for transaction acceptance + #[arg(long, short = 'w')] + wait: bool, + }, /// List the Subnet's validators #[command()] List, @@ -58,7 +92,7 @@ fn list( update_subnet_validators(&mut network, subnet_id)?; let subnet = network - .get_subnet(subnet_id) + .get_subnet(parse_id(subnet_id)?) .map_err(|e| CliError::dataerr(format!("Error listing validators: {e}")))?; if json { @@ -74,7 +108,7 @@ fn list( for validator in subnet.validators.iter() { println!( "{}", - template_validator_info(validator, subnet, true, 2, true) + template_validator_info(validator, subnet, true, true, 2) ); } @@ -93,11 +127,11 @@ fn info( update_subnet_validators(&mut network, subnet_id)?; let subnet = network - .get_subnet(subnet_id) + .get_subnet(parse_id(subnet_id)?) .map_err(|e| CliError::dataerr(format!("Error loading Subnet info: {e}")))?; let validator = subnet - .get_validator(id) + .get_validator(parse_node_id(id)?) .map_err(|e| CliError::dataerr(format!("Error loading Subnet info: {e}")))?; if json { @@ -106,12 +140,87 @@ fn info( } println!( "{}", - template_validator_info(validator, subnet, false, 0, true) + template_validator_info(validator, subnet, false, true, 0) ); Ok(()) } +fn add( + network_name: &str, + subnet_id: &str, + id: &str, + stake_or_weight: u64, + start_time: String, + end_time: String, + delegation_fee: u32, + private_key: String, + key_encoding: PrivateKeyEncoding, + wait: bool, + config: Option<&str>, + json: bool, +) -> Result<(), CliError> { + let node_id_parsed = parse_node_id(id)?; + let start_time_parsed = parse_datetime(&start_time)?; + let end_time_parsed = parse_datetime(&end_time)?; + + let mut network = load_network(network_name, config)?; + update_network_subnets(&mut network)?; + + let subnet = network + .get_subnet(parse_id(subnet_id)?) + .map_err(|e| CliError::dataerr(format!("Error loading Subnet info: {e}")))?; + let wallet = create_wallet(&network, &private_key, key_encoding)?; + + if wait { + eprintln!("Waiting for transaction to be accepted..."); + } + + let validator = match subnet.subnet_type { + AvalancheSubnetType::PrimaryNetwork => task::block_on(async { + subnet + .add_avalanche_validator( + &wallet, + node_id_parsed, + // Multiply by 1 billion to convert from AVAX to nAVAX + stake_or_weight * 1_000_000_000, + start_time_parsed, + end_time_parsed, + delegation_fee, + wait, + ) + .await + }), + AvalancheSubnetType::Permissioned => task::block_on(async { + subnet + .add_validator_permissioned( + &wallet, + node_id_parsed, + stake_or_weight, + start_time_parsed, + end_time_parsed, + wait, + ) + .await + }), + AvalancheSubnetType::Elastic => { + return Err(CliError::dataerr( + "Adding a validator to an elastic Subnet is not yet supported".to_string(), + )); + } + } + .map_err(|e| CliError::dataerr(format!("Error adding validator: {e}")))?; + + if json { + println!("{}", serde_json::to_string(&validator).unwrap()); + return Ok(()); + } + + println!("{}", template_validator_add(&validator, subnet, wait)); + + Ok(()) +} + // Parse validator subcommand pub(crate) fn parse( validator: ValidatorCommand, @@ -119,6 +228,29 @@ pub(crate) fn parse( json: bool, ) -> Result<(), CliError> { match validator.command { + ValidatorSubcommands::Add { + id, + stake_or_weight, + start_time, + end_time, + delegation_fee, + private_key, + key_encoding, + wait, + } => add( + &validator.network, + &validator.subnet_id, + &id, + stake_or_weight, + start_time, + end_time, + delegation_fee, + private_key, + key_encoding, + wait, + config, + json, + ), ValidatorSubcommands::Info { id } => { info(&validator.network, &validator.subnet_id, &id, config, json) } diff --git a/crates/ash_cli/src/avalanche/vm.rs b/crates/ash_cli/src/avalanche/vm.rs new file mode 100644 index 0000000..a98f1bd --- /dev/null +++ b/crates/ash_cli/src/avalanche/vm.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, E36 Knots + +// Module that contains the vm subcommand parser + +use crate::utils::{error::CliError, templating::*}; +use ash_sdk::avalanche::vms::{encode_genesis_data, AvalancheVmType}; +use clap::{Parser, Subcommand}; + +/// Interact with Avalanche VMs +#[derive(Parser)] +#[command()] +pub(crate) struct VmCommand { + #[command(subcommand)] + command: VmSubcommands, +} + +#[derive(Subcommand)] +enum VmSubcommands { + /// Encode a VM genesis (in JSON) to bytes + #[command()] + EncodeGenesis { + /// Path to the genesis JSON file + genesis_file: String, + /// VM type + #[arg(long, short = 't', default_value = "SubnetEVM")] + vm_type: AvalancheVmType, + }, +} + +fn encode_genesis( + genesis_file: &str, + vm_type: AvalancheVmType, + json: bool, +) -> Result<(), CliError> { + let genesis_json = std::fs::read_to_string(genesis_file).map_err(|e| { + CliError::dataerr(format!("Error reading genesis file {genesis_file}: {e}")) + })?; + + let genesis_bytes = encode_genesis_data(vm_type, &genesis_json).map_err(|e| { + CliError::dataerr(format!("Error encoding genesis file {genesis_file}: {e}")) + })?; + + if json { + println!( + "{}", + serde_json::json!({ "genesisBytes": format!("0x{}", hex::encode(genesis_bytes)) }) + ); + return Ok(()); + } + + println!("{}", template_genesis_encoded(genesis_bytes, 0)); + + Ok(()) +} + +// Parse vm subcommand +pub(crate) fn parse(x: VmCommand, json: bool) -> Result<(), CliError> { + match x.command { + VmSubcommands::EncodeGenesis { + genesis_file, + vm_type, + } => encode_genesis(&genesis_file, vm_type, json), + } +} diff --git a/crates/ash_cli/src/avalanche/wallet.rs b/crates/ash_cli/src/avalanche/wallet.rs index 6217f45..8618215 100644 --- a/crates/ash_cli/src/avalanche/wallet.rs +++ b/crates/ash_cli/src/avalanche/wallet.rs @@ -3,9 +3,10 @@ // Module that contains the wallet subcommand parser -use crate::utils::error::CliError; -use crate::utils::templating::template_wallet_info; -use crate::{avalanche::*, utils::templating::template_generate_private_key}; +use crate::{ + avalanche::*, + utils::{error::CliError, templating::*}, +}; use ash_sdk::avalanche::wallets::{generate_private_key, AvalancheWallet, AvalancheWalletInfo}; use clap::{Parser, Subcommand, ValueEnum}; use std::fmt::Display; diff --git a/crates/ash_cli/src/avalanche/x.rs b/crates/ash_cli/src/avalanche/x.rs index 4f9ec80..bfbe561 100644 --- a/crates/ash_cli/src/avalanche/x.rs +++ b/crates/ash_cli/src/avalanche/x.rs @@ -3,13 +3,14 @@ // Module that contains the x subcommand parser -use crate::utils::error::CliError; -use crate::utils::templating::template_xchain_transfer; -use crate::{avalanche::wallet::*, avalanche::*, utils::templating::template_xchain_balance}; +use crate::{ + avalanche::{wallet::*, *}, + utils::templating::template_xchain_balance, + utils::{error::CliError, templating::*}, +}; use async_std::task; use clap::{Parser, Subcommand}; -use rust_decimal::prelude::{FromPrimitive, ToPrimitive}; -use rust_decimal::Decimal; +use rust_decimal::prelude::{Decimal, FromPrimitive, ToPrimitive}; /// Interact with Avalanche X-Chain #[derive(Parser)] @@ -103,6 +104,13 @@ fn transfer( config: Option<&str>, json: bool, ) -> Result<(), CliError> { + // For now, only AVAX transfers are supported + if asset_id != "AVAX" { + return Err(CliError::dataerr( + "Error: only AVAX transfers are supported at this time".to_string(), + )); + } + let network = load_network(network_name, config)?; let wallet = create_wallet(&network, private_key, key_encoding)?; diff --git a/crates/ash_cli/src/utils.rs b/crates/ash_cli/src/utils.rs index b4bfec5..0d5e6dd 100644 --- a/crates/ash_cli/src/utils.rs +++ b/crates/ash_cli/src/utils.rs @@ -2,4 +2,5 @@ // Copyright (c) 2023, E36 Knots pub(crate) mod error; +pub(crate) mod parsing; pub(crate) mod templating; diff --git a/crates/ash_cli/src/utils/parsing.rs b/crates/ash_cli/src/utils/parsing.rs new file mode 100644 index 0000000..a696ece --- /dev/null +++ b/crates/ash_cli/src/utils/parsing.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, E36 Knots + +// Module that contains parsing utility functions + +use crate::utils::error::CliError; +use ash_sdk::ids::{node::Id as NodeId, Id}; +use chrono::{DateTime, Utc}; +use std::str::FromStr; + +// Parse an ID from a string +pub(crate) fn parse_id(id: &str) -> Result { + let id = Id::from_str(id).map_err(|e| CliError::dataerr(format!("Error parsing ID: {e}")))?; + Ok(id) +} + +// Parse a node ID from a string +pub(crate) fn parse_node_id(id: &str) -> Result { + let id = NodeId::from_str(id) + .map_err(|e| CliError::dataerr(format!("Error parsing NodeID: {e}")))?; + Ok(id) +} + +// Parse a DateTime from a string +pub(crate) fn parse_datetime(datetime: &str) -> Result, CliError> { + let datetime = DateTime::parse_from_rfc3339(datetime) + .map_err(|e| CliError::dataerr(format!("Error parsing DateTime: {e}")))?; + Ok(datetime.with_timezone(&Utc)) +} diff --git a/crates/ash_cli/src/utils/templating.rs b/crates/ash_cli/src/utils/templating.rs index d8fde23..02a9b3d 100644 --- a/crates/ash_cli/src/utils/templating.rs +++ b/crates/ash_cli/src/utils/templating.rs @@ -8,6 +8,7 @@ use ash_sdk::avalanche::{ wallets::AvalancheWalletInfo, AvalancheXChainBalance, }; +use chrono::{DateTime, NaiveDateTime, Utc}; use colored::{ColoredString, Colorize}; use indoc::formatdoc; @@ -35,6 +36,15 @@ where } } +pub(crate) fn human_readable_timestamp(timestamp: u64) -> String { + DateTime::::from_utc( + NaiveDateTime::from_timestamp_opt(timestamp as i64, 0).unwrap(), + Utc, + ) + .format("%Y-%m-%d %H:%M:%S") + .to_string() +} + pub(crate) fn template_horizontal_rule(character: char, length: usize) -> String { format!("{character}").repeat(length) } @@ -79,8 +89,8 @@ pub(crate) fn template_validator_info( validator: &AvalancheSubnetValidator, subnet: &AvalancheSubnet, list: bool, - indent: u8, extended: bool, + indent: u8, ) -> String { let mut info_str = String::new(); @@ -91,8 +101,8 @@ pub(crate) fn template_validator_info( End time: {} ", type_colorize(&validator.tx_id), - type_colorize(&validator.start_time), - type_colorize(&validator.end_time), + type_colorize(&human_readable_timestamp(validator.start_time)), + type_colorize(&human_readable_timestamp(validator.end_time)), ); let permissioned_subnet_info = &formatdoc!( @@ -224,7 +234,12 @@ pub(crate) fn template_validator_info( indent::indent_all_by(indent.into(), info_str) } -pub(crate) fn template_subnet_info(subnet: &AvalancheSubnet, list: bool, indent: u8) -> String { +pub(crate) fn template_subnet_info( + subnet: &AvalancheSubnet, + list: bool, + extended: bool, + indent: u8, +) -> String { let mut info_str = String::new(); let subindent = match list { @@ -244,7 +259,7 @@ pub(crate) fn template_subnet_info(subnet: &AvalancheSubnet, list: bool, indent: for validator in subnet.validators.iter() { validators_info.push_str(&format!( "\n{}", - template_validator_info(validator, subnet, true, subindent, false) + template_validator_info(validator, subnet, true, extended, subindent) )); } @@ -308,6 +323,70 @@ pub(crate) fn template_subnet_info(subnet: &AvalancheSubnet, list: bool, indent: indent::indent_all_by(indent.into(), info_str) } +pub(crate) fn template_subnet_creation(subnet: &AvalancheSubnet, wait: bool) -> String { + if wait { + formatdoc!( + " + Subnet created! (Tx ID: '{}') + {}", + type_colorize(&subnet.id), + template_subnet_info(subnet, false, false, 0) + ) + } else { + formatdoc!( + " + Initiated subnet creation! (Tx ID: '{}') + {}", + type_colorize(&subnet.id), + template_subnet_info(subnet, false, false, 0) + ) + } +} + +pub(crate) fn template_blockchain_creation(blockchain: &AvalancheBlockchain, wait: bool) -> String { + if wait { + formatdoc!( + " + Blockchain created! (Tx ID: '{}') + {}", + type_colorize(&blockchain.id), + template_blockchain_info(blockchain, false, 0) + ) + } else { + formatdoc!( + " + Initiated blockchain creation! (Tx ID: '{}') + {}", + type_colorize(&blockchain.id), + template_blockchain_info(blockchain, false, 0) + ) + } +} + +pub(crate) fn template_validator_add( + validator: &AvalancheSubnetValidator, + subnet: &AvalancheSubnet, + wait: bool, +) -> String { + if wait { + formatdoc!( + " + Validator added to Subnet! (Tx ID: '{}') + {}", + type_colorize(&validator.node_id), + template_validator_info(validator, &subnet, false, true, 0) + ) + } else { + formatdoc!( + " + Initiated validator addition to Subnet! (Tx ID: '{}') + {}", + type_colorize(&validator.node_id), + template_validator_info(validator, &subnet, false, true, 0) + ) + } +} + pub(crate) fn template_avalanche_node_info(node: &AvalancheNode, indent: u8) -> String { let mut info_str = String::new(); @@ -433,8 +512,8 @@ pub(crate) fn template_xchain_transfer( ) -> String { let mut transfer_str = String::new(); - match wait { - true => transfer_str.push_str(&formatdoc!( + if wait { + transfer_str.push_str(&formatdoc!( " Transfered {} of asset '{}' to '{}'! Transaction ID: {}", @@ -442,8 +521,9 @@ pub(crate) fn template_xchain_transfer( type_colorize(&asset_id), type_colorize(&to), type_colorize(&tx_id), - )), - false => transfer_str.push_str(&formatdoc!( + )); + } else { + transfer_str.push_str(&formatdoc!( " Initiated transfering {} of asset '{}' to '{}'! Transaction ID: {}", @@ -451,8 +531,21 @@ pub(crate) fn template_xchain_transfer( type_colorize(&asset_id), type_colorize(&to), type_colorize(&tx_id), - )), + )); } indent::indent_all_by(indent.into(), transfer_str) } + +pub(crate) fn template_genesis_encoded(genesis_bytes: Vec, indent: u8) -> String { + let mut genesis_str = String::new(); + + genesis_str.push_str(&formatdoc!( + " + Genesis bytes: + {}", + type_colorize(&format!("0x{}", hex::encode(genesis_bytes))), + )); + + indent::indent_all_by(indent.into(), genesis_str) +} diff --git a/crates/ash_sdk/Cargo.toml b/crates/ash_sdk/Cargo.toml index 0a44c7a..e518cd2 100644 --- a/crates/ash_sdk/Cargo.toml +++ b/crates/ash_sdk/Cargo.toml @@ -18,6 +18,7 @@ keywords.workspace = true avalanche-types = { version = "0.0.386", features = [ "jsonrpc_client", "wallet", + "subnet_evm", ] } config = { version = "0.13.3", features = ["yaml"] } ethers = { version = "1.0.2", features = ["rustls"] } @@ -33,6 +34,10 @@ async-std = { version = "1.10.0", features = ["attributes", "tokio1"] } # We need to enable the rustls-tls-native-roots feature to support self-signed certificates reqwest = { version = "0.11.14", features = ["rustls-tls-native-roots"] } enum-display-derive = "0.1.1" +serde_json = "1.0.96" +strum = { version = "0.24", features = ["derive"] } +chrono = { version = "0.4.24", features = ["clock"] } [dev-dependencies] +serial_test = "2.0.0" tempfile = "3.3.0" diff --git a/crates/ash_sdk/README.md b/crates/ash_sdk/README.md index e0640e0..0f25a6f 100644 --- a/crates/ash_sdk/README.md +++ b/crates/ash_sdk/README.md @@ -1,4 +1,4 @@ -# ash_sdk +# `ash_sdk` Crate `ash-rs` is a Rust SDK for [Avalanche](https://avax.network) and [Ash](https://ash.center) tools. @@ -66,17 +66,17 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://api.avax.network/ext/bc/P - id: 2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5 name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://api.avax.network/ext/bc/C/rpc - id: 2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://api.avax.network/ext/bc/X ``` @@ -88,9 +88,4 @@ One can check out the [CLI code](https://github.com/AshAvalanche/ash-rs/tree/mai ## Modules -- `conf`: Interact with the library configuration in YAML -- `errors`: Generate errors for the library -- `avalanche`: Interact with Avalanche networks, Subnets and blockchains - - `avalanche::subnets`: Interact with Avalanche Subnets and validators - - `avalanche::blockchains`: Interact with Avalanche blockchains - - `avalanche::jsonrpc`: Interact with Avalanche VMs JSON RPC endpoints +See the [docs.rs documentation](https://docs.rs/ash_sdk) for more details. diff --git a/crates/ash_sdk/conf/default.yml b/crates/ash_sdk/conf/default.yml index 25cdb79..e6491a8 100644 --- a/crates/ash_sdk/conf/default.yml +++ b/crates/ash_sdk/conf/default.yml @@ -10,17 +10,17 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://api.avax.network/ext/bc/P - id: 2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5 name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://api.avax.network/ext/bc/C/rpc - id: 2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://api.avax.network/ext/bc/X - name: fuji subnets: @@ -29,17 +29,17 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://api.avax-test.network/ext/bc/P - id: yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://api.avax-test.network/ext/bc/C/rpc - id: 2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://api.avax-test.network/ext/bc/X - name: mainnet-ankr subnets: @@ -48,17 +48,17 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://rpc.ankr.com/avalanche-p - id: 2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5 name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://rpc.ankr.com/avalanche-c - id: 2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://rpc.ankr.com/avalanche-x - name: fuji-ankr subnets: @@ -67,17 +67,17 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://rpc.ankr.com/avalanche_fuji-p - id: yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://rpc.ankr.com/avalanche_fuji-c - id: 2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://rpc.ankr.com/avalanche_fuji-x - name: mainnet-blast subnets: @@ -86,17 +86,17 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://ava-mainnet.public.blastapi.io/ext/bc/P - id: 2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5 name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://ava-mainnet.public.blastapi.io/ext/bc/C/rpc - id: 2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://ava-mainnet.public.blastapi.io/ext/bc/X - name: fuji-blast subnets: @@ -105,15 +105,15 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://ava-testnet.public.blastapi.io/ext/bc/P - id: 2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5 name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://ava-testnet.public.blastapi.io/ext/bc/C/rpc - id: 2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://ava-testnet.public.blastapi.io/ext/bc/X diff --git a/crates/ash_sdk/src/avalanche.rs b/crates/ash_sdk/src/avalanche.rs index 420d36c..38bef86 100644 --- a/crates/ash_sdk/src/avalanche.rs +++ b/crates/ash_sdk/src/avalanche.rs @@ -6,6 +6,7 @@ pub mod jsonrpc; pub mod nodes; pub mod subnets; pub mod txs; +pub mod vms; pub mod wallets; // Module that contains code to interact with Avalanche networks @@ -22,12 +23,13 @@ use crate::{ }; use async_std::task; use avalanche_types::{ - ids::short::Id as ShortId, + ids::{short::Id as ShortId, Id}, jsonrpc::{avm::GetBalanceResult, platformvm::ApiOwner}, key::secp256k1::address::avax_address_to_short_bytes, txs::utxo, }; use serde::{Deserialize, Serialize}; +use std::str::FromStr; /// Avalanche Primary Network ID /// This Subnet contains the P-Chain that is used for all Subnet operations @@ -35,53 +37,76 @@ use serde::{Deserialize, Serialize}; pub const AVAX_PRIMARY_NETWORK_ID: &str = "11111111111111111111111111111111LpoYY"; /// Convert a human readable address to a ShortId -fn address_to_short_id(address: &str, chain_alias: &str) -> ShortId { - let (_, addr_bytes) = avax_address_to_short_bytes(chain_alias, address).unwrap(); - ShortId::from_slice(&addr_bytes) +fn address_to_short_id(address: &str, chain_alias: &str) -> Result { + let (_, addr_bytes) = avax_address_to_short_bytes(chain_alias, address).map_err(|e| { + AvalancheNetworkError::InvalidAddress { + address: address.to_string(), + msg: e.to_string(), + } + })?; + + Ok(ShortId::from_slice(&addr_bytes)) } /// Avalanche network -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheNetwork { + /// Network name pub name: String, + /// Primary Network ID + #[serde(skip)] + pub primary_network_id: Id, /// List of the network's Subnets pub subnets: Vec, } +impl Default for AvalancheNetwork { + fn default() -> Self { + Self { + name: "mainnet".to_string(), + primary_network_id: Id::from_str(AVAX_PRIMARY_NETWORK_ID).unwrap(), + subnets: vec![], + } + } +} + impl AvalancheNetwork { /// Load an AvalancheNetwork from the configuration pub fn load(network_name: &str, config: Option<&str>) -> Result { let ash_config = AshConfig::load(config)?; - let avax_network = ash_config + let mut avax_network = ash_config .avalanche_networks .iter() .find(|&avax_network| avax_network.name == network_name) .ok_or(ConfigError::NotFound { target_type: "network".to_string(), target_value: network_name.to_string(), - })?; + })? + .clone(); + + avax_network.primary_network_id = Default::default(); // Error if the Primary Network is not found or if the P-Chain is not found let _ = avax_network - .get_subnet(AVAX_PRIMARY_NETWORK_ID)? - .get_blockchain(AVAX_PRIMARY_NETWORK_ID)?; + .get_subnet(avax_network.primary_network_id)? + .get_blockchain(avax_network.primary_network_id)?; - Ok(avax_network.clone()) + Ok(avax_network) } /// Get the P-Chain pub fn get_pchain(&self) -> Result<&AvalancheBlockchain, AshError> { let pchain = self - .get_subnet(AVAX_PRIMARY_NETWORK_ID)? - .get_blockchain(AVAX_PRIMARY_NETWORK_ID)?; + .get_subnet(self.primary_network_id)? + .get_blockchain(self.primary_network_id)?; Ok(pchain) } /// Get the C-Chain pub fn get_cchain(&self) -> Result<&AvalancheBlockchain, AshError> { let cchain = self - .get_subnet(AVAX_PRIMARY_NETWORK_ID)? + .get_subnet(self.primary_network_id)? .get_blockchain_by_name("C-Chain")?; Ok(cchain) } @@ -89,7 +114,7 @@ impl AvalancheNetwork { /// Get the X-Chain pub fn get_xchain(&self) -> Result<&AvalancheBlockchain, AshError> { let xchain = self - .get_subnet(AVAX_PRIMARY_NETWORK_ID)? + .get_subnet(self.primary_network_id)? .get_blockchain_by_name("X-Chain")?; Ok(xchain) } @@ -110,10 +135,10 @@ impl AvalancheNetwork { // Replace the Primary Network with the pre-configured one // This is done to ensure that the P-Chain is kept in the blockchains list // (it is not returned by the API) - let primary_network = self.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap().clone(); + let primary_network = self.get_subnet(self.primary_network_id).unwrap().clone(); let mut subnets = subnets .into_iter() - .filter(|subnet| subnet.id.to_string() != AVAX_PRIMARY_NETWORK_ID) + .filter(|subnet| subnet.id != self.primary_network_id) .collect::>(); subnets.push(primary_network); @@ -122,18 +147,15 @@ impl AvalancheNetwork { } /// Get a Subnet of the network by its ID - pub fn get_subnet(&self, id: &str) -> Result<&AvalancheSubnet, AshError> { - self.subnets - .iter() - .find(|&subnet| subnet.id.to_string() == id) - .ok_or( - AvalancheNetworkError::NotFound { - network: self.name.clone(), - target_type: "Subnet".to_string(), - target_value: id.to_string(), - } - .into(), - ) + pub fn get_subnet(&self, id: Id) -> Result<&AvalancheSubnet, AshError> { + self.subnets.iter().find(|&subnet| subnet.id == id).ok_or( + AvalancheNetworkError::NotFound { + network: self.name.clone(), + target_type: "Subnet".to_string(), + target_value: id.to_string(), + } + .into(), + ) } /// Update the AvalancheNetwork blockchains by querying an API endpoint @@ -153,11 +175,11 @@ impl AvalancheNetwork { // For each Subnet, replace the blockchains with the ones returned by the API // Skip the Primary Network, as the P-Chain is not returned by the API - let mut primary_network = self.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap().clone(); + let mut primary_network = self.get_subnet(self.primary_network_id).unwrap().clone(); let mut subnets = self .subnets .iter() - .filter(|subnet| subnet.id.to_string() != AVAX_PRIMARY_NETWORK_ID) + .filter(|subnet| subnet.id != self.primary_network_id) .map(|subnet| { let mut subnet = subnet.clone(); subnet.blockchains = blockchains @@ -206,7 +228,7 @@ impl AvalancheNetwork { } /// Update the validators of a Subnet by querying an API endpoint - pub fn update_subnet_validators(&mut self, subnet_id: &str) -> Result<(), AshError> { + pub fn update_subnet_validators(&mut self, subnet_id: Id) -> Result<(), AshError> { let rpc_url = &self.get_pchain()?.rpc_url; let validators = platformvm::get_current_validators(rpc_url, subnet_id)?; @@ -220,7 +242,7 @@ impl AvalancheNetwork { let subnet_index = self .subnets .iter() - .position(|subnet| subnet.id.to_string() == subnet_id) + .position(|subnet| subnet.id == subnet_id) .ok_or(AvalancheNetworkError::NotFound { network: self.name.clone(), target_type: "Subnet".to_string(), @@ -318,7 +340,7 @@ impl AvalancheNetwork { /// Avalanche output owners /// See https://docs.avax.network/specs/platform-transaction-serialization#secp256k1-output-owners-output -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheOutputOwners { pub locktime: u64, @@ -356,7 +378,7 @@ impl From for AvalancheXChainBalance { #[cfg(test)] mod tests { use super::*; - use crate::avalanche::blockchains::AvalancheBlockchain; + use crate::avalanche::{blockchains::AvalancheBlockchain, vms::AvalancheVmType}; use std::env; const AVAX_FUJI_CCHAIN_ID: &str = "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp"; @@ -401,7 +423,7 @@ mod tests { blockchains, .. } = &fuji.subnets[0]; - assert_eq!(id.to_string(), AVAX_PRIMARY_NETWORK_ID); + assert_eq!(id, &fuji.primary_network_id); assert_eq!(control_keys.len(), 0); assert_eq!(threshold, &0); assert_eq!(blockchains.len(), 3); @@ -413,10 +435,10 @@ mod tests { vm_type, .. } = &blockchains[1]; - assert_eq!(id.to_string(), AVAX_FUJI_CCHAIN_ID); + assert_eq!(id, &Id::from_str(AVAX_FUJI_CCHAIN_ID).unwrap()); assert_eq!(name, "C-Chain"); - assert_eq!(vm_id.to_string(), AVAX_FUJI_EVM_ID); - assert_eq!(vm_type, "EVM"); + assert_eq!(vm_id, &Id::from_str(AVAX_FUJI_EVM_ID).unwrap()); + assert_eq!(vm_type, &AvalancheVmType::Coreth); assert!(AvalancheNetwork::load("invalid", None).is_err()); } @@ -445,13 +467,13 @@ mod tests { let cchain = fuji.get_cchain().unwrap(); let xchain = fuji.get_xchain().unwrap(); - assert_eq!(pchain.id.to_string(), AVAX_PRIMARY_NETWORK_ID); + assert_eq!(pchain.id, fuji.primary_network_id); assert_eq!(pchain.name, "P-Chain"); - assert_eq!(cchain.id.to_string(), AVAX_FUJI_CCHAIN_ID); + assert_eq!(cchain.id, Id::from_str(AVAX_FUJI_CCHAIN_ID).unwrap()); assert_eq!(cchain.name, "C-Chain"); - assert_eq!(xchain.id.to_string(), AVAX_FUJI_XCHAIN_ID); + assert_eq!(xchain.id, Id::from_str(AVAX_FUJI_XCHAIN_ID).unwrap()); assert_eq!(xchain.name, "X-Chain"); } @@ -459,12 +481,14 @@ mod tests { fn test_avalanche_network_get_subnet() { let fuji = load_test_network(); - // Should never fail as AVAX_PRIMARY_NETWORK_ID should always be a valid key - let primary_network = fuji.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); - assert_eq!(primary_network.id.to_string(), AVAX_PRIMARY_NETWORK_ID); + // Should never fail as self.primary_network_id should always be a valid key + let primary_network = fuji.get_subnet(fuji.primary_network_id).unwrap(); + assert_eq!(primary_network.id, fuji.primary_network_id); assert_eq!(primary_network.blockchains.len(), 3); - assert!(fuji.get_subnet("invalid").is_err()); + assert!(fuji + .get_subnet(Id::from_str("7F8HV64nQER6ZupFNJwsYAKGbADv1T7pQYmoRPm1uVbeLMs7N").unwrap()) + .is_err()); } #[test] @@ -477,17 +501,22 @@ mod tests { // Test that the Primary Network is still present // and that the P-Chain is still present - let primary_network = fuji.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); - assert_eq!(primary_network.id.to_string(), AVAX_PRIMARY_NETWORK_ID); + let primary_network = fuji.get_subnet(fuji.primary_network_id).unwrap(); + assert_eq!(primary_network.id, fuji.primary_network_id); assert_eq!(primary_network.blockchains.len(), 3); assert!(primary_network .blockchains .iter() - .any(|blockchain| blockchain.id.to_string() == AVAX_PRIMARY_NETWORK_ID)); + .any(|blockchain| blockchain.id == fuji.primary_network_id)); // Test that the DFK Subnet is present - let dfk_subnet = fuji.get_subnet(AVAX_FUJI_DFK_SUBNET_ID).unwrap(); - assert_eq!(dfk_subnet.id.to_string(), AVAX_FUJI_DFK_SUBNET_ID); + let dfk_subnet = fuji + .get_subnet(Id::from_str(AVAX_FUJI_DFK_SUBNET_ID).unwrap()) + .unwrap(); + assert_eq!( + dfk_subnet.id, + Id::from_str(AVAX_FUJI_DFK_SUBNET_ID).unwrap() + ); } #[test] @@ -500,10 +529,12 @@ mod tests { assert!(fuji .subnets .iter() - .any(|subnet| subnet.id.to_string() == AVAX_PRIMARY_NETWORK_ID)); + .any(|subnet| subnet.id == fuji.primary_network_id)); // Test that the DFK Subnet contains the DFK chain - let dfk_subnet = fuji.get_subnet(AVAX_FUJI_DFK_SUBNET_ID).unwrap(); + let dfk_subnet = fuji + .get_subnet(Id::from_str(AVAX_FUJI_DFK_SUBNET_ID).unwrap()) + .unwrap(); assert!(dfk_subnet .blockchains .iter() @@ -522,7 +553,9 @@ mod tests { // Test that the X-Chain and C-Chain are present // They are not defined in the local-light network and should be fetched from the API - let primary_network = local_network.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); + let primary_network = local_network + .get_subnet(local_network.primary_network_id) + .unwrap(); assert!(primary_network .blockchains .iter() @@ -539,17 +572,17 @@ mod tests { // Tempoary workaround: use Ankr public endpoint let mut fuji = AvalancheNetwork::load("fuji-ankr", None).unwrap(); fuji.update_subnets().unwrap(); - fuji.update_subnet_validators(AVAX_PRIMARY_NETWORK_ID) + fuji.update_subnet_validators(fuji.primary_network_id) .unwrap(); // Test that the Primary Network is still present assert!(fuji .subnets .iter() - .any(|subnet| subnet.id.to_string() == AVAX_PRIMARY_NETWORK_ID)); + .any(|subnet| subnet.id == fuji.primary_network_id)); // Test that the Primary Network has validators - let primary_network = fuji.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); + let primary_network = fuji.get_subnet(fuji.primary_network_id).unwrap(); assert!(primary_network.validators.len() > 0); } diff --git a/crates/ash_sdk/src/avalanche/blockchains.rs b/crates/ash_sdk/src/avalanche/blockchains.rs index a9833d1..6334649 100644 --- a/crates/ash_sdk/src/avalanche/blockchains.rs +++ b/crates/ash_sdk/src/avalanche/blockchains.rs @@ -3,13 +3,16 @@ // Module that contains code to interact with Avalanche blockchains -use crate::errors::*; +use crate::{ + avalanche::{txs::p, vms::AvalancheVmType, wallets::AvalancheWallet}, + errors::*, +}; use avalanche_types::{ids::Id, jsonrpc::platformvm::Blockchain}; use ethers::providers::{Http, Provider}; use serde::{Deserialize, Serialize}; /// Avalanche blockchain -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheBlockchain { pub id: Id, @@ -19,7 +22,7 @@ pub struct AvalancheBlockchain { #[serde(default)] pub vm_id: Id, #[serde(default)] - pub vm_type: String, + pub vm_type: AvalancheVmType, #[serde(default)] pub rpc_url: String, } @@ -28,15 +31,12 @@ impl AvalancheBlockchain { /// Get an ethers Provider for this blockchain /// Only works for EVM blockchains pub fn get_ethers_provider(&self) -> Result, AshError> { - match self.vm_type.as_str() { - "EVM" => Ok( - Provider::::try_from(self.rpc_url.clone()).map_err(|e| { - AvalancheBlockchainError::EthersProvider { - blockchain_id: self.id.to_string(), - msg: e.to_string(), - } - })?, - ), + match self.vm_type { + AvalancheVmType::Coreth => Ok(Provider::::try_from(self.rpc_url.clone()) + .map_err(|e| AvalancheBlockchainError::EthersProvider { + blockchain_id: self.id.to_string(), + msg: e.to_string(), + })?), _ => Err(AvalancheBlockchainError::EthersProvider { blockchain_id: self.id.to_string(), msg: format!( @@ -47,6 +47,36 @@ impl AvalancheBlockchain { .into()), } } + + /// Create a new blockchain + pub async fn create( + wallet: &AvalancheWallet, + subnet_id: Id, + name: &str, + vm_type: AvalancheVmType, + vm_id: Id, + genesis_data: Vec, + check_acceptance: bool, + ) -> Result { + let tx_id = p::create_blockchain( + wallet, + subnet_id, + genesis_data, + vm_id, + name, + check_acceptance, + ) + .await?; + + Ok(Self { + id: tx_id, + name: name.to_string(), + subnet_id, + vm_id, + vm_type: vm_type.clone(), + ..Default::default() + }) + } } impl From for AvalancheBlockchain { @@ -63,8 +93,13 @@ impl From for AvalancheBlockchain { #[cfg(test)] mod tests { - use crate::avalanche::AvalancheNetwork; - use std::env; + use super::*; + use crate::avalanche::{vms::encode_genesis_data, AvalancheNetwork, AvalancheSubnet}; + use std::{env, fs, str::FromStr}; + + const AVAX_EWOQ_PRIVATE_KEY: &str = + "PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN"; + const SUBNET_EVM_VM_ID: &str = "spePNvBxaWSYL2tB5e2xMmMNBQkXMN8z2XEbz1ML2Aahatwoc"; // Load the test network from the ASH_TEST_CONFIG file fn load_test_network() -> AvalancheNetwork { @@ -73,6 +108,11 @@ mod tests { AvalancheNetwork::load("fuji", Some(&config_path)).unwrap() } + // Using avalanche-network-runner to run a test network + fn load_avalanche_network_runner() -> AvalancheNetwork { + AvalancheNetwork::load("local", Some("tests/conf/avalanche-network-runner.yml")).unwrap() + } + #[test] fn test_avalanche_blockchain_get_ethers_provider() { let fuji = load_test_network(); @@ -95,4 +135,44 @@ mod tests { // Test that we can't get an ethers Provider for the P-Chain assert!(fuji.get_pchain().unwrap().get_ethers_provider().is_err()); } + + #[async_std::test] + #[serial_test::serial] + #[ignore] + async fn test_avalanche_blockchain_create() { + let mut local_network = load_avalanche_network_runner(); + let wallet = local_network + .create_wallet_from_cb58(AVAX_EWOQ_PRIVATE_KEY) + .unwrap(); + let genesis_json = fs::read_to_string("tests/genesis/subnet-evm.json").unwrap(); + let genesis_data = encode_genesis_data(AvalancheVmType::SubnetEVM, &genesis_json).unwrap(); + + // Create an empty subnet + let created_subnet = AvalancheSubnet::create(&wallet, true).await.unwrap(); + + let created_blockchain = AvalancheBlockchain::create( + &wallet, + created_subnet.id, + "testAvalancheBlockchainCreate", + AvalancheVmType::SubnetEVM, + Id::from_str(SUBNET_EVM_VM_ID).unwrap(), + genesis_data, + true, + ) + .await + .unwrap(); + + local_network.update_subnets().unwrap(); + local_network.update_blockchains().unwrap(); + let network_subnet = local_network.get_subnet(created_subnet.id).unwrap(); + let mut network_blockchain = network_subnet + .get_blockchain(created_blockchain.id) + .unwrap() + .clone(); + + // Manually set the vm_type as it's not returned by the API + network_blockchain.vm_type = AvalancheVmType::SubnetEVM; + + assert_eq!(created_blockchain, network_blockchain); + } } diff --git a/crates/ash_sdk/src/avalanche/jsonrpc.rs b/crates/ash_sdk/src/avalanche/jsonrpc.rs index e70df87..7637504 100644 --- a/crates/ash_sdk/src/avalanche/jsonrpc.rs +++ b/crates/ash_sdk/src/avalanche/jsonrpc.rs @@ -9,7 +9,6 @@ pub mod platformvm; use crate::errors::*; use avalanche_types::jsonrpc::ResponseError; -use ureq; /// Trait that defines the methods to get the result and error of a JSON RPC response /// This is used to avoid code duplication when posting JSON RPC requests diff --git a/crates/ash_sdk/src/avalanche/jsonrpc/info.rs b/crates/ash_sdk/src/avalanche/jsonrpc/info.rs index 2b7928d..ef5a8c2 100644 --- a/crates/ash_sdk/src/avalanche/jsonrpc/info.rs +++ b/crates/ash_sdk/src/avalanche/jsonrpc/info.rs @@ -99,10 +99,12 @@ pub fn is_bootstrapped(rpc_url: &str, chain: &str) -> Result { #[cfg(test)] mod tests { - use std::net::{IpAddr, Ipv4Addr}; - use super::*; - use avalanche_types::jsonrpc::info::VmVersions; + use avalanche_types::{ids::node::Id as NodeId, jsonrpc::info::VmVersions}; + use std::{ + net::{IpAddr, Ipv4Addr}, + str::FromStr, + }; // Using avalanche-network-runner to run a test network const ASH_TEST_HTTP_HOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); @@ -118,7 +120,7 @@ mod tests { ASH_TEST_HTTP_HOST, ASH_TEST_HTTP_PORT, AVAX_INFO_API_ENDPOINT ); let node_id = get_node_id(&rpc_url).unwrap(); - assert_eq!(node_id.to_string(), ASH_TEST_NODE_ID); + assert_eq!(node_id, NodeId::from_str(ASH_TEST_NODE_ID).unwrap()); } #[test] diff --git a/crates/ash_sdk/src/avalanche/jsonrpc/platformvm.rs b/crates/ash_sdk/src/avalanche/jsonrpc/platformvm.rs index 3599ab6..c7c7c06 100644 --- a/crates/ash_sdk/src/avalanche/jsonrpc/platformvm.rs +++ b/crates/ash_sdk/src/avalanche/jsonrpc/platformvm.rs @@ -15,8 +15,6 @@ use avalanche_types::{ }; use serde::{Deserialize, Serialize}; use serde_aux::prelude::*; -use std::str::FromStr; -use ureq; /// Subnet with control keys as addresses /// This is done to avoid having to retransform the control keys to addresses later @@ -103,13 +101,13 @@ pub fn get_network_blockchains( /// Get the current validators of a Subnet by querying the P-Chain API pub fn get_current_validators( rpc_url: &str, - subnet_id: &str, + subnet_id: Id, ) -> Result, RpcError> { let current_validators = get_json_rpc_req_result::( rpc_url, "platform.getCurrentValidators", - Some(ureq::json!({ "subnetID": subnet_id })), + Some(ureq::json!({ "subnetID": subnet_id.to_string() })), )? .validators .ok_or(RpcError::GetFailure { @@ -119,13 +117,7 @@ pub fn get_current_validators( msg: "No validators found".to_string(), })? .iter() - .map(|validator| { - AvalancheSubnetValidator::from_api_primary_validator( - validator, - // Unwrap is safe because we checked for a response error above - Id::from_str(subnet_id).unwrap(), - ) - }) + .map(|validator| AvalancheSubnetValidator::from_api_primary_validator(validator, subnet_id)) .collect(); Ok(current_validators) @@ -136,7 +128,7 @@ mod tests { use super::*; use crate::avalanche::AvalancheNetwork; use avalanche_types::ids::node::Id as NodeId; - use std::env; + use std::{env, str::FromStr}; const AVAX_PRIMARY_NETWORK_ID: &str = "11111111111111111111111111111111LpoYY"; const AVAX_FUJI_CCHAIN_ID: &str = "yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp"; @@ -192,7 +184,7 @@ mod tests { let fuji = AvalancheNetwork::load("fuji-ankr", None).unwrap(); let rpc_url = &fuji.get_pchain().unwrap().rpc_url; - let validators = get_current_validators(rpc_url, AVAX_PRIMARY_NETWORK_ID).unwrap(); + let validators = get_current_validators(rpc_url, fuji.primary_network_id).unwrap(); // Test that the node operated by Ava Labs is present // Should not fail if the node is present diff --git a/crates/ash_sdk/src/avalanche/nodes.rs b/crates/ash_sdk/src/avalanche/nodes.rs index 1f2d88e..0a225fd 100644 --- a/crates/ash_sdk/src/avalanche/nodes.rs +++ b/crates/ash_sdk/src/avalanche/nodes.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr}; /// Avalanche node -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheNode { pub id: Id, @@ -151,7 +151,7 @@ impl AvalancheNode { } /// Avalanche node version -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheNodeVersions { pub avalanchego_version: String, @@ -174,7 +174,7 @@ impl From for AvalancheNodeVersions { } /// Avalanche node uptime -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheNodeUptime { pub rewarding_stake_percentage: f64, @@ -193,6 +193,7 @@ impl From for AvalancheNodeUptime { #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; // Using avalanche-network-runner to run a test network const ASH_TEST_HTTP_PORT: u16 = 9650; @@ -217,7 +218,7 @@ mod tests { node.update_info().unwrap(); // Test the node ID, network, public_ip and stacking_port - assert_eq!(node.id.to_string(), ASH_TEST_NODE_ID); + assert_eq!(node.id, Id::from_str(ASH_TEST_NODE_ID).unwrap()); assert_eq!(node.network, ASH_TEST_NETWORK_NAME); assert_eq!(node.public_ip, ASH_TEST_HTTP_HOST); assert_eq!(node.staking_port, ASH_TEST_STACKING_PORT); diff --git a/crates/ash_sdk/src/avalanche/subnets.rs b/crates/ash_sdk/src/avalanche/subnets.rs index 9a25448..3da06fc 100644 --- a/crates/ash_sdk/src/avalanche/subnets.rs +++ b/crates/ash_sdk/src/avalanche/subnets.rs @@ -4,19 +4,20 @@ // Module that contains code to interact with Avalanche Subnets and validators use crate::avalanche::{ - blockchains::AvalancheBlockchain, jsonrpc::platformvm::SubnetStringControlKeys, - AvalancheOutputOwners, AVAX_PRIMARY_NETWORK_ID, + blockchains::AvalancheBlockchain, jsonrpc::platformvm::SubnetStringControlKeys, txs::p, + wallets::AvalancheWallet, AvalancheOutputOwners, AVAX_PRIMARY_NETWORK_ID, }; use crate::errors::*; use avalanche_types::{ ids::{node::Id as NodeId, Id}, jsonrpc::platformvm::{ApiPrimaryDelegator, ApiPrimaryValidator}, }; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Display; /// Avalanche Subnet types -#[derive(Default, Debug, Display, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Display, Clone, Serialize, Deserialize, PartialEq)] pub enum AvalancheSubnetType { PrimaryNetwork, #[default] @@ -26,7 +27,7 @@ pub enum AvalancheSubnetType { } /// Avalanche Subnet -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheSubnet { pub id: Id, @@ -46,10 +47,10 @@ pub struct AvalancheSubnet { impl AvalancheSubnet { /// Get a blockchain of the Subnet by its ID - pub fn get_blockchain(&self, id: &str) -> Result<&AvalancheBlockchain, AshError> { + pub fn get_blockchain(&self, id: Id) -> Result<&AvalancheBlockchain, AshError> { self.blockchains .iter() - .find(|&blockchain| blockchain.id.to_string() == id) + .find(|&blockchain| blockchain.id == id) .ok_or( AvalancheSubnetError::NotFound { subnet_id: self.id.to_string(), @@ -76,10 +77,10 @@ impl AvalancheSubnet { } /// Get a validator of the Subnet by its ID - pub fn get_validator(&self, id: &str) -> Result<&AvalancheSubnetValidator, AshError> { + pub fn get_validator(&self, id: NodeId) -> Result<&AvalancheSubnetValidator, AshError> { self.validators .iter() - .find(|&validator| validator.node_id.to_string() == id) + .find(|&validator| validator.node_id == id) .ok_or( AvalancheSubnetError::NotFound { subnet_id: self.id.to_string(), @@ -89,6 +90,121 @@ impl AvalancheSubnet { .into(), ) } + + /// Create a new Subnet + /// TODO: Add control keys and threshold as parameters + /// See: https://github.com/ava-labs/avalanche-types-rs/pull/76 + pub async fn create( + wallet: &AvalancheWallet, + check_acceptance: bool, + ) -> Result { + let tx_id = p::create_subnet(wallet, check_acceptance).await?; + + Ok(Self { + id: tx_id, + control_keys: vec![wallet.pchain_wallet.p_address.clone()], + threshold: 1, + subnet_type: AvalancheSubnetType::Permissioned, + ..Default::default() + }) + } + + /// Add a validator to the Primary Network + /// Fail if the Subnet is not the Primary Network + pub async fn add_avalanche_validator( + &self, + wallet: &AvalancheWallet, + node_id: NodeId, + stake_amount: u64, + start_time: DateTime, + end_time: DateTime, + reward_fee_percent: u32, + check_acceptance: bool, + ) -> Result { + // Check if the Subnet is the Primary Network + if self.subnet_type != AvalancheSubnetType::PrimaryNetwork { + return Err(AvalancheSubnetError::OperationNotAllowed { + operation: "add_avalanche_validator".to_string(), + subnet_id: self.id.to_string(), + subnet_type: self.subnet_type.to_string(), + } + .into()); + } + + let tx_id = p::add_avalanche_validator( + wallet, + node_id, + stake_amount, + start_time, + end_time, + reward_fee_percent, + check_acceptance, + ) + .await?; + + Ok(AvalancheSubnetValidator { + tx_id, + node_id, + subnet_id: self.id, + start_time: start_time.timestamp() as u64, + end_time: end_time.timestamp() as u64, + stake_amount: Some(stake_amount), + delegation_fee: Some(reward_fee_percent as f32), + validation_reward_owner: Some(AvalancheOutputOwners { + locktime: 0, + threshold: 1, + addresses: vec![wallet.pchain_wallet.p_address.clone()], + }), + delegation_reward_owner: Some(AvalancheOutputOwners { + locktime: 0, + threshold: 1, + addresses: vec![wallet.pchain_wallet.p_address.clone()], + }), + ..Default::default() + }) + } + + /// Add a validator to a permissioned Subnet + pub async fn add_validator_permissioned( + &self, + wallet: &AvalancheWallet, + node_id: NodeId, + weight: u64, + start_time: DateTime, + end_time: DateTime, + check_acceptance: bool, + ) -> Result { + // Check if the Subnet is permissioned + if self.subnet_type != AvalancheSubnetType::Permissioned { + return Err(AvalancheSubnetError::OperationNotAllowed { + operation: "add_validator_permissioned".to_string(), + subnet_id: self.id.to_string(), + subnet_type: self.subnet_type.to_string(), + } + .into()); + } + + let tx_id = p::add_permissioned_subnet_validator( + wallet, + self.id, + node_id, + weight, + start_time, + end_time, + check_acceptance, + ) + .await?; + + Ok(AvalancheSubnetValidator { + tx_id, + node_id, + subnet_id: self.id, + start_time: start_time.timestamp() as u64, + end_time: end_time.timestamp() as u64, + weight: Some(weight), + ..Default::default() + }) + } } impl From for AvalancheSubnet { @@ -113,7 +229,7 @@ impl From for AvalancheSubnet { } /// Avalanche Subnet validator -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheSubnetValidator { #[serde(rename = "txID")] @@ -122,6 +238,7 @@ pub struct AvalancheSubnetValidator { pub node_id: NodeId, #[serde(skip)] pub subnet_id: Id, + // TODO: Store as DateTime::? pub start_time: u64, pub end_time: u64, pub stake_amount: Option, @@ -172,7 +289,7 @@ impl AvalancheSubnetValidator { } /// Avalanche Subnet delegator -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AvalancheSubnetDelegator { #[serde(rename = "txID")] @@ -202,10 +319,14 @@ impl From for AvalancheSubnetDelegator { #[cfg(test)] mod tests { - use crate::avalanche::{AvalancheNetwork, AVAX_PRIMARY_NETWORK_ID}; + use super::*; + use crate::avalanche::AvalancheNetwork; + use std::str::FromStr; const NETWORK_RUNNER_CCHAIN_ID: &str = "VctwH3nkmztWbkdNXbuo6eCYndsUuemtM9ZFmEUZ5QpA1Fu8G"; const NETWORK_RUNNER_NODE_ID: &str = "NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ"; + const AVAX_EWOQ_PRIVATE_KEY: &str = + "PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN"; // Load the test network using avalanche-network-runner fn load_test_network() -> AvalancheNetwork { @@ -215,33 +336,96 @@ mod tests { #[test] #[ignore] fn test_avalanche_subnet_get_blockchain() { - let fuji = load_test_network(); - let subnet = fuji.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); + let local_network = load_test_network(); + let subnet = local_network + .get_subnet(local_network.primary_network_id) + .unwrap(); - let blockchain = subnet.get_blockchain(NETWORK_RUNNER_CCHAIN_ID).unwrap(); + let blockchain = subnet + .get_blockchain(Id::from_str(NETWORK_RUNNER_CCHAIN_ID).unwrap()) + .unwrap(); assert_eq!(blockchain.name, "C-Chain"); } #[test] #[ignore] fn test_avalanche_subnet_get_blockchain_by_name() { - let fuji = load_test_network(); - let subnet = fuji.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); + let local_network = load_test_network(); + let subnet = local_network + .get_subnet(local_network.primary_network_id) + .unwrap(); let blockchain = subnet.get_blockchain_by_name("C-Chain").unwrap(); - assert_eq!(blockchain.id.to_string(), NETWORK_RUNNER_CCHAIN_ID); + assert_eq!( + blockchain.id, + Id::from_str(NETWORK_RUNNER_CCHAIN_ID).unwrap() + ); } #[test] #[ignore] fn test_avalanche_subnet_get_validator() { - let mut fuji = load_test_network(); - fuji.update_subnet_validators(AVAX_PRIMARY_NETWORK_ID) + let mut local_network = load_test_network(); + local_network + .update_subnet_validators(local_network.primary_network_id) + .unwrap(); + + let subnet = local_network + .get_subnet(local_network.primary_network_id) + .unwrap(); + + let validator = subnet + .get_validator(NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap()) + .unwrap(); + assert_eq!( + validator.node_id, + NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap() + ); + } + + #[async_std::test] + #[serial_test::serial] + #[ignore] + async fn test_avalanche_subnet_create() { + let mut local_network = load_test_network(); + let wallet = local_network + .create_wallet_from_cb58(AVAX_EWOQ_PRIVATE_KEY) .unwrap(); - let subnet = fuji.get_subnet(AVAX_PRIMARY_NETWORK_ID).unwrap(); + let created_subnet = AvalancheSubnet::create(&wallet, true).await.unwrap(); + + local_network.update_subnets().unwrap(); + let network_subnet = local_network.get_subnet(created_subnet.id).unwrap(); - let validator = subnet.get_validator(NETWORK_RUNNER_NODE_ID).unwrap(); - assert_eq!(validator.node_id.to_string(), NETWORK_RUNNER_NODE_ID); + assert_eq!(&created_subnet, network_subnet); + } + + #[async_std::test] + #[serial_test::serial] + #[ignore] + async fn test_avalanche_subnet_add_validator_permissioned() { + let local_network = load_test_network(); + let wallet = local_network + .create_wallet_from_cb58(AVAX_EWOQ_PRIVATE_KEY) + .unwrap(); + + // Only test if adding a validator to the Primary Network fails + // because adding a validator to a Subnet is too long and already tested + let primary_network = local_network + .get_subnet(local_network.primary_network_id) + .unwrap() + .clone(); + + assert!(primary_network + .add_validator_permissioned( + &wallet, + NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap(), + 100, + DateTime::::from_str("2025-01-01T00:00:00.000Z").unwrap(), + DateTime::::from_str("2025-02-01T00:00:00.000Z").unwrap(), + false + ) + .await + .is_err()); } } diff --git a/crates/ash_sdk/src/avalanche/txs.rs b/crates/ash_sdk/src/avalanche/txs.rs index ee9217e..754ce49 100644 --- a/crates/ash_sdk/src/avalanche/txs.rs +++ b/crates/ash_sdk/src/avalanche/txs.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2023, E36 Knots +pub mod p; pub mod x; // Module that contains code to issue transactions diff --git a/crates/ash_sdk/src/avalanche/txs/p.rs b/crates/ash_sdk/src/avalanche/txs/p.rs new file mode 100644 index 0000000..d929560 --- /dev/null +++ b/crates/ash_sdk/src/avalanche/txs/p.rs @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, E36 Knots + +// Module that contains code to issue transactions on the X-Chain + +use crate::{avalanche::wallets::AvalancheWallet, errors::*}; +use avalanche_types::{ + ids::{node::Id as NodeId, Id}, + wallet::p, +}; +use chrono::{DateTime, Duration, Utc}; + +/// Create a new subnet +/// TODO: Add control keys and threshold as parameters +/// See: https://github.com/ava-labs/avalanche-types-rs/pull/76 +pub async fn create_subnet( + wallet: &AvalancheWallet, + check_acceptance: bool, +) -> Result { + let tx_id = p::create_subnet::Tx::new(&wallet.pchain_wallet.p()) + .check_acceptance(check_acceptance) + .issue() + .await + .map_err(|e| AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "create_subnet".to_string(), + msg: format!("failed to create subnet: {e}"), + })?; + + Ok(tx_id) +} + +/// Create a new blockchain +pub async fn create_blockchain( + wallet: &AvalancheWallet, + subnet_id: Id, + genesis_data: Vec, + vm_id: Id, + name: &str, + check_acceptance: bool, +) -> Result { + let tx_id = p::create_chain::Tx::new(&wallet.pchain_wallet.p()) + .subnet_id(subnet_id) + .genesis_data(genesis_data) + .vm_id(vm_id) + .chain_name(name.to_string()) + .check_acceptance(check_acceptance) + .issue() + .await + .map_err(|e| AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "create_blockchain".to_string(), + msg: format!("failed to create blockchain on Subnet {subnet_id}: {e}"), + })?; + + Ok(tx_id) +} + +/// Add a validator to the Primary Network +pub async fn add_permissioned_subnet_validator( + wallet: &AvalancheWallet, + subnet_id: Id, + node_id: NodeId, + weight: u64, + start_time: DateTime, + end_time: DateTime, + check_acceptance: bool, +) -> Result { + let (tx_id, success) = p::add_subnet_validator::Tx::new(&wallet.pchain_wallet.p()) + .subnet_id(subnet_id) + .node_id(node_id) + .weight(weight) + .start_time(start_time) + .end_time(end_time) + .check_acceptance(check_acceptance) + .poll_initial_wait(Duration::seconds(1).to_std().unwrap()) + .issue() + .await + .map_err(|e| AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_subnet_validator".to_string(), + msg: format!("failed to add '{node_id}' as validator to Subnet '{subnet_id}': {e}"), + })?; + + // Check if the validator was successfully added + // If the validator is already a validator, tx_id is empty and success false + match success { + true => Ok(tx_id), + false => { + if Id::is_empty(&tx_id) { + Err(AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_validator".to_string(), + msg: format!("'{node_id}' is already a validator to Subnet '{subnet_id}'"), + } + .into()) + } else { + // This is theoretically unreachable + Err(AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_validator".to_string(), + msg: format!( + "failed to add '{node_id}' as validator to Subnet '{subnet_id}': Unknown error" + ), + } + .into()) + } + } + } +} + +/// Add a validator to a permissioned Subnet +pub async fn add_avalanche_validator( + wallet: &AvalancheWallet, + node_id: NodeId, + stake_amount: u64, + start_time: DateTime, + end_time: DateTime, + reward_fee_percent: u32, + check_acceptance: bool, +) -> Result { + let (tx_id, success) = p::add_validator::Tx::new(&wallet.pchain_wallet.p()) + .node_id(node_id) + .stake_amount(stake_amount) + .start_time(start_time) + .end_time(end_time) + .reward_fee_percent(reward_fee_percent) + .check_acceptance(check_acceptance) + .poll_initial_wait(Duration::seconds(1).to_std().unwrap()) + .issue() + .await + .map_err(|e| AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_validator".to_string(), + msg: format!("failed to add '{node_id}' as Avalanche validator: {e}"), + })?; + + // Check if the validator was successfully added + // If the validator is already a validator, tx_id is empty and success false + match success { + true => Ok(tx_id), + false => { + if Id::is_empty(&tx_id) { + Err(AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_validator".to_string(), + msg: format!("'{node_id}' is already an Avalanche validator"), + } + .into()) + } else { + // This is theoretically unreachable + Err(AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_validator".to_string(), + msg: format!("failed to add '{node_id}' as Avalanche validator: Unknown error"), + } + .into()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::avalanche::{ + vms::{encode_genesis_data, AvalancheVmType}, + AvalancheNetwork, + }; + use chrono::Duration; + use std::{fs, str::FromStr}; + + const AVAX_EWOQ_PRIVATE_KEY: &str = + "PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN"; + const NETWORK_RUNNER_PCHAIN_ADDR: &str = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p"; + const SUBNET_EVM_VM_ID: &str = "spePNvBxaWSYL2tB5e2xMmMNBQkXMN8z2XEbz1ML2Aahatwoc"; + const NETWORK_RUNNER_NODE_ID: &str = "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg"; + + // Load the test network using avalanche-network-runner + fn load_test_network() -> AvalancheNetwork { + AvalancheNetwork::load("local", Some("tests/conf/avalanche-network-runner.yml")).unwrap() + } + + #[async_std::test] + #[serial_test::serial] + #[ignore] + async fn test_create_subnet() { + let mut local_network = load_test_network(); + let local_wallet = local_network + .create_wallet_from_cb58(AVAX_EWOQ_PRIVATE_KEY) + .unwrap(); + + let tx_id = create_subnet(&local_wallet, true).await.unwrap(); + + // Check that the Subnet was created + // The Subnet has the same ID as the transaction that created it + local_network.update_subnets().unwrap(); + let subnet = local_network.get_subnet(tx_id).unwrap(); + + assert_eq!(subnet.threshold, 1); + assert_eq!(subnet.control_keys.len(), 1); + assert_eq!(subnet.control_keys[0], NETWORK_RUNNER_PCHAIN_ADDR); + } + + #[async_std::test] + #[serial_test::serial] + #[ignore] + async fn test_create_blockchain() { + let mut local_network = load_test_network(); + let local_wallet = local_network + .create_wallet_from_cb58(AVAX_EWOQ_PRIVATE_KEY) + .unwrap(); + let genesis_str = fs::read_to_string("tests/genesis/subnet-evm.json").unwrap(); + let genesis_data = encode_genesis_data(AvalancheVmType::SubnetEVM, &genesis_str).unwrap(); + + // Create a Subnet to create the Blockchain on + let subnet_id = create_subnet(&local_wallet, true).await.unwrap(); + + let tx_id = create_blockchain( + &local_wallet, + subnet_id, + genesis_data, + Id::from_str(SUBNET_EVM_VM_ID).unwrap(), + "testCreateBlockchain", + true, + ) + .await + .unwrap(); + + // Check that the Blockchain was created + // The Blockchain has the same ID as the transaction that created it + local_network.update_subnets().unwrap(); + local_network.update_blockchains().unwrap(); + + let subnet = local_network.get_subnet(subnet_id).unwrap(); + let blockchain = subnet.get_blockchain(tx_id).unwrap(); + + assert_eq!(blockchain.name, "testCreateBlockchain"); + assert_eq!(blockchain.vm_id, Id::from_str(SUBNET_EVM_VM_ID).unwrap()); + } + + #[async_std::test] + #[serial_test::serial] + #[ignore] + async fn test_add_validators() { + let mut local_network = load_test_network(); + let local_wallet = local_network + .create_wallet_from_cb58(AVAX_EWOQ_PRIVATE_KEY) + .unwrap(); + + // Create a Subnet + let subnet_id = create_subnet(&local_wallet, true).await.unwrap(); + + // Add a validator to the Subnet + // The validator is added with a start time of 20 seconds from now and an end time of 24 hours from now + let start_time = Utc::now() + Duration::seconds(20); + let end_time = Utc::now() + Duration::seconds(86420); + add_permissioned_subnet_validator( + &local_wallet, + subnet_id, + NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap(), + 100, + start_time, + end_time, + true, + ) + .await + .unwrap(); + + // Check that the validator was added + local_network.update_subnets().unwrap(); + local_network.update_subnet_validators(subnet_id).unwrap(); + + let subnet_validator = local_network + .get_subnet(subnet_id) + .unwrap() + .get_validator(NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap()); + + assert!(subnet_validator.is_ok()); + assert_eq!(subnet_validator.unwrap().weight, Some(100)); + + // Try to add a validator that already exists on the Primary Network + let avalanche_validator = add_avalanche_validator( + &local_wallet, + NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap(), + 1 * 1_000_000_000, + start_time, + end_time, + 2, + true, + ) + .await; + + assert!(avalanche_validator.is_err()); + assert_eq!( + avalanche_validator.err(), + Some(AshError::AvalancheWalletError( + AvalancheWalletError::IssueTx { + blockchain_name: "P-Chain".to_string(), + tx_type: "add_validator".to_string(), + msg: format!( + "'{}' is already an Avalanche validator", + NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap() + ), + } + )) + ) + } +} diff --git a/crates/ash_sdk/src/avalanche/txs/x.rs b/crates/ash_sdk/src/avalanche/txs/x.rs index cfbbd5e..98681ea 100644 --- a/crates/ash_sdk/src/avalanche/txs/x.rs +++ b/crates/ash_sdk/src/avalanche/txs/x.rs @@ -10,14 +10,14 @@ use avalanche_types::{ }; /// Transfer AVAX from a wallet to the receiver -pub async fn transfer( +pub async fn transfer_avax( wallet: &AvalancheWallet, receiver: ShortId, amount: u64, check_acceptance: bool, ) -> Result { let tx_id = transfer::Tx::new(&wallet.xchain_wallet.x()) - .receiver(receiver) + .receiver(receiver.clone()) .amount(amount) .check_acceptance(check_acceptance) .issue() @@ -25,7 +25,7 @@ pub async fn transfer( .map_err(|e| AvalancheWalletError::IssueTx { blockchain_name: "X-Chain".to_string(), tx_type: "transfer".to_string(), - msg: e.to_string(), + msg: format!("failed to transfer {amount} AVAX to '{receiver}': {e}"), })?; Ok(tx_id) @@ -35,7 +35,6 @@ pub async fn transfer( mod tests { use super::*; use crate::avalanche::{address_to_short_id, jsonrpc::avm::get_balance, AvalancheNetwork}; - use async_std; const AVAX_EWOQ_PRIVATE_KEY: &str = "PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN"; @@ -47,6 +46,7 @@ mod tests { } #[async_std::test] + #[serial_test::serial] #[ignore] async fn test_transfer() { let local_network = load_test_network(); @@ -56,9 +56,9 @@ mod tests { let rpc_url = &local_network.get_xchain().unwrap().rpc_url; let init_balance = get_balance(rpc_url, AVAX_LOCAL_XCHAIN_ADDR, "AVAX").unwrap(); - transfer( + transfer_avax( &local_wallet, - address_to_short_id(AVAX_LOCAL_XCHAIN_ADDR, "X"), + address_to_short_id(AVAX_LOCAL_XCHAIN_ADDR, "X").unwrap(), 100000000, true, ) diff --git a/crates/ash_sdk/src/avalanche/vms.rs b/crates/ash_sdk/src/avalanche/vms.rs new file mode 100644 index 0000000..d159ce5 --- /dev/null +++ b/crates/ash_sdk/src/avalanche/vms.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, E36 Knots + +pub mod subnet_evm; + +// Module that contains code to interact with Avalanche VMs + +use crate::errors::*; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use strum::EnumString; + +/// List of Avalanche VM types +#[derive(Default, Debug, Display, Clone, Serialize, Deserialize, PartialEq, EnumString)] +pub enum AvalancheVmType { + /// Coreth (Avalanche C-Chain) + Coreth, + /// Platform VM (Avalanche P-Chain) + PlatformVM, + /// Avalanche VM (Avalanche X-Chain) + AvalancheVM, + /// Subnet EVM + #[default] + SubnetEVM, + /// Any other custom VM + Custom(String), +} + +/// Encode the genesis data (JSON) to bytes +pub fn encode_genesis_data( + vm_type: AvalancheVmType, + genesis_json: &str, +) -> Result, AshError> { + match vm_type { + AvalancheVmType::SubnetEVM => subnet_evm::encode_genesis_data(genesis_json), + _ => Err(AvalancheVMError::GenesisEncoding(format!( + "encoding is not supported for VM '{}'", + vm_type + )) + .into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encode_genesis_data_unsupported_vm() { + let vm_type: AvalancheVmType = serde_json::from_str(r#"{"Custom": "SuperVM"}"#).unwrap(); + + assert_eq!( + encode_genesis_data(vm_type, "").err(), + Some(AshError::from(AvalancheVMError::GenesisEncoding( + "encoding is not supported for VM 'SuperVM'".to_string() + ))) + ); + } +} diff --git a/crates/ash_sdk/src/avalanche/vms/subnet_evm.rs b/crates/ash_sdk/src/avalanche/vms/subnet_evm.rs new file mode 100644 index 0000000..4e14fef --- /dev/null +++ b/crates/ash_sdk/src/avalanche/vms/subnet_evm.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023, E36 Knots + +// Module that contains code to interact with the Subnet EVM + +use crate::errors::*; +use avalanche_types::subnet_evm::genesis::Genesis; + +/// Known ID for the Subnet EVM +pub const AVAX_SUBNET_EVM_ID: &str = "spePNvBxaWSYL2tB5e2xMmMNBQkXMN8z2XEbz1ML2Aahatwoc"; + +/// Encode the genesis data (JSON) to bytes +pub fn encode_genesis_data(genesis_json: &str) -> Result, AshError> { + let genesis: Genesis = serde_json::from_str(genesis_json) + .map_err(|e| AvalancheVMError::GenesisEncoding(e.to_string()))?; + + let bytes = genesis + .to_bytes() + .map_err(|e| AvalancheVMError::GenesisEncoding(e.to_string()))?; + + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + const SUBNET_EVM_GENESIS_BYTES: &[u8] = &[ + 123, 34, 99, 111, 110, 102, 105, 103, 34, 58, 123, 34, 99, 104, 97, 105, 110, 73, 100, 34, + 58, 49, 51, 50, 49, 51, 44, 34, 102, 101, 101, 67, 111, 110, 102, 105, 103, 34, 58, 123, + 34, 103, 97, 115, 76, 105, 109, 105, 116, 34, 58, 56, 48, 48, 48, 48, 48, 48, 44, 34, 116, + 97, 114, 103, 101, 116, 66, 108, 111, 99, 107, 82, 97, 116, 101, 34, 58, 50, 44, 34, 109, + 105, 110, 66, 97, 115, 101, 70, 101, 101, 34, 58, 50, 53, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 44, 34, 116, 97, 114, 103, 101, 116, 71, 97, 115, 34, 58, 49, 53, 48, 48, 48, 48, 48, + 48, 44, 34, 98, 97, 115, 101, 70, 101, 101, 67, 104, 97, 110, 103, 101, 68, 101, 110, 111, + 109, 105, 110, 97, 116, 111, 114, 34, 58, 51, 54, 44, 34, 109, 105, 110, 66, 108, 111, 99, + 107, 71, 97, 115, 67, 111, 115, 116, 34, 58, 48, 44, 34, 109, 97, 120, 66, 108, 111, 99, + 107, 71, 97, 115, 67, 111, 115, 116, 34, 58, 49, 48, 48, 48, 48, 48, 48, 44, 34, 98, 108, + 111, 99, 107, 71, 97, 115, 67, 111, 115, 116, 83, 116, 101, 112, 34, 58, 50, 48, 48, 48, + 48, 48, 125, 44, 34, 104, 111, 109, 101, 115, 116, 101, 97, 100, 66, 108, 111, 99, 107, 34, + 58, 48, 44, 34, 101, 105, 112, 49, 53, 48, 66, 108, 111, 99, 107, 34, 58, 48, 44, 34, 101, + 105, 112, 49, 53, 48, 72, 97, 115, 104, 34, 58, 34, 48, 120, 50, 48, 56, 54, 55, 57, 57, + 97, 101, 101, 98, 101, 97, 101, 49, 51, 53, 99, 50, 52, 54, 99, 54, 53, 48, 50, 49, 99, 56, + 50, 98, 52, 101, 49, 53, 97, 50, 99, 52, 53, 49, 51, 52, 48, 57, 57, 51, 97, 97, 99, 102, + 100, 50, 55, 53, 49, 56, 56, 54, 53, 49, 52, 102, 48, 34, 44, 34, 101, 105, 112, 49, 53, + 53, 66, 108, 111, 99, 107, 34, 58, 48, 44, 34, 101, 105, 112, 49, 53, 56, 66, 108, 111, 99, + 107, 34, 58, 48, 44, 34, 98, 121, 122, 97, 110, 116, 105, 117, 109, 66, 108, 111, 99, 107, + 34, 58, 48, 44, 34, 99, 111, 110, 115, 116, 97, 110, 116, 105, 110, 111, 112, 108, 101, 66, + 108, 111, 99, 107, 34, 58, 48, 44, 34, 112, 101, 116, 101, 114, 115, 98, 117, 114, 103, 66, + 108, 111, 99, 107, 34, 58, 48, 44, 34, 105, 115, 116, 97, 110, 98, 117, 108, 66, 108, 111, + 99, 107, 34, 58, 48, 44, 34, 109, 117, 105, 114, 71, 108, 97, 99, 105, 101, 114, 66, 108, + 111, 99, 107, 34, 58, 48, 44, 34, 115, 117, 98, 110, 101, 116, 69, 86, 77, 84, 105, 109, + 101, 115, 116, 97, 109, 112, 34, 58, 48, 125, 44, 34, 110, 111, 110, 99, 101, 34, 58, 34, + 48, 120, 48, 34, 44, 34, 116, 105, 109, 101, 115, 116, 97, 109, 112, 34, 58, 34, 48, 120, + 48, 34, 44, 34, 101, 120, 116, 114, 97, 68, 97, 116, 97, 34, 58, 34, 48, 120, 48, 48, 34, + 44, 34, 103, 97, 115, 76, 105, 109, 105, 116, 34, 58, 34, 48, 120, 55, 97, 49, 50, 48, 48, + 34, 44, 34, 100, 105, 102, 102, 105, 99, 117, 108, 116, 121, 34, 58, 34, 48, 120, 48, 34, + 44, 34, 109, 105, 120, 72, 97, 115, 104, 34, 58, 34, 48, 120, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 34, 44, 34, 99, 111, 105, 110, 98, 97, 115, + 101, 34, 58, 34, 48, 120, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 34, 44, 34, 97, 108, 108, 111, 99, 34, 58, 123, 34, 56, 100, 98, 57, 55, 67, 55, 99, + 69, 99, 69, 50, 52, 57, 99, 50, 98, 57, 56, 98, 68, 67, 48, 50, 50, 54, 67, 99, 52, 67, 50, + 65, 53, 55, 66, 70, 53, 50, 70, 67, 34, 58, 123, 34, 98, 97, 108, 97, 110, 99, 101, 34, 58, + 34, 48, 120, 50, 57, 53, 98, 101, 57, 54, 101, 54, 52, 48, 54, 54, 57, 55, 50, 48, 48, 48, + 48, 48, 48, 34, 125, 125, 44, 34, 110, 117, 109, 98, 101, 114, 34, 58, 34, 48, 120, 48, 34, + 44, 34, 103, 97, 115, 85, 115, 101, 100, 34, 58, 34, 48, 120, 48, 34, 44, 34, 112, 97, 114, + 101, 110, 116, 72, 97, 115, 104, 34, 58, 34, 48, 120, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 34, 125, + ]; + + #[test] + fn test_encode_json_genesis() { + let genesis_str = fs::read_to_string("tests/genesis/subnet-evm.json").unwrap(); + + let genesis_bytes = encode_genesis_data(&genesis_str).unwrap(); + + assert_eq!(genesis_bytes, SUBNET_EVM_GENESIS_BYTES); + } +} diff --git a/crates/ash_sdk/src/avalanche/wallets.rs b/crates/ash_sdk/src/avalanche/wallets.rs index ff8a7e5..e8c11f5 100644 --- a/crates/ash_sdk/src/avalanche/wallets.rs +++ b/crates/ash_sdk/src/avalanche/wallets.rs @@ -108,8 +108,8 @@ impl AvalancheWallet { amount: u64, check_acceptance: bool, ) -> Result { - let receiver = address_to_short_id(to, "X"); - let tx_id = x::transfer(self, receiver, amount, check_acceptance).await?; + let receiver = address_to_short_id(to, "X")?; + let tx_id = x::transfer_avax(self, receiver, amount, check_acceptance).await?; Ok(tx_id) } @@ -148,7 +148,6 @@ pub fn generate_private_key() -> Result { mod tests { use super::*; use crate::avalanche::AvalancheNetwork; - use async_std; const AVAX_CB58_PRIVATE_KEY: &str = "PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN"; diff --git a/crates/ash_sdk/src/conf.rs b/crates/ash_sdk/src/conf.rs index 8abd084..1103faf 100644 --- a/crates/ash_sdk/src/conf.rs +++ b/crates/ash_sdk/src/conf.rs @@ -70,8 +70,11 @@ impl AshConfig { mod tests { use super::*; use crate::avalanche::{ - blockchains::AvalancheBlockchain, subnets::AvalancheSubnet, AVAX_PRIMARY_NETWORK_ID, + blockchains::AvalancheBlockchain, subnets::AvalancheSubnet, vms::AvalancheVmType, + AVAX_PRIMARY_NETWORK_ID, }; + use avalanche_types::ids::Id; + use std::str::FromStr; const AVAX_PCHAIN_ID: &str = AVAX_PRIMARY_NETWORK_ID; const AVAX_MAINNET_CCHAIN_ID: &str = "2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5"; @@ -101,7 +104,7 @@ mod tests { blockchains, .. } = &mainnet.subnets[0]; - assert_eq!(id.to_string(), AVAX_PRIMARY_NETWORK_ID); + assert_eq!(id, &mainnet.primary_network_id); assert_eq!(control_keys.len(), 0); assert_eq!(threshold, &0); assert_eq!(blockchains.len(), 3); @@ -114,10 +117,10 @@ mod tests { rpc_url, .. } = &blockchains[1]; - assert_eq!(id.to_string(), AVAX_MAINNET_CCHAIN_ID); + assert_eq!(id, &Id::from_str(AVAX_MAINNET_CCHAIN_ID).unwrap()); assert_eq!(name, "C-Chain"); - assert_eq!(vm_id.to_string(), AVAX_MAINNET_EVM_ID); - assert_eq!(vm_type, "EVM"); + assert_eq!(vm_id, &Id::from_str(AVAX_MAINNET_EVM_ID).unwrap()); + assert_eq!(vm_type, &AvalancheVmType::Coreth); assert_eq!(rpc_url, AVAX_MAINNET_CCHAIN_RPC); } @@ -145,7 +148,7 @@ mod tests { blockchains, .. } = &custom.subnets[0]; - assert_eq!(id.to_string(), AVAX_PRIMARY_NETWORK_ID); + assert_eq!(id, &custom.primary_network_id); assert_eq!(control_keys.len(), 0); assert_eq!(threshold, &0); assert_eq!(blockchains.len(), 3); @@ -157,9 +160,9 @@ mod tests { rpc_url, .. } = &blockchains[0]; - assert_eq!(id.to_string(), AVAX_PCHAIN_ID); + assert_eq!(id, &Id::from_str(AVAX_PCHAIN_ID).unwrap()); assert_eq!(name, "P-Chain"); - assert_eq!(vm_type, "PVM"); + assert_eq!(vm_type, &AvalancheVmType::PlatformVM); assert_eq!(rpc_url, "https://api.ash.center/ext/bc/P"); } diff --git a/crates/ash_sdk/src/errors.rs b/crates/ash_sdk/src/errors.rs index 7c32d77..2b9725d 100644 --- a/crates/ash_sdk/src/errors.rs +++ b/crates/ash_sdk/src/errors.rs @@ -6,7 +6,7 @@ use thiserror::Error; /// Ash library errors enum -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum AshError { #[error("Config error: {0}")] ConfigError(#[from] ConfigError), @@ -20,11 +20,11 @@ pub enum AshError { AvalancheBlockchainError(#[from] AvalancheBlockchainError), #[error("AvalancheWallet error: {0}")] AvalancheWalletError(#[from] AvalancheWalletError), - #[error("AshNode error: {0}")] - AshNodeError(#[from] AshNodeError), + #[error("Avalanche VM error: {0}")] + AvalancheVMError(#[from] AvalancheVMError), } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ConfigError { #[error("failed to build configuration: {0}")] BuildFailure(String), @@ -45,7 +45,7 @@ pub enum ConfigError { }, } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum RpcError { #[error("failed to get {data_type} for {target_type} '{target_value}': {msg}")] GetFailure { @@ -70,7 +70,7 @@ pub enum RpcError { Unknown(String), } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum AvalancheNetworkError { #[error("{target_type} '{target_value}' not found in network '{network}'")] NotFound { @@ -80,9 +80,11 @@ pub enum AvalancheNetworkError { }, #[error("{operation} is not allowed on network '{network}'")] OperationNotAllowed { operation: String, network: String }, + #[error("'{address}' is not a valid address: {msg}")] + InvalidAddress { address: String, msg: String }, } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum AvalancheSubnetError { #[error("{target_type} '{target_value}' not found in Subnet '{subnet_id}'")] NotFound { @@ -90,15 +92,21 @@ pub enum AvalancheSubnetError { target_type: String, target_value: String, }, + #[error("{operation} is not allowed on {subnet_type} Subnet '{subnet_id}'")] + OperationNotAllowed { + operation: String, + subnet_id: String, + subnet_type: String, + }, } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum AvalancheBlockchainError { #[error("failed to get ethers Provider for blockchain '{blockchain_id}': {msg}")] EthersProvider { blockchain_id: String, msg: String }, } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum AvalancheWalletError { #[error("failed to generate private key: {0}")] PrivateKeyGenerationFailure(String), @@ -114,8 +122,10 @@ pub enum AvalancheWalletError { }, } -#[derive(Error, Debug)] -pub enum AshNodeError { - #[error("'{id}' is not a valid node ID: {msg}")] - InvalidId { id: String, msg: String }, +#[derive(Error, Debug, PartialEq)] +pub enum AvalancheVMError { + #[error("unsupported VM '{0}'")] + UnsupportedVM(String), + #[error("failed to encode genesis data: {0}")] + GenesisEncoding(String), } diff --git a/crates/ash_sdk/src/lib.rs b/crates/ash_sdk/src/lib.rs index 09a4c66..af7313c 100644 --- a/crates/ash_sdk/src/lib.rs +++ b/crates/ash_sdk/src/lib.rs @@ -7,3 +7,5 @@ pub mod errors; #[macro_use] extern crate enum_display_derive; + +pub use avalanche_types::ids; diff --git a/crates/ash_sdk/tests/conf/avalanche-network-runner.yml b/crates/ash_sdk/tests/conf/avalanche-network-runner.yml index cf533a2..3b175fd 100644 --- a/crates/ash_sdk/tests/conf/avalanche-network-runner.yml +++ b/crates/ash_sdk/tests/conf/avalanche-network-runner.yml @@ -10,15 +10,15 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: http://127.0.0.1:9650/ext/bc/P - id: VctwH3nkmztWbkdNXbuo6eCYndsUuemtM9ZFmEUZ5QpA1Fu8G name: C-Chain - vmType: EVM + vmType: Coreth rpcUrl: http://127.0.0.1:9650/ext/bc/C/rpc - id: qzfF3A11KzpcHkkqznEyQgupQrCNS6WV6fTUTwZpEKqhj1QE7 name: X-Chain - vmType: AVM + vmType: AvalancheVM rpcUrl: http://127.0.0.1:9650/ext/bc/X - name: local-light subnets: @@ -28,5 +28,5 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: http://127.0.0.1:9650/ext/bc/P diff --git a/crates/ash_sdk/tests/conf/custom.yml b/crates/ash_sdk/tests/conf/custom.yml index 2bdedf5..c7c5021 100644 --- a/crates/ash_sdk/tests/conf/custom.yml +++ b/crates/ash_sdk/tests/conf/custom.yml @@ -10,13 +10,13 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://api.ash.center/ext/bc/P - id: yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp name: C-Chain - vmType: EVM + vmType: Coreth rpcUrl: https://api.ash.center/ext/bc/C/rpc - id: 2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm name: X-Chain - vmType: AVM + vmType: AvalancheVM rpcUrl: https://api.ash.center/ext/bc/X diff --git a/crates/ash_sdk/tests/conf/quicknode.yml b/crates/ash_sdk/tests/conf/quicknode.yml index cb8cfde..5bf2281 100644 --- a/crates/ash_sdk/tests/conf/quicknode.yml +++ b/crates/ash_sdk/tests/conf/quicknode.yml @@ -10,15 +10,15 @@ avalancheNetworks: blockchains: - id: 11111111111111111111111111111111LpoYY name: P-Chain - vmType: PVM + vmType: PlatformVM rpcUrl: https://${ASH_QUICKNODE_FUJI_ENDPOINT}.discover.quiknode.pro/${ASH_QUICKNODE_FUJI_TOKEN}/ext/bc/P - id: yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp name: C-Chain vmId: mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 - vmType: EVM + vmType: Coreth rpcUrl: https://${ASH_QUICKNODE_FUJI_ENDPOINT}.discover.quiknode.pro/${ASH_QUICKNODE_FUJI_TOKEN}/ext/bc/C/rpc - id: 2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm name: X-Chain vmId: jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq - vmType: AVM + vmType: AvalancheVM rpcUrl: https://${ASH_QUICKNODE_FUJI_ENDPOINT}.discover.quiknode.pro/${ASH_QUICKNODE_FUJI_TOKEN}/ext/bc/X diff --git a/crates/ash_sdk/tests/conf/wrong.yml b/crates/ash_sdk/tests/conf/wrong.yml index 5c08e13..01e5dcc 100644 --- a/crates/ash_sdk/tests/conf/wrong.yml +++ b/crates/ash_sdk/tests/conf/wrong.yml @@ -10,7 +10,7 @@ avalancheNetworks: blockchains: - id: 2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm name: MyChain - vmType: EVM + vmType: Coreth rpcUrl: https://api.ash.center/ext/bc/mychain/rpc - name: no-pchain subnets: @@ -19,5 +19,5 @@ avalancheNetworks: blockchains: - id: yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp name: C-Chain - vmType: EVM + vmType: Coreth rpcUrl: https://api.ash.center/ext/bc/C/rpc diff --git a/crates/ash_sdk/tests/genesis/subnet-evm.json b/crates/ash_sdk/tests/genesis/subnet-evm.json new file mode 100644 index 0000000..f8720bc --- /dev/null +++ b/crates/ash_sdk/tests/genesis/subnet-evm.json @@ -0,0 +1,41 @@ +{ + "config": { + "chainId": 13213, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "subnetEVMTimestamp": 0, + "feeConfig": { + "gasLimit": 8000000, + "minBaseFee": 25000000000, + "targetGas": 15000000, + "baseFeeChangeDenominator": 36, + "minBlockGasCost": 0, + "maxBlockGasCost": 1000000, + "targetBlockRate": 2, + "blockGasCostStep": 200000 + } + }, + "alloc": { + "8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC": { + "balance": "0x295BE96E64066972000000" + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "0x7A1200", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +}