use super::{Action, Decision, Message, Path, Spend, hold_invoice_deadline};
use crate::{
driver::{
config::ChainConfig,
observe::{Observed, Output, Spender},
record::{CancelReason, Status, SwapRecord},
},
swap_script::{SpendPath, Terms},
utxo,
};
pub(super) fn decide(
record: &SwapRecord,
terms: &Terms,
observed: &Observed,
chain_config: &ChainConfig,
) -> Decision {
let (opening, _strays) = observed.split(record.receive_amount);
let Some(opening) = opening else {
return funding(record, observed);
};
let preimage_left = record.sent.preimage
|| record.dest.server_privkey.is_some()
|| record.preimage.is_some()
|| keypath_claim(opening);
match &opening.spend {
Some(spend) if observed.confs(spend.height) >= chain_config.min_confs => {
spent(opening, spend)
}
_ if preimage_left => claiming(record, observed, chain_config, opening),
_ => confirming(terms, observed, chain_config, opening),
}
}
fn funding(record: &SwapRecord, observed: &Observed) -> Decision {
if let Some(cancel) = &record.server_cancel {
return Decision {
status: Status::Canceled {
opening: None,
reason: CancelReason::ServerCanceled(cancel.clone()),
},
action: None,
};
}
let expires_at = hold_invoice_deadline(record.started_at, record.source.invoice.as_deref());
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(
terms: &Terms,
observed: &Observed,
chain_config: &ChainConfig,
opening: &Output,
) -> Decision {
let confs = observed.confs(opening.height);
let csv_left = terms.csv.saturating_sub(confs);
if let Some(height) = opening.height
&& csv_left < chain_config.claim_margin
{
return Decision {
status: Status::Canceled {
opening: Some(opening.utxo.txid.clone()),
reason: CancelReason::ClaimWindowClosed {
at: height + terms.csv,
},
},
action: None,
};
}
let fee_rate = observed.fee_rate;
let claim_pays = fee_rate <= chain_config.max_fee_rate
&& fee_rate <= utxo::max_claim_fee_rate(terms.chain, opening.value);
let send_preimage = opening.spend.is_none()
&& confs >= chain_config.min_confs
&& csv_left >= chain_config.claim_margin
&& claim_pays;
Decision {
status: Status::Confirming {
txid: opening.utxo.txid.clone(),
confs,
needed: chain_config.min_confs,
max_fee_rate: chain_config.max_fee_rate,
fee_rate_spike: (!claim_pays && !fee_rate.is_nan()).then_some(fee_rate),
},
action: send_preimage.then_some(Action::Send(Message::Preimage)),
}
}
fn claiming(
record: &SwapRecord,
observed: &Observed,
chain_config: &ChainConfig,
opening: &Output,
) -> Decision {
let our_claim = opening
.spend
.as_ref()
.filter(|spend| spend.path != SpendPath::Csv);
Decision {
status: Status::Claiming {
opening: opening.utxo.txid.clone(),
claim: our_claim.map(|spend| spend.txid.clone()),
confs: our_claim.map_or(0, |spend| observed.confs(spend.height)),
fee: our_claim.and_then(|spend| spend.fee),
},
action: claim(record, observed, chain_config, opening),
}
}
fn claim(
record: &SwapRecord,
observed: &Observed,
chain_config: &ChainConfig,
opening: &Output,
) -> Option<Action> {
let pending = opening.spend.as_ref();
if pending.is_some_and(|spend| spend.height.is_some()) {
return None;
}
let preimage = observed.confs(opening.height) >= chain_config.min_confs
&& (record.sent.preimage || record.preimage.is_some() || keypath_claim(opening));
let path = match (record.dest.server_privkey, pending.map(|spend| spend.path)) {
(Some(server_privkey), None | Some(SpendPath::Keypath)) => Path::Keypath { server_privkey },
_ if preimage => Path::Preimage,
(Some(server_privkey), _) => Path::Keypath { server_privkey },
(None, _) => return None,
};
let affordable = utxo::max_claim_fee_rate(opening.utxo.chain(), opening.value);
let fee_rate = match pending.map(|spend| spend.path) {
Some(SpendPath::Csv) => chain_config.max_fee_rate,
Some(SpendPath::Keypath | SpendPath::Preimage(_)) | None => {
observed.fee_rate.min(chain_config.max_fee_rate)
}
}
.min(affordable);
Some(Action::Spend(Spend::Claim {
utxo: opening.utxo.clone(),
path,
fee_rate,
replaces: pending.map(|spend| spend.txid.clone()),
}))
}
fn keypath_claim(opening: &Output) -> bool {
opening
.spend
.as_ref()
.is_some_and(|spend| spend.path == SpendPath::Keypath)
}
fn spent(opening: &Output, spend: &Spender) -> Decision {
let opening = opening.utxo.txid.clone();
let status = match spend.path {
SpendPath::Keypath | SpendPath::Preimage(_) => Status::Completed {
opening,
payout: Some(spend.txid.clone()),
fee: spend.fee,
},
SpendPath::Csv => Status::Canceled {
opening: Some(opening),
reason: CancelReason::ServerRefunded,
},
};
Decision {
status,
action: None,
}
}