Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
"If this step fails, you can manually redeem your funds",
]}
/>
<SwapMoneroRecoveryButton
swap={swap}
size="small"
variant="contained"
/>
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
</Box>
);
}
Expand Down Expand Up @@ -179,6 +175,10 @@ export function StateAlert({
timelock: ExpiredTimelocks | null;
isRunning: boolean;
}) {
if (swap == null) {
return null;
}

switch (swap.state_name) {
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
// It cannot be punished anymore
Expand Down Expand Up @@ -208,10 +208,7 @@ export function StateAlert({
);
case "Cancel":
return (
<BitcoinPossiblyCancelledAlert
timelock={timelock}
swap={swap}
/>
<BitcoinPossiblyCancelledAlert timelock={timelock} swap={swap} />
);
case "Punish":
return <PunishTimelockExpiredAlert />;
Expand Down Expand Up @@ -246,10 +243,16 @@ export default function SwapStatusAlert({
swap,
onlyShowIfUnusualAmountOfTimeHasPassed,
}: {
swap: GetSwapInfoResponseExt;
swap: GetSwapInfoResponseExt | null;
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
}) {
const timelock = useAppSelector(selectSwapTimelock(swap.swap_id));
const swapId = swap?.swap_id ?? "";
const timelock = useAppSelector(selectSwapTimelock(swapId));
const isRunning = useIsSpecificSwapRunning(swapId);

if (swap == null) {
return null;
}

if (!isGetSwapInfoResponseRunningSwap(swap)) {
return null;
Expand All @@ -267,11 +270,9 @@ export default function SwapStatusAlert({
return null;
}

const isRunning = useIsSpecificSwapRunning(swap.swap_id);

return (
<Alert
key={swap.swap_id}
key={swapId}
severity="warning"
variant="filled"
classes={{ message: "alert-message-flex-grow" }}
Expand All @@ -290,8 +291,7 @@ export default function SwapStatusAlert({
)
) : (
<>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is
not running
Swap <TruncatedText>{swapId}</TruncatedText> is not running
</>
)}
</AlertTitle>
Expand Down
2 changes: 2 additions & 0 deletions swap-core/src/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod lock;
mod punish;
mod redeem;
mod refund;
mod refund_amnesty;
mod timelocks;

pub use crate::bitcoin::cancel::{CancelTimelock, PunishTimelock, TxCancel};
Expand All @@ -12,6 +13,7 @@ pub use crate::bitcoin::lock::TxLock;
pub use crate::bitcoin::punish::TxPunish;
pub use crate::bitcoin::redeem::TxRedeem;
pub use crate::bitcoin::refund::TxRefund;
pub use crate::bitcoin::refund_amnesty::TxRefundAmnesty;
pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks};
pub use ::bitcoin::amount::Amount;
pub use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
Expand Down
36 changes: 36 additions & 0 deletions swap-core/src/bitcoin/cancel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,42 @@ impl TxCancel {
}
}

pub fn build_refund_with_amnesty_transaction(
&self,
refund_address: &Address,
amnesty_descriptor: &Descriptor<::bitcoin::PublicKey>,
amnesty_amount: Amount,
spending_fee: Amount,
) -> Transaction {
let previous_output = self.as_outpoint();

let tx_in = TxIn {
previous_output,
script_sig: Default::default(),
sequence: Sequence(0xFFFF_FFFF),
witness: Default::default(),
};

let refund_amount = self.amount() - amnesty_amount - spending_fee;

let tx_out_refund = TxOut {
value: refund_amount,
script_pubkey: refund_address.script_pubkey(),
};

let tx_out_amnesty = TxOut {
value: amnesty_amount,
script_pubkey: amnesty_descriptor.script_pubkey(),
};

Transaction {
version: Version(2),
lock_time: PackedLockTime::from_height(0).expect("0 to be below lock time threshold"),
input: vec![tx_in],
output: vec![tx_out_refund, tx_out_amnesty],
}
}

pub fn weight() -> Weight {
Weight::from_wu(596)
}
Expand Down
67 changes: 60 additions & 7 deletions swap-core/src/bitcoin/refund.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::bitcoin;
use crate::bitcoin::{
verify_sig, Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey,
TooManyInputs, Transaction, TxCancel,
build_shared_output_descriptor, verify_sig, Address, Amount, EmptyWitnessStack, NoInputs,
NotThreeWitnesses, PublicKey, TooManyInputs, Transaction, TxCancel,
};
use ::bitcoin::sighash::SighashCache;
use ::bitcoin::{secp256k1, ScriptBuf, Weight};
Expand All @@ -21,16 +21,32 @@ pub struct TxRefund {
inner: Transaction,
digest: Sighash,
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
pub(in crate::bitcoin) amnesty_output_descriptor: Descriptor<::bitcoin::PublicKey>,
watch_script: ScriptBuf,
}

impl TxRefund {
pub fn new(tx_cancel: &TxCancel, refund_address: &Address, spending_fee: Amount) -> Self {
let tx_refund = tx_cancel.build_spend_transaction(refund_address, None, spending_fee);
pub fn new(
tx_cancel: &TxCancel,
refund_address: &Address,
A: PublicKey,
B: PublicKey,
amnesty_amount: Amount,
spending_fee: Amount,
) -> Result<Self> {
let amnesty_output_descriptor = build_shared_output_descriptor(A.0, B.0)?;

let tx_refund = tx_cancel.build_refund_with_amnesty_transaction(
refund_address,
&amnesty_output_descriptor,
amnesty_amount,
spending_fee,
);

let digest = SighashCache::new(&tx_refund)
.p2wsh_signature_hash(
0, // Only one input: cancel transaction
// Only one input: cancel transaction
0,
&tx_cancel
.output_descriptor
.script_code()
Expand All @@ -40,12 +56,13 @@ impl TxRefund {
)
.expect("sighash");

Self {
Ok(Self {
inner: tx_refund,
digest,
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
amnesty_output_descriptor,
watch_script: refund_address.script_pubkey(),
}
})
}

pub fn txid(&self) -> Txid {
Expand All @@ -56,6 +73,41 @@ impl TxRefund {
self.digest
}

pub fn amnesty_amount(&self) -> Amount {
self.inner.output[1].value
}

pub fn amnesty_outpoint(&self) -> ::bitcoin::OutPoint {
::bitcoin::OutPoint::new(self.txid(), 1)
}

pub fn build_amnesty_spend_transaction(
&self,
refund_address: &Address,
spending_fee: Amount,
) -> Transaction {
use ::bitcoin::{transaction::Version, locktime::absolute::LockTime as PackedLockTime, Sequence, TxIn, TxOut};

let tx_in = TxIn {
previous_output: self.amnesty_outpoint(),
script_sig: Default::default(),
sequence: Sequence(0xFFFF_FFFF),
witness: Default::default(),
};

let tx_out = TxOut {
value: self.amnesty_amount() - spending_fee,
script_pubkey: refund_address.script_pubkey(),
};

Transaction {
version: Version(2),
lock_time: PackedLockTime::from_height(0).expect("0 to be below lock time threshold"),
input: vec![tx_in],
output: vec![tx_out],
}
}

pub fn add_signatures(
self,
(A, sig_a): (PublicKey, Signature),
Expand All @@ -75,6 +127,7 @@ impl TxRefund {

let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?;
let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?;

// The order in which these are inserted doesn't matter
satisfier.insert(
A,
Expand Down
118 changes: 118 additions & 0 deletions swap-core/src/bitcoin/refund_amnesty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::bitcoin;
use crate::bitcoin::{
verify_sig, Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey,
TooManyInputs, Transaction, TxRefund,
};
use ::bitcoin::sighash::SighashCache;
use ::bitcoin::{secp256k1, ScriptBuf, Weight};
use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, Txid};
use anyhow::{bail, Context, Result};
use bdk_wallet::miniscript::Descriptor;
use bitcoin_wallet::primitives::Watchable;
use curve25519_dalek::scalar::Scalar;
use ecdsa_fun::Signature;
use std::collections::HashMap;
use std::sync::Arc;

use super::extract_ecdsa_sig;

#[derive(Debug, Clone)]
pub struct TxRefundAmnesty {
inner: Transaction,
digest: Sighash,
amensty_output_descriptor: Descriptor<::bitcoin::PublicKey>,
watch_script: ScriptBuf,
}

impl TxRefundAmnesty {
pub fn new(tx_refund: &TxRefund, refund_address: &Address, spending_fee: Amount) -> Self {
let tx_refund_amnesty = tx_refund.build_amnesty_spend_transaction(refund_address, spending_fee);

let digest = SighashCache::new(&tx_refund_amnesty)
.p2wsh_signature_hash(
0, // Only one input: amnesty box from tx_refund
&tx_refund
.amnesty_output_descriptor
.script_code()
.expect("scriptcode"),
tx_refund.amnesty_amount(),
EcdsaSighashType::All,
)
.expect("sighash");

Self {
inner: tx_refund_amnesty,
digest,
amensty_output_descriptor: tx_refund.amnesty_output_descriptor.clone(),
watch_script: refund_address.script_pubkey(),
}
}

pub fn txid(&self) -> Txid {
self.inner.compute_txid()
}

pub fn digest(&self) -> Sighash {
self.digest
}

pub fn add_signatures(
self,
(A, sig_a): (PublicKey, Signature),
(B, sig_b): (PublicKey, Signature),
) -> Result<Transaction> {
let satisfier = {
let mut satisfier = HashMap::with_capacity(2);

let A = ::bitcoin::PublicKey {
compressed: true,
inner: secp256k1::PublicKey::from_slice(&A.0.to_bytes())?,
};
let B = ::bitcoin::PublicKey {
compressed: true,
inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?,
};

let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?;
let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?;

// The order in which these are inserted doesn't matter
satisfier.insert(
A,
::bitcoin::ecdsa::Signature {
signature: sig_a,
sighash_type: EcdsaSighashType::All,
},
);
satisfier.insert(
B,
::bitcoin::ecdsa::Signature {
signature: sig_b,
sighash_type: EcdsaSighashType::All,
},
);

satisfier
};

let mut tx_refund = self.inner;
self.amensty_output_descriptor
.satisfy(&mut tx_refund.input[0], satisfier)?;

Ok(tx_refund)
}

pub fn weight() -> Weight {
Weight::from_wu(548)
}
}

impl Watchable for TxRefundAmnesty {
fn id(&self) -> Txid {
self.txid()
}

fn script(&self) -> ScriptBuf {
self.watch_script.clone()
}
}
Loading