Skip to content

feat(gui): Voluntary donations#418

Merged
binarybaron merged 26 commits intomasterfrom
feat/voluntary-donate
Jun 25, 2025
Merged

feat(gui): Voluntary donations#418
binarybaron merged 26 commits intomasterfrom
feat/voluntary-donate

Conversation

@Einliterflasche
Copy link
Copy Markdown

@Einliterflasche Einliterflasche commented Jun 17, 2025

Add a button/switch/... that enables users to voluntarily donate a small part (i.e. 0.1%) of their swap to the devs, only if it succeeds

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for splitting Monero swap payouts across multiple addresses, each with customizable percentage allocations and labels.
    • Introduced the ability to create multi-destination Monero sweep transactions, distributing funds according to specified ratios.
    • Added an optional donation tip feature in the GUI allowing users to contribute a small percentage of their swap amount to project development, with transparent display and anonymity via Monero.
  • Improvements

    • Updated the user interface and swap logic to handle pools of Monero addresses rather than a single address.
    • Enhanced database and protocol layers to store and retrieve multiple Monero addresses with associated percentages for each swap.
    • Refined wallet sweep methods to return detailed transaction receipts including keys and block heights.
    • Improved tracing context propagation in wallet threads.
    • Added animated UI elements and better layout for swap progress and address pool breakdown.
    • Enhanced logging for Monero wallet transfers and sweeping operations.
  • Database Migration

    • Migrated the database schema to support multiple Monero addresses per swap, including a new percentage column.
  • Bug Fixes

    • Adjusted related event handling and transaction publishing to accommodate the new multi-address structure.
  • Build and Testing

    • Added embedded patch application to Monero source during build for wallet fee subtraction support.
    • Extended asynchronous and property-based testing for transaction distribution logic.
    • Updated dependencies to support async backoff and testing frameworks.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jun 17, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This change introduces support for splitting Monero swap payouts across multiple addresses with configurable percentages. It adds new abstractions for labeled address pools, updates the database schema and protocol to handle multiple addresses, and implements multi-address sweep functionality in both the Rust and C++ FFI layers. Related application logic, CLI, and GUI code are refactored to use address pools instead of single addresses.

Changes

File(s) Change Summary
.vscode/settings.json Adds a newline at the end of the file.
monero-sys/src/bridge.h, monero-sys/src/bridge.rs, monero-sys/src/lib.rs Adds multi-address sweep transaction support in C++ and Rust FFI, including helper functions for string vectors and proportional distribution logic.
monero-harness/src/lib.rs, monero-harness/tests/wallet.rs Adds multi-recipient sweep tests and replaces fund_address with generate_block.
monero-sys/build.rs, monero-sys/patches/wallet2_api_allow_subtract_from_fee.patch Adds embedded patch application logic to build script; patches wallet2 API to support subtract-fee-from-outputs.
src-gui/src/renderer/rpc.ts Changes buyXmr to use a monero_receive_pool array with donation/swap split instead of a single address.
src-gui/src/renderer/components/pages/help/SettingsBox.tsx, src-gui/src/store/features/settingsSlice.ts Adds a donation tip setting with selectable percentages to support project funding via Monero donations.
src-gui/src/renderer/components/modal/swap/pages/in_progress/SwapSetupInflightPage.tsx Refactors swap setup UI to display Monero receive pool breakdown with animated arrow and improved layout.
src-gui/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx Updates UI to show Monero sent to multiple addresses with detailed pool info.
src-gui/src/renderer/components/modal/swap/InfoBox.tsx Allows title prop to be null and conditionally renders title.
src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx Wraps transaction ID display with truncation component and improves layout with MUI Box.
src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx Adds display of Monero receive pool details in expanded history row.
src-gui/.env.development Comments out local testnet stub provider address variable.
swap/migrations/20250530215011_multiple_monero_receive_addresses.sql Migrates DB schema to support multiple Monero addresses per swap with percentage allocation.
swap/.sqlx/query-7c37de52b3bb2ccd0868ccb861127416848d85eaebe8245c58d5beac7d537087.json,
swap/.sqlx/query-dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b.json
Adds new SQL queries for inserting and selecting multiple Monero addresses with percentages and labels.
swap/.sqlx/query-50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0.json,
swap/.sqlx/query-ce270dd4a4b9615695a79864240c5401e2122077365e5e5a19408c068c7f9454.json
Deletes old single-address SQL query files.
swap/src/cli/api/request.rs, swap/src/cli/api/tauri_bindings.rs, swap/src/cli/command.rs Refactors CLI and Tauri API to use MoneroAddressPool instead of single address in swap requests and events.
swap/src/database/sqlite.rs, swap/src/protocol.rs Updates database trait and implementation to store and retrieve address pools with labels and percentages.
swap/src/monero.rs Introduces LabeledMoneroAddress and MoneroAddressPool structs with logic for splitting amounts by percentage.
swap/src/protocol/bob.rs,
swap/src/protocol/bob/state.rs,
swap/src/protocol/bob/swap.rs
Refactors Bob protocol logic to use address pools for Monero redemption, updates method signatures accordingly.
swap/tests/harness/mod.rs Updates test harness to convert Monero addresses into address pools when constructing Bob swap parameters.
monero-sys/Cargo.toml Adds features to backoff crate and adds diffy and quickcheck dependencies for patching and testing support.

Sequence Diagram(s)

sequenceDiagram
    participant GUI
    participant CLI
    participant Protocol
    participant DB
    participant MoneroWallet
    participant FFI

    GUI->>CLI: buyXmr({monero_receive_pool: [...]})
    CLI->>Protocol: buy_xmr(monero_receive_pool)
    Protocol->>DB: insert_monero_address_pool(swap_id, monero_receive_pool)
    Protocol->>MoneroWallet: sweep_multi(addresses, ratios)
    MoneroWallet->>FFI: createTransactionMultiDest(addresses, amounts)
    FFI-->>MoneroWallet: PendingTransaction*
    MoneroWallet->>FFI: publish(PendingTransaction*)
    FFI-->>MoneroWallet: tx_ids
    MoneroWallet->>Protocol: tx_ids
    Protocol->>CLI: swap_progress_event(monero_receive_pool)
    CLI->>GUI: swapProgressEvent(monero_receive_pool)
Loading

Possibly related PRs

  • feat(monero-sys): Monero bindings #303: Implements native Monero bindings and multi-address sweep support using the same FFI and Rust abstractions, directly overlapping with this PR's changes.

Suggested labels

P1

Suggested reviewers

  • Einliterflasche

Poem

🐇
Splitting coins, we hop with glee,
From one to many, Monero runs free!
Addresses pooled, with labels and care,
Percentages dancing, rewards to share.
Database grows, the protocol too—
A leap for the swap, and a hop for the crew!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed30b0 and e371b1f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (40)
  • .vscode/settings.json (1 hunks)
  • CHANGELOG.md (1 hunks)
  • monero-harness/src/lib.rs (2 hunks)
  • monero-harness/tests/wallet.rs (4 hunks)
  • monero-sys/Cargo.toml (2 hunks)
  • monero-sys/build.rs (3 hunks)
  • monero-sys/patches/wallet2_api_allow_subtract_from_fee.patch (1 hunks)
  • monero-sys/src/bridge.h (3 hunks)
  • monero-sys/src/bridge.rs (1 hunks)
  • monero-sys/src/lib.rs (12 hunks)
  • src-gui/.env.development (1 hunks)
  • src-gui/src/renderer/components/modal/swap/InfoBox.tsx (2 hunks)
  • src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx (3 hunks)
  • src-gui/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx (3 hunks)
  • src-gui/src/renderer/components/modal/swap/pages/in_progress/SwapSetupInflightPage.tsx (2 hunks)
  • src-gui/src/renderer/components/modal/swap/pages/init/InitPage.tsx (2 hunks)
  • src-gui/src/renderer/components/pages/help/SettingsBox.tsx (4 hunks)
  • src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx (2 hunks)
  • src-gui/src/renderer/rpc.ts (3 hunks)
  • src-gui/src/store/features/settingsSlice.ts (5 hunks)
  • swap/.gitignore (0 hunks)
  • swap/.sqlx/query-081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6.json (1 hunks)
  • swap/.sqlx/query-0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c.json (1 hunks)
  • swap/.sqlx/query-5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8.json (1 hunks)
  • swap/.sqlx/query-6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6.json (1 hunks)
  • swap/.sqlx/query-7c37de52b3bb2ccd0868ccb861127416848d85eaebe8245c58d5beac7d537087.json (1 hunks)
  • swap/.sqlx/query-88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf.json (1 hunks)
  • swap/.sqlx/query-98a8b7f4971e0eb4ab8f5aa688aa22e7fdc6b925de211f7784782f051c2dcd8c.json (1 hunks)
  • swap/.sqlx/query-d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2.json (1 hunks)
  • swap/.sqlx/query-dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b.json (1 hunks)
  • swap/.sqlx/query-e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646.json (1 hunks)
  • swap/.sqlx/query-e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae.json (1 hunks)
  • swap/src/cli/api.rs (1 hunks)
  • swap/src/cli/api/request.rs (10 hunks)
  • swap/src/cli/api/tauri_bindings.rs (3 hunks)
  • swap/src/database/sqlite.rs (3 hunks)
  • swap/src/monero.rs (2 hunks)
  • swap/src/protocol.rs (2 hunks)
  • swap/src/protocol/bob/state.rs (3 hunks)
  • swap/src/protocol/bob/swap.rs (11 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Einliterflasche Einliterflasche force-pushed the feat/voluntary-donate branch from d3315d2 to 5ed30b0 Compare June 17, 2025 13:44
@binarybaron
Copy link
Copy Markdown

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jun 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🔭 Outside diff range comments (1)
swap/migrations/20250530215011_multiple_monero_receive_addresses.sql (1)

6-30: Schema allows duplicates & lacks percentage-sum enforcement

  • Dropping the primary key without adding a replacement means the same (swap_id, address) pair can be inserted multiple times.
  • Nothing enforces that Σ percentage = 1 for a given swap_id.

Consider:

-- composite PK prevents duplicates
CREATE TABLE monero_addresses_temp (
    swap_id    TEXT NOT NULL,
    address    TEXT NOT NULL,
    percentage REAL NOT NULL CHECK (percentage > 0 AND percentage <= 1),
    label      TEXT NOT NULL DEFAULT 'user address',
    PRIMARY KEY (swap_id, address)
);

-- Optional but recommended: guarantee the pool sums to 1
-- via trigger
CREATE TRIGGER trg_monero_percentage_sum
AFTER INSERT OR UPDATE ON monero_addresses
BEGIN
  DELETE FROM monero_addresses_error; -- optional helper
  SELECT
    CASE
      WHEN (SELECT ROUND(SUM(percentage), 5)
            FROM monero_addresses
            WHERE swap_id = NEW.swap_id) <> 1.0
      THEN RAISE(ABORT, 'Percentages must sum to 1')
    END;
END;

This keeps data consistent and prevents subtle runtime errors.

♻️ Duplicate comments (2)
swap/src/protocol/bob/swap.rs (2)

720-729: Same payload mismatch in cooperative-redeem path

Repeat of the issue above – ensure the Tauri event schema is consistently updated.


795-796: And again for XmrRedeemed terminal state

Update or wrap the pool into a displayable representation before emitting.

🧹 Nitpick comments (7)
.vscode/settings.json (1)

1-71: Out-of-scope editor configuration change.

The modifications to VSCode file associations are unrelated to the “voluntary donate” GUI feature. Consider isolating or removing this commit to keep the PR focused on the donation functionality.

swap/src/cli/command.rs (1)

71-75: Consider validating the percentage-sum invariant when constructing pools

monero_receive_pool is blindly created from a single address.
Down-stream layers rely on the invariant “percentages per swap sum to 1”.
Even though this pool currently contains exactly one 100 % entry, an early sanity–check here would prevent accidental creation of ill-formed pools in the future when CLI parameters are extended (e.g. via --donate).

 let monero_receive_pool: MoneroAddressPool =
-    monero_address::validate_is_testnet(monero_receive_address, is_testnet)?.into();
+    monero_address::validate_is_testnet(monero_receive_address, is_testnet)?
+        .into()
+        .ensure_percentage_sum()?;

(Assumes a lightweight ensure_percentage_sum() helper returning Result).
Adding this guard keeps the CLI the first line of defence.

swap/src/cli/api/request.rs (1)

650-660: Legacy swaps will panic after DB migration

insert_monero_address_pool is always invoked for new swaps, but resume_swap blindly calls get_monero_address_pool.
Swaps created before the migration only have a single address entry → the DB will return NotFound and the resume command aborts.

Consider:

let monero_receive_pool = context
    .db
    .get_monero_address_pool(swap_id)
    .await
    .or_else(|_| context.db.get_legacy_monero_address(swap_id).await.map(|a| a.into_pool()))?;

or present a clear error message instructing users to upgrade / re-start the swap.

swap/src/protocol/bob.rs (1)

25-28: monero_receive_pool is stored only in memory

Swap::new receives the pool but does not persist it – persistence relies on the earlier call site to insert_monero_address_pool.
If another constructor is added later and forgets that DB write, we create silent inconsistencies.

Add an invariant or helper:

debug_assert!(
    db.has_monero_address_pool(id).await.expect("db") ,
    "monero_receive_pool must be inserted into DB before Swap::new"
);

or make Swap::new perform the insertion itself, guaranteeing consistency.

swap/src/database/sqlite.rs (1)

112-116: Avoid Decimal → f64 round-trip

Converting Decimal to f64 (to_f64) and back (from_f64) sacrifices precision and can introduce rounding errors for small donation percentages.

Persist the exact string (Decimal::to_string()) or the underlying integer representation instead.

monero-sys/src/lib.rs (2)

22-24: Duplicate import

use cxx::let_cxx_string; is already imported on the previous line – drop the second import.


419-429: Bring sweep_multi in line with existing retry strategy

transfer and sweep are wrapped in retry_notify, but sweep_multi is not.
Add the same retry/back-off pattern for behavioural consistency.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7042b1b and 5ed30b0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .vscode/settings.json (1 hunks)
  • monero-sys/src/bridge.h (2 hunks)
  • monero-sys/src/bridge.rs (1 hunks)
  • monero-sys/src/lib.rs (3 hunks)
  • src-gui/src/renderer/rpc.ts (2 hunks)
  • swap/.sqlx/query-50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0.json (0 hunks)
  • swap/.sqlx/query-ce270dd4a4b9615695a79864240c5401e2122077365e5e5a19408c068c7f9454.json (0 hunks)
  • swap/migrations/20250530215011_multiple_monero_receive_addresses.sql (1 hunks)
  • swap/src/cli/api/request.rs (7 hunks)
  • swap/src/cli/api/tauri_bindings.rs (2 hunks)
  • swap/src/cli/command.rs (3 hunks)
  • swap/src/database/sqlite.rs (3 hunks)
  • swap/src/monero.rs (1 hunks)
  • swap/src/protocol.rs (2 hunks)
  • swap/src/protocol/bob.rs (6 hunks)
  • swap/src/protocol/bob/state.rs (3 hunks)
  • swap/src/protocol/bob/swap.rs (6 hunks)
  • swap/tests/harness/mod.rs (2 hunks)
💤 Files with no reviewable changes (2)
  • swap/.sqlx/query-ce270dd4a4b9615695a79864240c5401e2122077365e5e5a19408c068c7f9454.json
  • swap/.sqlx/query-50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0.json
🧰 Additional context used
🧬 Code Graph Analysis (5)
swap/src/cli/command.rs (1)
swap/src/monero.rs (1)
  • validate_is_testnet (577-587)
src-gui/src/renderer/rpc.ts (1)
src-gui/src/utils/multiAddrUtils.ts (1)
  • providerToConcatenatedMultiAddr (10-14)
monero-sys/src/bridge.rs (1)
monero-sys/src/bridge.h (4)
  • createMultiSweepTransaction (132-171)
  • createMultiSweepTransaction (132-135)
  • vector_string_push_back (212-217)
  • vector_string_push_back (212-214)
swap/src/database/sqlite.rs (2)
swap/src/protocol.rs (2)
  • insert_monero_address_pool (144-148)
  • get_monero_address_pool (149-149)
swap/src/monero.rs (7)
  • address (242-244)
  • addresses (275-277)
  • percentage (246-248)
  • label (250-252)
  • new (234-240)
  • new (262-264)
  • new (411-413)
monero-sys/src/lib.rs (2)
monero-sys/src/bridge.h (6)
  • vector_string_push_back (212-217)
  • vector_string_push_back (212-214)
  • createMultiSweepTransaction (132-171)
  • createMultiSweepTransaction (132-135)
  • pendingTransactionTxIds (204-207)
  • pendingTransactionTxIds (204-204)
monero-sys/src/bridge.rs (3)
  • vector_string_push_back (242-242)
  • createMultiSweepTransaction (236-240)
  • pendingTransactionTxIds (254-256)
🪛 Biome (1.9.4)
.vscode/settings.json

[error] 71-71: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 71-71: expected } but instead the file ends

the file ends here

(parse)

🔇 Additional comments (6)
swap/tests/harness/mod.rs (1)

495-501: LGTM – adapts to MoneroAddressPool API

The additional .into() calls correctly convert the validated monero::Address into the new pool type.
No further issues spotted.

swap/src/protocol/bob/state.rs (1)

720-752: ```shell
#!/bin/bash
set -e

Locate LabeledMoneroAddress definition

rg -n "struct LabeledMoneroAddress" -n swap/src/monero.rs -A5


</details>
<details>
<summary>swap/src/protocol.rs (1)</summary>

`140-150`: **Trait change breaks implementors without a default impl**

All DB back-ends must now implement `insert_monero_address_pool` / `get_monero_address_pool`.  
Make sure `sqlite.rs`, `mock_db.rs` (tests) and any out-of-tree adapters are updated; otherwise compilation fails.

If you want partial compatibility, provide default implementations that return `todo!()` so downstream crates fail at runtime instead of compile-time.

</details>
<details>
<summary>swap/src/database/sqlite.rs (1)</summary>

`515-530`: **Restore coverage for the new multi-address API**

The old single-address test was commented out and not replaced.  
Add tests exercising `insert_monero_address_pool`/`get_monero_address_pool`, asserting round-trip integrity and percentage accuracy.

</details>
<details>
<summary>swap/src/protocol/bob/swap.rs (1)</summary>

`517-526`: **Event payload now mismatches the type expected by the frontend**

`TauriSwapProgressEvent::XmrRedeemInMempool { xmr_redeem_address, … }` previously carried a `monero::Address`.  
You now pass `MoneroAddressPool`, which will not compile unless the enum and UI were updated accordingly.  Verify the event definition and adjust the field name/documentation.

</details>
<details>
<summary>swap/src/monero.rs (1)</summary>

`266-273`: [web_search]
```web
Does rust_decimal PartialEq consider scale when comparing Decimal values?

Comment on lines 141 to 143
const DONATE_PERCENTAGE = 0.1;

export async function buyXmr(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Donation percentage off by two orders of magnitude

PR description: “voluntarily donate 0.1 % of the swap amount”.
DONATE_PERCENTAGE = 0.1 represents 10 %, not 0.1 %. Users will unintentionally donate 100× more.

-const DONATE_PERCENTAGE = 0.1;
+// 0.1 % expressed as a fraction
+const DONATE_PERCENTAGE = 0.001;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DONATE_PERCENTAGE = 0.1;
export async function buyXmr(
// 0.1 % expressed as a fraction
const DONATE_PERCENTAGE = 0.001;
export async function buyXmr(
🤖 Prompt for AI Agents
In src-gui/src/renderer/rpc.ts around lines 141 to 143, the DONATE_PERCENTAGE
constant is set to 0.1, which represents 10%, but the intended donation
percentage is 0.1%. Fix this by changing the value of DONATE_PERCENTAGE to 0.001
to correctly represent 0.1%.

Comment on lines +242 to +243
fn vector_string_push_back(v: Pin<&mut CxxVector<CxxString>>, s: &CxxString);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

vector_string_push_back mutates a pinned vector – check pin-safety

Pin<&mut CxxVector<_>> promises the vector won’t be re-allocated, yet pushing can re-allocate.
cxx internally allows this but only for an unpinned &mut CxxVector. Either:

  1. Accept &mut CxxVector<CxxString> (like CxxVector::push) or
  2. Document why re-allocation is impossible here (e.g. the vector was pre-reserved).

Otherwise this is unsound.

🤖 Prompt for AI Agents
In monero-sys/src/bridge.rs around lines 242 to 243, the function
vector_string_push_back takes a Pin<&mut CxxVector<CxxString>>, which promises
no re-allocation, but pushing can cause re-allocation making this unsafe. To
fix, change the parameter to accept a plain &mut CxxVector<CxxString> instead of
a pinned reference, matching the signature of CxxVector::push, or add clear
documentation explaining why re-allocation cannot happen here (e.g., the vector
is pre-reserved and cannot grow).

Comment on lines +311 to +332
// Iterate over all but the last address
for address in first_addresses.iter() {
// Calculate the amount for this address
let split_amount = address.percentage.saturating_mul(amount_decimal).floor();

// Convert the Decimal to a u64
// We can use expect because we know that the Decimal is floored
let split_amount = split_amount
.to_u64()
.expect("Floored Decimal should be representable as u64");

let split_amount = monero::Amount::from_pico(split_amount);
result.push((address.address, split_amount));
}

// Give the remainder to the last address
let distributed_amount =
Amount::from_piconero(result.iter().map(|(_, amount)| amount.as_pico()).sum());
let remainder = amount - distributed_amount.into();

result.push((last_address.address, remainder));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Borrow-checker violation – moving address out of a &LabeledMoneroAddress

address.address moves a non-Copy field out of a shared reference and will not compile.

-            result.push((address.address, split_amount));
+            result.push((address.address(), split_amount));-        result.push((last_address.address, remainder));
+        result.push((last_address.address(), remainder));

Compile failure example:

cannot move out of `*address` which is behind a shared reference
🤖 Prompt for AI Agents
In swap/src/monero.rs around lines 311 to 332, the code attempts to move the
non-Copy field `address.address` out of a shared reference, causing a
borrow-checker error. To fix this, change the code to clone or otherwise copy
the `address.address` value instead of moving it, for example by calling
`.clone()` on `address.address` when pushing it into the result vector. This
preserves the original reference and satisfies the borrow checker.

Comment on lines +132 to +171
inline PendingTransaction *createMultiSweepTransaction(
Wallet &wallet,
const std::vector<std::string> &dest_addresses,
const std::vector<double> &sweep_ratios)
{
size_t N = dest_addresses.size();
if (N == 0 || sweep_ratios.size() != N)
return nullptr;

uint64_t bal = wallet.unlockedBalance();
std::vector<uint64_t> amounts(N);
uint64_t sum = 0;
for (size_t i = 0; i + 1 < N; ++i)
{
amounts[i] = static_cast<uint64_t>(std::floor(bal * sweep_ratios[i]));
sum += amounts[i];
}
// reserve last slot for fee carve‐out
amounts[N - 1] = 0;

// build temporary dest list for fee estimation
std::vector<std::pair<std::string, uint64_t>> fee_dests;
fee_dests.reserve(N);
for (size_t i = 0; i < N; ++i)
fee_dests.emplace_back(dest_addresses[i], amounts[i]);

// ask wallet to estimate fee assuming N outputs + 1 change
uint64_t fee = wallet.estimateTransactionFee(fee_dests, PendingTransaction::Priority_Default);

// carve the fee out of the last output, so sum(amounts)+fee == balance
amounts[N - 1] = bal - sum - fee;

// build the actual multi‐dest transaction (no change left ⇒ wallet drops it)
return wallet.createTransactionMultDest(
dest_addresses,
/*payment_id=*/"",
Monero::optional<std::vector<uint64_t>>(amounts),
/*mixin_count=*/0,
PendingTransaction::Priority_Default);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Potential underflow & unchecked invariants in createMultiSweepTransaction

  1. sweep_ratios are not validated to be positive and to sum to 1.
  2. amounts[N-1] = bal - sum - fee; will underflow (wrap to UINT64_MAX) when fee + sum > bal, e.g. high fee or rounding error.
  3. Single-destination sweeps (N == 1) still estimate the fee with amounts[0] = 0, likely producing an incorrect/negative final amount.

Recommendation:

if (N == 0) return nullptr;
double ratio_sum = std::accumulate(sweep_ratios.begin(), sweep_ratios.end(), 0.0);
if (std::fabs(ratio_sum - 1.0) > 1e-12) return nullptr;
/* … */
if (fee + sum > bal) return nullptr;   // or clamp / re-estimate

Consider early-return or throwing an error instead of silently producing an invalid transaction.

🤖 Prompt for AI Agents
In monero-sys/src/bridge.h between lines 132 and 171, the
createMultiSweepTransaction function lacks validation for sweep_ratios being
positive and summing to 1, risks underflow when fee plus sum exceeds balance,
and mishandles single-destination cases by estimating fees with zero amounts. To
fix this, add checks to ensure N is not zero, verify that the sum of
sweep_ratios is approximately 1, and confirm that fee plus sum does not exceed
the balance before subtracting; if these conditions fail, return nullptr or
handle the error appropriately. Also, adjust the fee estimation logic for
single-destination sweeps to avoid incorrect amounts.

@Einliterflasche Einliterflasche changed the title feat(gui): enable users to voluntary donate feat(gui): voluntary donations Jun 20, 2025
@Einliterflasche Einliterflasche self-assigned this Jun 20, 2025
return nullptr;
}

// To estimate the correct fee we build a transation that spends 1 piconero
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will fail if we have less piconeros than addresses to spend to.

add a sanity check to fail if we have less piconeros than sweep addresses.

bail!("Number of addresses and ratios must match")
}

const TOLERANCE: f64 = 1e-12;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a swap amount of $100k this allows for a tolerance of: $0.0000001

Really no need to be this precise. Let's allow for more tolerance.

You could also scale the ratios to 1, meaning if they sum to 0.9, you scale the ratios such that they add up to 1

seller: providerToConcatenatedMultiAddr(seller),
monero_receive_pool: [
{
address: monero_receive_address,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the donation address here

Use the configured donation percentage from the settings

label: String,
}

impl LabeledMoneroAddress {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed anymore?

Copy link
Copy Markdown

@binarybaron binarybaron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts

@binarybaron binarybaron changed the title feat(gui): voluntary donations feat(gui): Voluntary donations Jun 20, 2025
@binarybaron
Copy link
Copy Markdown

@Einliterflasche is this ready for review? Have you tested it yet?

@Einliterflasche
Copy link
Copy Markdown
Author

Einliterflasche commented Jun 22, 2025

Tests still failing, not really ready for review, except maybe the distribute function /tests

uint64_t num_outputs)
{
std::vector<std::pair<std::string, uint64_t>> fee_dests;
for (uint64_t i = 0; i < num_outputs; ++i)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong, we need to:

  • ensure balance (in piconeros is > num_outputs)
  • drop the last address because wallet2 will create a change output. The change output will only be dropped if there is no XMR left after subtracting the fee

// Check if we have any destinations at all
if (n == 0)
{
// wallet.setStatusError("Number of destinations must be greater than 0");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we set this error?

@binarybaron
Copy link
Copy Markdown

thread 'fund_transfer_and_check_tx_key' panicked at monero-harness/tests/wallet.rs:88:10:
called Result::unwrap() on an Err value: Failed to perform sweep

Caused by:
0: Failed to publish transaction
1: Failed to publish transaction
2: Failed to create transaction
3: Experienced pending transaction error (error): not enough money to transfer, available only 0.996383200000, transaction amount 0.997145200000 = 0.993816400000 + 0.003328800000 (fee)
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

Clearly an issue with the implementation, not just the test

…reads, use new subtract_fee wallet2 functionality

let state5 = state.attempt_cooperative_redeem(s_a, lock_transfer_proof);

match retry(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this reverted

use uuid::Uuid;

const PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS: u64 = 120;
const PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS: u64 = 120 * 10;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this increase

#[tokio::test]
async fn test_insert_load_monero_address() -> Result<()> {
let db = setup_test_db().await?;
// #[tokio::test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this test

)
.await
.context("Failed to redeem Monero")?;
let xmr_redeem_txids = state
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the retry here?

@binarybaron
Copy link
Copy Markdown

@coderabbitai review this

@eigenwallet eigenwallet deleted a comment from coderabbitai bot Jun 25, 2025
@binarybaron binarybaron marked this pull request as ready for review June 25, 2025 14:37
@binarybaron binarybaron merged commit 11b891f into master Jun 25, 2025
1 of 2 checks passed
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wallet Catch-Up Warning Misfires

The wait_for_wallet_to_catch_up function unconditionally logs a warning "Wallet {} not caught up to expected balance of {}" at its conclusion. This is misleading as it occurs even when the wallet successfully reaches the expected balance, implying a failure that did not occur. The warning should only be logged if the wallet fails to catch up after all retry attempts.

monero-harness/tests/wallet.rs#L128-L134

https://github.com/UnstoppableSwap/core/blob/e371b1f4a83f01d44539f5160b1245827f987894/monero-harness/tests/wallet.rs#L128-L134

Fix in Cursor


Bug: Percentage Scale Mismatch Causes Validation Errors

The percentage scale is inconsistent across the application. The GUI sends percentages in a 0-100 scale, but the LabeledMoneroAddress::new() function validates them as 0-1. Furthermore, the monero-sys distribute() function expects percentages that sum to 100. This mismatch leads to runtime validation errors when creating labeled addresses and distributing funds.

swap/src/monero.rs#L252-L267

https://github.com/UnstoppableSwap/core/blob/e371b1f4a83f01d44539f5160b1245827f987894/swap/src/monero.rs#L252-L267

src-gui/src/renderer/rpc.ts#L179-L196

https://github.com/UnstoppableSwap/core/blob/e371b1f4a83f01d44539f5160b1245827f987894/src-gui/src/renderer/rpc.ts#L179-L196

Fix in Cursor


BugBot free trial expires on July 22, 2025
You have used $0.00 of your $0.00 spend limit so far. Manage your spend limit in the Cursor dashboard.

Was this report helpful? Give feedback by reacting with 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants