diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 29bdde64..662e3a5e 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -54,7 +54,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout tagged commit - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 with: ref: ${{ github.event.release.target_commitish }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb869ccf..6b3fdbf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: dtolnay/rust-toolchain@master with: @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: Swatinem/rust-cache@v2.7.3 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: Swatinem/rust-cache@v2.7.3 @@ -78,7 +78,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: Swatinem/rust-cache@v2.7.3 @@ -131,7 +131,7 @@ jobs: tool-cache: false - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: Swatinem/rust-cache@v2.7.3 @@ -164,11 +164,12 @@ jobs: ensure_same_swap_id, concurrent_bobs_before_xmr_lock_proof_sent, alice_manually_redeems_after_enc_sig_learned, + happy_path_bob_offline_while_alice_redeems_btc, ] runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: Swatinem/rust-cache@v2.7.3 @@ -179,7 +180,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: Swatinem/rust-cache@v2.7.3 @@ -190,7 +191,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 2e52519a..443c4418 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -11,7 +11,7 @@ jobs: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Extract version from branch name id: extract-version diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 09df0bd7..74ff34ba 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -12,7 +12,7 @@ jobs: name: "Draft a new release" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 with: token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 63ecfe6f..ccd7e641 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,7 +10,7 @@ jobs: name: Create preview release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Delete 'preview' release uses: larryjoelane/delete-release-action@v1.0.24 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bff8d13..28a24cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CLI: Buffer received transfer proofs for later processing if we're currently running a different swap +## [0.13.1] - 2024-06-10 +- Add retry logic to monero-wallet-rpc wallet refresh + ## [0.13.0] - 2024-05-29 - Minimum Supported Rust Version (MSRV) bumped to 1.74 @@ -358,7 +361,8 @@ It is possible to migrate critical data from the old db to the sqlite but there - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. -[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.0...HEAD +[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.1...HEAD +[0.13.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.0...0.13.1 [0.13.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.3...0.13.0 [0.12.3]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.2...0.12.3 [0.12.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...0.12.2 diff --git a/Cargo.lock b/Cargo.lock index 298978fc..203c718d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -743,7 +743,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml 0.8.13", + "toml 0.8.14", ] [[package]] @@ -1017,17 +1017,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "dashmap" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8858831f7781322e539ea39e72449c46b059638250c14344fec8d0aa6e539c" -dependencies = [ - "cfg-if 1.0.0", - "num_cpus", - "parking_lot 0.12.0", -] - [[package]] name = "data-encoding" version = "2.6.0" @@ -1788,19 +1777,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.0.0", "hyper 1.3.1", "hyper-util", - "rustls 0.22.2", + "rustls 0.23.10", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots 0.26.1", ] [[package]] @@ -2597,13 +2587,13 @@ dependencies = [ [[package]] name = "mockito" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c84fe1f1d8c56dc157f79942056fad4b9efceebba374a01b222428b553facb" +checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e" dependencies = [ "assert-json-diff", "colored", - "futures", + "futures-core", "hyper 0.14.28", "log", "rand 0.8.3", @@ -2837,9 +2827,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -3271,6 +3261,53 @@ dependencies = [ "pin-project-lite 0.1.12", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite 0.2.13", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.10", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand 0.8.3", + "ring 0.17.3", + "rustc-hash", + "rustls 0.23.10", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.5", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.35" @@ -3442,9 +3479,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", @@ -3464,7 +3501,8 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite 0.2.13", - "rustls 0.22.2", + "quinn", + "rustls 0.23.10", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", @@ -3472,7 +3510,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tokio-socks", "tokio-util", "tower-service", @@ -3644,11 +3682,11 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ - "log", + "once_cell", "ring 0.17.3", "rustls-pki-types", "rustls-webpki", @@ -3701,15 +3739,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.3.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring 0.17.3", "rustls-pki-types", @@ -3751,6 +3789,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.19" @@ -3787,6 +3834,12 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "seahash" version = "4.1.0" @@ -4030,23 +4083,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" dependencies = [ - "dashmap", "futures", - "lazy_static", "log", + "once_cell", "parking_lot 0.12.0", + "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", @@ -4456,7 +4509,7 @@ checksum = "8049cf85f0e715d6af38dde439cb0ccb91f67fb9f5f63c80f8b43e48356e1a3f" [[package]] name = "swap" -version = "0.13.0" +version = "0.13.1" dependencies = [ "anyhow", "async-compression", @@ -4520,7 +4573,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "toml 0.8.13", + "toml 0.8.14", "torut", "tracing", "tracing-appender", @@ -4557,9 +4610,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "synstructure" @@ -4719,9 +4772,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -4738,9 +4791,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -4771,11 +4824,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.22.2", + "rustls 0.23.10", "rustls-pki-types", "tokio", ] @@ -4860,9 +4913,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -4881,9 +4934,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.1.0", "serde", @@ -5250,9 +5303,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna 0.5.0", diff --git a/docs/asb/README.md b/docs/asb/README.md index b1acc877..88448b71 100644 --- a/docs/asb/README.md +++ b/docs/asb/README.md @@ -110,7 +110,7 @@ The minimum and maximum amount as well as a spread, that is added on top of the 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. -You can plug in a different price ticker websocket using the the `price_ticker_ws_url` configuration option. +You can plug in a different price ticker websocket using the `price_ticker_ws_url` configuration option. You will have to make sure that the format returned is the same as the format used by Kraken. Currently, we use a spot-price model, i.e. the ASB dictates the price to the CLI. diff --git a/monero-rpc/src/wallet.rs b/monero-rpc/src/wallet.rs index bc78e7a6..3e21ad1b 100644 --- a/monero-rpc/src/wallet.rs +++ b/monero-rpc/src/wallet.rs @@ -162,6 +162,12 @@ pub struct BlockHeight { pub height: u32, } +impl fmt::Display for BlockHeight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.height) + } +} + #[derive(Clone, Copy, Debug, Deserialize)] #[serde(from = "CheckTxKeyResponse")] pub struct CheckTxKey { diff --git a/swap/Cargo.toml b/swap/Cargo.toml index efcc736e..fd2b0b39 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap" -version = "0.13.0" +version = "0.13.1" authors = [ "The COMIT guys " ] edition = "2021" description = "XMR/BTC trustless atomic swaps." @@ -81,13 +81,13 @@ bitcoin-harness = { git = "https://github.com/delta1/bitcoin-harness-rs.git", re get-port = "3" hyper = "1.3" jsonrpsee = { version = "0.16.2", features = [ "ws-client" ] } -mockito = "1.3.0" +mockito = "1.4" monero-harness = { path = "../monero-harness" } port_check = "0.2" proptest = "1" sequential-test = "0.2.4" serde_cbor = "0.11" -serial_test = "3.0" +serial_test = "3.1" tempfile = "3" testcontainers = "0.15" diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs index f8ca5023..800e1c06 100644 --- a/swap/src/api/request.rs +++ b/swap/src/api/request.rs @@ -882,19 +882,24 @@ where loop { let min_outstanding = bid_quote.min_quantity - max_giveable; - let min_fee = estimate_fee(min_outstanding).await?; - let min_deposit = min_outstanding + min_fee; + let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?; + let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee; + let max_deposit_until_maximum_amount_is_reached = + maximum_amount - max_giveable + min_bitcoin_lock_tx_fee; tracing::info!( "Deposit at least {} to cover the min quantity with fee!", - min_deposit + min_deposit_until_swap_will_start ); tracing::info!( %deposit_address, - %min_deposit, + %min_deposit_until_swap_will_start, + %max_deposit_until_maximum_amount_is_reached, %max_giveable, %minimum_amount, %maximum_amount, + %min_bitcoin_lock_tx_fee, + price = %bid_quote.price, "Waiting for Bitcoin deposit", ); @@ -913,7 +918,7 @@ where tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); if max_giveable < bid_quote.min_quantity { - tracing::info!("Deposited amount is less than `min_quantity`"); + tracing::info!("Deposited amount is not enough to cover `min_quantity` when accounting for network fees"); continue; } diff --git a/swap/src/asb/recovery/cancel.rs b/swap/src/asb/recovery/cancel.rs index ec70a8df..8da1508f 100644 --- a/swap/src/asb/recovery/cancel.rs +++ b/swap/src/asb/recovery/cancel.rs @@ -39,7 +39,7 @@ pub async fn cancel( | AliceState::BtcRedeemed | AliceState::XmrRefunded | AliceState::BtcPunished - | AliceState::SafelyAborted => bail!("Swap is is in state {} which is not cancelable", state), + | AliceState::SafelyAborted => bail!("Swap is in state {} which is not cancelable", state), }; let txid = match state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await { diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 56fd8e60..99fa206a 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -45,6 +45,7 @@ impl Wallet { pub async fn connect(client: wallet::Client, name: String, env_config: Config) -> Result { let main_address = monero::Address::from_str(client.get_address(0).await?.address.as_str())?; + Ok(Self { inner: Mutex::new(client), network: env_config.monero_network, @@ -125,13 +126,14 @@ impl Wallet { let temp_wallet_address = Address::standard(self.network, public_spend_key, public_view_key); - let wallet = self.inner.lock().await; - // Close the default wallet before generating the other wallet to ensure that // it saves its state correctly - let _ = wallet.close_wallet().await?; + let _ = self.inner.lock().await.close_wallet().await?; - let _ = wallet + let _ = self + .inner + .lock() + .await .generate_from_keys( file_name, temp_wallet_address.to_string(), @@ -144,8 +146,14 @@ impl Wallet { .await?; // Try to send all the funds from the generated wallet to the default wallet - match wallet.refresh().await { - Ok(_) => match wallet.sweep_all(self.main_address.to_string()).await { + match self.refresh(3).await { + Ok(_) => match self + .inner + .lock() + .await + .sweep_all(self.main_address.to_string()) + .await + { Ok(sweep_all) => { for tx in sweep_all.tx_hash_list { tracing::info!( @@ -166,7 +174,12 @@ impl Wallet { } } - let _ = wallet.open_wallet(self.name.clone()).await?; + let _ = self + .inner + .lock() + .await + .open_wallet(self.name.clone()) + .await?; Ok(()) } @@ -261,8 +274,44 @@ impl Wallet { self.main_address } - pub async fn refresh(&self) -> Result { - Ok(self.inner.lock().await.refresh().await?) + pub async fn refresh(&self, max_attempts: usize) -> Result { + const RETRY_INTERVAL: Duration = Duration::from_secs(1); + + for i in 1..=max_attempts { + tracing::info!(name = %self.name, attempt=i, "Syncing Monero wallet"); + + let result = self.inner.lock().await.refresh().await; + + match result { + Ok(refreshed) => { + tracing::info!(name = %self.name, "Monero wallet synced"); + return Ok(refreshed); + } + Err(error) => { + let attempts_left = max_attempts - i; + + // We would not want to fail here if the height is not available + // as it is not critical for the operation of the wallet. + // We can just log a warning and continue. + let height = match self.inner.lock().await.get_height().await { + Ok(height) => height.to_string(), + Err(_) => { + tracing::warn!(name = %self.name, "Failed to fetch Monero wallet height during sync"); + "unknown".to_string() + } + }; + + tracing::warn!(attempt=i, %height, %attempts_left, name = %self.name, %error, "Failed to sync Monero wallet"); + + if attempts_left == 0 { + return Err(error.into()); + } + } + } + + tokio::time::sleep(RETRY_INTERVAL).await; + } + unreachable!("Loop should have returned by now"); } } diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index 8af1167e..609c3eab 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -352,6 +352,7 @@ impl WalletRpc { .arg("--disable-rpc-login") .arg("--wallet-dir") .arg(self.working_dir.join("monero-data")) + .arg("--no-initial-sync") .spawn()?; let stdout = child @@ -369,7 +370,7 @@ impl WalletRpc { } // If we do not hear from the monero_wallet_rpc process for 3 seconds we assume - // it is is ready + // it is ready #[cfg(target_os = "windows")] while let Ok(line) = tokio::time::timeout(std::time::Duration::from_secs(3), reader.next_line()).await @@ -479,7 +480,7 @@ mod tests { #[tokio::test] async fn test_is_daemon_available_success() { - let mut server = mockito::Server::new(); + let mut server = mockito::Server::new_async().await; let _ = server .mock("GET", "/get_info") @@ -510,7 +511,7 @@ mod tests { #[tokio::test] async fn test_is_daemon_available_wrong_network_failure() { - let mut server = mockito::Server::new(); + let mut server = mockito::Server::new_async().await; let _ = server .mock("GET", "/get_info") @@ -541,7 +542,7 @@ mod tests { #[tokio::test] async fn test_is_daemon_available_not_synced_failure() { - let mut server = mockito::Server::new(); + let mut server = mockito::Server::new_async().await; let _ = server .mock("GET", "/get_info") diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index aa0045ea..58c1d723 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -489,6 +489,27 @@ pub struct State4 { } impl State4 { + pub async fn check_for_tx_redeem(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + let tx_redeem = + bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee); + let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); + + let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?; + + let tx_redeem_sig = + tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; + let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?; + let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); + + Ok(State5 { + s_a, + s_b: self.s_b, + v: self.v, + tx_lock: self.tx_lock.clone(), + monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight, + }) + } + pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee); diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 14f47ddc..7a702d77 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -202,6 +202,13 @@ async fn next_state( } } BobState::XmrLocked(state) => { + // In case we send the encrypted signature to Alice, but she doesn't give us a confirmation + // We need to check if she still published the Bitcoin redeem transaction + // Otherwise we risk staying stuck in "XmrLocked" + if let Ok(state5) = state.check_for_tx_redeem(bitcoin_wallet).await { + return Ok(BobState::BtcRedeemed(state5)); + } + let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { @@ -226,6 +233,13 @@ async fn next_state( } } BobState::EncSigSent(state) => { + // We need to make sure that Alice did not publish the redeem transaction while we were offline + // Even if the cancel timelock expired, if Alice published the redeem transaction while we were away we cannot miss it + // If we do we cannot refund and will never be able to leave the "CancelTimelockExpired" state + if let Ok(state5) = state.check_for_tx_redeem(bitcoin_wallet).await { + return Ok(BobState::BtcRedeemed(state5)); + } + let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { @@ -263,13 +277,13 @@ async fn next_state( // might not be able to ever transfer the Monero. tracing::warn!("Failed to generate monero wallet from keys: {:#}", e); tracing::info!(%wallet_file_name, - "Falling back to trying to open the the wallet if it already exists", + "Falling back to trying to open the wallet if it already exists", ); monero_wallet.open(wallet_file_name).await?; } // Ensure that the generated wallet is synced so we have a proper balance - monero_wallet.refresh().await?; + monero_wallet.refresh(20).await?; // Sweep (transfer all funds) to the given address let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?; diff --git a/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs b/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs new file mode 100644 index 00000000..c958c4f0 --- /dev/null +++ b/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs @@ -0,0 +1,44 @@ +pub mod harness; + +use crate::harness::bob_run_until::is_encsig_sent; +use swap::asb::FixedRate; +use swap::protocol::bob::BobState; +use swap::protocol::{alice, bob}; +use tokio::join; + +#[tokio::test] +async fn given_bob_restarts_while_alice_redeems_btc() { + harness::setup_test(harness::SlowCancelConfig, |mut ctx| async move { + let (bob_swap, bob_handle) = ctx.bob_swap().await; + let swap_id = bob_swap.id; + + let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_encsig_sent)); + + 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; + assert!(matches!(bob_state??, BobState::EncSigSent { .. })); + + let (bob_swap, _) = ctx.stop_and_resume_bob_from_db(bob_handle, swap_id).await; + + if let BobState::EncSigSent(state4) = bob_swap.state.clone() { + bob_swap + .bitcoin_wallet + .subscribe_to(state4.tx_lock) + .await + .wait_until_confirmed_with(state4.cancel_timelock) + .await?; + } else { + panic!("Bob in unexpected state {}", bob_swap.state); + } + + // Restart Bob + let bob_state = bob::run(bob_swap).await?; + ctx.assert_bob_redeemed(bob_state).await; + + Ok(()) + }) + .await; +} diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 119dd096..f1327ef4 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -892,7 +892,7 @@ impl Wallet for monero::Wallet { type Amount = monero::Amount; async fn refresh(&self) -> Result<()> { - self.refresh().await?; + self.refresh(1).await?; Ok(()) } @@ -997,6 +997,10 @@ pub mod alice_run_until { pub fn is_encsig_learned(state: &AliceState) -> bool { matches!(state, AliceState::EncSigLearned { .. }) } + + pub fn is_btc_redeemed(state: &AliceState) -> bool { + matches!(state, AliceState::BtcRedeemed { .. }) + } } pub mod bob_run_until {