Skip to main content

Refunds

Anything paid to a SwapIn's address, beyond the deposit the server was paid for, stays the user's. swap.funds() lists each such output and where its refund stands:

ListedWhen
A stray: a wrong amount, a second deposit, a late onealways
The depositonce the swap is Canceled

An output the server took by its own path is not listed.

Fund stateMeaning
LockedUnder the CSV until block at, or unconfirmed with no at.
RefundableSpendable now and worth its refund: the next refund takes it.
DustNot worth its refund at the last rate: the fee, plus 546 sat on Bitcoin for the smallest output any address takes. Refunded once fees fall far enough.
RefundingIn our refund txid, paying fee, unconfirmed.
RefundedIn our refund txid, paying fee, confirmed.

A refund's fee is the whole transaction's, so every fund it took shows the same one. Group by txid before adding fees up.

When a fund is refundable​

SituationRefund
The server canceled and handed over its keyat once, by the keypath
The server is silentby the CSV path, once the CSV matures
A stray lands after the swap is Completedby the CSV path; until then the server holds both keys and can take it first

One refund takes at most 8 funds, the most valuable first. The rest stay Refundable and go in the next refund, once this one confirms. A refund pays the current fee estimate, capped at max_fee_rate.

Where refunds go​

The swap hasRefunds
A refund address, given at creationgo there by themselves
None, and a destinations hookgo where the hook says, by themselves
Neitherwait for swap.refund(&address)

The hook gives the address of a SwapIn's refund, and of a SwapOut created without a claim address:

#[async_trait]
impl Destinations for MyWallet {
async fn claim_address(&self, chain: Chain) -> Result<String, String> {
self.next_address(chain).await.map_err(|e| e.to_string())
}
async fn refund_address(&self, chain: Chain) -> Result<String, String> {
self.next_address(chain).await.map_err(|e| e.to_string())
}
}

let driver = driver.with_destinations(my_wallet);

An error from the hook fails the call that asked. A slow hook delays every swap, so bound any remote call in it with a timeout, and never wait on the user there.

refund makes one pass that refunds every Refundable fund to the address, in any status. It returns the refund's txid, or None while nothing is refundable or an earlier refund is unconfirmed.

Late deposits​

In the background, run keeps watching a SwapIn after it ends: while its funds are still moving, and for refund_timeout after that. It does not see a deposit sent later. Find one with a pass by hand, step:

if let Some(swap) = driver.get(swap_id).await? {
swap.step().await?;
let funds = swap.funds().await?;
}

The deposit shows in funds, and the status stays as it was. With somewhere to refund it, run drives the swap again until that refund confirms. step finds only a deposit already sent.