use crate::script::csv_sig_script;
use crate::script::multisig_script;
use crate::script::tr_script_pubkey;
use crate::Error;
use crate::ExitDelayKind;
use crate::ExplorerUtxo;
use crate::UNSPENDABLE_KEY;
use bitcoin::key::PublicKey;
use bitcoin::key::Secp256k1;
use bitcoin::key::Verification;
use bitcoin::taproot;
use bitcoin::taproot::LeafVersion;
use bitcoin::taproot::TaprootBuilder;
use bitcoin::taproot::TaprootSpendInfo;
use bitcoin::Address;
use bitcoin::Amount;
use bitcoin::Network;
use bitcoin::OutPoint;
use bitcoin::ScriptBuf;
use bitcoin::XOnlyPublicKey;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BoardingOutput {
server: XOnlyPublicKey,
owner: XOnlyPublicKey,
spend_info: TaprootSpendInfo,
address: Address,
exit_delay: bitcoin::Sequence,
exit_delay_kind: ExitDelayKind,
}
impl BoardingOutput {
pub fn new<C>(
secp: &Secp256k1<C>,
server: XOnlyPublicKey,
owner: XOnlyPublicKey,
exit_delay: bitcoin::Sequence,
network: Network,
) -> Result<Self, Error>
where
C: Verification,
{
let unspendable_key: PublicKey = UNSPENDABLE_KEY
.parse()
.map_err(|e| Error::ad_hoc(format!("invalid unspendable key: {e}")))?;
let (unspendable_key, _) = unspendable_key.inner.x_only_public_key();
let multisig_script = multisig_script(server, owner);
let exit_script = csv_sig_script(exit_delay, owner);
let spend_info = TaprootBuilder::new()
.add_leaf(1, multisig_script)
.map_err(|e| Error::ad_hoc(format!("invalid multisig leaf: {e}")))?
.add_leaf(1, exit_script)
.map_err(|e| Error::ad_hoc(format!("invalid exit leaf: {e}")))?
.finalize(secp, unspendable_key)
.map_err(|_| Error::ad_hoc("failed to finalize taproot builder"))?;
let exit_delay_kind = ExitDelayKind::from_sequence(exit_delay)?;
let script_pubkey = tr_script_pubkey(&spend_info);
let address = Address::from_script(&script_pubkey, network)
.map_err(|e| Error::ad_hoc(format!("invalid script: {e}")))?;
Ok(Self {
server,
owner,
spend_info,
address,
exit_delay,
exit_delay_kind,
})
}
pub fn address(&self) -> &Address {
&self.address
}
pub fn owner_pk(&self) -> XOnlyPublicKey {
self.owner
}
pub fn server_pk(&self) -> XOnlyPublicKey {
self.server
}
pub fn script_pubkey(&self) -> ScriptBuf {
self.address.script_pubkey()
}
pub fn forfeit_spend_info(&self) -> (ScriptBuf, taproot::ControlBlock) {
let forfeit_script = self.forfeit_script();
let control_block = self
.spend_info
.control_block(&(forfeit_script.clone(), LeafVersion::TapScript))
.expect("forfeit script");
(forfeit_script, control_block)
}
pub fn exit_spend_info(&self) -> (ScriptBuf, taproot::ControlBlock) {
let exit_script = self.exit_script();
let control_block = self
.spend_info
.control_block(&(exit_script.clone(), LeafVersion::TapScript))
.expect("exit script");
(exit_script, control_block)
}
pub fn exit_delay(&self) -> bitcoin::Sequence {
self.exit_delay
}
pub fn output_key(&self) -> bitcoin::key::TweakedPublicKey {
self.spend_info.output_key()
}
pub fn to_ark_address(&self, network: Network, server: XOnlyPublicKey) -> crate::ArkAddress {
crate::ArkAddress::new(network, server, self.output_key())
}
pub fn tapscripts(&self) -> Vec<ScriptBuf> {
let (exit_script, _) = self.exit_spend_info();
let (forfeit_script, _) = self.forfeit_spend_info();
vec![exit_script, forfeit_script]
}
pub fn can_be_claimed_unilaterally_by_owner(
&self,
now: Duration,
confirmation_blocktime: Duration,
confirmations: u64,
) -> bool {
match self.exit_delay_kind {
ExitDelayKind::Time(seconds) => {
let exit_path_time = confirmation_blocktime + seconds;
now > exit_path_time
}
ExitDelayKind::Blocks(confirmations_required) => {
confirmations >= confirmations_required
}
}
}
fn forfeit_script(&self) -> ScriptBuf {
multisig_script(self.server, self.owner)
}
fn exit_script(&self) -> ScriptBuf {
csv_sig_script(self.exit_delay, self.owner)
}
}
#[derive(Debug, Clone, Default)]
pub struct BoardingOutpoints {
pub spendable: Vec<(OutPoint, Amount, BoardingOutput)>,
pub expired: Vec<(OutPoint, Amount, BoardingOutput)>,
pub pending: Vec<(OutPoint, Amount, BoardingOutput)>,
pub spent: Vec<(OutPoint, Amount)>,
}
impl BoardingOutpoints {
pub fn spendable_balance(&self) -> Amount {
self.spendable.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
}
pub fn expired_balance(&self) -> Amount {
self.expired.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
}
pub fn pending_balance(&self) -> Amount {
self.pending.iter().fold(Amount::ZERO, |acc, x| acc + x.1)
}
}
pub fn list_boarding_outpoints<F>(
find_outpoints_fn: F,
boarding_outputs: &[BoardingOutput],
) -> Result<BoardingOutpoints, Error>
where
F: Fn(&Address) -> Result<Vec<ExplorerUtxo>, Error>,
{
let mut spendable = Vec::new();
let mut expired = Vec::new();
let mut pending = Vec::new();
let mut spent = Vec::new();
for boarding_output in boarding_outputs.iter() {
let boarding_address = boarding_output.address();
let boarding_utxos = find_outpoints_fn(boarding_address)?;
for boarding_utxo in boarding_utxos.iter() {
match *boarding_utxo {
ExplorerUtxo {
confirmation_blocktime: Some(confirmation_blocktime),
confirmations,
outpoint,
amount,
is_spent: false,
} => {
let now = std::time::UNIX_EPOCH.elapsed().map_err(Error::ad_hoc)?;
if boarding_output.can_be_claimed_unilaterally_by_owner(
now,
Duration::from_secs(confirmation_blocktime),
confirmations,
) {
expired.push((outpoint, amount, boarding_output.clone()));
}
else {
spendable.push((outpoint, amount, boarding_output.clone()));
}
}
ExplorerUtxo {
confirmation_blocktime: None,
outpoint,
amount,
is_spent: false,
..
} => {
pending.push((outpoint, amount, boarding_output.clone()));
}
ExplorerUtxo {
outpoint,
amount,
is_spent: true,
..
} => spent.push((outpoint, amount)),
}
}
}
Ok(BoardingOutpoints {
spendable,
expired,
pending,
spent,
})
}