feat(wallet): Use mempool histogram for fee estimation (#358)

* feat(wallet): Use mempool.space as a secondary fee estimation source

* fix: warn if mempool client cannot be instantiated

* make clippy happy

* nitpick: rename clippy_check to clippy in justfile

* rename `estimate_fee_rate_from_mempool` to `estimate_fee_rate_from_histogram` for clarity

* dprint fmt

* make clippy happy

* change teacing level back to debug!

* change log levels

* refactors

* refactor: estimate_fee and min_relay_fee

* serde camel case

Co-authored-by: Byron Hambly <byron@hambly.dev>

* refactors

* Add comments, use Weight struct where possible

* fmt, fix testrs

* dont fallback to bitcoin::MAX, fail instead

* make mempool space optional

* fmt

* refactor: use estimate_fee(...) in max_giveable(...)

* refactor max_giveable(...)

* refactor max_giveeable to return fee as well, remove safety margin for fee

* fix compile

* fmtr

* fix(integration test): Use pre-calculated cancel / punish fees for assert_alice_punished

* fix(integration test): Use real fees for asserts

* sync wallet before transaction_fee call

* split send_to_address into sweep_balance_to_address_dynamic_fee

---------

Co-authored-by: Byron Hambly <byron@hambly.dev>
This commit is contained in:
Mohan 2025-05-27 15:41:24 +02:00 committed by GitHub
parent 854b14939e
commit 091ba57547
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 995 additions and 358 deletions

View file

@ -5,3 +5,6 @@
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 849f8b01f49fc9a913100203698a9151d8de8a37564e1d3b1e3b4169e192f58a # shrinks to funding_amount = 290250686, num_utxos = 3, sats_per_vb = 75.35638, key = ExtendedPrivKey { network: Regtest, depth: 0, parent_fingerprint: 00000000, child_number: Normal { index: 0 }, private_key: [private key data], chain_code: 0b7a29ca6990bbc9b9187c1d1a07e2cf68e32f5ce55d2df01edf8a4ac2ee2a4b }, alice = Point<Normal,Public,NonZero>(0299a8c6a662e2e9e8ee7c6889b75a51c432812b4bf70c1d76eace63abc1bdfb1b), bob = Point<Normal,Public,NonZero>(027165b1f9924030c90d38c511da0f4397766078687997ed34d6ef2743d2a7bbed)
cc ec2c53bbf967d46a4e9a394da96f8089ea77dcca794dec9fcc13c5a7141eb929 # shrinks to funding_amount = 271331, num_utxos = 4, sats_per_vb = 308, key = Xpriv { network: Test, depth: 0, parent_fingerprint: 00000000, child_number: Normal { index: 0 }, private_key: SecretKey(#dc659542f09c329f), chain_code: c94a49bbee951f7f7401c801db73c23cf17b0b3aa2f6246a8616fe2205a6ca51 }, alice = Point<Normal,Public,NonZero>(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), bob = Point<Normal,Public,NonZero>(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)
cc 2062fe113880e48af4d170fca9a2690e57a55f8985f86a66bd53e8bbe5c62dbd # shrinks to funding_amount = 3000, num_utxos = 1, sats_per_vb = 7, key = Xpriv { network: Test, depth: 0, parent_fingerprint: 00000000, child_number: Normal { index: 0 }, private_key: SecretKey(#dc659542f09c329f), chain_code: c94a49bbee951f7f7401c801db73c23cf17b0b3aa2f6246a8616fe2205a6ca51 }, alice = Point<Normal,Public,NonZero>(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), bob = Point<Normal,Public,NonZero>(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)
cc 381f4c0a72579c36107b0fd7a5b9c8aad2f02c0f9908b3a137b71ff05e7d8d33 # shrinks to funding_amount = 3000, num_utxos = 1, sats_per_vb = 7, key = Xpriv { network: Test, depth: 0, parent_fingerprint: 00000000, child_number: Normal { index: 0 }, private_key: SecretKey(#dc659542f09c329f), chain_code: c94a49bbee951f7f7401c801db73c23cf17b0b3aa2f6246a8616fe2205a6ca51 }, alice = Point<Normal,Public,NonZero>(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798), bob = Point<Normal,Public,NonZero>(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)

View file

@ -185,6 +185,12 @@ pub struct Bitcoin {
pub finality_confirmations: Option<u32>,
#[serde(with = "crate::bitcoin::network")]
pub network: bitcoin::Network,
#[serde(default = "default_use_mempool_space_fee_estimation")]
pub use_mempool_space_fee_estimation: bool,
}
fn default_use_mempool_space_fee_estimation() -> bool {
true
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
@ -377,6 +383,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
target_block,
finality_confirmations: None,
network: bitcoin_network,
use_mempool_space_fee_estimation: true,
},
monero: Monero {
wallet_rpc_url: monero_wallet_rpc_url,
@ -421,6 +428,7 @@ mod tests {
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Testnet,
use_mempool_space_fee_estimation: true,
},
network: Network {
listen: vec![defaults.listen_address_tcp],
@ -465,6 +473,7 @@ mod tests {
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Bitcoin,
use_mempool_space_fee_estimation: true,
},
network: Network {
listen: vec![defaults.listen_address_tcp],
@ -519,6 +528,7 @@ mod tests {
target_block: defaults.bitcoin_confirmation_target,
finality_confirmations: None,
network: bitcoin::Network::Bitcoin,
use_mempool_space_fee_estimation: true,
},
network: Network {
listen,

View file

@ -310,19 +310,22 @@ pub async fn main() -> Result<()> {
Command::WithdrawBtc { amount, address } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let amount = match amount {
Some(amount) => amount,
let withdraw_tx_unsigned = match amount {
Some(amount) => {
bitcoin_wallet
.send_to_address_dynamic_fee(address, amount, None)
.await?
}
None => {
bitcoin_wallet
.max_giveable(address.script_pubkey().len())
.sweep_balance_to_address_dynamic_fee(address)
.await?
}
};
let psbt = bitcoin_wallet
.send_to_address(address, amount, None)
let signed_tx = bitcoin_wallet
.sign_and_finalize(withdraw_tx_unsigned)
.await?;
let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?;
bitcoin_wallet.broadcast(signed_tx, "withdraw").await?;
}
@ -420,6 +423,7 @@ async fn init_bitcoin_wallet(
})
.finality_confirmations(env_config.bitcoin_finality_confirmations)
.target_block(config.bitcoin.target_block)
.use_mempool_space_fee_estimation(config.bitcoin.use_mempool_space_fee_estimation)
.sync_interval(env_config.bitcoin_sync_interval())
.build()
.await

View file

@ -51,8 +51,8 @@ mod tests {
async fn given_no_balance_and_transfers_less_than_max_swaps_max_giveable() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.0009).unwrap(),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::from_btc(0.0009).unwrap(), Amount::from_sat(1000)),
])));
let (amount, fees) = determine_btc_to_swap(
@ -65,7 +65,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -78,10 +77,10 @@ mod tests {
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
INFO swap::api::request: Deposit at least 0.00001 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
INFO swap::api::request: Received Bitcoin new_balance=0.001 BTC max_giveable=0.0009 BTC
r" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0 BTC maximum_amount=0.01000000 BTC
INFO swap::cli::api::request: Deposit at least 0.00001000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.00001000 BTC max_deposit_until_maximum_amount_is_reached=0.01001000 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
INFO swap::cli::api::request: Received Bitcoin new_balance=0.00100000 BTC max_giveable=0.00090000 BTC
"
);
}
@ -90,8 +89,8 @@ mod tests {
async fn given_no_balance_and_transfers_more_then_swaps_max_quantity_from_quote() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.1).unwrap(),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::from_btc(0.1).unwrap(), Amount::from_sat(1000)),
])));
let (amount, fees) = determine_btc_to_swap(
@ -104,7 +103,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -117,10 +115,10 @@ mod tests {
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
INFO swap::api::request: Deposit at least 0.00001 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
INFO swap::api::request: Received Bitcoin new_balance=0.1001 BTC max_giveable=0.1 BTC
r" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0 BTC maximum_amount=0.01000000 BTC
INFO swap::cli::api::request: Deposit at least 0.00001000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.00001000 BTC max_deposit_until_maximum_amount_is_reached=0.01001000 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
INFO swap::cli::api::request: Received Bitcoin new_balance=0.10010000 BTC max_giveable=0.10000000 BTC
"
);
}
@ -129,8 +127,8 @@ mod tests {
async fn given_initial_balance_below_max_quantity_swaps_max_giveable() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0049).unwrap(),
Amount::from_btc(99.9).unwrap(),
(Amount::from_btc(0.0049).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(99.9).unwrap(), Amount::from_sat(1000)),
])));
let (amount, fees) = determine_btc_to_swap(
@ -143,7 +141,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -156,7 +153,7 @@ mod tests {
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC\n"
" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0 BTC maximum_amount=0.01000000 BTC\n"
);
}
@ -164,8 +161,8 @@ mod tests {
async fn given_initial_balance_above_max_quantity_swaps_max_quantity() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.1).unwrap(),
Amount::from_btc(99.9).unwrap(),
(Amount::from_btc(0.1).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(99.9).unwrap(), Amount::from_sat(1000)),
])));
let (amount, fees) = determine_btc_to_swap(
@ -178,7 +175,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -191,7 +187,7 @@ mod tests {
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC\n"
" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0 BTC maximum_amount=0.01000000 BTC\n"
);
}
@ -199,8 +195,8 @@ mod tests {
async fn given_no_initial_balance_then_min_wait_for_sufficient_deposit() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.01).unwrap(),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
])));
let (amount, fees) = determine_btc_to_swap(
@ -213,7 +209,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -226,10 +221,10 @@ mod tests {
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Deposit at least 0.01001 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.01001 BTC max_giveable=0 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC
r" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0.01000000 BTC maximum_amount=21000000 BTC
INFO swap::cli::api::request: Deposit at least 0.01001000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.01001000 BTC max_deposit_until_maximum_amount_is_reached=21000000.00001000 BTC max_giveable=0 BTC minimum_amount=0.01000000 BTC maximum_amount=21000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
INFO swap::cli::api::request: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
"
);
}
@ -238,8 +233,8 @@ mod tests {
async fn given_balance_less_then_min_wait_for_sufficient_deposit() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0001).unwrap(),
Amount::from_btc(0.01).unwrap(),
(Amount::from_btc(0.0001).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
])));
let (amount, fees) = determine_btc_to_swap(
@ -252,7 +247,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -265,10 +259,10 @@ mod tests {
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Deposit at least 0.00991 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00991 BTC max_giveable=0.0001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC
r" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0.01000000 BTC maximum_amount=21000000 BTC
INFO swap::cli::api::request: Deposit at least 0.00991000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.00991000 BTC max_deposit_until_maximum_amount_is_reached=20999999.99991000 BTC max_giveable=0.00010000 BTC minimum_amount=0.01000000 BTC maximum_amount=21000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
INFO swap::cli::api::request: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
"
);
}
@ -277,11 +271,11 @@ mod tests {
async fn given_no_initial_balance_and_transfers_less_than_min_keep_waiting() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.01).unwrap(),
Amount::from_btc(0.01).unwrap(),
Amount::from_btc(0.01).unwrap(),
Amount::from_btc(0.01).unwrap(),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
])));
let error = tokio::time::timeout(
@ -296,7 +290,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
),
@ -307,13 +300,13 @@ mod tests {
assert!(matches!(error, tokio::time::error::Elapsed { .. }));
assert_eq!(
writer.captured(),
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Deposit at least 0.10001 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001 BTC max_giveable=0 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC
INFO swap::api::request: Deposited amount is less than `min_quantity`
INFO swap::api::request: Deposit at least 0.09001 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.09001 BTC max_giveable=0.01 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
r" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0.10000000 BTC maximum_amount=21000000 BTC
INFO swap::cli::api::request: Deposit at least 0.10001000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.10001000 BTC max_deposit_until_maximum_amount_is_reached=21000000.00001000 BTC max_giveable=0 BTC minimum_amount=0.10000000 BTC maximum_amount=21000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
INFO swap::cli::api::request: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
INFO swap::cli::api::request: Deposited amount is not enough to cover `min_quantity` when accounting for network fees
INFO swap::cli::api::request: Deposit at least 0.09001000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.09001000 BTC max_deposit_until_maximum_amount_is_reached=20999999.99001000 BTC max_giveable=0.01000000 BTC minimum_amount=0.10000000 BTC maximum_amount=21000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
"
);
}
@ -322,16 +315,16 @@ mod tests {
async fn given_longer_delay_until_deposit_should_not_spam_user() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::from_btc(0.2).unwrap(),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::ZERO, Amount::from_sat(1000)),
(Amount::from_btc(0.2).unwrap(), Amount::from_sat(1000)),
])));
tokio::time::timeout(
@ -347,7 +340,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
),
@ -358,10 +350,10 @@ mod tests {
assert_eq!(
writer.captured(),
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Deposit at least 0.10001 BTC to cover the min quantity with fee!
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001 BTC max_giveable=0 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
INFO swap::api::request: Received Bitcoin new_balance=0.21 BTC max_giveable=0.2 BTC
r" INFO swap::cli::api::request: Received quote price=0.00100000 BTC minimum_amount=0.10000000 BTC maximum_amount=21000000 BTC
INFO swap::cli::api::request: Deposit at least 0.10001000 BTC to cover the min quantity with fee!
INFO swap::cli::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit_until_swap_will_start=0.10001000 BTC max_deposit_until_maximum_amount_is_reached=21000000.00001000 BTC max_giveable=0 BTC minimum_amount=0.10000000 BTC maximum_amount=21000000 BTC min_bitcoin_lock_tx_fee=0.00001000 BTC price=0.00100000 BTC
INFO swap::cli::api::request: Received Bitcoin new_balance=0.21000000 BTC max_giveable=0.20000000 BTC
"
);
}
@ -369,8 +361,8 @@ mod tests {
#[tokio::test]
async fn given_bid_quote_max_amount_0_return_error() {
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0001).unwrap(),
Amount::from_btc(0.01).unwrap(),
(Amount::from_btc(0.0001).unwrap(), Amount::from_sat(1000)),
(Amount::from_btc(0.01).unwrap(), Amount::from_sat(1000)),
])));
let determination_error = determine_btc_to_swap(
@ -383,7 +375,6 @@ mod tests {
result.give()
},
|| async { Ok(()) },
|_| async { Ok(Amount::from_sat(1000)) },
None,
None,
)
@ -396,18 +387,18 @@ mod tests {
}
struct MaxGiveable {
amounts: Vec<Amount>,
amounts: Vec<(Amount, Amount)>,
call_counter: usize,
}
impl MaxGiveable {
fn new(amounts: Vec<Amount>) -> Self {
fn new(amounts: Vec<(Amount, Amount)>) -> Self {
Self {
amounts,
call_counter: 0,
}
}
fn give(&mut self) -> Result<Amount> {
fn give(&mut self) -> Result<(Amount, Amount)> {
let amount = self
.amounts
.get(self.call_counter)
@ -428,7 +419,7 @@ mod tests {
fn quote_with_min(btc: f64) -> BidQuote {
BidQuote {
price: Amount::from_btc(0.001).unwrap(),
max_quantity: Amount::MAX,
max_quantity: Amount::MAX_MONEY,
min_quantity: Amount::from_btc(btc).unwrap(),
}
}

View file

@ -574,6 +574,11 @@ mod tests {
.estimate_fee(TxPunish::weight(), btc_amount)
.await
.unwrap();
let tx_lock_fee = alice_wallet
.estimate_fee(TxLock::weight(), btc_amount)
.await
.unwrap();
let redeem_address = alice_wallet.new_address().await.unwrap();
let punish_address = alice_wallet.new_address().await.unwrap();
@ -600,6 +605,7 @@ mod tests {
config.monero_finality_confirmations,
spending_fee,
spending_fee,
tx_lock_fee,
);
let message0 = bob_state0.next_message();
@ -633,10 +639,10 @@ mod tests {
.unwrap();
let refund_transaction = bob_state6.signed_refund_transaction().unwrap();
assert_weight(redeem_transaction, TxRedeem::weight() as u64, "TxRedeem");
assert_weight(cancel_transaction, TxCancel::weight() as u64, "TxCancel");
assert_weight(punish_transaction, TxPunish::weight() as u64, "TxPunish");
assert_weight(refund_transaction, TxRefund::weight() as u64, "TxRefund");
assert_weight(redeem_transaction, TxRedeem::weight().to_wu(), "TxRedeem");
assert_weight(cancel_transaction, TxCancel::weight().to_wu(), "TxCancel");
assert_weight(punish_transaction, TxPunish::weight().to_wu(), "TxPunish");
assert_weight(refund_transaction, TxRefund::weight().to_wu(), "TxRefund");
}
// Weights fluctuate because of the length of the signatures. Valid ecdsa

View file

@ -5,6 +5,7 @@ use crate::bitcoin::{
};
use ::bitcoin::sighash::SighashCache;
use ::bitcoin::transaction::Version;
use ::bitcoin::Weight;
use ::bitcoin::{
locktime::absolute::LockTime as PackedLockTime, secp256k1, sighash::SegwitV0Sighash as Sighash,
EcdsaSighashType, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid,
@ -283,8 +284,8 @@ impl TxCancel {
}
}
pub fn weight() -> usize {
596
pub fn weight() -> Weight {
Weight::from_wu(596)
}
}

View file

@ -28,6 +28,7 @@ impl TxLock {
impl EstimateFeeRate + Send + Sync + 'static,
>,
amount: Amount,
spending_fee: Amount,
A: PublicKey,
B: PublicKey,
change: bitcoin::Address,
@ -38,7 +39,7 @@ impl TxLock {
.expect("can derive address from descriptor");
let psbt = wallet
.send_to_address(address, amount, Some(change))
.send_to_address(address, amount, spending_fee, Some(change))
.await?;
Ok(Self {
@ -177,8 +178,8 @@ impl TxLock {
}
}
pub fn weight() -> usize {
TX_LOCK_WEIGHT
pub fn weight() -> ::bitcoin::Weight {
::bitcoin::Weight::from_wu(TX_LOCK_WEIGHT as u64)
}
}
@ -201,15 +202,28 @@ impl Watchable for TxLock {
#[cfg(test)]
mod tests {
use super::*;
use crate::bitcoin::TestWalletBuilder;
use crate::bitcoin::wallet::TestWalletBuilder;
use crate::bitcoin::Amount;
use ::bitcoin::psbt::Psbt as PartiallySignedTransaction;
// Basic setup function for tests
async fn setup() -> (
PublicKey,
PublicKey,
Wallet<bdk_wallet::rusqlite::Connection, crate::bitcoin::wallet::StaticFeeRate>,
) {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(100_000).build().await;
(A, B, wallet)
}
#[tokio::test]
async fn given_bob_sends_good_psbt_when_reconstructing_then_succeeeds() {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(50_000).build().await;
let (A, B, wallet) = setup().await;
let agreed_amount = Amount::from_sat(10000);
let spending_fee = Amount::from_sat(1000);
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount, spending_fee).await;
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect("PSBT to be valid");
@ -217,31 +231,28 @@ mod tests {
#[tokio::test]
async fn bob_can_fund_without_a_change_output() {
let (A, B) = alice_and_bob();
let fees = 300;
let agreed_amount = Amount::from_sat(10000);
let amount = agreed_amount.to_sat() + fees;
let wallet = TestWalletBuilder::new(amount).build().await;
let (A, B, _) = setup().await;
let amount = 10_000;
let agreed_amount = Amount::from_sat(amount);
let spending_fee = Amount::from_sat(300);
let wallet = TestWalletBuilder::new(amount + 300).build().await;
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount, spending_fee).await;
assert_eq!(
psbt.unsigned_tx.output.len(),
1,
"psbt should only have a single output"
"Expected no change output"
);
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect("PSBT to be valid");
}
#[tokio::test]
async fn given_bob_is_sending_less_than_agreed_when_reconstructing_txlock_then_fails() {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(50_000).build().await;
let (A, B, wallet) = setup().await;
let agreed_amount = Amount::from_sat(10000);
let spending_fee = Amount::from_sat(1000);
let bad_amount = Amount::from_sat(5000);
let psbt = bob_make_psbt(A, B, &wallet, bad_amount).await;
let psbt = bob_make_psbt(A, B, &wallet, bad_amount, spending_fee).await;
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect_err("PSBT to be invalid");
@ -249,12 +260,12 @@ mod tests {
#[tokio::test]
async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() {
let (A, B) = alice_and_bob();
let wallet = TestWalletBuilder::new(50_000).build().await;
let (A, B, wallet) = setup().await;
let agreed_amount = Amount::from_sat(10000);
let spending_fee = Amount::from_sat(1000);
let E = eve();
let psbt = bob_make_psbt(E, B, &wallet, agreed_amount).await;
let psbt = bob_make_psbt(E, B, &wallet, agreed_amount, spending_fee).await;
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
result.expect_err("PSBT to be invalid");
@ -271,9 +282,7 @@ mod tests {
}
}
/// Helper function that represents Bob's action of constructing the PSBT.
///
/// Extracting this allows us to keep the tests concise.
// Helper function for testing PSBT creation by Bob
async fn bob_make_psbt(
A: PublicKey,
B: PublicKey,
@ -282,9 +291,10 @@ mod tests {
impl EstimateFeeRate + Send + Sync + 'static,
>,
amount: Amount,
spending_fee: Amount,
) -> PartiallySignedTransaction {
let change = wallet.new_address().await.unwrap();
TxLock::new(wallet, amount, A, B, change)
TxLock::new(wallet, amount, spending_fee, A, B, change)
.await
.unwrap()
.into()

View file

@ -1,8 +1,8 @@
use crate::bitcoin::wallet::Watchable;
use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid};
use ::bitcoin::sighash::SighashCache;
use ::bitcoin::ScriptBuf;
use ::bitcoin::{secp256k1, sighash::SegwitV0Sighash as Sighash, EcdsaSighashType};
use ::bitcoin::{ScriptBuf, Weight};
use anyhow::{Context, Result};
use bdk_wallet::miniscript::Descriptor;
use std::collections::HashMap;
@ -93,8 +93,8 @@ impl TxPunish {
Ok(tx_punish)
}
pub fn weight() -> usize {
548
pub fn weight() -> Weight {
Weight::from_wu(548)
}
}

View file

@ -7,8 +7,8 @@ use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, Txid};
use anyhow::{bail, Context, Result};
use bdk_wallet::miniscript::Descriptor;
use bitcoin::sighash::SighashCache;
use bitcoin::EcdsaSighashType;
use bitcoin::{secp256k1, ScriptBuf};
use bitcoin::{EcdsaSighashType, Weight};
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
use ecdsa_fun::fun::Scalar;
use ecdsa_fun::nonce::Deterministic;
@ -145,8 +145,8 @@ impl TxRedeem {
Ok(sig)
}
pub fn weight() -> usize {
548
pub fn weight() -> Weight {
Weight::from_wu(548)
}
#[cfg(test)]

View file

@ -5,7 +5,7 @@ use crate::bitcoin::{
};
use crate::{bitcoin, monero};
use ::bitcoin::sighash::SighashCache;
use ::bitcoin::{secp256k1, ScriptBuf};
use ::bitcoin::{secp256k1, ScriptBuf, Weight};
use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, Txid};
use anyhow::{bail, Context, Result};
use bdk_wallet::miniscript::Descriptor;
@ -152,8 +152,8 @@ impl TxRefund {
Ok(sig)
}
pub fn weight() -> usize {
548
pub fn weight() -> Weight {
Weight::from_wu(548)
}
}

File diff suppressed because it is too large Load diff

View file

@ -729,8 +729,10 @@ pub async fn buy_xmr(
}
},
swap_result = async {
let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size());
let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount);
let max_givable = || async {
let (amount, fee) = bitcoin_wallet.max_giveable(TxLock::script_size()).await?;
Ok((amount, fee))
};
let determine_amount = determine_btc_to_swap(
context.config.json,
@ -739,12 +741,11 @@ pub async fn buy_xmr(
|| bitcoin_wallet.balance(),
max_givable,
|| bitcoin_wallet.sync(),
estimate_fee,
context.tauri_handle.clone(),
Some(swap_id)
);
let (amount, fees) = match determine_amount.await {
let (tx_lock_amount, tx_lock_fee) = match determine_amount.await {
Ok(val) => val,
Err(error) => match error.downcast::<ZeroQuoteReceived>() {
Ok(_) => {
@ -754,7 +755,7 @@ pub async fn buy_xmr(
},
};
tracing::info!(%amount, %fees, "Determined swap amount");
tracing::info!(%tx_lock_amount, %tx_lock_fee, "Determined swap amount");
context.db.insert_peer_id(swap_id, seller_peer_id).await?;
@ -767,7 +768,8 @@ pub async fn buy_xmr(
event_loop_handle,
monero_receive_address,
bitcoin_change_address,
amount,
tx_lock_amount,
tx_lock_fee
).with_event_emitter(context.tauri_handle.clone());
bob::run(swap).await
@ -1004,25 +1006,39 @@ pub async fn withdraw_btc(
.as_ref()
.context("Could not get Bitcoin wallet")?;
let amount = match amount {
Some(amount) => amount,
let (withdraw_tx_unsigned, amount) = match amount {
Some(amount) => {
let withdraw_tx_unsigned = bitcoin_wallet
.send_to_address_dynamic_fee(address, amount, None)
.await?;
(withdraw_tx_unsigned, amount)
}
None => {
bitcoin_wallet
let (max_giveable, spending_fee) = bitcoin_wallet
.max_giveable(address.script_pubkey().len())
.await?
.await?;
let withdraw_tx_unsigned = bitcoin_wallet
.send_to_address(address, max_giveable, spending_fee, None)
.await?;
(withdraw_tx_unsigned, max_giveable)
}
};
let psbt = bitcoin_wallet
.send_to_address(address, amount, None)
let withdraw_tx = bitcoin_wallet
.sign_and_finalize(withdraw_tx_unsigned)
.await?;
let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?;
bitcoin_wallet
.broadcast(signed_tx.clone(), "withdraw")
.broadcast(withdraw_tx.clone(), "withdraw")
.await?;
let txid = withdraw_tx.compute_txid();
Ok(WithdrawBtcResponse {
txid: signed_tx.compute_txid().to_string(),
txid: txid.to_string(),
amount,
})
}
@ -1175,26 +1191,23 @@ fn qr_code(value: &impl ToString) -> Result<String> {
}
#[allow(clippy::too_many_arguments)]
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS, FFE, TFE>(
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS>(
json: bool,
bid_quote: BidQuote,
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
balance: FB,
max_giveable_fn: FMG,
sync: FS,
estimate_fee: FFE,
event_emitter: Option<TauriHandle>,
swap_id: Option<Uuid>,
) -> Result<(bitcoin::Amount, bitcoin::Amount)>
where
TB: Future<Output = Result<bitcoin::Amount>>,
FB: Fn() -> TB,
TMG: Future<Output = Result<bitcoin::Amount>>,
TMG: Future<Output = Result<(bitcoin::Amount, bitcoin::Amount)>>,
FMG: Fn() -> TMG,
TS: Future<Output = Result<()>>,
FS: Fn() -> TS,
FFE: Fn(bitcoin::Amount) -> TFE,
TFE: Future<Output = Result<bitcoin::Amount>>,
{
if bid_quote.max_quantity == bitcoin::Amount::ZERO {
bail!(ZeroQuoteReceived)
@ -1208,23 +1221,30 @@ where
);
sync().await.context("Failed to sync of Bitcoin wallet")?;
let mut max_giveable = max_giveable_fn().await?;
let (mut max_giveable, mut spending_fee) = max_giveable_fn().await?;
if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity {
let deposit_address = get_new_address.await?;
let minimum_amount = bid_quote.min_quantity;
let maximum_amount = bid_quote.max_quantity;
// To avoid any issus, we clip maximum_amount to never go above the
// total maximim Bitcoin supply
let maximum_amount = maximum_amount.min(bitcoin::Amount::MAX_MONEY);
if !json {
eprintln!("{}", qr_code(&deposit_address)?);
}
loop {
let min_outstanding = bid_quote.min_quantity - max_giveable;
let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?;
let min_bitcoin_lock_tx_fee = spending_fee; // Use the fee from max_giveable
let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee;
let max_deposit_until_maximum_amount_is_reached =
maximum_amount - max_giveable + min_bitcoin_lock_tx_fee;
let max_deposit_until_maximum_amount_is_reached = maximum_amount
.checked_sub(max_giveable)
.context("Overflow when subtracting max_giveable from maximum_amount")?
.checked_add(min_bitcoin_lock_tx_fee)
.context(format!("Overflow when adding min_bitcoin_lock_tx_fee ({min_bitcoin_lock_tx_fee}) to max_giveable ({max_giveable}) with maximum_amount ({maximum_amount})"))?;
tracing::info!(
"Deposit at least {} to cover the min quantity with fee!",
@ -1256,14 +1276,14 @@ where
);
}
max_giveable = loop {
(max_giveable, spending_fee) = loop {
sync()
.await
.context("Failed to sync Bitcoin wallet while waiting for deposit")?;
let new_max_givable = max_giveable_fn().await?;
let (new_max_givable, new_fee) = max_giveable_fn().await?;
if new_max_givable > max_giveable {
break new_max_givable;
break (new_max_givable, new_fee);
}
tokio::time::sleep(Duration::from_secs(1)).await;

View file

@ -1,3 +1,4 @@
use super::request::BalanceResponse;
use crate::bitcoin;
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
use anyhow::{anyhow, Context, Result};
@ -14,8 +15,6 @@ use typeshare::typeshare;
use url::Url;
use uuid::Uuid;
use super::request::BalanceResponse;
#[typeshare]
#[derive(Clone, Serialize)]
#[serde(tag = "channelName", content = "event")]

View file

@ -12,6 +12,7 @@ pub enum Bob {
btc_amount: bitcoin::Amount,
#[serde(with = "crate::bitcoin::address_serde")]
change_address: bitcoin::Address,
tx_lock_fee: bitcoin::Amount,
},
ExecutionSetupDone {
state2: bob::State2,
@ -54,9 +55,11 @@ impl From<BobState> for Bob {
BobState::Started {
btc_amount,
change_address,
tx_lock_fee,
} => Bob::Started {
btc_amount,
change_address,
tx_lock_fee,
},
BobState::SwapSetupCompleted(state2) => Bob::ExecutionSetupDone { state2 },
BobState::BtcLocked {
@ -96,9 +99,11 @@ impl From<Bob> for BobState {
Bob::Started {
btc_amount,
change_address,
tx_lock_fee,
} => BobState::Started {
btc_amount,
change_address,
tx_lock_fee,
},
Bob::ExecutionSetupDone { state2 } => BobState::SwapSetupCompleted(state2),
Bob::BtcLocked {

View file

@ -136,6 +136,7 @@ impl Handler {
pub struct NewSwap {
pub swap_id: Uuid,
pub btc: bitcoin::Amount,
pub tx_lock_fee: bitcoin::Amount,
pub tx_refund_fee: bitcoin::Amount,
pub tx_cancel_fee: bitcoin::Amount,
pub bitcoin_refund_address: bitcoin::Address,
@ -211,10 +212,11 @@ impl ConnectionHandler for Handler {
xmr,
env_config.bitcoin_cancel_timelock,
env_config.bitcoin_punish_timelock,
new_swap_request.bitcoin_refund_address,
new_swap_request.bitcoin_refund_address.clone(),
env_config.monero_finality_confirmations,
new_swap_request.tx_refund_fee,
new_swap_request.tx_cancel_fee,
new_swap_request.tx_lock_fee,
);
write_cbor_message(&mut substream, state0.next_message())

View file

@ -402,11 +402,11 @@ pub struct State3 {
#[serde(with = "::bitcoin::amount::serde::as_sat")]
tx_redeem_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::amount::serde::as_sat")]
tx_punish_fee: bitcoin::Amount,
pub tx_punish_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::amount::serde::as_sat")]
tx_refund_fee: bitcoin::Amount,
pub tx_refund_fee: bitcoin::Amount,
#[serde(with = "::bitcoin::amount::serde::as_sat")]
tx_cancel_fee: bitcoin::Amount,
pub tx_cancel_fee: bitcoin::Amount,
}
impl State3 {

View file

@ -39,10 +39,12 @@ impl Swap {
monero_receive_address: monero::Address,
bitcoin_change_address: bitcoin::Address,
btc_amount: bitcoin::Amount,
tx_lock_fee: bitcoin::Amount,
) -> Self {
Self {
state: BobState::Started {
btc_amount,
tx_lock_fee,
change_address: bitcoin_change_address,
},
event_loop_handle,

View file

@ -27,6 +27,7 @@ pub enum BobState {
Started {
#[serde(with = "::bitcoin::amount::serde::as_sat")]
btc_amount: bitcoin::Amount,
tx_lock_fee: bitcoin::Amount,
#[serde(with = "address_serde")]
change_address: bitcoin::Address,
},
@ -124,6 +125,7 @@ pub struct State0 {
min_monero_confirmations: u64,
tx_refund_fee: bitcoin::Amount,
tx_cancel_fee: bitcoin::Amount,
tx_lock_fee: bitcoin::Amount,
}
impl State0 {
@ -139,6 +141,7 @@ impl State0 {
min_monero_confirmations: u64,
tx_refund_fee: bitcoin::Amount,
tx_cancel_fee: bitcoin::Amount,
tx_lock_fee: bitcoin::Amount,
) -> Self {
let b = bitcoin::SecretKey::new_random(rng);
@ -165,6 +168,7 @@ impl State0 {
min_monero_confirmations,
tx_refund_fee,
tx_cancel_fee,
tx_lock_fee,
}
}
@ -208,6 +212,7 @@ impl State0 {
let tx_lock = bitcoin::TxLock::new(
wallet,
self.btc,
self.tx_lock_fee,
msg.A,
self.b.public(),
self.refund_address.clone(),

View file

@ -107,6 +107,7 @@ async fn next_state(
BobState::Started {
btc_amount,
change_address,
tx_lock_fee,
} => {
let tx_refund_fee = bitcoin_wallet
.estimate_fee(TxRefund::weight(), btc_amount)
@ -129,6 +130,7 @@ async fn next_state(
.setup_swap(NewSwap {
swap_id,
btc: btc_amount,
tx_lock_fee,
tx_refund_fee,
tx_cancel_fee,
bitcoin_refund_address: change_address,

View file

@ -15,7 +15,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use swap::asb::FixedRate;
use swap::bitcoin::{CancelTimelock, PunishTimelock, TxCancel, TxPunish, TxRedeem, TxRefund};
use swap::bitcoin::{CancelTimelock, PunishTimelock};
use swap::cli::api;
use swap::database::{AccessMode, SqliteDatabase};
use swap::env::{Config, GetConfig};
@ -486,6 +486,7 @@ impl BobParams {
self.monero_wallet.lock().await.get_main_address(),
self.bitcoin_wallet.new_address().await?,
btc_amount,
bitcoin::Amount::from_sat(1000), // Fixed fee of 1000 satoshis for now
);
Ok((swap, event_loop))
@ -655,12 +656,16 @@ impl TestContext {
}
pub async fn assert_alice_punished(&self, state: AliceState) {
assert!(matches!(state, AliceState::BtcPunished { .. }));
let (cancel_fee, punish_fee) = match state {
AliceState::BtcPunished { state3 } => (state3.tx_cancel_fee, state3.tx_punish_fee),
_ => panic!("Alice is not in btc punished state: {:?}", state),
};
assert_eventual_balance(
self.alice_bitcoin_wallet.as_ref(),
Ordering::Equal,
self.alice_punished_btc_balance().await,
self.alice_punished_btc_balance(cancel_fee, punish_fee)
.await,
)
.await
.unwrap();
@ -698,10 +703,13 @@ impl TestContext {
pub async fn assert_bob_refunded(&self, state: BobState) {
self.bob_bitcoin_wallet.sync().await.unwrap();
let lock_tx_id = if let BobState::BtcRefunded(state4) = state {
state4.tx_lock_id()
} else {
panic!("Bob is not in btc refunded state: {:?}", state);
let (lock_tx_id, cancel_fee, refund_fee) = match state {
BobState::BtcRefunded(state6) => (
state6.tx_lock_id(),
state6.tx_cancel_fee,
state6.tx_refund_fee,
),
_ => panic!("Bob is not in btc refunded state: {:?}", state),
};
let lock_tx_bitcoin_fee = self
.bob_bitcoin_wallet
@ -711,17 +719,6 @@ impl TestContext {
let btc_balance_after_swap = self.bob_bitcoin_wallet.balance().await.unwrap();
let cancel_fee = self
.alice_bitcoin_wallet
.estimate_fee(TxCancel::weight(), self.btc_amount)
.await
.expect("To estimate fee correctly");
let refund_fee = self
.alice_bitcoin_wallet
.estimate_fee(TxRefund::weight(), self.btc_amount)
.await
.expect("To estimate fee correctly");
let bob_cancelled_and_refunded = btc_balance_after_swap
== self.bob_starting_balances.btc - lock_tx_bitcoin_fee - cancel_fee - refund_fee;
@ -759,11 +756,21 @@ impl TestContext {
}
async fn alice_redeemed_btc_balance(&self) -> bitcoin::Amount {
// Get the last transaction Alice published
// This should be btc_redeem
let txid = self
.alice_bitcoin_wallet
.last_published_txid()
.await
.unwrap();
// Get the fee for the last transaction
let fee = self
.alice_bitcoin_wallet
.estimate_fee(TxRedeem::weight(), self.btc_amount)
.transaction_fee(txid)
.await
.expect("To estimate fee correctly");
self.alice_starting_balances.btc + self.btc_amount - fee
}
@ -801,17 +808,11 @@ impl TestContext {
self.alice_starting_balances.xmr - self.xmr_amount
}
async fn alice_punished_btc_balance(&self) -> bitcoin::Amount {
let cancel_fee = self
.alice_bitcoin_wallet
.estimate_fee(TxCancel::weight(), self.btc_amount)
.await
.expect("To estimate fee correctly");
let punish_fee = self
.alice_bitcoin_wallet
.estimate_fee(TxPunish::weight(), self.btc_amount)
.await
.expect("To estimate fee correctly");
async fn alice_punished_btc_balance(
&self,
cancel_fee: bitcoin::Amount,
punish_fee: bitcoin::Amount,
) -> bitcoin::Amount {
self.alice_starting_balances.btc + self.btc_amount - cancel_fee - punish_fee
}