Allow taking aggregate offers with pass-through NFTs - #890
judeallred wants to merge 3 commits into
Conversation
|
|
||
| let arbitrage = offer.arbitrage(); | ||
|
|
||
| let mut requested_nfts = IndexMap::new(); |
There was a problem hiding this comment.
Note to self, while this code is working for resolving the bug, this may have regressed non-arb case. Need to test and verify that locally.
There was a problem hiding this comment.
Verified: no regression in the non-aggregate case. Tests are in 35b5737.
The existing NFT offer tests only checked that the take landed on chain. That catches an underpaid royalty (the maker's NFT spend asserts the exact payment), but not an overpaid one. So I added exact taker balance checks to the six existing NFT tests, plus three new single-maker tests: NFT for NFT (both with royalties), NFT for XCH + CAT, and NFT + XCH for XCH.
I ran the same test file against three versions of take_offer, each with a clean build:
| Tests | main |
2e9fb11 (fix) | 6d23848 (unified royalties) |
|---|---|---|---|
| Existing NFT/XCH/CAT offer tests with exact balances, NFT for NFT, NFT for XCH + CAT | pass | pass | pass |
| Aggregate with pass-through NFT | Missing asset |
pass | pass |
| NFT + XCH offered for XCH | AssertPuzzleAnnouncementFailed |
AssertPuzzleAnnouncementFailed |
pass |
Every non-aggregate NFT offer that works on main settles with identical balances after the change.
The one behavioural difference is a pre-existing bug that the unified calculation fixes. When a maker offers an NFT plus some XCH and requests XCH, their NFT spend commits to a trade price of the full requested amount. The old code priced the royalty on the net amount the taker adds (900 of 1000), underpaid it, and the chain rejected the take.
test_take_aggregate_offer_with_pass_through_nft fails with Missing asset and test_offer_nft_and_xch_for_xch fails with AssertPuzzleAnnouncementFailed. Also adds exact taker balance checks to the existing NFT offer tests, which pass unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Taking an aggregate offer where the same NFT is offered and requested no longer requires the taker to own the NFT. Co-authored-by: Cursor <cursoragent@cursor.com>
Pays royalties for every offered NFT, including pass-through NFTs in aggregate offers, on the trade prices makers commit to. Co-authored-by: Cursor <cursoragent@cursor.com>
35b5737 to
775be32
Compare
|
@Rigidity for your consideration |
|
found a regression; need to fix first. ignore me :-) |
Reproduction
Create an offer to sell a Chia Buddy NFT for 10 XCS:
Offer 1: Chia Buddy for 10 XCS
Make an offer to buy the same Chia Buddy for 100 XCS:
Offer 2: 100 XCS for the same Chia Buddy
Combine the two offers and give the combined offer to Sage (Take Offer), from a wallet that doesn't own the Chia Buddy:
Combined offer
Expected: the take works, netting a successful arbitrage of 89.7 XCS.
Actual (before this fix): Sage refused, saying the wallet doesn't have the NFT in its possession. That's true, but it shouldn't block the take: the NFT comes in from one side of the combined offer and goes out to the other, so it nets out and the taker never needs to own it.
Fix
select_spends_excludingincrates/sage-wallet/src/wallet.rs): any NFT with an output (here, the payment to the bidder) was looked up in the wallet database, even when the offer itself already put that NFT inspends. Coin selection now skips DIDs, NFTs, and options that are already inspends. This also stops the offer's NFT from being overwritten by a database copy.take_offerincrates/sage-wallet/src/wallet/offer/take_offer.rs): with only the first fix, the chain rejected the take withAssertPuzzleAnnouncementFailed. The seller's NFT spend asserts a royalty payment on its trade price, but the taker only paid royalties on NFTs it ends up keeping, priced on the net arbitrage. Royalties are now computed in a single path for every offered NFT (offer.requested_royalties()), priced on the makers' gross requested amounts (requested_payments().amounts()), which is what makers commit to inmake_offer. This is identical for single offers, also fixes aggregates whose legs net XCH or CATs against each other, and removes therequested_nfts/NftOfferInforebuild. With multiple royalty-bearing NFTs across different legs the split is approximate; a mismatch results in a rejected spend rather than lost funds.Test plan
test_take_aggregate_offer_with_pass_through_nft: Alice sells an NFT for 500, Bob bids 1000, Carol (no NFT, no XCH) takes the aggregate. Verified it fails without each fix individually (Missing asset, thenAssertPuzzleAnnouncementFailed).sage-wallettests pass (50).cargo fmt --all -- --files-with-diff --checkcargo clippy --workspace --all-features --all-targetsMade with Cursor