avalanche-sdk 0.73.1

Avalanche API/SDK
Documentation
pub mod evm;
pub mod p;
pub mod x;

use std::{
    fmt,
    io::{self, Error, ErrorKind},
    sync::{Arc, Mutex},
};

use crate::{info as api_info, x as api_x};
use avalanche_types::{
    ids::{self, short},
    key, units,
};
use ethers::prelude::*;
use ethers_providers::{Middleware, Provider};

#[derive(Debug, Clone)]
pub struct Builder<T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone> {
    pub key: T,

    pub http_rpcs: Vec<String>,
    pub subnet_evm_blockchain_id: Option<String>,
}

impl<T> Builder<T>
where
    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
{
    pub fn new(key: &T) -> Self {
        Self {
            http_rpcs: Vec::new(),
            key: key.clone(),
            subnet_evm_blockchain_id: None,
        }
    }

    /// Adds an HTTP rpc endpoint to the `http_rpcs` field in the Builder.
    #[must_use]
    pub fn http_rpc(mut self, http_rpc: String) -> Self {
        if self.http_rpcs.is_empty() {
            self.http_rpcs = vec![http_rpc];
        } else {
            self.http_rpcs.push(http_rpc);
        }
        self
    }

    /// Overwrites the HTTP rpc endpoints to the `http_rpcs` field in the Builder.
    #[must_use]
    pub fn http_rpcs(mut self, http_rpcs: Vec<String>) -> Self {
        self.http_rpcs = http_rpcs;
        self
    }

    /// Sets the `subnet_evm_blockchain_id` field in the Builder to the provided value.
    #[must_use]
    pub fn subnet_evm_blockchain_id(mut self, id: String) -> Self {
        self.subnet_evm_blockchain_id = Some(id);
        self
    }

    pub async fn build(&self) -> io::Result<Wallet<T>> {
        log::info!("building wallet with {} endpoints", self.http_rpcs.len());

        let keychain = key::secp256k1::keychain::Keychain::new(vec![self.key.clone()]);
        let ethers_signing_key = keychain.keys[0].ethers_signing_key()?;
        let local_wallet: LocalWallet = ethers_signing_key.clone().into();

        let resp = api_info::get_network_id(&self.http_rpcs[0]).await?;
        let network_id = resp
            .result
            .expect("unexpected None GetNetworkIdResponse")
            .network_id;
        let resp = api_info::get_network_name(&self.http_rpcs[0]).await?;
        let network_name = resp
            .result
            .expect("unexpected None GetNetworkNameResponse")
            .network_name;

        let c_url_path = String::from("/ext/bc/C/rpc");
        let mut c_providers = Vec::new();
        for ep in self.http_rpcs.iter() {
            let c_provider =
                Provider::<Http>::try_from(ep.clone() + c_url_path.as_str()).map_err(|e| {
                    Error::new(
                        ErrorKind::Other,
                        format!("failed to create provider '{}'", e),
                    )
                })?;
            c_providers.push(c_provider);
        }
        let c_chain_id_u256 = c_providers[0].get_chainid().await.map_err(|e| {
            Error::new(
                ErrorKind::Other,
                format!("failed to get chainId for C-chain '{}'", e),
            )
        })?;

        let (subnet_evm_url_path, subnet_evm_providers, subnet_evm_chain_id_u256) =
            if let Some(sv) = &self.subnet_evm_blockchain_id {
                let subnet_evm_url_path = format!("/ext/bc/{}/rpc", sv).to_string();

                let mut subnet_evm_providers = Vec::new();
                for ep in self.http_rpcs.iter() {
                    let subnet_evm_provider =
                        Provider::<Http>::try_from(ep.clone() + subnet_evm_url_path.as_str())
                            .map_err(|e| {
                                Error::new(
                                    ErrorKind::Other,
                                    format!("failed to create provider '{}'", e),
                                )
                            })?;
                    subnet_evm_providers.push(subnet_evm_provider);
                }
                let subnet_evm_chain_id_u256 =
                    subnet_evm_providers[0].get_chainid().await.map_err(|e| {
                        Error::new(
                            ErrorKind::Other,
                            format!("failed to get chainId for subnet-evm '{}'", e),
                        )
                    })?;
                (
                    Some(subnet_evm_url_path),
                    Some(subnet_evm_providers),
                    Some(subnet_evm_chain_id_u256),
                )
            } else {
                (None, None, None)
            };

        let resp = api_info::get_blockchain_id(&self.http_rpcs[0], "X").await?;
        let x_chain_id = resp
            .result
            .expect("unexpected None GetBlockchainIdResponse")
            .blockchain_id;

        let resp = api_info::get_blockchain_id(&self.http_rpcs[0], "P").await?;
        let p_chain_id = resp
            .result
            .expect("unexpected None GetBlockchainIdResponse")
            .blockchain_id;

        let resp = api_info::get_blockchain_id(&self.http_rpcs[0], "C").await?;
        let c_chain_id = resp
            .result
            .expect("unexpected None GetBlockchainIdResponse")
            .blockchain_id;

        let resp = api_x::get_asset_description(&self.http_rpcs[0], "AVAX").await?;
        let resp = resp
            .result
            .expect("unexpected None GetAssetDescriptionResult");
        let avax_asset_id = resp.asset_id;

        let resp = api_info::get_tx_fee(&self.http_rpcs[0]).await?;
        let tx_fee = resp.result.unwrap().tx_fee;

        let (create_subnet_tx_fee, create_blockchain_tx_fee) = if network_id == 1 {
            // ref. "genesi/genesis_mainnet.go"
            (1 * units::AVAX, 1 * units::AVAX)
        } else {
            // ref. "genesi/genesis_fuji.go"
            // ref. "genesi/genesis_local.go"
            (100 * units::MILLI_AVAX, 100 * units::MILLI_AVAX)
        };

        let h160_address = keychain.keys[0].get_h160_address();
        let w = Wallet {
            keychain,
            ethers_signing_key,
            local_wallet,

            http_rpcs: self.http_rpcs.clone(),
            http_rpc_idx: Arc::new(Mutex::new(0)),

            network_id,
            network_name,

            c_url_path,
            subnet_evm_url_path,

            c_providers,
            subnet_evm_providers,

            h160_address,
            x_address: self.key.get_address(network_id, "X").unwrap(),
            p_address: self.key.get_address(network_id, "P").unwrap(),
            c_address: self.key.get_address(network_id, "C").unwrap(),
            short_address: self.key.get_short_address().unwrap(),
            eth_address: self.key.get_eth_address(),

            x_chain_id,
            p_chain_id,
            c_chain_id,
            c_chain_id_u256,
            subnet_evm_chain_id_u256,

            avax_asset_id,

            tx_fee,
            add_primary_network_validator_fee: ADD_PRIMARY_NETWORK_VALIDATOR_FEE,
            create_subnet_tx_fee,
            create_blockchain_tx_fee,
        };
        log::info!("initiated the wallet:\n{}", w);

        Ok(w)
    }
}

// ref. https://docs.avax.network/learn/platform-overview/transaction-fees/#fee-schedule
pub const ADD_PRIMARY_NETWORK_VALIDATOR_FEE: u64 = 0;

#[derive(Debug, Clone)]
pub struct Wallet<T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone> {
    pub keychain: key::secp256k1::keychain::Keychain<T>,
    pub ethers_signing_key: ethers_core::k256::ecdsa::SigningKey,
    pub local_wallet: LocalWallet,

    pub http_rpcs: Vec<String>,
    pub http_rpc_idx: Arc<Mutex<usize>>,

    pub network_id: u32,
    pub network_name: String,

    pub c_url_path: String,
    pub subnet_evm_url_path: Option<String>,

    /// C-chain providers for each HTTP RPC endpoint in the same order.
    pub c_providers: Vec<Provider<Http>>,
    /// subnet-evm providers for each HTTP RPC endpoint in the same order.
    pub subnet_evm_providers: Option<Vec<Provider<Http>>>,

    pub h160_address: H160,
    pub x_address: String,
    pub p_address: String,
    pub c_address: String,
    pub short_address: short::Id,
    pub eth_address: String,

    pub x_chain_id: ids::Id,
    pub p_chain_id: ids::Id,
    pub c_chain_id: ids::Id,
    pub c_chain_id_u256: U256,
    pub subnet_evm_chain_id_u256: Option<U256>,

    pub avax_asset_id: ids::Id,

    /// Fee that is burned by every non-state creating transaction.
    pub tx_fee: u64,
    /// Transaction fee for adding a primary network validator.
    pub add_primary_network_validator_fee: u64,
    /// Transaction fee to create a new subnet.
    pub create_subnet_tx_fee: u64,
    /// Transaction fee to create a new blockchain.
    pub create_blockchain_tx_fee: u64,
}

/// ref. https://doc.rust-lang.org/std/string/trait.ToString.html
/// ref. https://doc.rust-lang.org/std/fmt/trait.Display.html
/// Use "Self.to_string()" to directly invoke this
impl<T> fmt::Display for Wallet<T>
where
    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "http_rpcs: {:?}\n", self.http_rpcs)?;
        write!(f, "network_id: {}\n", self.network_id)?;
        write!(f, "network_name: {}\n", self.network_name)?;

        write!(f, "c_url_path: {}\n", self.c_url_path)?;
        if let Some(v) = &self.subnet_evm_url_path {
            write!(f, "subnet_evm_url_path: {}\n", v)?;
        } else {
            write!(f, "subnet_evm_url_path: None\n")?;
        }

        write!(f, "h160_address: {}\n", self.h160_address)?;
        write!(f, "x_address: {}\n", self.x_address)?;
        write!(f, "p_address: {}\n", self.p_address)?;
        write!(f, "c_address: {}\n", self.c_address)?;
        write!(f, "short_address: {}\n", self.short_address)?;
        write!(f, "eth_address: {}\n", self.eth_address)?;
        write!(f, "x_chain_id: {}\n", self.x_chain_id)?;
        write!(f, "p_chain_id: {}\n", self.p_chain_id)?;

        write!(f, "c_chain_id: {}\n", self.c_chain_id)?;
        write!(f, "c_chain_id_u256: {}\n", self.c_chain_id_u256)?;
        if let Some(v) = self.subnet_evm_chain_id_u256 {
            write!(f, "subnet_evm_chain_id_u256: {}\n", v)?;
        } else {
            write!(f, "subnet_evm_chain_id_u256: None\n")?;
        }

        write!(f, "avax_asset_id: {}\n", self.avax_asset_id)?;

        write!(f, "tx_fee: {}\n", self.tx_fee)?;
        write!(
            f,
            "add_primary_network_validator_fee: {}\n",
            self.add_primary_network_validator_fee
        )?;
        write!(f, "create_subnet_tx_fee: {}\n", self.create_subnet_tx_fee)?;
        write!(
            f,
            "create_blockchain_tx_fee: {}\n",
            self.create_blockchain_tx_fee
        )
    }
}

impl<T> Wallet<T>
where
    T: key::secp256k1::ReadOnly + key::secp256k1::SignOnly + Clone,
{
    /// Picks one endpoint in roundrobin, and updates the cursor for next calls.
    /// Returns the pair of an index and its corresponding endpoint.
    pub fn pick_http_rpc(&self) -> (usize, String) {
        let mut idx = self.http_rpc_idx.lock().unwrap();

        let picked = *idx;
        let http_rpc = self.http_rpcs[picked].clone();
        *idx = (picked + 1) % self.http_rpcs.len();

        log::debug!("picked http rpc {} at index {}", http_rpc, picked);
        (picked, http_rpc)
    }

    #[must_use]
    pub fn x(&self) -> x::X<T> {
        x::X {
            inner: self.clone(),
        }
    }

    #[must_use]
    pub fn p(&self) -> p::P<T> {
        p::P {
            inner: self.clone(),
        }
    }

    #[must_use]
    pub fn c(&self) -> evm::c::C<T> {
        evm::c::C {
            inner: self.clone(),
        }
    }

    #[must_use]
    pub fn subnet_evm(&self) -> evm::subnet_evm::SubnetEvm<T> {
        assert!(
            self.subnet_evm_url_path.is_some(),
            "subnet-evm URL path is None"
        );
        assert!(
            self.subnet_evm_providers.is_some(),
            "subnet-evm providers is None"
        );
        assert!(
            !self.subnet_evm_providers.clone().unwrap().is_empty(),
            "subnet-evm providers are empty"
        );
        assert!(
            self.subnet_evm_chain_id_u256.is_some(),
            "subnet-evm chain Id is None"
        );

        evm::subnet_evm::SubnetEvm {
            inner: self.clone(),
        }
    }
}