Skip to content

Support multi-recipient Monero payment URIs - #1459

Open
waozixyz wants to merge 1 commit into
cypherstack:stagingfrom
waozixyz:monero-multi-recipient-uri
Open

waozixyz wants to merge 1 commit into
cypherstack:stagingfrom
waozixyz:monero-multi-recipient-uri

Conversation

@waozixyz

@waozixyz waozixyz commented Oct 2, 2026 •

Copy link
Copy Markdown

Monero payment URIs can request payment to several recipients at once, as described in the Monero URI scheme: ;-separated addresses with a matching number of tx_amount values, e.g. monero:A;B?tx_amount=1;2&recipient_name=x;y (used by split-tip pages such as XMRChat). Stack Wallet currently can't parse these, so scanning one leaves an invalid address and no amount.

  • AddressUtils.parsePaymentUri splits the ;-separated addresses, amounts and names into PaymentUriData.recipients, requiring one amount (and, if given, one name) per address. Callers opt in with allowMultipleRecipients, so screens that take a single address reject these URIs instead of dropping recipients.
  • MoneroWallet now reports supportsMultiRecipient, and the send screens only accept multi-recipient URIs for wallets that do.
  • Mobile and desktop send: "Add recipient", or a scanned, pasted or typed multi-recipient URI, replaces the address field with the FROST Recipient forms, one per recipient (editable address and amount, scan/paste, Remove, Add recipient). A multi-recipient URI scanned or pasted into one of those forms asks before replacing all recipients. A multi-recipient URI rejected for its amounts clears the previous recipient and amount, so Preview can't send them instead. The amount field shows the total, and removing all but one recipient returns to the normal form. The transaction has one output per recipient.
  • The Recipient form now shows the request's name for its address and flags an invalid address, fills in address, amount and name from a pasted payment request as it did from a scanned one, scans with the desktop QrCodeScannerDialog on desktop, and keeps its amount when the display unit changes (previously e.g. 1000 mXMR could be read as 1000 XMR after switching to XMR). The send screens' amount field does the same, keeping a Xelis send all. Recipient forms are keyed by wallet, so one wallet's recipients can't appear in another's form.
  • The confirmation screen lists every recipient with its name and amount.
  • prepareSend no longer treats a multi-recipient total equal to the whole balance as sending the whole balance (which it then rejected for multiple recipients).

Tested with unit and widget tests (including the unit change), and on Android (moto e6 play) with an empty Monero wallet by scanning QR codes: a two-recipient request from the normal send form; a single-recipient request into an added recipient's own scan button; a two-recipient request into a recipient form, which asked to replace the recipients and did; editing amounts and removing recipients back to the normal form. With the display unit set to mXMR, the forms showed 1 and 2.5 mXMR for 0.001 and 0.0025 XMR, and Preview built a transaction for exactly 0.005 XMR after entering 1 and 4 mXMR. Preview fails with "not enough money" as expected for an empty wallet; not yet tested with a funded wallet through to broadcast, or on desktop beyond widget tests.

@waozixyz
waozixyz force-pushed the monero-multi-recipient-uri branch from 67f08f1 to 9b3a522 Compare October 4, 2026 16:29
@julian-CStack

Copy link
Copy Markdown
Collaborator

There are several issues with this, the biggest being using a single destination send UI for a multi dest transaction

@waozixyz
waozixyz force-pushed the monero-multi-recipient-uri branch from 9b3a522 to a404cb9 Compare October 5, 2026 19:48
@waozixyz

waozixyz commented Oct 5, 2026

Copy link
Copy Markdown
Author

@julian-CStack thanks for taking a look. I changed it so a multi-recipient payment no longer goes through the single-address form: each recipient now has its own editable address and amount, with Remove and Add recipient, and the amount field shows their total. Removing recipients down to one returns to the normal form, and Add recipient is available there too.

Also:

  • It's Monero only: MoneroWallet sets supportsMultiRecipient, and the send screens only accept multi-recipient URIs when the wallet supports them.
  • The confirmation screen shows each recipient's name from the request.
  • A multi-recipient total equal to the whole balance is no longer treated as send all, which prepareSend then rejected.

I squashed it into one commit and tested it on Android by scanning a two-recipient QR code (details in the description). You mentioned several issues; could you list the others so I can fix them too?

@julian-CStack

Copy link
Copy Markdown
Collaborator

There are issues with form state. Paste/clear could get messed up. Also if display units have been changed, parsing/updating amounts can be updated to 1000x by accident. If you are using an LLM you can ask about these for more info, if not I can provide more details.

We had started doing multi recipient send UI with FROST bitcoin so there is a base which is probably a better starting point for the monero multi dest send ui rather than starting over. The existing stuff doesn't have labels/names or address validation errors per destination but it does have editable fields so if you want to manually do multi dest sends you can do that. Each recipient field has a scan QR button as well.

Not sure what the best UX is for handling a scan/paste of a multi destination qr/uri, but maybe a single dialog asking if user wants to reset all recipient fields (if more than one on screen) and fill in the recipients from the URI? I'm open to suggestions

@waozixyz
waozixyz force-pushed the monero-multi-recipient-uri branch from a404cb9 to 306e646 Compare October 7, 2026 02:31
@waozixyz

waozixyz commented Oct 7, 2026

Copy link
Copy Markdown
Author

@julian-CStack thanks, that helped. I've redone it on top of the FROST recipient forms:

  • FROST base: sending to several recipients now uses the Recipient forms from the FROST send view (editable address and amount, scan/paste, Remove, Add recipient) instead of a separate form. I added the request's name and a per-recipient "Invalid address" error to them.
  • Display units: the recipient forms and the send screens' amount field now keep the amount when the display unit changes. Before, the number typed was re-read in the new unit, so 1000 mXMR became 1000 XMR after switching to XMR (reachable on desktop, where the send form stays open behind settings).
  • Paste/clear: pasting a payment request into a recipient now fills in its address, amount and name, the same as scanning one, and clearing an address also drops its name. Recipient state is also kept per wallet now, so recipients entered for one wallet can't show up in another wallet's form (including a FROST wallet's).
  • Multi-recipient QR/URI: from the normal send form it fills in the recipients directly, since only one is on screen. Scanned or pasted into a recipient form, it shows one "Replace recipients?" dialog first, as you suggested.

Tested on Android by scanning QR codes for each of those cases, including with the unit set to mXMR (details in the description). If the form state issues you saw were something else, could you point me at them?

@julian-CStack

Copy link
Copy Markdown
Collaborator

Thats a step in the right direction, thanks!

I'll take another look once the conflicts have been resolved.

@waozixyz
waozixyz force-pushed the monero-multi-recipient-uri branch from 306e646 to 5b85d70 Compare October 7, 2026 22:17
@waozixyz

waozixyz commented Oct 7, 2026

Copy link
Copy Markdown
Author

@julian-CStack thanks! I've rebased onto the latest staging and resolved the conflicts. Both send screens now pass the new xelisSendAll flag alongside the multi-recipient outputs. I re-tested on Android with cs_monero 4.0.0 by scanning a multi-recipient QR code.

@julian-CStack

Copy link
Copy Markdown
Collaborator

Couple little things:

  1. Invalidate rejected payment requests. In both applyMultiRecipientUri implementations, rejecting zero/subatomic amounts must invalidate the destination and disable Preview. Currently, pasting over a valid payment can leave the new URI visible while Preview sends the old address and amount.
  2. Use the desktop QR scanner in recipient fields. Recipient._onQrTapped currently opens the mobile-only scanner. On desktop, reuse the existing QrCodeScannerDialog; retain the current mobile path
  3. Preserve send-all intent during unit formatting. The new formatter listeners trigger _cryptoAmountChanged, which clears _xelisSendAll despite the canonical amount being unchanged. Preserve that flag during display-only updates in both send screens. Reproduce with: Send all
    → change units → balance increases → Preview.
  4. Explicitly inject Util.layoutPlatform with FakePlatform when emulating mobile layout; Util.screenWidth alone does not select mobile layout on macOS or Windows.

The send form state in general is a bit messy but that is also pre existing so out of the scope of this PR. Fixing the above issues should be sufficient for now.

Monero payment URIs can list several recipients, e.g.
`monero:A;B?tx_amount=1;2&recipient_name=x;y`. Parse these into
PaymentUriData.recipients, requiring one amount (and, when given, one
name) per address. Callers opt in with allowMultipleRecipients, so screens
that handle a single address reject these URIs rather than dropping
recipients.

Monero wallets now report supportsMultiRecipient. On their mobile and
desktop send screens, "Add recipient" or a multi-recipient URI replaces
the address field with the Recipient forms started for FROST, one per
recipient, which can be added to and removed. A multi-recipient URI
scanned or pasted into one of those forms asks before replacing all the
recipients. A multi-recipient URI rejected for its amounts clears the
previous recipient and amount, so Preview can't send them instead. The
amount field shows their total, and removing all but one recipient
returns to the normal form. The transaction has one output per
recipient, and the confirmation screen lists each recipient with its
name and amount.

The Recipient form now:
- shows the payment request's name for its address, and flags an
  invalid address;
- fills in the address, amount, and name from a pasted payment request,
  as it did from a scanned one;
- scans with the desktop QR code scanner on desktop;
- keeps its amount, rather than the number typed, when the display unit
  changes, which otherwise read e.g. 1000 mXMR as 1000 XMR. The send
  screens' amount field does the same, keeping any Xelis send all.
Recipient forms are kept per wallet, so recipients entered for one
wallet never appear in another wallet's form.

prepareSend no longer treats a multi-recipient total equal to the whole
balance as sending the whole balance, which it then rejected for
multiple recipients; the fee has to fit in the balance as for any other
amount.
@waozixyz
waozixyz force-pushed the monero-multi-recipient-uri branch from 166bf15 to 81b0f4d Compare October 8, 2026 18:08
@waozixyz

waozixyz commented Oct 8, 2026

Copy link
Copy Markdown
Author

@julian-CStack thanks, all four are fixed:

  1. Rejected payment requests: when applyMultiRecipientUri rejects a request for zero/subatomic amounts, both send screens now clear the previous address and amount, so Preview is disabled instead of sending the old payment. The address field's clear/paste icons now follow the field's text, since a request can replace or clear it.
  2. Desktop QR scanner: on desktop, the recipient forms' scan button opens QrCodeScannerDialog; mobile still uses the barcode scanner.
  3. Send all intent: the unit reformatting in both send screens now keeps _xelisSendAll, since only the display changes.
  4. Test layout: the widget tests set Util.layoutPlatform with FakePlatform (android/linux) and restore it, instead of relying on Util.screenWidth.

I rebased onto staging again, so it's one commit on top of the same staging you merged. On Android, a valid payment request followed by one with a zero amount now clears the form and disables Preview. The desktop scanner and the Xelis send all change are covered by code and tests only.

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