avalanche-sdk 0.43.0

Avalanche API/SDK
Documentation
use std::{
    cmp,
    io::{self, Error, ErrorKind},
    time::{Duration, SystemTime},
};

use crate::x as api_x;
use avalanche_types::{
    avax, avm, choices::status::Status, formatting, ids::short, key, secp256k1fx,
};
use tokio::time::sleep;

impl<T> crate::wallet::Wallet<T>
where
    T: key::ReadOnly + key::SignOnly + Clone,
{
    pub async fn transfer_x_avax(
        &self,
        http_rpc: Option<String>,
        receiver_short_addr: short::Id,
        amount_to_transfer: u64,
        check_acceptance: bool,
    ) -> io::Result<()> {
        let http_rpc_ep = if let Some(ep) = &http_rpc {
            ep.to_string()
        } else {
            self.http_rpc.clone()
        };
        let sender_short_addr = self.keychain.keys[0].get_short_address();
        log::info!(
            "transferring {} from {} to {} via {}",
            amount_to_transfer,
            sender_short_addr,
            receiver_short_addr,
            http_rpc_ep
        );

        let sender_x_utxos = api_x::get_utxos(&http_rpc_ep, &self.x_address).await?;
        let sender_x_utxos_result = sender_x_utxos.result.unwrap();
        let sender_x_utxos = sender_x_utxos_result.utxos.unwrap();
        log::debug!(
            "fetched UTXOs for inputs: numFetched {:?}, endIndex {:?} and {} UTXOs",
            sender_x_utxos_result.num_fetched,
            sender_x_utxos_result.end_index,
            sender_x_utxos.len()
        );
        // TODO: paginate next results

        let mut inputs: Vec<avax::TransferableInput> = Vec::new();
        let mut outputs: Vec<avax::TransferableOutput> = vec![
            // receiver
            avax::TransferableOutput {
                asset_id: self.avax_asset_id.clone(),
                transfer_output: Some(secp256k1fx::TransferOutput {
                    amount: amount_to_transfer,
                    output_owners: secp256k1fx::OutputOwners {
                        locktime: 0,
                        threshold: 1,
                        addrs: vec![receiver_short_addr],
                    },
                }),
                ..Default::default()
            },
        ];

        // ref. "avalanchego/wallet/chain/x"
        // "math.Add64(toBurn[assetID], out.Out.Amount())"
        let mut remaining_amount_to_burn = amount_to_transfer + self.tx_fee;

        // ref. "avalanchego/vms/avm#Service.SendMultiple"
        let now_unix = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .expect("unexpected None duration_since")
            .as_secs();

        for utxo in sender_x_utxos.iter() {
            if utxo.asset_id != self.avax_asset_id {
                continue;
            }

            // consumed enough, no need to burn more
            if remaining_amount_to_burn == 0 {
                continue;
            }

            if let Some(out) = &utxo.transfer_output {
                let (input, _) = self.keychain.spend(out, now_unix).unwrap();

                inputs.push(avax::TransferableInput {
                    utxo_id: utxo.utxo_id.clone(),
                    asset_id: utxo.asset_id.clone(),
                    transfer_input: Some(input.clone()),
                    ..Default::default()
                });

                // burn any value that should be burned
                let amount_to_burn = cmp::min(
                    remaining_amount_to_burn, // amount we still need to burn
                    out.amount,               // amount available to burn
                );
                remaining_amount_to_burn -= amount_to_burn;

                let remaining_amount = out.amount - amount_to_burn;
                if remaining_amount > 0 {
                    // this input had extra value, so some must be returned
                    outputs.push(avax::TransferableOutput {
                        asset_id: self.avax_asset_id.clone(),
                        transfer_output: Some(secp256k1fx::TransferOutput {
                            amount: remaining_amount,
                            output_owners: secp256k1fx::OutputOwners {
                                locktime: 0,
                                threshold: 1,
                                addrs: vec![sender_short_addr.clone()],
                            },
                        }),
                        ..Default::default()
                    })
                }
            }
        }
        inputs.sort();
        outputs.sort();

        log::debug!(
            "baseTx has {} inputs and {} outputs",
            inputs.len(),
            outputs.len()
        );
        let base_tx = avax::BaseTx {
            network_id: self.network_id,
            blockchain_id: self.x_chain_id.clone(),
            transferable_outputs: Some(outputs),
            transferable_inputs: Some(inputs.clone()),
            ..Default::default()
        };

        // make sure it does not incur "tx has 1 credentials but 2 inputs. Should be same" error
        let mut signers: Vec<Vec<T>> = Vec::new();
        for _ in 0..inputs.len() {
            signers.push(vec![self.keychain.keys[0].clone()]);
        }
        if inputs.len() > 1 {
            log::debug!("signing for multiple inputs ({} inputs)", inputs.len());
        }

        let mut tx = avm::tx::Tx::new(base_tx);
        tx.sign(signers).unwrap();

        let signed_bytes = tx.unsigned_tx.metadata.clone().unwrap().bytes;
        let hex_tx = formatting::encode_hex_with_checksum(&signed_bytes);
        let resp = api_x::issue_tx(&http_rpc_ep, &hex_tx).await?;

        if resp.result.is_none() {
            return Err(Error::new(
                ErrorKind::Other,
                format!("failed to issue tx {:?}", resp.error),
            ));
        }

        let tx_id = resp.result.unwrap().tx_id.to_string();
        log::info!("{} successfully issued", tx_id);

        if !check_acceptance {
            log::debug!("skipping checking acceptance...");
            return Ok(());
        }

        // enough time for txs processing
        sleep(Duration::from_millis(700)).await;

        loop {
            let resp = api_x::get_tx_status(&http_rpc_ep, &tx_id).await?;

            let status = resp.result.unwrap().status;
            if status == Status::Accepted {
                log::info!("{} successfully accepted", tx_id);
                break;
            }

            log::warn!("{} {} (not accepted yet in {})", tx_id, status, http_rpc_ep);
            sleep(Duration::from_secs(1)).await;
        }

        Ok(())
    }
}