Skip to content
Merged
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
59 changes: 53 additions & 6 deletions ddk-manager/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ static NB_CONFIRMATIONS: Lazy<u32> = Lazy::new(|| match std::env::var("NB_CONFIR
Err(_) => DEFAULT_NB_CONFIRMATIONS,
});

/// Automatically broadcast the refund transaction in `check_confirmed_contracts`
static AUTOMATIC_REFUND: Lazy<bool> = Lazy::new(|| match std::env::var("AUTOMATIC_REFUND") {
Ok(val) => val.parse().unwrap_or(true),
Err(_) => true,
});

/// The delay to set the refund value to.
pub const REFUND_DELAY: u32 = 86400 * 7;
/// The nSequence value used for CETs in DLC channels
Expand Down Expand Up @@ -1415,10 +1421,15 @@ where
Ok(Contract::Closed(closed_contract))
}

// TODO: Make this public to refund
/// Check if the refund locktime has passed and broadcast the refund transaction if it has.
#[tracing::instrument(skip_all, level = "debug")]
async fn check_refund(&self, contract: &SignedContract) -> Result<(), Error> {
// TODO(tibo): should check for confirmation of refund before updating state
pub async fn check_and_broadcast_refund(
&self,
contract_id: &ContractId,
) -> Result<Contract, Error> {
// Assert the contract is confirmed
let contract = get_contract_in_state!(self, contract_id, Confirmed, None::<PublicKey>)?;

if contract
.accepted_contract
.dlc_transactions
Expand Down Expand Up @@ -1448,16 +1459,52 @@ where
let signer = self.signer_provider.derive_contract_signer(offer.keys_id)?;
let refund = crate::contract_updater::get_signed_refund(
&self.secp,
contract,
&contract,
&signer,
&self.logger,
)?;
self.blockchain.send_transaction(&refund).await?;
}

self.store
.update_contract(&Contract::Refunded(contract.clone()))
let refunded = Contract::Refunded(contract.clone());
self.store.update_contract(&refunded).await?;
Ok(refunded)
} else {
return Err(Error::InvalidParameters(
"Contract maturity has not passed to broadcast refund".to_string(),
));
}
}

#[tracing::instrument(skip_all, level = "debug")]
async fn check_refund(&self, contract: &SignedContract) -> Result<(), Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove the TODO since it's already public

if contract
.accepted_contract
.dlc_transactions
.refund
.lock_time
.to_consensus_u32() as u64
<= self.time.unix_time_now()
{
let refund_txid = contract
.accepted_contract
.dlc_transactions
.refund
.compute_txid();
let confirmations = self
.blockchain
.get_transaction_confirmations(&refund_txid)
.await?;

if confirmations > 0 {
// Counterparty (or we) already broadcast the refund tx. Update state.
self.store
.update_contract(&Contract::Refunded(contract.clone()))
.await?;
} else if *AUTOMATIC_REFUND {
self.check_and_broadcast_refund(&contract.accepted_contract.get_contract_id())
.await?;
}
}

Ok(())
Expand Down
52 changes: 50 additions & 2 deletions ddk-manager/tests/manager_execution_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ async fn numerical_common_diff_nb_digits(
enum TestPath {
Close,
Refund,
ManualRefund,
CooperativeClose,
BadAcceptCetSignature,
BadAcceptRefundSignature,
Expand Down Expand Up @@ -430,6 +431,17 @@ async fn enum_single_oracle_refund_manual_test() {
.await;
}

#[tokio::test]
#[ignore]
async fn enum_single_oracle_manual_refund_test() {
manager_execution_test(
get_enum_test_params(1, 1, Some(get_enum_oracles(1, 0).await)).await,
TestPath::ManualRefund,
false,
)
.await;
}

#[tokio::test]
#[ignore]
async fn enum_single_oracle_bad_accept_cet_sig_test() {
Expand Down Expand Up @@ -909,7 +921,7 @@ async fn manager_execution_test(test_params: TestParams, path: TestPath, manual_
sync_receive.recv().await.expect("Error synchronizing");
assert_contract_state!(alice_manager_send, contract_id, FailedSign);
}
TestPath::Close | TestPath::Refund => {
TestPath::Close | TestPath::Refund | TestPath::ManualRefund => {
alice_send
.send(Some(Message::Accept(accept_msg)))
.await
Expand All @@ -936,7 +948,7 @@ async fn manager_execution_test(test_params: TestParams, path: TestPath, manual_
alice_wallet.sync().await.unwrap();
bob_wallet.sync().await.unwrap();
match path {
TestPath::Close | TestPath::Refund => {
TestPath::Close | TestPath::Refund | TestPath::ManualRefund => {
if !manual_close {
test_utils::set_time((EVENT_MATURITY as u64) + 1);
}
Expand Down Expand Up @@ -1049,6 +1061,42 @@ async fn manager_execution_test(test_params: TestParams, path: TestPath, manual_

periodic_check!(second, contract_id, Refunded);
}
TestPath::ManualRefund => {
alice_wallet.sync().await.unwrap();
bob_wallet.sync().await.unwrap();
periodic_check!(first, contract_id, Confirmed);
periodic_check!(second, contract_id, Confirmed);

test_utils::set_time(
((EVENT_MATURITY + ddk_manager::manager::REFUND_DELAY) as u64)
+ 1,
);

generate_blocks(10, electrs.clone(), sink.clone()).await;

alice_wallet.sync().await.unwrap();
bob_wallet.sync().await.unwrap();

// Manually broadcast the refund for the first party.
first
.lock()
.await
.check_and_broadcast_refund(&contract_id)
.await
.expect("Error manually broadcasting refund");
assert_contract_state!(first, contract_id, Refunded);

// Randomly check with or without having the Refund mined.
if thread_rng().next_u32() % 2 == 0 {
generate_blocks(1, electrs.clone(), sink.clone()).await;
}

alice_wallet.sync().await.unwrap();
bob_wallet.sync().await.unwrap();

// Second party picks it up via periodic check.
periodic_check!(second, contract_id, Refunded);
}
_ => unreachable!(),
}
}
Expand Down
12 changes: 12 additions & 0 deletions ddk/src/ddk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,18 @@ where
Ok((contract_id, counter_party, accept_dlc))
}

/// Refunds a DLC contract.
///
/// This method checks if the refund locktime has passed and broadcasts the refund transaction if it has.
///
/// # Arguments
/// * `contract_id` - The ID of the contract to refund
///
/// # Errors if the contract maturity has not passed to broadcast refund
pub async fn refund_dlc(&self, contract_id: &[u8; 32]) -> Result<Contract> {
Ok(self.manager.check_and_broadcast_refund(contract_id).await?)
}

/// Retrieves the current balance state, including:
/// - Confirmed balance
/// - Unconfirmed changes
Expand Down