mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-24 09:53:09 -05:00
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:
parent
854b14939e
commit
091ba57547
23 changed files with 995 additions and 358 deletions
|
|
@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- We add a safety margin of 25% to the Bitcoin fee estimation. This ensures pre-signed transactions will get confirmed in time.
|
||||
- The Bitcoin fee estimation is now more accurate. It uses a combination of `estimatesmartfee` from Bitcoin Core and `mempool.get_fee_histogram` from Electrum to ensure our distance from the mempool tip is appropriate. If our Electrum server doesn't support fee estimation, we use the mempool.space API. The mempool space API can be disabled using the `bitcoin.use_mempool_space_fee_estimation` option in the config file. It defaults to `true`.
|
||||
|
||||
## [1.1.2] - 2025-05-24
|
||||
|
||||
|
|
|
|||
|
|
@ -25,4 +25,4 @@
|
|||
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab",
|
||||
"https://plugins.dprint.dev/prettier-0.26.6.json@0118376786f37496e41bb19dbcfd1e7214e2dc859a55035c5e54d1107b4c9c57"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
justfile
4
justfile
|
|
@ -50,7 +50,7 @@ update_submodules:
|
|||
cd monero-sys && git submodule update --init --recursive --force
|
||||
|
||||
# Run clippy checks
|
||||
clippy_check:
|
||||
clippy:
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
# Check the bindings for the Tauri API
|
||||
|
|
@ -71,4 +71,4 @@ fmt:
|
|||
|
||||
# Sometimes you have to prune the docker network to get the integration tests to work
|
||||
docker-prune-network:
|
||||
docker network prune -f
|
||||
docker network prune -f
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue