use super::{Action, Decision, Message, funding_deadline};
use crate::{
driver::{
config::{ChainConfig, Config},
observe::{Observed, Output, Spender},
record::{CancelReason, Fund, FundState, Status, SwapRecord},
},
swap_script::{SpendPath, Terms},
utxo,
};
pub(super) fn decide(
record: &SwapRecord,
terms: &Terms,
observed: &Observed,
config: &Config,
chain_config: &ChainConfig,
) -> Decision {
let (opening, _strays) = observed.split(record.send_amount);
match (record.source.server_privkey, record.preimage, opening) {
(Some(_), _, opening) => {
let cancel = record.server_cancel.clone().unwrap_or_default();
canceled(opening, CancelReason::ServerCanceled(cancel))
}
(None, Some(_), Some(opening)) => completed(record, opening),
(None, _, Some(opening)) if observed.confs(opening.height) >= terms.csv => {
canceled(Some(opening), CancelReason::CsvMatured)
}
(None, _, Some(opening)) => confirming(observed, chain_config, opening),
(None, _, None) => funding(record, observed, config),
}
}
fn funding(record: &SwapRecord, observed: &Observed, config: &Config) -> Decision {
let expires_at = funding_deadline(record.started_at, None, config);
let send_cancel = observed.now >= expires_at && !record.sent.cancel;
Decision {
status: Status::Funding {
expires_at,
cancel_sent: record.sent.cancel || send_cancel,
},
action: send_cancel.then_some(Action::Send(Message::Cancel)),
}
}
fn confirming(observed: &Observed, chain_config: &ChainConfig, opening: &Output) -> Decision {
Decision {
status: Status::Confirming {
txid: opening.utxo.txid.clone(),
confs: observed.confs(opening.height),
needed: chain_config.min_confs,
},
action: None,
}
}
fn completed(record: &SwapRecord, opening: &Output) -> Decision {
Decision {
status: Status::Completed {
opening: opening.utxo.txid.clone(),
payout: opening.spend.as_ref().map(|spend| spend.txid.clone()),
fee: None,
},
action: (!record.sent.refund_key).then_some(Action::Send(Message::RefundKey)),
}
}
fn canceled(opening: Option<&Output>, reason: CancelReason) -> Decision {
Decision {
status: Status::Canceled {
opening: opening.map(|opening| opening.utxo.txid.clone()),
reason,
},
action: None,
}
}
pub(super) fn funds(
record: &SwapRecord,
terms: &Terms,
observed: &Observed,
chain_config: &ChainConfig,
status: &Status,
) -> Vec<Fund> {
let (opening, strays) = observed.split(record.send_amount);
let ours = match status {
Status::Canceled { .. } => strays.into_iter().chain(opening).collect(),
_ => strays,
};
let fee_rate = observed.fee_rate.min(chain_config.max_fee_rate);
let refund_fee = utxo::refund_fee(terms.chain, fee_rate);
ours.into_iter()
.filter_map(|output| {
let state = state(record, terms, observed, refund_fee, output)?;
Some(Fund {
outpoint: output.utxo.outpoint(),
amount: output.value,
state,
})
})
.collect()
}
fn state(
record: &SwapRecord,
terms: &Terms,
observed: &Observed,
refund_fee: Option<u64>,
output: &Output,
) -> Option<FundState> {
Some(match &output.spend {
Some(spend) if !our_refund(record, spend) => return None,
Some(spend) if spend.height.is_none() => FundState::Refunding {
txid: spend.txid.clone(),
fee: spend.fee,
},
Some(spend) => FundState::Refunded {
txid: spend.txid.clone(),
fee: spend.fee,
},
None if record.locked(terms.csv, observed.confs(output.height)) => FundState::Locked {
at: output.height.map(|height| height + terms.csv),
},
None if refund_fee.is_none_or(|fee| output.value <= fee) => FundState::Dust,
None => FundState::Refundable,
})
}
fn our_refund(record: &SwapRecord, spend: &Spender) -> bool {
match spend.path {
SpendPath::Csv => true,
SpendPath::Keypath => !record.sent.refund_key,
SpendPath::Preimage(_) => false,
}
}