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
+ [](https://crates.io/crates/ash_sdk)
+ [](https://docs.rs/ash_sdk)
+- [ash_cli](crates/ash_cli): Ash CLI
+ [](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"
+}