ark-client 0.10.0

Main client library for interacting with Ark servers
Documentation
use crate::error::ErrorContext;
use crate::swap_storage::SwapStorage;
use crate::wallet::OnchainWallet;
use crate::Blockchain;
use crate::Client;
use crate::Error;
use ark_core::contract::SpendPathKind;
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>(
    client: &Client<B, W, S>,
    target_amount: Amount,
) -> Result<
    (
        Vec<unilateral_exit::OnChainInput>,
        Vec<unilateral_exit::VtxoInput>,
    ),
    Error,
>
where
    B: Blockchain,
    W: OnchainWallet,
    S: SwapStorage + 'static,
{
    let boarding_outputs = client.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");

                    let script_pubkey = boarding_output.script_pubkey();
                    let spend_selection = boarding_output.spend_selection(SpendPathKind::Exit)?;

                    if selected_boarding_outputs.insert(
                        unilateral_exit::OnChainInput::new_with_spend_selection(
                            boarding_output.exit_delay(),
                            script_pubkey,
                            spend_selection,
                            *amount,
                            *outpoint,
                        ),
                    ) {
                        selected_amount += *amount;
                    }
                }
            }
        }
    }

    let mut selected_vtxo_outputs = HashSet::new();

    for contract in client.active_offchain_contracts()? {
        let vtxo = &contract.vtxo;
        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");

                    let script_pubkey = vtxo.script_pubkey();
                    let spend_selection = contract.spend_selection(SpendPathKind::Exit)?;

                    selected_vtxo_outputs.insert(
                        unilateral_exit::VtxoInput::new_with_spend_selection(
                            *outpoint,
                            vtxo.exit_delay(),
                            TxOut {
                                value: *amount,
                                script_pubkey,
                            },
                            spend_selection,
                        ),
                    );
                    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(),
    ))
}