mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-17 17:44:02 -05:00
feat(asb): Disabled-by-default developer tip (#566)
* wip * filter out destinations with zero amount * add changelog entry * do not use subtract_fee_from_outputs for multi lock tx * make developer tip address configurable * create developer_tip_monero_wallet in integration test * add happy_path_alice_developer_tip integration test * run happy_path_alice_developer_tip integration test in ci * make stub tests * document `developer_tip` and add log at startup * take tip consideration when crafting quote * remove double g++ * fix bash * full stack trace in logs * add DEFAULT_DEVELOPER_TIP_ADDRESS_STAGENET * fix issue where --testnet could not be detected * triple bitcoin testnet timelocks * assert hardcoded developer tip address is on the correct network * fix: interpret developer_tip = 0 as no tip in log at startup * change developer_tip type to non-option, clamp tips below 100_00 piconero to 0 * create dedidcated TipConfig struct to replace (Decimal, monero::Address) * small refactorings * move tip config init out of of function call params * refactoring * unit tests wrong arguments passed in * document `developer_tip` allows configuring your maker to donate a small part of swaps to funding further development of the project. This is disabled by default. You can manually enable it if you choose to do so. Set it to a number between 0 and 1. Setting it to 0.02 will donate 2% of each swap to the donation address of the project. The tip is sent by adding an additional output the Monero lock transaction of a swap. This means this will not impact document `maker.developer_tip` in docs/pages/becoming_a_maker/overview.mdx * do not panic if cxx_addrs / cxx_amounts is null * formatting, full error stack trace in swap/src/asb/event_loop.rs when we cannot construct a quote * increase MIN_USEFUL_TIP_AMOUNT_PICONERO to 30m piconero (usd), correct params in unit tests * prompt for developer_tip in orchestrator and asb wizard * just fmt * fmt * address comments * fmt * spelling mistakes --------- Co-authored-by: binarybaron <binarybaron@mail.mail>
This commit is contained in:
parent
5f2c737a85
commit
2ec6323c45
46 changed files with 690 additions and 148 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -109,6 +109,8 @@ jobs:
|
|||
include:
|
||||
- package: swap
|
||||
test_name: happy_path
|
||||
- package: swap
|
||||
test_name: happy_path_alice_developer_tip
|
||||
- package: swap
|
||||
test_name: happy_path_restart_bob_after_xmr_locked
|
||||
- package: swap
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
|
||||
- ORCHESTRATOR: Introduce a new `asb-tracing-logger` container within the `docker-compose.yml`. The `asb-tracing-logger` gives you access to the tracing (high verbosity) logs of your asb. Download the new `orchestrator` and run it. Then run `docker compose up -d`. The new `asb-tracing-logger` container will be created. Then run `docker compose logs -f --tail 100 asb-tracing-logger` to view detailed logging and see what is going on behind the scenes. The `asb` will continue printing less-verbose logs like before.
|
||||
- ASB: You can now configure your maker to donate a small part of swaps to funding further development of the project. This is disabled by default. You can manually enable it if you choose to do so. Set `maker.developer_tip` to a number between 0 and 1. Setting `maker.developer_tip` to `0.02` will donate 2% of each swap to the [donation address](https://github.com/eigenwallet/core?tab=readme-ov-file#donations) of the project. This is defined [here](https://github.com/eigenwallet/core/blob/ce4a85bfdd3b3fd6fbdf6c4c1ab0e1c3188b7fc2/swap-env/src/defaults.rs#L9) in the code. The tip is sent by adding an additional output to the Monero lock transaction of a swap. This means this will not impact the availability of your UTXOs (unlocked funds) as it does not require an additonal transaction. Because tips are only ever sent in Monero you maintain full privacy.
|
||||
- ASB + CLI + GUI (Testnet only): Bitcoin timelocks have been tripled. This has no affect for mainnet swaps. Blocktimes are too low on testnet to be able to test reliably.
|
||||
|
||||
## [3.0.2] - 2025-09-21
|
||||
|
||||
|
|
|
|||
54
Cargo.lock
generated
54
Cargo.lock
generated
|
|
@ -2127,7 +2127,7 @@ dependencies = [
|
|||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2461,6 +2461,19 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width 0.2.1",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
|
@ -3495,7 +3508,7 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||
dependencies = [
|
||||
"console",
|
||||
"console 0.15.11",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
|
|
@ -13566,6 +13579,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bitcoin 0.32.7",
|
||||
"config",
|
||||
"console 0.16.1",
|
||||
"dialoguer",
|
||||
"libp2p",
|
||||
"monero",
|
||||
|
|
@ -13573,6 +13587,7 @@ dependencies = [
|
|||
"serde",
|
||||
"swap-fs",
|
||||
"swap-serde",
|
||||
"terminal_size",
|
||||
"thiserror 1.0.69",
|
||||
"time 0.3.41",
|
||||
"toml 0.9.5",
|
||||
|
|
@ -17456,7 +17471,7 @@ dependencies = [
|
|||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
|
|
@ -17499,7 +17514,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
|||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings",
|
||||
]
|
||||
|
|
@ -17511,7 +17526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
|
|
@ -17565,6 +17580,12 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
|
|
@ -17572,7 +17593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -17581,7 +17602,7 @@ version = "0.5.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings",
|
||||
]
|
||||
|
|
@ -17601,7 +17622,7 @@ version = "0.3.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -17610,7 +17631,7 @@ version = "0.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -17658,6 +17679,15 @@ dependencies = [
|
|||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
|
||||
dependencies = [
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
|
|
@ -17710,7 +17740,7 @@ version = "0.53.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
|
|
@ -17727,7 +17757,7 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -17736,7 +17766,7 @@ version = "0.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ max_buy_btc = 0.0001
|
|||
ask_spread = 0.02
|
||||
price_ticker_ws_url = "wss://ws.kraken.com"
|
||||
external_bitcoin_address = "bc1..."
|
||||
developer_tip = 0.02
|
||||
```
|
||||
|
||||
The minimum and maximum amount as well as a spread, that is added on top of the price fetched from a central exchange, can be configured.
|
||||
|
|
@ -127,6 +128,8 @@ The minimum and maximum amount as well as a spread, that is added on top of the
|
|||
`external_bitcoin_address` allows to specify the Bitcoin address that the ASB will use to redeem or punish swaps.
|
||||
If the option is not set, a new address from the internal wallet is used for every swap.
|
||||
|
||||
`developer_tip` allows configuring your maker to donate a small part of swaps to funding further development of the project. This is disabled by default. You can manually enable it if you choose to do so. Set it to a number between 0 and 1. Setting it to 0.02 will donate 2% of each swap to the donation address of the project. The tip is sent by adding an additional output to the Monero lock transaction of a swap. This means this will not impact the availability of your UTXOs (unlocked funds) as it does not require an additonal transaction.
|
||||
|
||||
In order to be able to trade, the ASB must define a price to be able to agree on the amounts to be swapped with a CLI.
|
||||
The `XMR<>BTC` price is currently determined by the price from the central exchange Kraken.
|
||||
Upon startup the ASB connects to the Kraken price websocket and listens on the stream for price updates.
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ max_buy_btc = 0.1
|
|||
ask_spread = 0.02
|
||||
price_ticker_ws_url = "wss://ws.kraken.com/"
|
||||
external_bitcoin_address = "bc1..."
|
||||
developer_tip = 0.02
|
||||
|
||||
# ...
|
||||
```
|
||||
|
|
@ -131,6 +132,7 @@ Below an explanation of what each option does:
|
|||
| `ask_spread` | The markup the asb will charge compared to the market price, as a factor. The market price is fetched via the `price_ticker_ws_url`. A value of `0.02` means the asb will charge 2% more than the market price. |
|
||||
| `price_ticker_ws_url` | The URL of a websocket that provides the market price. The default is the Kraken API, but you can build your own websocket server which mimics the Kraken API. |
|
||||
| `external_bitcoin_address` | Bitcoin address used by the asb when redeeming or punishing swaps. If omitted, a new internal address is generated for each swap. |
|
||||
| `developer_tip` | Optional donation as a fraction between 0 and 1 (e.g. `0.02` = 2%). Disabled by default. Sent in Monero by adding an extra output to the Monero lock transaction, so no extra transaction and no impact on unlocked UTXOs; privacy preserved. |
|
||||
|
||||
### Bitcoin Section
|
||||
|
||||
|
|
|
|||
3
justfile
3
justfile
|
|
@ -60,6 +60,9 @@ tests:
|
|||
docker_test_happy_path:
|
||||
cargo test --package swap --test happy_path -- --nocapture
|
||||
|
||||
docker_test_happy_path_with_developer_tip:
|
||||
cargo test --package swap --test happy_path_alice_developer_tip -- --nocapture
|
||||
|
||||
docker_test_all:
|
||||
cargo test --package swap --test all -- --nocapture
|
||||
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ impl MoneroWallet {
|
|||
);
|
||||
let amount = Amount::from_pico(amount_pico);
|
||||
self.wallet
|
||||
.transfer(address, amount)
|
||||
.transfer_single_destination(address, amount)
|
||||
.await
|
||||
.context("Failed to perform transfer")
|
||||
}
|
||||
|
|
@ -515,7 +515,7 @@ impl MoneroWallet {
|
|||
self.balance().await?;
|
||||
|
||||
self.wallet
|
||||
.sweep_multi(addresses, ratios)
|
||||
.sweep_multi_destination(addresses, ratios)
|
||||
.await
|
||||
.context("Failed to perform sweep")?
|
||||
.into_iter()
|
||||
|
|
|
|||
|
|
@ -156,12 +156,15 @@ namespace Monero
|
|||
|
||||
/**
|
||||
* Creates a transaction that spends the unlocked balance to multiple destinations with given ratios.
|
||||
* Ratiosn must sum to 1.
|
||||
*/
|
||||
inline PendingTransaction *createTransactionMultiDest(
|
||||
Wallet &wallet,
|
||||
const std::vector<std::string> &dest_addresses,
|
||||
const std::vector<uint64_t> &amounts)
|
||||
const std::vector<uint64_t> &amounts,
|
||||
// If set to true, the fee will be subtracted from output with the highest amount
|
||||
// If set to false, the fee will be paid by the wallet and the exact amounts will be sent to the destinations
|
||||
bool subtract_fee_from_outputs
|
||||
)
|
||||
{
|
||||
size_t n = dest_addresses.size();
|
||||
|
||||
|
|
@ -185,9 +188,15 @@ namespace Monero
|
|||
|
||||
// Find the highest output and choose it for subtract_fee_indices
|
||||
std::set<uint32_t> subtract_fee_indices;
|
||||
auto max_it = std::max_element(amounts.begin(), amounts.end());
|
||||
size_t max_index = std::distance(amounts.begin(), max_it);
|
||||
subtract_fee_indices.insert(static_cast<uint32_t>(max_index));
|
||||
|
||||
// If subtract_fee_from_outputs = false, this will not be executed and
|
||||
// subtract_fee_indices will remain empty which symbolizes that the fee will be paid by the wallet
|
||||
// and the exact amounts will be sent to the destinations
|
||||
if (subtract_fee_from_outputs) {
|
||||
auto max_it = std::max_element(amounts.begin(), amounts.end());
|
||||
size_t max_index = std::distance(amounts.begin(), max_it);
|
||||
subtract_fee_indices.insert(static_cast<uint32_t>(max_index));
|
||||
}
|
||||
|
||||
return wallet.createTransactionMultDest(
|
||||
dest_addresses,
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ pub mod ffi {
|
|||
wallet: Pin<&mut Wallet>,
|
||||
dest_addresses: &CxxVector<CxxString>,
|
||||
amounts: &CxxVector<u64>,
|
||||
subtract_fee_from_outputs: bool,
|
||||
) -> *mut PendingTransaction;
|
||||
|
||||
fn vector_string_push_back(v: Pin<&mut CxxVector<CxxString>>, s: &CxxString);
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ impl WalletHandle {
|
|||
}
|
||||
|
||||
/// Transfer funds to an address without approval.
|
||||
pub async fn transfer(
|
||||
pub async fn transfer_single_destination(
|
||||
&self,
|
||||
address: &monero::Address,
|
||||
amount: monero::Amount,
|
||||
|
|
@ -467,7 +467,7 @@ impl WalletHandle {
|
|||
let address = *address;
|
||||
|
||||
retry_notify(backoff(None, None), || async {
|
||||
self.call(move |wallet| wallet.transfer(&address, amount))
|
||||
self.call(move |wallet| wallet.transfer_single_destination(&address, amount))
|
||||
.await
|
||||
.map_err(backoff::Error::transient)
|
||||
}, |error, duration: Duration| {
|
||||
|
|
@ -477,6 +477,26 @@ impl WalletHandle {
|
|||
.map_err(|e| anyhow!("Failed to transfer funds after multiple attempts: {e}"))
|
||||
}
|
||||
|
||||
/// Transfer funds to multiple addresses in a single transaction without approval.
|
||||
pub async fn transfer_multi_destination(
|
||||
&self,
|
||||
destinations: &[(monero::Address, monero::Amount)],
|
||||
) -> anyhow::Result<TxReceipt> {
|
||||
let destinations = destinations.to_vec();
|
||||
|
||||
retry_notify(backoff(None, None), || async {
|
||||
let destinations = destinations.clone();
|
||||
|
||||
self.call(move |wallet| wallet.transfer_multi_destination(&destinations))
|
||||
.await
|
||||
.map_err(backoff::Error::transient)
|
||||
}, |error, duration: Duration| {
|
||||
tracing::error!(error=?error, "Failed to transfer funds to multiple destinations, retrying in {} secs", duration.as_secs());
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to transfer funds to multiple destinations after multiple attempts: {e:?}"))
|
||||
}
|
||||
|
||||
/// Sweep all funds to an address.
|
||||
pub async fn sweep(&self, address: &monero::Address) -> anyhow::Result<Vec<TxReceipt>> {
|
||||
let address = *address;
|
||||
|
|
@ -486,10 +506,10 @@ impl WalletHandle {
|
|||
.await
|
||||
.map_err(backoff::Error::transient)
|
||||
}, |error, duration: Duration| {
|
||||
tracing::error!(error=%error, "Failed to sweep funds, retrying in {} secs", duration.as_secs());
|
||||
tracing::error!(error=?error, "Failed to sweep funds, retrying in {} secs", duration.as_secs());
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to sweep funds after multiple attempts: {e}"))
|
||||
.map_err(|e| anyhow!("Failed to sweep funds after multiple attempts: {e:?}"))
|
||||
}
|
||||
|
||||
/// Get the seed of the wallet.
|
||||
|
|
@ -505,7 +525,7 @@ impl WalletHandle {
|
|||
/// Sweep all funds to a set of addresses.
|
||||
/// If the address is `None`, the address will be set to the primary address of the
|
||||
/// wallet
|
||||
pub async fn sweep_multi(
|
||||
pub async fn sweep_multi_destination(
|
||||
&self,
|
||||
addresses: &[monero::Address],
|
||||
percentages: &[f64],
|
||||
|
|
@ -834,7 +854,9 @@ impl WalletHandle {
|
|||
let (uuid, txid, amount, fee) = self
|
||||
.call_with_pending_txs(move |wallet, pending_txs| {
|
||||
let pending_tx = match amount {
|
||||
Some(amount) => wallet.create_pending_transaction(&address, amount)?,
|
||||
Some(amount) => {
|
||||
wallet.create_pending_transaction_single_dest(&address, amount)?
|
||||
}
|
||||
None => wallet.create_pending_sweep_transaction(&address)?,
|
||||
};
|
||||
|
||||
|
|
@ -1878,12 +1900,25 @@ impl FfiWallet {
|
|||
/// Transfer a specified amount of monero to a specified address and return a receipt containing
|
||||
/// the transaction id, transaction key and current blockchain height. This can be used later
|
||||
/// to prove the transfer or to wait for confirmations.
|
||||
fn transfer(
|
||||
fn transfer_single_destination(
|
||||
&mut self,
|
||||
address: &monero::Address,
|
||||
amount: monero::Amount,
|
||||
) -> anyhow::Result<TxReceipt> {
|
||||
let mut pending_tx = self.create_pending_transaction(address, amount)?;
|
||||
let mut pending_tx = self.create_pending_transaction_single_dest(address, amount)?;
|
||||
let result = self.publish_pending_transaction(&mut pending_tx);
|
||||
self.dispose_pending_transaction(pending_tx);
|
||||
result
|
||||
}
|
||||
|
||||
/// Transfer specified amounts of monero to multiple addresses in a single transaction and return a receipt containing
|
||||
/// the transaction id, transaction key and current blockchain height. This can be used later
|
||||
/// to prove the transfer or to wait for confirmations.
|
||||
fn transfer_multi_destination(
|
||||
&mut self,
|
||||
destinations: &[(monero::Address, monero::Amount)],
|
||||
) -> anyhow::Result<TxReceipt> {
|
||||
let mut pending_tx = self.create_pending_transaction_multi_dest(destinations, false)?;
|
||||
let result = self.publish_pending_transaction(&mut pending_tx);
|
||||
self.dispose_pending_transaction(pending_tx);
|
||||
result
|
||||
|
|
@ -1891,7 +1926,7 @@ impl FfiWallet {
|
|||
|
||||
/// Create a pending transaction without publishing it.
|
||||
/// Returns the pending transaction that can be inspected before publishing.
|
||||
fn create_pending_transaction(
|
||||
fn create_pending_transaction_single_dest(
|
||||
&mut self,
|
||||
address: &monero::Address,
|
||||
amount: monero::Amount,
|
||||
|
|
@ -1907,6 +1942,58 @@ impl FfiWallet {
|
|||
Ok(pending_tx)
|
||||
}
|
||||
|
||||
/// Create a pending transaction that spends to multiple destinations without publishing it.
|
||||
/// Returns the pending transaction that can be inspected before publishing.
|
||||
///
|
||||
/// Destinations with zero amount are filtered out.
|
||||
fn create_pending_transaction_multi_dest(
|
||||
&mut self,
|
||||
destinations: &[(monero::Address, monero::Amount)],
|
||||
// If set to true, the fee will be subtracted from output with the highest amount
|
||||
// If set to false, the fee will be paid by the wallet and the exact amounts will be sent to the destinations
|
||||
subtract_fee_from_outputs: bool,
|
||||
) -> anyhow::Result<PendingTransaction> {
|
||||
// Filter out any destinations with zero amount
|
||||
let destinations = destinations
|
||||
.iter()
|
||||
.filter(|(_, amount)| amount.as_pico() > 0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Build a C++ vector of destination addresses
|
||||
let mut cxx_addrs: UniquePtr<CxxVector<CxxString>> = CxxVector::<CxxString>::new();
|
||||
|
||||
// Build a C++ vector of amounts
|
||||
let mut cxx_amounts: UniquePtr<CxxVector<u64>> = CxxVector::<u64>::new();
|
||||
|
||||
for (address, amount) in destinations {
|
||||
let_cxx_string!(s = address.to_string());
|
||||
ffi::vector_string_push_back(cxx_addrs.pin_mut(), &s);
|
||||
cxx_amounts.pin_mut().push(amount.as_pico());
|
||||
}
|
||||
|
||||
let cxx_addrs = cxx_addrs
|
||||
.as_ref()
|
||||
.context("cxx_addrs was just created, should not be null")?;
|
||||
let cxx_amounts = cxx_amounts
|
||||
.as_ref()
|
||||
.context("cxx_amounts was just created, should not be null")?;
|
||||
|
||||
// Create the multi-destination pending transaction
|
||||
let raw_tx = ffi::createTransactionMultiDest(
|
||||
self.inner.pinned(),
|
||||
cxx_addrs,
|
||||
cxx_amounts,
|
||||
subtract_fee_from_outputs,
|
||||
);
|
||||
|
||||
if raw_tx.is_null() {
|
||||
self.check_error()
|
||||
.context("Failed to create multi-destination transaction")?;
|
||||
}
|
||||
|
||||
Ok(PendingTransaction(raw_tx))
|
||||
}
|
||||
|
||||
/// Create a pending sweep transaction without publishing it.
|
||||
/// Returns the pending transaction that can be inspected before publishing.
|
||||
fn create_pending_sweep_transaction(
|
||||
|
|
@ -2022,8 +2109,6 @@ impl FfiWallet {
|
|||
addresses: &[monero::Address],
|
||||
ratios: &[f64],
|
||||
) -> anyhow::Result<Vec<TxReceipt>> {
|
||||
tracing::warn!("STARTED MULTI SWEEP");
|
||||
|
||||
if addresses.is_empty() {
|
||||
bail!("No addresses to sweep to");
|
||||
}
|
||||
|
|
@ -2047,33 +2132,18 @@ impl FfiWallet {
|
|||
|
||||
tracing::debug!(%balance, num_outputs = addresses.len(), outputs=?amounts, "Distributing funds to outputs");
|
||||
|
||||
// Build a C++ vector of destination addresses
|
||||
let mut cxx_addrs: UniquePtr<CxxVector<CxxString>> = CxxVector::<CxxString>::new();
|
||||
for addr in addresses {
|
||||
let_cxx_string!(s = addr.to_string());
|
||||
ffi::vector_string_push_back(cxx_addrs.pin_mut(), &s);
|
||||
}
|
||||
// Build destinations vector for create_pending_transaction_multi_dest
|
||||
let destinations: Vec<(monero::Address, monero::Amount)> = addresses
|
||||
.iter()
|
||||
.zip(amounts.iter())
|
||||
.map(|(addr, &amount)| (addr.clone(), amount))
|
||||
.collect();
|
||||
|
||||
// Build a C++ vector of amounts
|
||||
let mut cxx_amounts: UniquePtr<CxxVector<u64>> = CxxVector::<u64>::new();
|
||||
for &amount in &amounts {
|
||||
cxx_amounts.pin_mut().push(amount.as_pico());
|
||||
}
|
||||
|
||||
// Create the multi-sweep pending transaction
|
||||
let raw_tx = ffi::createTransactionMultiDest(
|
||||
self.inner.pinned(),
|
||||
cxx_addrs.as_ref().unwrap(),
|
||||
cxx_amounts.as_ref().unwrap(),
|
||||
);
|
||||
|
||||
if raw_tx.is_null() {
|
||||
self.check_error()
|
||||
.context("Failed to create multi-sweep transaction")?;
|
||||
anyhow::bail!("Failed to create multi-sweep transaction");
|
||||
}
|
||||
|
||||
let mut pending_tx = PendingTransaction(raw_tx);
|
||||
// Create the multi-sweep pending transaction using the shared function
|
||||
// Use subtract_fee_from_outputs=true since we're sweeping and want to distribute the full balance
|
||||
let mut pending_tx = self
|
||||
.create_pending_transaction_multi_dest(&destinations, true)
|
||||
.context("Failed to create multi-sweep transaction")?;
|
||||
|
||||
// Get the txids from the pending transaction before we publish,
|
||||
// otherwise it might be null.
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ async fn main() {
|
|||
tracing::info!("Transferring 1 XMR to ourselves");
|
||||
|
||||
wallet
|
||||
.transfer(&wallet.main_address().await, transfer_amount)
|
||||
.transfer_single_destination(&wallet.main_address().await, transfer_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": ["cli:default", "cli:allow-cli-matches"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|||
curl \
|
||||
lbzip2 \
|
||||
gperf \
|
||||
g++- \
|
||||
libglib2.0-dev \
|
||||
# Left out as we are not compiling for Windows in this Dockerfile
|
||||
# mingw-w64-x86-64 \
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use rust_decimal::prelude::FromPrimitive;
|
|||
use rust_decimal::Decimal;
|
||||
use std::convert::TryInto;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use structopt::clap;
|
||||
use structopt::clap::ErrorKind;
|
||||
|
|
@ -34,7 +35,7 @@ use swap::database::{open_db, AccessMode};
|
|||
use swap::network::rendezvous::XmrBtcNamespace;
|
||||
use swap::network::swarm;
|
||||
use swap::protocol::alice::swap::is_complete;
|
||||
use swap::protocol::alice::{run, AliceState};
|
||||
use swap::protocol::alice::{run, AliceState, TipConfig};
|
||||
use swap::protocol::{Database, State};
|
||||
use swap::seed::Seed;
|
||||
use swap::{bitcoin, monero};
|
||||
|
|
@ -170,18 +171,13 @@ pub async fn main() -> Result<()> {
|
|||
} => {
|
||||
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;
|
||||
|
||||
// check and warn for duplicate rendezvous points
|
||||
let mut rendezvous_addrs = config.network.rendezvous_point.clone();
|
||||
let prev_len = rendezvous_addrs.len();
|
||||
rendezvous_addrs.sort();
|
||||
rendezvous_addrs.dedup();
|
||||
let new_len = rendezvous_addrs.len();
|
||||
|
||||
if new_len < prev_len {
|
||||
tracing::warn!(
|
||||
"`rendezvous_point` config has {} duplicate entries, they are being ignored.",
|
||||
prev_len - new_len
|
||||
let developer_tip = config.maker.developer_tip;
|
||||
if developer_tip.is_zero() {
|
||||
tracing::info!(
|
||||
"Not tipping the developers (maker.developer_tip = 0 or not set in config)"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(%developer_tip, "Tipping to the developers is enabled. Thank you for your support!");
|
||||
}
|
||||
|
||||
// Initialize Monero wallet
|
||||
|
|
@ -241,7 +237,7 @@ pub async fn main() -> Result<()> {
|
|||
resume_only,
|
||||
env_config,
|
||||
namespace,
|
||||
&rendezvous_addrs,
|
||||
&config.network.rendezvous_point,
|
||||
tor_client,
|
||||
config.tor.register_hidden_service,
|
||||
config.tor.hidden_service_num_intro_points,
|
||||
|
|
@ -274,6 +270,29 @@ pub async fn main() -> Result<()> {
|
|||
swarm.add_external_address(external_address.clone());
|
||||
}
|
||||
|
||||
let tip_config = {
|
||||
let tip_address = monero::Address::from_str(match env_config.monero_network {
|
||||
monero::Network::Mainnet => {
|
||||
swap_env::defaults::DEFAULT_DEVELOPER_TIP_ADDRESS_MAINNET
|
||||
}
|
||||
monero::Network::Stagenet => {
|
||||
swap_env::defaults::DEFAULT_DEVELOPER_TIP_ADDRESS_STAGENET
|
||||
}
|
||||
monero::Network::Testnet => panic!("Testnet is not supported"),
|
||||
})
|
||||
.expect("Hardcoded developer tip address to be valid");
|
||||
|
||||
assert_eq!(
|
||||
tip_address.network, env_config.monero_network,
|
||||
"Developer tip address must be on the correct Monero network"
|
||||
);
|
||||
|
||||
TipConfig {
|
||||
ratio: config.maker.developer_tip,
|
||||
address: tip_address,
|
||||
}
|
||||
};
|
||||
|
||||
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
||||
let (event_loop, mut swap_receiver, event_loop_service) = EventLoop::new(
|
||||
swarm,
|
||||
|
|
@ -285,6 +304,7 @@ pub async fn main() -> Result<()> {
|
|||
config.maker.min_buy_btc,
|
||||
config.maker.max_buy_btc,
|
||||
config.maker.external_bitcoin_redeem_address,
|
||||
tip_config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ edition = "2024"
|
|||
anyhow = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
config = { version = "0.14", default-features = false, features = ["toml"] }
|
||||
console = "0.16"
|
||||
dialoguer = { workspace = true }
|
||||
libp2p = { workspace = true, features = ["serde"] }
|
||||
monero = { workspace = true }
|
||||
|
|
@ -14,6 +15,7 @@ rust_decimal = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
swap-fs = { path = "../swap-fs" }
|
||||
swap-serde = { path = "../swap-serde" }
|
||||
terminal_size = "0.4"
|
||||
thiserror = { workspace = true }
|
||||
time = "0.3"
|
||||
toml = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -101,6 +101,15 @@ pub struct Maker {
|
|||
pub price_ticker_ws_url: Url,
|
||||
#[serde(default, with = "swap_serde::bitcoin::address_serde::option")]
|
||||
pub external_bitcoin_redeem_address: Option<bitcoin::Address>,
|
||||
/// Percentage (between 0.0 and 1.0) of the swap amount
|
||||
// that will be donated to the project as part of the Monero lock transaction
|
||||
#[serde(default = "default_developer_tip")]
|
||||
pub developer_tip: Decimal,
|
||||
}
|
||||
|
||||
fn default_developer_tip() -> Decimal {
|
||||
// By default, we do not tip
|
||||
Decimal::ZERO
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
@ -184,6 +193,7 @@ pub fn query_user_for_initial_config_with_network(
|
|||
let max_buy = prompt::max_buy_amount()?;
|
||||
let ask_spread = prompt::ask_spread()?;
|
||||
let rendezvous_points = prompt::rendezvous_points()?;
|
||||
let developer_tip = prompt::developer_tip()?;
|
||||
|
||||
println!();
|
||||
|
||||
|
|
@ -216,6 +226,7 @@ pub fn query_user_for_initial_config_with_network(
|
|||
ask_spread,
|
||||
price_ticker_ws_url: defaults.price_ticker_ws_url,
|
||||
external_bitcoin_redeem_address: None,
|
||||
developer_tip,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,32 @@
|
|||
use crate::env::{Mainnet, Testnet};
|
||||
use anyhow::{Context, Result};
|
||||
use libp2p::Multiaddr;
|
||||
use rust_decimal::Decimal;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use swap_fs::{system_config_dir, system_data_dir};
|
||||
use url::Url;
|
||||
|
||||
/*
|
||||
Here's the GPG signature of the donation address.
|
||||
|
||||
Signed by the public key present in `utils/gpg_keys/binarybaron.asc`
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
87QwQmWZQwS6RvuprCqWuJgmystL8Dw6BCx8SrrCjVJhZYGc5s6kf9A2awfFfStvEGCGeNTBNqLGrHzH6d4gi7jLM2aoq9o is our donation address for Github (signed by binarybaron)
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEARYKAB0WIQQ1qETX9LVbxE4YD/GZt10+FHaibgUCaJTWlQAKCRCZt10+FHai
|
||||
bhasAQDGrAkZu+FFwDZDUEZzrIVS42he+GeMiS+ykpXyL5I7RQD/dXCR3f39zFsK
|
||||
1A7y45B3a8ZJYTzC7bbppg6cEnCoWQE=
|
||||
=j+Vz
|
||||
-----END PGP SIGNATURE-----
|
||||
*/
|
||||
pub const DEFAULT_DEVELOPER_TIP_ADDRESS_MAINNET: &str = "87QwQmWZQwS6RvuprCqWuJgmystL8Dw6BCx8SrrCjVJhZYGc5s6kf9A2awfFfStvEGCGeNTBNqLGrHzH6d4gi7jLM2aoq9o";
|
||||
pub const DEFAULT_DEVELOPER_TIP_ADDRESS_STAGENET: &str = "54ZYC5tgGRoKMJDLviAcJF2aHittSZGGkFZE6wCLkuAdUyHaaiQrjTxeSyfvxycn3yiexL4YNqdUmHuaReAk6JD4DQssQcF";
|
||||
|
||||
pub const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
|
||||
pub const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
|
||||
pub const DEFAULT_SPREAD: f64 = 0.02f64;
|
||||
|
|
@ -91,6 +112,7 @@ pub struct Defaults {
|
|||
pub price_ticker_ws_url: Url,
|
||||
pub bitcoin_confirmation_target: u16,
|
||||
pub use_mempool_space_fee_estimation: bool,
|
||||
pub developer_tip: Decimal,
|
||||
}
|
||||
|
||||
impl GetDefaults for Mainnet {
|
||||
|
|
@ -105,6 +127,7 @@ impl GetDefaults for Mainnet {
|
|||
price_ticker_ws_url: Url::parse(KRAKEN_PRICE_TICKER_WS_URL)?,
|
||||
bitcoin_confirmation_target: 1,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
developer_tip: Decimal::ZERO,
|
||||
};
|
||||
|
||||
Ok(defaults)
|
||||
|
|
@ -123,6 +146,7 @@ impl GetDefaults for Testnet {
|
|||
price_ticker_ws_url: Url::parse(KRAKEN_PRICE_TICKER_WS_URL)?,
|
||||
bitcoin_confirmation_target: 1,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
developer_tip: Decimal::ZERO,
|
||||
};
|
||||
|
||||
Ok(defaults)
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ impl GetConfig for Testnet {
|
|||
bitcoin_lock_confirmed_timeout: 1.std_hours(),
|
||||
bitcoin_finality_confirmations: 1,
|
||||
bitcoin_avg_block_time: 10.std_minutes(),
|
||||
bitcoin_cancel_timelock: 12,
|
||||
bitcoin_punish_timelock: 24,
|
||||
bitcoin_cancel_timelock: 12 * 3,
|
||||
bitcoin_punish_timelock: 24 * 3,
|
||||
bitcoin_network: bitcoin::Network::Testnet,
|
||||
monero_avg_block_time: 2.std_minutes(),
|
||||
monero_lock_retry_timeout: 10.std_minutes(),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::defaults::{
|
|||
default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use console::Style;
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::{theme::ColorfulTheme, Input, Select};
|
||||
use libp2p::Multiaddr;
|
||||
|
|
@ -22,6 +23,7 @@ pub fn data_directory(default_data_dir: &Path) -> Result<PathBuf> {
|
|||
.to_string(),
|
||||
)
|
||||
.interact_text()?;
|
||||
|
||||
Ok(data_dir.as_str().parse()?)
|
||||
}
|
||||
|
||||
|
|
@ -50,13 +52,20 @@ pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result<Vec<Multia
|
|||
|
||||
/// Prompt user for electrum RPC URLs
|
||||
pub fn electrum_rpc_urls(default_electrum_urls: &Vec<Url>) -> Result<Vec<Url>> {
|
||||
println!(
|
||||
let mut info_lines = vec![
|
||||
"You can configure multiple Electrum servers for redundancy. At least one is required."
|
||||
.to_string(),
|
||||
"The following default Electrum RPC URLs are available. We recommend using them."
|
||||
.to_string(),
|
||||
String::new(),
|
||||
];
|
||||
info_lines.extend(
|
||||
default_electrum_urls
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, url)| format!("{}: {}", i + 1, url)),
|
||||
);
|
||||
println!("The following default Electrum RPC URLs are available. We recommend using them.");
|
||||
for (i, url) in default_electrum_urls.iter().enumerate() {
|
||||
println!("{}: {}", i + 1, url);
|
||||
}
|
||||
print_info_box(info_lines);
|
||||
|
||||
// Ask if the user wants to use the default Electrum RPC URLs
|
||||
let mut electrum_rpc_urls = match Confirm::with_theme(&ColorfulTheme::default())
|
||||
|
|
@ -122,13 +131,12 @@ pub fn monero_daemon_url() -> Result<Option<Url>> {
|
|||
|
||||
/// Prompt user for Tor hidden service registration
|
||||
pub fn tor_hidden_service() -> Result<bool> {
|
||||
println!("Your ASB needs to be reachable from the outside world to provide quotes to takers.");
|
||||
println!(
|
||||
"Your ASB can run a hidden service for itself. It'll be reachable at an .onion address."
|
||||
);
|
||||
println!("You do not have to run a Tor daemon yourself. You do not have to manage anything.");
|
||||
println!("This will hide your IP address and allow you to run from behind a firewall without opening ports.");
|
||||
println!();
|
||||
print_info_box([
|
||||
"Your ASB needs to be reachable from the outside world to provide quotes to takers.",
|
||||
"Your ASB can run a hidden service for itself. It'll be reachable at an .onion address.",
|
||||
"You do not have to run a Tor daemon yourself. You do not have to manage anything.",
|
||||
"This will hide your IP address and allow you to run from behind a firewall without opening ports.",
|
||||
]);
|
||||
|
||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want a Tor hidden service to be created?")
|
||||
|
|
@ -157,6 +165,7 @@ pub fn max_buy_amount() -> Result<bitcoin::Amount> {
|
|||
.with_prompt("Enter maximum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
|
||||
.default(DEFAULT_MAX_BUY_AMOUNT)
|
||||
.interact_text()?;
|
||||
|
||||
bitcoin::Amount::from_btc(max_buy).map_err(Into::into)
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +177,10 @@ pub fn ask_spread() -> Result<Decimal> {
|
|||
.interact_text()?;
|
||||
|
||||
if !(0.0..=1.0).contains(&ask_spread) {
|
||||
bail!(format!("Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", ask_spread))
|
||||
bail!(format!(
|
||||
"Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.",
|
||||
ask_spread
|
||||
))
|
||||
}
|
||||
|
||||
Decimal::from_f64(ask_spread).context("Unable to parse spread")
|
||||
|
|
@ -176,20 +188,23 @@ pub fn ask_spread() -> Result<Decimal> {
|
|||
|
||||
/// Prompt user for rendezvous points
|
||||
pub fn rendezvous_points() -> Result<Vec<Multiaddr>> {
|
||||
println!("Your ASB can register with multiple rendezvous nodes for discoverability.");
|
||||
println!(
|
||||
"They act as sort of bootstrap nodes for peer discovery within the peer-to-peer network."
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
"The following rendezvous points are ran by community members. We recommend using them."
|
||||
);
|
||||
println!();
|
||||
|
||||
let default_rendezvous_points = default_rendezvous_points();
|
||||
for (i, point) in default_rendezvous_points.iter().enumerate() {
|
||||
println!("{}: {}", i + 1, point);
|
||||
}
|
||||
let mut info_lines = vec![
|
||||
"Your ASB can register with multiple rendezvous nodes for discoverability.".to_string(),
|
||||
"They act as sort of bootstrap nodes for peer discovery within the peer-to-peer network."
|
||||
.to_string(),
|
||||
String::new(),
|
||||
"The following rendezvous points are ran by community members. We recommend using them."
|
||||
.to_string(),
|
||||
String::new(),
|
||||
];
|
||||
info_lines.extend(
|
||||
default_rendezvous_points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, point)| format!("{}: {}", i + 1, point)),
|
||||
);
|
||||
print_info_box(info_lines);
|
||||
|
||||
// Ask if the user wants to use the default rendezvous points
|
||||
let use_default_rendezvous_points = Select::with_theme(&ColorfulTheme::default())
|
||||
|
|
@ -203,7 +218,7 @@ pub fn rendezvous_points() -> Result<Vec<Multiaddr>> {
|
|||
|
||||
let mut rendezvous_points = match use_default_rendezvous_points {
|
||||
0 => {
|
||||
println!("You can now configure additional rendezvous points.");
|
||||
print_info_box(["You can now configure additional rendezvous points."]);
|
||||
default_rendezvous_points
|
||||
}
|
||||
_ => Vec::new(),
|
||||
|
|
@ -233,3 +248,89 @@ pub fn rendezvous_points() -> Result<Vec<Multiaddr>> {
|
|||
|
||||
Ok(rendezvous_points)
|
||||
}
|
||||
|
||||
pub fn developer_tip() -> Result<Decimal> {
|
||||
// We first ask if the user wants to enable developer tipping at all
|
||||
// We do not select a default here as to not bias the user
|
||||
//
|
||||
// If not, we return 0
|
||||
// If yes, we ask for the percentage and default to 1% (0.01)
|
||||
let lines = [
|
||||
"This project has been developed by a small team of volunteers since 2022",
|
||||
"We rely on donations and the Monero CCS to continue our efforts.",
|
||||
"",
|
||||
"You can choose to donate a small part of each swap toward development.",
|
||||
"",
|
||||
"Donations will be used for Github bounties among other things.",
|
||||
"",
|
||||
"The tip is sent as an additional output of the Monero lock transaction.",
|
||||
"It does not require an extra transaction and you remain fully private.",
|
||||
"",
|
||||
"If enabled, you'll enter the percentage in the next step.",
|
||||
];
|
||||
print_info_box(lines);
|
||||
|
||||
let enable_developer_tip = Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want to enable developer tipping?")
|
||||
.interact()?;
|
||||
|
||||
if !enable_developer_tip {
|
||||
return Ok(Decimal::ZERO);
|
||||
}
|
||||
|
||||
let developer_tip = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter developer tip percentage (in percent; value between 0.x and 1.0; 0.01 means 1% of the swap amount is donated)")
|
||||
.default(Decimal::from_f64(0.01).unwrap())
|
||||
.interact_text()?;
|
||||
|
||||
if !(Decimal::ZERO..=Decimal::ONE).contains(&developer_tip) {
|
||||
bail!(format!(
|
||||
"Invalid developer tip {}. For the developer tip value floating point number in interval [0..1] are allowed.",
|
||||
developer_tip
|
||||
))
|
||||
}
|
||||
|
||||
let developer_tip_percentage =
|
||||
developer_tip.saturating_mul(Decimal::from_u64(100).expect("100 to fit in u64"));
|
||||
|
||||
print_info_box([&format!(
|
||||
"You will tip {}% of each swap to the developers. Thank you for your support!",
|
||||
developer_tip_percentage
|
||||
)]);
|
||||
|
||||
Ok(developer_tip)
|
||||
}
|
||||
|
||||
/// Print a boxed info message using console styling to match dialoguer output
|
||||
pub fn print_info_box<L, S>(lines: L)
|
||||
where
|
||||
L: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let terminal_width = terminal_size::terminal_size().map_or(200, |(width, _)| width.0 as usize);
|
||||
|
||||
let border = Style::new().cyan();
|
||||
let content = Style::new().bold();
|
||||
|
||||
let mut collected: Vec<String> = lines.into_iter().map(|s| s.as_ref().to_string()).collect();
|
||||
|
||||
if collected.is_empty() {
|
||||
collected.push(String::new());
|
||||
}
|
||||
|
||||
let content_width = collected
|
||||
.iter()
|
||||
.map(|s| s.len())
|
||||
.max()
|
||||
.expect("Failed to get line width");
|
||||
let line_width = (content_width + 2).min(terminal_width);
|
||||
|
||||
let top = format!("┌{}", "─".repeat(line_width.saturating_sub(1)));
|
||||
let bottom = format!("└{}", "─".repeat(line_width.saturating_sub(1)));
|
||||
println!("");
|
||||
println!("{}", border.apply_to(&top));
|
||||
for l in collected {
|
||||
println!("{} {}", border.apply_to("│"), content.apply_to(l));
|
||||
}
|
||||
println!("{}", border.apply_to(&bottom));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ fn main() {
|
|||
.expect("Failed to prompt for listen addresses");
|
||||
let monero_node_type = prompt::monero_node_type();
|
||||
let electrum_server_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls);
|
||||
let developer_tip =
|
||||
config_prompt::developer_tip().expect("Failed to prompt for developer tip");
|
||||
|
||||
let electrs_url = Url::parse(&format!("tcp://electrs:{}", recipe.ports.electrs))
|
||||
.expect("electrs url to be convertible to a valid url");
|
||||
|
|
@ -183,6 +185,7 @@ fn main() {
|
|||
ask_spread,
|
||||
price_ticker_ws_url: defaults.price_ticker_ws_url,
|
||||
external_bitcoin_redeem_address: None,
|
||||
developer_tip,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,9 @@ pub mod address {
|
|||
use std::str::FromStr;
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
|
||||
#[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
||||
#[error(
|
||||
"Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}"
|
||||
)]
|
||||
pub struct MoneroAddressNetworkMismatch {
|
||||
pub expected: monero::Network,
|
||||
pub actual: monero::Network,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::network::quote::BidQuote;
|
|||
use crate::network::swap_setup::alice::WalletSnapshot;
|
||||
use crate::network::transfer_proof;
|
||||
use crate::protocol::alice::swap::has_already_processed_enc_sig;
|
||||
use crate::protocol::alice::{AliceState, State3, Swap};
|
||||
use crate::protocol::alice::{AliceState, State3, Swap, TipConfig};
|
||||
use crate::protocol::{Database, State};
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
|
@ -19,6 +19,7 @@ use libp2p::request_response::{OutboundFailure, OutboundRequestId, ResponseChann
|
|||
use libp2p::swarm::SwarmEvent;
|
||||
use libp2p::{PeerId, Swarm};
|
||||
use moka::future::Cache;
|
||||
use rust_decimal::Decimal;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Debug;
|
||||
|
|
@ -45,6 +46,7 @@ where
|
|||
min_buy: bitcoin::Amount,
|
||||
max_buy: bitcoin::Amount,
|
||||
external_redeem_address: Option<bitcoin::Address>,
|
||||
developer_tip: TipConfig,
|
||||
|
||||
/// Cache for quotes
|
||||
quote_cache: Cache<QuoteCacheKey, Result<Arc<BidQuote>, Arc<anyhow::Error>>>,
|
||||
|
|
@ -135,6 +137,7 @@ where
|
|||
min_buy: bitcoin::Amount,
|
||||
max_buy: bitcoin::Amount,
|
||||
external_redeem_address: Option<bitcoin::Address>,
|
||||
developer_tip: TipConfig,
|
||||
) -> Result<(Self, mpsc::Receiver<Swap>, EventLoopService)> {
|
||||
let swap_channel = MpscChannels::default();
|
||||
let (outgoing_transfer_proofs_sender, outgoing_transfer_proofs_requests) =
|
||||
|
|
@ -154,6 +157,7 @@ where
|
|||
min_buy,
|
||||
max_buy,
|
||||
external_redeem_address,
|
||||
developer_tip,
|
||||
quote_cache,
|
||||
recv_encrypted_signature: Default::default(),
|
||||
inflight_encrypted_signatures: Default::default(),
|
||||
|
|
@ -215,6 +219,7 @@ where
|
|||
db: self.db.clone(),
|
||||
state: state.try_into().expect("Alice state loaded from db"),
|
||||
swap_id,
|
||||
developer_tip: self.developer_tip.clone(),
|
||||
};
|
||||
|
||||
match self.swap_sender.send(swap).await {
|
||||
|
|
@ -256,7 +261,7 @@ where
|
|||
tracing::warn!(%peer, "Ignoring spot price request: {}", error);
|
||||
}
|
||||
SwarmEvent::Behaviour(OutEvent::QuoteRequested { channel, peer }) => {
|
||||
match self.make_quote_or_use_cached(self.min_buy, self.max_buy).await {
|
||||
match self.make_quote_or_use_cached(self.min_buy, self.max_buy, self.developer_tip.ratio).await {
|
||||
Ok(quote_arc) => {
|
||||
if self.swarm.behaviour_mut().quote.send_response(channel, *quote_arc).is_err() {
|
||||
tracing::debug!(%peer, "Failed to respond with quote");
|
||||
|
|
@ -517,6 +522,7 @@ where
|
|||
&mut self,
|
||||
min_buy: bitcoin::Amount,
|
||||
max_buy: bitcoin::Amount,
|
||||
developer_tip: Decimal,
|
||||
) -> Result<Arc<BidQuote>, Arc<anyhow::Error>> {
|
||||
// We use the min and max buy amounts to create a unique key for the cache
|
||||
// Although these values stay constant over the lifetime of an instance of the asb, this might change in the future
|
||||
|
|
@ -559,6 +565,7 @@ where
|
|||
rate,
|
||||
get_unlocked_balance,
|
||||
get_reserved_items,
|
||||
developer_tip,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
@ -568,7 +575,7 @@ where
|
|||
|
||||
// If the quote failed, we log the error
|
||||
if let Err(err) = result.clone() {
|
||||
tracing::warn!(%err, "Failed to make quote. We will retry again later.");
|
||||
tracing::warn!(?err, "Failed to make quote. We will retry again later.");
|
||||
}
|
||||
|
||||
// Return the computed quote
|
||||
|
|
@ -595,6 +602,7 @@ where
|
|||
db: self.db.clone(),
|
||||
state: initial_state,
|
||||
swap_id,
|
||||
developer_tip: self.developer_tip.clone(),
|
||||
};
|
||||
|
||||
match self.db.insert_peer_id(swap_id, bob_peer_id).await {
|
||||
|
|
@ -794,6 +802,7 @@ mod service {
|
|||
mod quote {
|
||||
use crate::monero::Amount;
|
||||
use anyhow::{anyhow, Context};
|
||||
use rust_decimal::Decimal;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use swap_feed::LatestRate;
|
||||
use tokio::time::timeout;
|
||||
|
|
@ -817,6 +826,7 @@ mod quote {
|
|||
mut latest_rate: LR,
|
||||
get_unlocked_balance: F,
|
||||
get_reserved_items: I,
|
||||
developer_tip: Decimal,
|
||||
) -> Result<Arc<BidQuote>, Arc<anyhow::Error>>
|
||||
where
|
||||
LR: LatestRate,
|
||||
|
|
@ -847,8 +857,11 @@ mod quote {
|
|||
.map(|item| item.reserved_monero())
|
||||
.collect();
|
||||
|
||||
let unreserved_xmr_balance =
|
||||
unreserved_monero_balance(unlocked_balance, reserved_amounts.into_iter());
|
||||
let unreserved_xmr_balance = unreserved_monero_balance(
|
||||
unlocked_balance,
|
||||
reserved_amounts.into_iter(),
|
||||
developer_tip,
|
||||
);
|
||||
|
||||
let max_bitcoin_for_monero = unreserved_xmr_balance
|
||||
.max_bitcoin_for_price(ask_price)
|
||||
|
|
@ -899,15 +912,53 @@ mod quote {
|
|||
pub fn unreserved_monero_balance(
|
||||
unlocked_balance: Amount,
|
||||
reserved_amounts: impl Iterator<Item = Amount>,
|
||||
developer_tip: Decimal,
|
||||
) -> Amount {
|
||||
// Get the sum of all the individual reserved amounts
|
||||
let total_reserved = reserved_amounts.fold(Amount::ZERO, |acc, amount| acc + amount);
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
// Check how much of our unlocked balance is left when we
|
||||
// take into account the reserved amounts
|
||||
unlocked_balance
|
||||
.checked_sub(total_reserved)
|
||||
.unwrap_or(Amount::ZERO)
|
||||
let unlocked_balance_piconero = unlocked_balance.as_piconero_decimal();
|
||||
|
||||
// If a developer tip is configured, we need to account for the fact that
|
||||
// to lock X XMR for a swap, we actually need X * (1 + tip_percentage) XMR
|
||||
// because the tip is sent as an additional output in the same transaction
|
||||
|
||||
// To find how much we can actually use for swaps, we need to solve:
|
||||
// swap_amount * multiplier <= available_after_reserved
|
||||
// <=> swap_amount <= available_after_reserved / multiplier
|
||||
|
||||
// Calculate the effective multiplier: 1 + tip_percentage
|
||||
let multiplier = Decimal::ONE + developer_tip;
|
||||
|
||||
// The amount of Monero we can send somewhere if for every transaction we send,
|
||||
// we send a tip within the same transaction as an additional output
|
||||
//
|
||||
// This does not take the fee into account.
|
||||
//
|
||||
// When we call `max_bitcoin_for_price`, it uses the `CONSERVATIVE_MONERO_FEE` constantdefined in `swap/src/monero.rs`
|
||||
// to take into account the fee.
|
||||
let unlocked_balance_piconero_after_accounting_for_tip =
|
||||
unlocked_balance_piconero / multiplier;
|
||||
|
||||
// Get the sum of all the individual reserved amounts
|
||||
// This is the amount of Monero that is required for ongoing swaps
|
||||
//
|
||||
// Swaps where the Monero hasn't been locked yet but we know we will lock it soon
|
||||
//
|
||||
// Note: It is important that we subtract this AFTER accounting for the tip
|
||||
// as these other swaps will also require a tip output
|
||||
let total_reserved_piconero = reserved_amounts
|
||||
.fold(Amount::ZERO, |acc, amount| acc + amount)
|
||||
.as_piconero_decimal();
|
||||
|
||||
// Unlocked balance after accounting for the tip and the reserved amounts
|
||||
let unreserved_unlocked_piconero_after_accounting_for_tip =
|
||||
unlocked_balance_piconero_after_accounting_for_tip
|
||||
.checked_sub(total_reserved_piconero)
|
||||
.unwrap_or(Decimal::ZERO)
|
||||
.to_u64()
|
||||
.unwrap_or(0);
|
||||
|
||||
Amount::from_piconero(unreserved_unlocked_piconero_after_accounting_for_tip)
|
||||
}
|
||||
|
||||
/// Returns the unlocked Monero balance from the wallet
|
||||
|
|
@ -967,7 +1018,8 @@ mod tests {
|
|||
let balance = Amount::from_monero(10.0).unwrap();
|
||||
let reserved_amounts = vec![];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result =
|
||||
unreserved_monero_balance(balance, reserved_amounts.into_iter(), Decimal::ZERO);
|
||||
|
||||
assert_eq!(result, balance);
|
||||
}
|
||||
|
|
@ -980,7 +1032,8 @@ mod tests {
|
|||
monero::Amount::from_monero(3.0).unwrap(),
|
||||
];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result =
|
||||
unreserved_monero_balance(balance, reserved_amounts.into_iter(), Decimal::ZERO);
|
||||
|
||||
let expected = monero::Amount::from_monero(5.0).unwrap();
|
||||
assert_eq!(result, expected);
|
||||
|
|
@ -994,7 +1047,8 @@ mod tests {
|
|||
monero::Amount::from_monero(4.0).unwrap(), // Total reserved > balance
|
||||
];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result =
|
||||
unreserved_monero_balance(balance, reserved_amounts.into_iter(), Decimal::ZERO);
|
||||
|
||||
// Should return zero when reserved > balance
|
||||
assert_eq!(result, monero::Amount::ZERO);
|
||||
|
|
@ -1008,7 +1062,8 @@ mod tests {
|
|||
monero::Amount::from_monero(6.0).unwrap(), // Exactly equals balance
|
||||
];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result =
|
||||
unreserved_monero_balance(balance, reserved_amounts.into_iter(), Decimal::ZERO);
|
||||
|
||||
assert_eq!(result, monero::Amount::ZERO);
|
||||
}
|
||||
|
|
@ -1018,7 +1073,8 @@ mod tests {
|
|||
let balance = monero::Amount::ZERO;
|
||||
let reserved_amounts = vec![monero::Amount::from_monero(1.0).unwrap()];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result =
|
||||
unreserved_monero_balance(balance, reserved_amounts.into_iter(), Decimal::ZERO);
|
||||
|
||||
assert_eq!(result, monero::Amount::ZERO);
|
||||
}
|
||||
|
|
@ -1028,7 +1084,11 @@ mod tests {
|
|||
let balance = monero::Amount::from_monero(5.0).unwrap();
|
||||
let reserved_amounts: Vec<MockReservedItem> = vec![];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result = unreserved_monero_balance(
|
||||
balance,
|
||||
reserved_amounts.into_iter().map(|item| item.reserved),
|
||||
Decimal::ZERO,
|
||||
);
|
||||
|
||||
assert_eq!(result, balance);
|
||||
}
|
||||
|
|
@ -1038,7 +1098,8 @@ mod tests {
|
|||
let balance = monero::Amount::from_piconero(1_000_000_000);
|
||||
let reserved_amounts = vec![monero::Amount::from_piconero(300_000_000)];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
let result =
|
||||
unreserved_monero_balance(balance, reserved_amounts.into_iter(), Decimal::ZERO);
|
||||
|
||||
let expected = monero::Amount::from_piconero(700_000_000);
|
||||
assert_eq!(result, expected);
|
||||
|
|
@ -1058,6 +1119,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1088,6 +1150,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1113,6 +1176,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1136,6 +1200,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1164,6 +1229,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1186,6 +1252,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Err(anyhow::anyhow!("Failed to get balance")) },
|
||||
|| async { Ok(reserved_items) },
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
@ -1210,6 +1277,7 @@ mod tests {
|
|||
rate.clone(),
|
||||
|| async { Ok(balance) },
|
||||
|| async { Ok(reserved_items) },
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1220,6 +1288,11 @@ mod tests {
|
|||
assert_eq!(result.max_quantity, max_buy);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_make_quote_with_developer_tip() {
|
||||
todo!("implement once unit tests compile again")
|
||||
}
|
||||
|
||||
// Mock struct for testing
|
||||
#[derive(Debug, Clone)]
|
||||
struct MockReservedItem {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
//! Alice holds XMR and wishes receive BTC.
|
||||
use crate::protocol::Database;
|
||||
use crate::{asb, bitcoin, monero};
|
||||
use rust_decimal::Decimal;
|
||||
use std::sync::Arc;
|
||||
use swap_env::env::Config;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -18,6 +19,19 @@ pub struct Swap {
|
|||
pub bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
pub monero_wallet: Arc<monero::Wallets>,
|
||||
pub env_config: Config,
|
||||
pub developer_tip: TipConfig,
|
||||
pub swap_id: Uuid,
|
||||
pub db: Arc<dyn Database + Send + Sync>,
|
||||
}
|
||||
|
||||
/// Configures how much the and where the user wants to send tips to
|
||||
///
|
||||
/// The ratio is a number between 0 and 1
|
||||
///
|
||||
/// ratio = 0 means that no tip will be sent
|
||||
/// ratio = 0.5 means that for a swap of 1 XMR, 0.5 XMR will be tipped
|
||||
#[derive(Clone)]
|
||||
pub struct TipConfig {
|
||||
pub ratio: Decimal,
|
||||
pub address: ::monero::Address,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ use crate::asb::{EventLoopHandle, LatestRate};
|
|||
use crate::bitcoin::ExpiredTimelocks;
|
||||
use crate::common::retry;
|
||||
use crate::monero::TransferProof;
|
||||
use crate::protocol::alice::{AliceState, Swap};
|
||||
use crate::protocol::alice::{AliceState, Swap, TipConfig};
|
||||
use crate::{bitcoin, monero};
|
||||
use ::bitcoin::consensus::encode::serialize_hex;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use rust_decimal::Decimal;
|
||||
use swap_env::env::Config;
|
||||
use tokio::select;
|
||||
use tokio::time::timeout;
|
||||
|
|
@ -43,6 +44,7 @@ where
|
|||
swap.bitcoin_wallet.as_ref(),
|
||||
swap.monero_wallet.clone(),
|
||||
&swap.env_config,
|
||||
swap.developer_tip.clone(),
|
||||
rate_service.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -62,6 +64,7 @@ async fn next_state<LR>(
|
|||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
env_config: &Config,
|
||||
developer_tip: TipConfig,
|
||||
mut rate_service: LR,
|
||||
) -> Result<AliceState>
|
||||
where
|
||||
|
|
@ -159,11 +162,14 @@ where
|
|||
.lock_xmr_transfer_request()
|
||||
.address_and_amount(env_config.monero_network);
|
||||
|
||||
let destinations =
|
||||
build_transfer_destinations(address, amount, developer_tip.clone())?;
|
||||
|
||||
// Lock the Monero
|
||||
let receipt = monero_wallet
|
||||
.main_wallet()
|
||||
.await
|
||||
.transfer(&address, amount)
|
||||
.transfer_multi_destination(&destinations)
|
||||
.await
|
||||
.map_err(|e| tracing::error!(err=%e, "Failed to lock Monero"))
|
||||
.ok();
|
||||
|
|
@ -658,6 +664,43 @@ pub fn is_complete(state: &AliceState) -> bool {
|
|||
)
|
||||
}
|
||||
|
||||
/// Build transfer destinations for the Monero lock transaction, optionally including a developer tip.
|
||||
///
|
||||
/// If the tip.ratio > 0 and the effective tip is >= MIN_USEFUL_TIP_AMOUNT_PICONERO:
|
||||
/// returns two destinations: one for the lock output, one for the tip output
|
||||
///
|
||||
/// Otherwise:
|
||||
/// returns one destination: for the lock output
|
||||
fn build_transfer_destinations(
|
||||
lock_address: ::monero::Address,
|
||||
lock_amount: ::monero::Amount,
|
||||
tip: TipConfig,
|
||||
) -> anyhow::Result<Vec<(::monero::Address, ::monero::Amount)>> {
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
// If the effective tip is less than this amount, we do not include the tip output
|
||||
// Any values below `MIN_USEFUL_TIP_AMOUNT_PICONERO` are clamped to zero
|
||||
//
|
||||
// At $300/XMR, this is around one cent
|
||||
const MIN_USEFUL_TIP_AMOUNT_PICONERO: u64 = 30_000_000;
|
||||
|
||||
// TODO: Move this code into the impl of TipConfig
|
||||
let tip_amount_piconero = tip
|
||||
.ratio
|
||||
.saturating_mul(Decimal::from(lock_amount.as_pico()))
|
||||
.floor()
|
||||
.to_u64()
|
||||
.context("Developer tip amount should not overflow")?;
|
||||
|
||||
if tip_amount_piconero >= MIN_USEFUL_TIP_AMOUNT_PICONERO {
|
||||
let tip_amount = ::monero::Amount::from_pico(tip_amount_piconero);
|
||||
|
||||
Ok(vec![(lock_address, lock_amount), (tip.address, tip_amount)])
|
||||
} else {
|
||||
Ok(vec![(lock_address, lock_amount)])
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is used to check if Alice is in a state where it is clear that she has already received the encrypted signature from Bob.
|
||||
/// This allows us to acknowledge the encrypted signature multiple times
|
||||
/// If our acknowledgement does not reach Bob, he might send the encrypted signature again.
|
||||
|
|
@ -669,3 +712,31 @@ pub(crate) fn has_already_processed_enc_sig(state: &AliceState) -> bool {
|
|||
| AliceState::BtcRedeemed
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_build_transfer_destinations_without_tip() {
|
||||
todo!("implement once unit tests compile again")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_transfer_destinations_with_tip() {
|
||||
todo!("implement once unit tests compile again")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_transfer_destinations_with_small_tip() {
|
||||
todo!("implement once unit tests compile again")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_transfer_destinations_with_zero_tip() {
|
||||
todo!("implement once unit tests compile again")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_transfer_destinations_with_fractional_tip() {
|
||||
todo!("implement once unit tests compile again")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -760,7 +760,7 @@ impl State5 {
|
|||
let main_address = monero_wallet.main_wallet().await.main_address().await;
|
||||
|
||||
let tx_hashes = wallet
|
||||
.sweep_multi(
|
||||
.sweep_multi_destination(
|
||||
&monero_receive_pool.fill_empty_addresses(main_address),
|
||||
&monero_receive_pool.percentages(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use swap::{asb, cli};
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() {
|
||||
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use swap::{asb, cli};
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use swap::{asb, cli};
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_alice_and_bob_manually_cancel_and_refund_after_funds_locked_both_refund() {
|
||||
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::harness::SlowCancelConfig;
|
|||
|
||||
#[tokio::test]
|
||||
async fn alice_zero_xmr_refunds_bitcoin() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_handle) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::harness::SlowCancelConfig;
|
|||
|
||||
#[tokio::test]
|
||||
async fn alice_zero_xmr_refunds_bitcoin() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_handle) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use swap::protocol::{alice, bob};
|
|||
/// punish command. Bob then cooperates with Alice and redeems XMR with her key.
|
||||
#[tokio::test]
|
||||
async fn alice_manually_punishes_after_bob_dead() {
|
||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastPunishConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use swap::protocol::{alice, bob};
|
|||
/// punish command. Then Bob tries to refund.
|
||||
#[tokio::test]
|
||||
async fn alice_manually_punishes_after_bob_dead_and_bob_cancels() {
|
||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastPunishConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use swap::protocol::{alice, bob};
|
|||
/// after learning encsig from Bob
|
||||
#[tokio::test]
|
||||
async fn alice_manually_redeems_after_enc_sig_learned() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, _) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run(bob_swap));
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use swap::protocol::{alice, bob};
|
|||
/// the encsig and fail to refund or redeem. Alice cancels and punishes. Bob then cooperates with Alice and redeems XMR with her key.
|
||||
#[tokio::test]
|
||||
async fn alice_punishes_after_restart_if_bob_dead() {
|
||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastPunishConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use swap::protocol::{alice, bob};
|
|||
/// Eventually Alice comes back online and refunds as well.
|
||||
#[tokio::test]
|
||||
async fn alice_refunds_after_restart_if_bob_already_refunded() {
|
||||
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, _) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run(bob_swap));
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use swap::protocol::{alice, bob};
|
|||
|
||||
#[tokio::test]
|
||||
async fn concurrent_bobs_after_xmr_lock_proof_sent() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await;
|
||||
|
||||
let swap_id = bob_swap_1.id;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use swap::protocol::{alice, bob};
|
|||
|
||||
#[tokio::test]
|
||||
async fn concurrent_bobs_before_xmr_lock_proof_sent() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap_1, bob_join_handle_1) = ctx.bob_swap().await;
|
||||
|
||||
let swap_id = bob_swap_1.id;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use swap::protocol::bob;
|
|||
|
||||
#[tokio::test]
|
||||
async fn ensure_same_swap_id_for_alice_and_bob() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, _) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
tokio::spawn(bob::run(bob_swap));
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use tokio::join;
|
|||
|
||||
#[tokio::test]
|
||||
async fn happy_path() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, _) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run(bob_swap));
|
||||
|
||||
|
|
|
|||
31
swap/tests/happy_path_alice_developer_tip.rs
Normal file
31
swap/tests/happy_path_alice_developer_tip.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
pub mod harness;
|
||||
|
||||
use harness::SlowCancelConfig;
|
||||
use rust_decimal::Decimal;
|
||||
use swap::asb::FixedRate;
|
||||
use swap::protocol::{alice, bob};
|
||||
use tokio::join;
|
||||
|
||||
#[tokio::test]
|
||||
async fn happy_path_alice_developer_tip() {
|
||||
harness::setup_test(
|
||||
SlowCancelConfig,
|
||||
Some(Decimal::from_f32_retain(0.1).unwrap()),
|
||||
|mut ctx| async move {
|
||||
let (bob_swap, _) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run(bob_swap));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default()));
|
||||
|
||||
let (bob_state, alice_state) = join!(bob_swap, alice_swap);
|
||||
|
||||
ctx.assert_alice_redeemed(alice_state??).await;
|
||||
ctx.assert_bob_redeemed(bob_state??).await;
|
||||
ctx.assert_alice_developer_tip_received().await;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use tokio::join;
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_bob_restarts_while_alice_redeems_btc() {
|
||||
harness::setup_test(harness::SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(harness::SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_handle) = ctx.bob_swap().await;
|
||||
let swap_id = bob_swap.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use swap::protocol::{alice, bob};
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_alice_restarts_after_xmr_is_locked_resume_swap() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, _) = ctx.bob_swap().await;
|
||||
let bob_swap = tokio::spawn(bob::run(bob_swap));
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use swap::protocol::{alice, bob};
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked));
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use swap::protocol::{alice, bob};
|
|||
|
||||
#[tokio::test]
|
||||
async fn given_bob_restarts_after_xmr_is_locked_resume_swap() {
|
||||
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
|
||||
harness::setup_test(SlowCancelConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_xmr_locked));
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use libp2p::core::Multiaddr;
|
|||
use libp2p::PeerId;
|
||||
use monero_harness::{image, Monero};
|
||||
use monero_sys::Daemon;
|
||||
use rust_decimal::Decimal;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -25,7 +26,7 @@ use swap::monero::wallet::no_listener;
|
|||
use swap::monero::Wallets;
|
||||
use swap::network::rendezvous::XmrBtcNamespace;
|
||||
use swap::network::swarm;
|
||||
use swap::protocol::alice::{AliceState, Swap};
|
||||
use swap::protocol::alice::{AliceState, Swap, TipConfig};
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob, Database};
|
||||
use swap::seed::Seed;
|
||||
|
|
@ -43,7 +44,7 @@ use tokio::time::{interval, timeout};
|
|||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn setup_test<T, F, C>(_config: C, testfn: T)
|
||||
pub async fn setup_test<T, F, C>(_config: C, developer_tip_ratio: Option<Decimal>, testfn: T)
|
||||
where
|
||||
T: Fn(TestContext) -> F,
|
||||
F: Future<Output = Result<()>>,
|
||||
|
|
@ -67,12 +68,41 @@ where
|
|||
|
||||
let btc_amount = bitcoin::Amount::from_sat(1_000_000);
|
||||
let xmr_amount = monero::Amount::from_monero(btc_amount.to_btc() / FixedRate::RATE).unwrap();
|
||||
let electrs_rpc_port = containers.electrs.get_host_port_ipv4(electrs::RPC_PORT);
|
||||
|
||||
let developer_seed = Seed::random().unwrap();
|
||||
let developer_starting_balances =
|
||||
StartingBalances::new(bitcoin::Amount::ZERO, monero::Amount::ZERO, None);
|
||||
let developer_tip_monero_dir = TempDir::new()
|
||||
.unwrap()
|
||||
.path()
|
||||
.join("developer_tip-monero-wallets");
|
||||
let (_, developer_tip_monero_wallet) = init_test_wallets(
|
||||
"developer_tip",
|
||||
containers.bitcoind_url.clone(),
|
||||
&monero,
|
||||
&containers._monerod_container,
|
||||
developer_tip_monero_dir,
|
||||
developer_starting_balances.clone(),
|
||||
electrs_rpc_port,
|
||||
&developer_seed,
|
||||
env_config,
|
||||
)
|
||||
.await;
|
||||
let developer_tip_monero_wallet_address = developer_tip_monero_wallet
|
||||
.main_wallet()
|
||||
.await
|
||||
.main_address()
|
||||
.await
|
||||
.into();
|
||||
|
||||
let developer_tip = TipConfig {
|
||||
ratio: developer_tip_ratio.unwrap_or(Decimal::ZERO),
|
||||
address: developer_tip_monero_wallet_address,
|
||||
};
|
||||
|
||||
let alice_starting_balances =
|
||||
StartingBalances::new(bitcoin::Amount::ZERO, xmr_amount, Some(10));
|
||||
|
||||
let electrs_rpc_port = containers.electrs.get_host_port_ipv4(electrs::RPC_PORT);
|
||||
|
||||
let alice_seed = Seed::random().unwrap();
|
||||
let alice_db_path = NamedTempFile::new().unwrap().path().to_path_buf();
|
||||
let alice_monero_dir = TempDir::new().unwrap().path().join("alice-monero-wallets");
|
||||
|
|
@ -101,6 +131,7 @@ where
|
|||
env_config,
|
||||
alice_bitcoin_wallet.clone(),
|
||||
alice_monero_wallet.clone(),
|
||||
developer_tip.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
@ -109,7 +140,7 @@ where
|
|||
let bob_monero_dir = TempDir::new().unwrap().path().join("bob-monero-wallets");
|
||||
let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets(
|
||||
MONERO_WALLET_NAME_BOB,
|
||||
containers.bitcoind_url,
|
||||
containers.bitcoind_url.clone(),
|
||||
&monero,
|
||||
&containers._monerod_container,
|
||||
bob_monero_dir,
|
||||
|
|
@ -148,6 +179,8 @@ where
|
|||
bob_starting_balances,
|
||||
bob_bitcoin_wallet,
|
||||
bob_monero_wallet,
|
||||
developer_tip_monero_wallet,
|
||||
developer_tip,
|
||||
monerod_container_id: containers._monerod_container.id().to_string(),
|
||||
};
|
||||
|
||||
|
|
@ -238,6 +271,7 @@ async fn start_alice(
|
|||
env_config: Config,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
developer_tip: TipConfig,
|
||||
) -> (AliceApplicationHandle, Receiver<alice::Swap>) {
|
||||
if let Some(parent_dir) = db_path.parent() {
|
||||
ensure_directory_exists(parent_dir).unwrap();
|
||||
|
|
@ -282,6 +316,7 @@ async fn start_alice(
|
|||
min_buy,
|
||||
max_buy,
|
||||
None,
|
||||
developer_tip,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -594,6 +629,7 @@ pub struct TestContext {
|
|||
|
||||
btc_amount: bitcoin::Amount,
|
||||
xmr_amount: monero::Amount,
|
||||
developer_tip: TipConfig,
|
||||
|
||||
alice_seed: Seed,
|
||||
alice_db_path: PathBuf,
|
||||
|
|
@ -610,6 +646,8 @@ pub struct TestContext {
|
|||
bob_bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bob_monero_wallet: Arc<monero::Wallets>,
|
||||
|
||||
developer_tip_monero_wallet: Arc<monero::Wallets>,
|
||||
|
||||
// Store the container ID as String instead of reference
|
||||
monerod_container_id: String,
|
||||
}
|
||||
|
|
@ -636,6 +674,7 @@ impl TestContext {
|
|||
self.env_config,
|
||||
self.alice_bitcoin_wallet.clone(),
|
||||
self.alice_monero_wallet.clone(),
|
||||
self.developer_tip.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
@ -716,6 +755,16 @@ impl TestContext {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn assert_alice_developer_tip_received(&self) {
|
||||
assert_eventual_balance(
|
||||
&*self.developer_tip_monero_wallet.main_wallet().await,
|
||||
Ordering::Equal,
|
||||
self.developer_tip_wallet_received_xmr_balance(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn assert_alice_punished(&self, state: AliceState) {
|
||||
let (cancel_fee, punish_fee) = match state {
|
||||
AliceState::BtcPunished { state3, .. } => (state3.tx_cancel_fee, state3.tx_punish_fee),
|
||||
|
|
@ -854,6 +903,25 @@ impl TestContext {
|
|||
self.alice_starting_balances.xmr - self.xmr_amount
|
||||
}
|
||||
|
||||
fn developer_tip_wallet_received_xmr_balance(&self) -> monero::Amount {
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
let effective_tip_amount = monero::Amount::from_piconero(
|
||||
self.developer_tip
|
||||
.ratio
|
||||
.saturating_mul(self.xmr_amount.as_piconero_decimal())
|
||||
.to_u64()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// This is defined in `swap/src/protocol/alice/swap.rs` in `build_transfer_destinations`
|
||||
if effective_tip_amount.as_piconero() < 30_000_000 {
|
||||
return monero::Amount::ZERO;
|
||||
}
|
||||
|
||||
effective_tip_amount
|
||||
}
|
||||
|
||||
fn alice_refunded_btc_balance(&self) -> bitcoin::Amount {
|
||||
self.alice_starting_balances.btc
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use swap::protocol::{alice, bob};
|
|||
/// the encsig and fail to refund or redeem. Alice punishes. Bob then cooperates with Alice and redeems XMR with her key.
|
||||
#[tokio::test]
|
||||
async fn alice_punishes_if_bob_never_acts_after_fund() {
|
||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||
harness::setup_test(FastPunishConfig, None, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue