ark-client 0.9.0

Main client library for interacting with Ark servers
Documentation
use crate::error::ErrorContext;
use crate::swap_storage::SwapStorage;
use crate::wallet::BoardingWallet;
use crate::wallet::OnchainWallet;
use crate::Blockchain;
use crate::Client;
use crate::Error;
use ark_core::unilateral_exit;
use ark_core::ExplorerUtxo;
use bitcoin::Amount;
use bitcoin::TxOut;
use jiff::Timestamp;
use std::collections::HashSet;
use std::time::Duration;

/// Select boarding outputs and VTXOs to be used as inputs in on-chain transactions, exiting the Ark
/// ecosystem.
///
/// This function prioritizes boarding outputs over VTXOs. That is, we may not select any VTXOs if
/// the `target_amount` is covered using only boarding outputs.
///
/// TODO: We should use a coin selection algorithm that takes into account fees e.g.
/// https://github.com/bitcoindevkit/coin-select.
///
/// TODO: Part of this logic needs to be extracted into `ark-core`.
pub async fn coin_select_for_onchain<B, W, S, K>(
    client: &Client<B, W, S, K>,
    target_amount: Amount,
) -> Result<
    (
        Vec<unilateral_exit::OnChainInput>,
        Vec<unilateral_exit::VtxoInput>,
    ),
    Error,
>
where
    B: Blockchain,
    W: BoardingWallet + OnchainWallet,
    S: SwapStorage + 'static,
    K: crate::KeyProvider,
{
    let boarding_outputs = client.inner.wallet.get_boarding_outputs()?;

    let now = Timestamp::now();

    let mut selected_boarding_outputs = HashSet::new();
    let mut selected_amount = Amount::ZERO;

    for boarding_output in boarding_outputs.iter() {
        if target_amount <= selected_amount {
            return Ok((selected_boarding_outputs.into_iter().collect(), Vec::new()));
        }

        let outpoints = client
            .blockchain()
            .find_outpoints(boarding_output.address())
            .await?;

        for o in outpoints.iter() {
            // Find outpoints for each boarding output.
            if let ExplorerUtxo {
                outpoint,
                amount,
                confirmation_blocktime: Some(confirmation_blocktime),
                confirmations,
                is_spent: false,
            } = o
            {
                // For each confirmed outpoint, check if they can already be spent unilaterally
                // using the exit path.
                if boarding_output.can_be_claimed_unilaterally_by_owner(
                    now.as_duration().try_into().context("invalid now")?,
                    Duration::from_secs(*confirmation_blocktime),
                    *confirmations,
                ) {
                    tracing::debug!(?outpoint, %amount, ?boarding_output, "Selected boarding output");

                    if selected_boarding_outputs.insert(unilateral_exit::OnChainInput::new(
                        boarding_output.clone(),
                        *amount,
                        *outpoint,
                    )) {
                        selected_amount += *amount;
                    }
                }
            }
        }
    }

    let mut selected_vtxo_outputs = HashSet::new();

    for (_, vtxo) in client.get_offchain_addresses()? {
        if target_amount <= selected_amount {
            return Ok((
                selected_boarding_outputs.into_iter().collect(),
                selected_vtxo_outputs.into_iter().collect(),
            ));
        }

        let outpoints = client.blockchain().find_outpoints(vtxo.address()).await?;
        for o in outpoints.iter() {
            // Find outpoints for each VTXO.
            if let ExplorerUtxo {
                outpoint,
                amount,
                confirmation_blocktime: Some(confirmation_blocktime),
                confirmations,
                is_spent: false,
            } = o
            {
                // For each confirmed outpoint, check if they can already be spent unilaterally
                // using the exit path.
                if vtxo.can_be_claimed_unilaterally_by_owner(
                    now.as_duration().try_into().map_err(Error::ad_hoc)?,
                    Duration::from_secs(*confirmation_blocktime),
                    *confirmations,
                ) {
                    tracing::debug!(?outpoint, %amount, ?vtxo, "Selected VTXO");

                    selected_vtxo_outputs.insert(unilateral_exit::VtxoInput::new(
                        *outpoint,
                        vtxo.exit_delay(),
                        TxOut {
                            value: *amount,
                            script_pubkey: vtxo.script_pubkey(),
                        },
                        vtxo.exit_spend_info()?,
                    ));
                    selected_amount += *amount;
                }
            }
        }
    }

    if selected_amount < target_amount {
        return Err(Error::coin_select(format!(
            "insufficient funds: selected = {selected_amount}, needed = {target_amount}"
        )));
    }

    Ok((
        selected_boarding_outputs.into_iter().collect(),
        selected_vtxo_outputs.into_iter().collect(),
    ))
}