Skip to content

Add coin_ids to send_xch, send_cat, cancel_offer and cancel_offers - #895

Open
awizardxch wants to merge 2 commits into
xch-dev:mainfrom
awizardxch:feat/send-cancel-coin-ids
Open

awizardxch wants to merge 2 commits into
xch-dev:mainfrom
awizardxch:feat/send-cancel-coin-ids

Conversation

@awizardxch

Copy link
Copy Markdown
Contributor

Closes #780 (priorities 1 and 2: the cancel and send endpoints).

Builds on #894. cancel_offers here relies on #894 building the whole batch as one spend, so the first commit in this branch is #894's. Once #894 merges, only the second commit remains.

What

An optional coin_ids: Option<Vec<String>> on SendXch, SendCat, CancelOffer and CancelOffers, wired the same way MakeOffer.coin_ids is. Chosen coins are spent first, and coin selection only adds what they don't cover. When the field is left out, behavior is unchanged.

A market maker can now give every concurrent cancel or send its own fee coin. Without that, the selector picks the same fee coin for two transactions, or uses a large trading coin for a tiny fee and breaks a prepared coin layout. Those are the two failures in #780.

Security: chosen coins must be selectable

prepare_spends_for_selection looks a chosen coin up in wallet_coins. That table also holds coins that are spent, in the mempool, or locked in an open offer. Taking caller ids as-is would let a send spend the coin behind one of the wallet's own open offers, which silently cancels that offer, or pull an NFT or DID coin into a fee.

So each requested id has to pass a check at the RPC boundary, selectable_coin_ids in crates/sage/src/utils/coin_selection.rs:

  • it must be in the same set automatic selection draws from (selectable_xch_coins / selectable_cat_coins): the wallet's own coins that are confirmed, unspent, not in the mempool and not locked in an offer;
  • it must be a kind the transaction spends: XCH, or coins of the CAT being sent. NFT, DID and option coins, and other CATs, are never accepted.

Anything else is refused with a new CoinNotSelectable error (an API error, HTTP 400) instead of being spent. Duplicate ids are de-duplicated.

The check reuses existing queries, so no new sqlx query or .sqlx entry is needed. The app bridge's send_xch keeps automatic selection (coin_ids: None), so in-wallet apps don't get coin choice.

Wallet API

Wallet::send_xch_with_coins and Wallet::send_cat_with_coins take the chosen coins. send_xch and send_cat call them with none, so existing callers are untouched. Wallet::cancel_offers gains a fee_coin_ids argument.

Tests

sage-rpc test_send_xch_with_coin_ids runs through the real router:

  • a chosen coin is spent (400 to self, 600 change);
  • an unknown coin id and an already-spent coin are refused with CoinNotSelectable;
  • a coin locked in an open offer (made via make_offer with auto_import) is refused, and the free coin still sends.

With the selectability check disabled, this test fails.

Also run: cargo test -p sage-wallet 50 passed, cargo test -p sage-rpc test_send_xch 2 passed, cargo clippy --workspace --all-targets and cargo fmt --check clean. src/bindings.ts is updated by hand in the generator's format (coin_ids?: string[] | null).

Not in this PR: #780's priority 3 (fee coin choice for split/combine).

🤖 Generated with Claude Code

awizardxch and others added 2 commits October 13, 2026 00:21
…twice

cancel_offers cancelled each offer in its own SpendContext and joined the
results. When an offer holds no XCH of its own (a CAT or NFT offer), each
cancel selected an XCH fee coin independently, so two cancels could pick the
same coin and the joined bundle spent it twice (MEMPOOL_CONFLICT / DOUBLE_SPEND).

The wallet now cancels every offer in one spend: one coin per offer, and the
fee coins selected once. The fee stays per offer (fee x count), as the UI
labels it; cancel_offer is the one-offer case of the same code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Callers can choose which coins pay, as make_offer already allows (xch-dev#780): a
market maker keeps a pool of small fee coins, and concurrent sends and cancels
each get their own instead of the selector picking the same one, or a large
trading coin, for a tiny fee.

Chosen coins are spent first and coin selection only adds what they do not
cover. Each must be one the wallet would select by itself: its own,
confirmed, unspent, not in the mempool and not locked in an open offer, and
of a kind the transaction spends (XCH; coins of the CAT for send_cat).
Anything else is refused with CoinNotSelectable instead of being spent, so a
chosen coin can never cancel an open offer or pull an NFT or DID into a send.
App-bridge sends keep automatic selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Feature request: Add coin_ids support to cancel_offer, cancel_offers, send_xch, send_cat (and fee coin selection for split/combine)

1 participant