Add a mandatory --change-address parameter to buy-xmr

Fixes #513.
This commit is contained in:
Thomas Eizinger 2021-07-06 19:17:17 +10:00
parent 683d565679
commit 5463bde4f8
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96
12 changed files with 135 additions and 13 deletions

View File

@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
The rendezvous node address (`rendezvous_point`), as well as the ASB's external addresses (`external_addresses`) to be registered, is configured in the `network` section of the ASB config file. The rendezvous node address (`rendezvous_point`), as well as the ASB's external addresses (`external_addresses`) to be registered, is configured in the `network` section of the ASB config file.
A rendezvous node is provided at `/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o` which is used as default for discovery in the CLI. A rendezvous node is provided at `/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o` which is used as default for discovery in the CLI.
Upon discovery using `list-sellers` CLI users are provided with quote and connection information for each ASB discovered through the rendezvous node. Upon discovery using `list-sellers` CLI users are provided with quote and connection information for each ASB discovered through the rendezvous node.
- A mandatory `--change-address` parameter to the CLI's `buy-xmr` command.
The provided address is used to transfer Bitcoin in case of a refund and in case the user transfers more than the specified amount into the swap.
For more information see [#513](https://github.com/comit-network/xmr-btc-swap/issues/513).
### Fixed ### Fixed

View File

@ -45,7 +45,7 @@ rust_decimal_macros = "1"
serde = { version = "1", features = [ "derive" ] } serde = { version = "1", features = [ "derive" ] }
serde_cbor = "0.11" serde_cbor = "0.11"
serde_json = "1" serde_json = "1"
serde_with = { version = "1.9.4", features = [ "macros" ] } serde_with = { version = "1", features = [ "macros" ] }
sha2 = "0.9" sha2 = "0.9"
sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde" ] } sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde" ] }
sled = "0.34" sled = "0.34"

View File

@ -231,7 +231,9 @@ async fn main() -> Result<()> {
} }
}; };
let psbt = bitcoin_wallet.send_to_address(address, amount).await?; let psbt = bitcoin_wallet
.send_to_address(address, amount, None)
.await?;
let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?; let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?;
bitcoin_wallet.broadcast(signed_tx, "withdraw").await?; bitcoin_wallet.broadcast(signed_tx, "withdraw").await?;

View File

@ -59,6 +59,7 @@ async fn main() -> Result<()> {
seller, seller,
bitcoin_electrum_rpc_url, bitcoin_electrum_rpc_url,
bitcoin_target_block, bitcoin_target_block,
bitcoin_change_address,
monero_receive_address, monero_receive_address,
monero_daemon_address, monero_daemon_address,
tor_socks5_port, tor_socks5_port,
@ -122,6 +123,7 @@ async fn main() -> Result<()> {
env_config, env_config,
event_loop_handle, event_loop_handle,
monero_receive_address, monero_receive_address,
bitcoin_change_address,
amount, amount,
); );

View File

@ -24,6 +24,7 @@ impl TxLock {
amount: Amount, amount: Amount,
A: PublicKey, A: PublicKey,
B: PublicKey, B: PublicKey,
change: bitcoin::Address,
) -> Result<Self> ) -> Result<Self>
where where
C: EstimateFeeRate, C: EstimateFeeRate,
@ -34,7 +35,9 @@ impl TxLock {
.address(wallet.get_network()) .address(wallet.get_network())
.expect("can derive address from descriptor"); .expect("can derive address from descriptor");
let psbt = wallet.send_to_address(address, amount).await?; let psbt = wallet
.send_to_address(address, amount, Some(change))
.await?;
Ok(Self { Ok(Self {
inner: psbt, inner: psbt,
@ -251,7 +254,11 @@ mod tests {
wallet: &Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate>, wallet: &Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate>,
amount: Amount, amount: Amount,
) -> PartiallySignedTransaction { ) -> PartiallySignedTransaction {
TxLock::new(&wallet, amount, A, B).await.unwrap().into() let change = wallet.new_address().await.unwrap();
TxLock::new(&wallet, amount, A, B, change)
.await
.unwrap()
.into()
} }
fn alice_and_bob() -> (PublicKey, PublicKey) { fn alice_and_bob() -> (PublicKey, PublicKey) {

View File

@ -309,11 +309,18 @@ where
&self, &self,
address: Address, address: Address,
amount: Amount, amount: Amount,
change_override: Option<Address>,
) -> Result<PartiallySignedTransaction> { ) -> Result<PartiallySignedTransaction> {
if self.network != address.network { if self.network != address.network {
bail!("Cannot build PSBT because network of given address is {} but wallet is on network {}", address.network, self.network); bail!("Cannot build PSBT because network of given address is {} but wallet is on network {}", address.network, self.network);
} }
if let Some(change) = change_override.as_ref() {
if self.network != change.network {
bail!("Cannot build PSBT because network of given address is {} but wallet is on network {}", change.network, self.network);
}
}
let wallet = self.wallet.lock().await; let wallet = self.wallet.lock().await;
let client = self.client.lock().await; let client = self.client.lock().await;
let fee_rate = client.estimate_feerate(self.target_block)?; let fee_rate = client.estimate_feerate(self.target_block)?;
@ -340,6 +347,15 @@ where
_ => bail!("Unexpected transaction layout"), _ => bail!("Unexpected transaction layout"),
} }
if let ([_, change], [_, psbt_output], Some(change_override)) = (
&mut psbt.global.unsigned_tx.output.as_mut_slice(),
&mut psbt.outputs.as_mut_slice(),
change_override,
) {
change.script_pubkey = change_override.script_pubkey();
psbt_output.bip32_derivation.clear();
}
Ok(psbt) Ok(psbt)
} }
@ -1059,7 +1075,8 @@ mod tests {
// if the change output is below dust it will be dropped by the BDK // if the change output is below dust it will be dropped by the BDK
for amount in above_dust..(balance - (above_dust - 1)) { for amount in above_dust..(balance - (above_dust - 1)) {
let (A, B) = (PublicKey::random(), PublicKey::random()); let (A, B) = (PublicKey::random(), PublicKey::random());
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B) let change = wallet.new_address().await.unwrap();
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B, change)
.await .await
.unwrap(); .unwrap();
let txlock_output = txlock.script_pubkey(); let txlock_output = txlock.script_pubkey();
@ -1074,4 +1091,30 @@ mod tests {
); );
} }
} }
#[tokio::test]
async fn can_override_change_address() {
let wallet = Wallet::new_funded_default_fees(50_000);
let custom_change = "bcrt1q08pfqpsyrt7acllzyjm8q5qsz5capvyahm49rw"
.parse::<Address>()
.unwrap();
let psbt = wallet
.send_to_address(
wallet.new_address().await.unwrap(),
Amount::from_sat(10_000),
Some(custom_change.clone()),
)
.await
.unwrap();
let transaction = wallet.sign_and_finalize(psbt).await.unwrap();
match transaction.output.as_slice() {
[first, change] => {
assert_eq!(first.value, 10_000);
assert_eq!(change.script_pubkey, custom_change.script_pubkey());
}
_ => panic!("expected exactly two outputs"),
}
}
} }

View File

@ -74,6 +74,7 @@ where
bitcoin_electrum_rpc_url, bitcoin_electrum_rpc_url,
bitcoin_target_block, bitcoin_target_block,
}, },
bitcoin_change_address,
monero: monero:
Monero { Monero {
monero_receive_address, monero_receive_address,
@ -92,6 +93,7 @@ where
is_testnet, is_testnet,
)?, )?,
bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet), bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet),
bitcoin_change_address,
monero_receive_address: validate_monero_address( monero_receive_address: validate_monero_address(
monero_receive_address, monero_receive_address,
is_testnet, is_testnet,
@ -214,6 +216,7 @@ pub enum Command {
seller: Multiaddr, seller: Multiaddr,
bitcoin_electrum_rpc_url: Url, bitcoin_electrum_rpc_url: Url,
bitcoin_target_block: usize, bitcoin_target_block: usize,
bitcoin_change_address: bitcoin::Address,
monero_receive_address: monero::Address, monero_receive_address: monero::Address,
monero_daemon_address: String, monero_daemon_address: String,
tor_socks5_port: u16, tor_socks5_port: u16,
@ -287,6 +290,12 @@ pub enum RawCommand {
#[structopt(flatten)] #[structopt(flatten)]
bitcoin: Bitcoin, bitcoin: Bitcoin,
#[structopt(
long = "change-address",
help = "The bitcoin address where any form of change or excess funds should be sent to"
)]
bitcoin_change_address: bitcoin::Address,
#[structopt(flatten)] #[structopt(flatten)]
monero: Monero, monero: Monero,
@ -347,7 +356,7 @@ pub enum RawCommand {
#[derive(structopt::StructOpt, Debug)] #[derive(structopt::StructOpt, Debug)]
pub struct Monero { pub struct Monero {
#[structopt(long = "receive-address", #[structopt(long = "receive-address",
help = "Provide the monero address where you would like to receive monero", help = "The monero address where you would like to receive monero",
parse(try_from_str = parse_monero_address) parse(try_from_str = parse_monero_address)
)] )]
pub monero_receive_address: monero::Address, pub monero_receive_address: monero::Address,
@ -520,7 +529,9 @@ mod tests {
const MAINNET: &str = "mainnet"; const MAINNET: &str = "mainnet";
const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a";
const BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv";
const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa"; const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa";
const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325";
const MULTI_ADDRESS: &str = const MULTI_ADDRESS: &str =
"/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi";
const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
@ -532,6 +543,8 @@ mod tests {
"buy-xmr", "buy-xmr",
"--receive-address", "--receive-address",
MONERO_MAINNET_ADDRESS, MONERO_MAINNET_ADDRESS,
"--change-address",
BITCOIN_MAINNET_ADDRESS,
"--seller", "--seller",
MULTI_ADDRESS, MULTI_ADDRESS,
]; ];
@ -550,6 +563,8 @@ mod tests {
"buy-xmr", "buy-xmr",
"--receive-address", "--receive-address",
MONERO_STAGENET_ADDRESS, MONERO_STAGENET_ADDRESS,
"--change-address",
BITCOIN_TESTNET_ADDRESS,
"--seller", "--seller",
MULTI_ADDRESS, MULTI_ADDRESS,
]; ];
@ -569,6 +584,8 @@ mod tests {
"buy-xmr", "buy-xmr",
"--receive-address", "--receive-address",
MONERO_STAGENET_ADDRESS, MONERO_STAGENET_ADDRESS,
"--change-address",
BITCOIN_TESTNET_ADDRESS,
"--seller", "--seller",
MULTI_ADDRESS, MULTI_ADDRESS,
]; ];
@ -592,6 +609,8 @@ mod tests {
"buy-xmr", "buy-xmr",
"--receive-address", "--receive-address",
MONERO_MAINNET_ADDRESS, MONERO_MAINNET_ADDRESS,
"--change-address",
BITCOIN_MAINNET_ADDRESS,
"--seller", "--seller",
MULTI_ADDRESS, MULTI_ADDRESS,
]; ];
@ -703,6 +722,8 @@ mod tests {
"--data-dir", "--data-dir",
data_dir, data_dir,
"buy-xmr", "buy-xmr",
"--change-address",
BITCOIN_MAINNET_ADDRESS,
"--receive-address", "--receive-address",
MONERO_MAINNET_ADDRESS, MONERO_MAINNET_ADDRESS,
"--seller", "--seller",
@ -725,6 +746,8 @@ mod tests {
"--data-dir", "--data-dir",
data_dir, data_dir,
"buy-xmr", "buy-xmr",
"--change-address",
BITCOIN_TESTNET_ADDRESS,
"--receive-address", "--receive-address",
MONERO_STAGENET_ADDRESS, MONERO_STAGENET_ADDRESS,
"--seller", "--seller",
@ -791,6 +814,8 @@ mod tests {
BINARY_NAME, BINARY_NAME,
"--debug", "--debug",
"buy-xmr", "buy-xmr",
"--change-address",
BITCOIN_MAINNET_ADDRESS,
"--receive-address", "--receive-address",
MONERO_MAINNET_ADDRESS, MONERO_MAINNET_ADDRESS,
"--seller", "--seller",
@ -808,6 +833,8 @@ mod tests {
"--testnet", "--testnet",
"--debug", "--debug",
"buy-xmr", "buy-xmr",
"--change-address",
BITCOIN_TESTNET_ADDRESS,
"--receive-address", "--receive-address",
MONERO_STAGENET_ADDRESS, MONERO_STAGENET_ADDRESS,
"--seller", "--seller",
@ -860,6 +887,8 @@ mod tests {
BINARY_NAME, BINARY_NAME,
"--json", "--json",
"buy-xmr", "buy-xmr",
"--change-address",
BITCOIN_MAINNET_ADDRESS,
"--receive-address", "--receive-address",
MONERO_MAINNET_ADDRESS, MONERO_MAINNET_ADDRESS,
"--seller", "--seller",
@ -877,6 +906,8 @@ mod tests {
"--testnet", "--testnet",
"--json", "--json",
"buy-xmr", "buy-xmr",
"--change-address",
BITCOIN_TESTNET_ADDRESS,
"--receive-address", "--receive-address",
MONERO_STAGENET_ADDRESS, MONERO_STAGENET_ADDRESS,
"--seller", "--seller",
@ -935,6 +966,7 @@ mod tests {
bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET)
.unwrap(), .unwrap(),
bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET,
bitcoin_change_address: BITCOIN_TESTNET_ADDRESS.parse().unwrap(),
monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS) monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS)
.unwrap(), .unwrap(),
monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string(), monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string(),
@ -953,6 +985,7 @@ mod tests {
seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(), seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(),
bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET,
bitcoin_change_address: BITCOIN_MAINNET_ADDRESS.parse().unwrap(),
monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS) monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS)
.unwrap(), .unwrap(),
monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS.to_string(), monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS.to_string(),

View File

@ -3,13 +3,17 @@ use crate::protocol::bob;
use crate::protocol::bob::BobState; use crate::protocol::bob::BobState;
use monero_rpc::wallet::BlockHeight; use monero_rpc::wallet::BlockHeight;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::fmt; use std::fmt;
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Bob { pub enum Bob {
Started { Started {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")] #[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc_amount: bitcoin::Amount, btc_amount: bitcoin::Amount,
#[serde_as(as = "DisplayFromStr")]
change_address: bitcoin::Address,
}, },
ExecutionSetupDone { ExecutionSetupDone {
state2: bob::State2, state2: bob::State2,
@ -45,7 +49,13 @@ pub enum BobEndState {
impl From<BobState> for Bob { impl From<BobState> for Bob {
fn from(bob_state: BobState) -> Self { fn from(bob_state: BobState) -> Self {
match bob_state { match bob_state {
BobState::Started { btc_amount } => Bob::Started { btc_amount }, BobState::Started {
btc_amount,
change_address,
} => Bob::Started {
btc_amount,
change_address,
},
BobState::SwapSetupCompleted(state2) => Bob::ExecutionSetupDone { state2 }, BobState::SwapSetupCompleted(state2) => Bob::ExecutionSetupDone { state2 },
BobState::BtcLocked(state3) => Bob::BtcLocked { state3 }, BobState::BtcLocked(state3) => Bob::BtcLocked { state3 },
BobState::XmrLockProofReceived { BobState::XmrLockProofReceived {
@ -77,7 +87,13 @@ impl From<BobState> for Bob {
impl From<Bob> for BobState { impl From<Bob> for BobState {
fn from(db_state: Bob) -> Self { fn from(db_state: Bob) -> Self {
match db_state { match db_state {
Bob::Started { btc_amount } => BobState::Started { btc_amount }, Bob::Started {
btc_amount,
change_address,
} => BobState::Started {
btc_amount,
change_address,
},
Bob::ExecutionSetupDone { state2 } => BobState::SwapSetupCompleted(state2), Bob::ExecutionSetupDone { state2 } => BobState::SwapSetupCompleted(state2),
Bob::BtcLocked { state3 } => BobState::BtcLocked(state3), Bob::BtcLocked { state3 } => BobState::BtcLocked(state3),
Bob::XmrLockProofReceived { Bob::XmrLockProofReceived {

View File

@ -33,10 +33,14 @@ impl Swap {
env_config: env::Config, env_config: env::Config,
event_loop_handle: cli::EventLoopHandle, event_loop_handle: cli::EventLoopHandle,
monero_receive_address: monero::Address, monero_receive_address: monero::Address,
bitcoin_change_address: bitcoin::Address,
btc_amount: bitcoin::Amount, btc_amount: bitcoin::Amount,
) -> Self { ) -> Self {
Self { Self {
state: BobState::Started { btc_amount }, state: BobState::Started {
btc_amount,
change_address: bitcoin_change_address,
},
event_loop_handle, event_loop_handle,
db, db,
bitcoin_wallet, bitcoin_wallet,
@ -47,6 +51,7 @@ impl Swap {
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn from_db( pub fn from_db(
db: Database, db: Database,
id: Uuid, id: Uuid,

View File

@ -25,6 +25,7 @@ use uuid::Uuid;
pub enum BobState { pub enum BobState {
Started { Started {
btc_amount: bitcoin::Amount, btc_amount: bitcoin::Amount,
change_address: bitcoin::Address,
}, },
SwapSetupCompleted(State2), SwapSetupCompleted(State2),
BtcLocked(State3), BtcLocked(State3),
@ -169,7 +170,14 @@ impl State0 {
bail!("Alice's dleq proof doesn't verify") bail!("Alice's dleq proof doesn't verify")
} }
let tx_lock = bitcoin::TxLock::new(wallet, self.btc, msg.A, self.b.public()).await?; let tx_lock = bitcoin::TxLock::new(
wallet,
self.btc,
msg.A,
self.b.public(),
self.refund_address.clone(),
)
.await?;
let v = msg.v_a + self.v_b; let v = msg.v_a + self.v_b;
Ok(State1 { Ok(State1 {

View File

@ -61,8 +61,10 @@ async fn next_state(
tracing::trace!(%state, "Advancing state"); tracing::trace!(%state, "Advancing state");
Ok(match state { Ok(match state {
BobState::Started { btc_amount } => { BobState::Started {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?; btc_amount,
change_address,
} => {
let tx_refund_fee = bitcoin_wallet let tx_refund_fee = bitcoin_wallet
.estimate_fee(TxRefund::weight(), btc_amount) .estimate_fee(TxRefund::weight(), btc_amount)
.await?; .await?;
@ -76,7 +78,7 @@ async fn next_state(
btc: btc_amount, btc: btc_amount,
tx_refund_fee, tx_refund_fee,
tx_cancel_fee, tx_cancel_fee,
bitcoin_refund_address, bitcoin_refund_address: change_address,
}) })
.await?; .await?;

View File

@ -434,6 +434,7 @@ impl BobParams {
self.env_config, self.env_config,
handle, handle,
self.monero_wallet.get_main_address(), self.monero_wallet.get_main_address(),
self.bitcoin_wallet.new_address().await?,
btc_amount, btc_amount,
); );