Skip to content

Allow taking aggregate offers with pass-through NFTs - #890

Draft
judeallred wants to merge 3 commits into
xch-dev:mainfrom
judeallred:take-aggregate-offer-pass-through-nft
Draft

judeallred wants to merge 3 commits into
xch-dev:mainfrom
judeallred:take-aggregate-offer-pass-through-nft

Conversation

@judeallred

@judeallred judeallred commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Reproduction

  1. Create an offer to sell a Chia Buddy NFT for 10 XCS:

    Offer 1: Chia Buddy for 10 XCS
    offer1qqr83wcuu2rykcmqvpsvaand8q7dlpaflf0aptljcnqf6r5pfda3uun9gscx08wl84k54axlqpunh308j2vk6m6ermj54l70jthd0e2llecu0p650yglmh4d8g95xlqcrqvp3tye3sg7m48p8mrs6dncc0jqmmjj6wdgapgxnf695kmdzhdzzcm374l9t8h7d88vk82x2wxv56av9lzaexfm9kst4um4v05ejkjnqwc9dvypl7xl7teqzj507kavxtnnyd8ulahl7l7pnc6gdpdhqtneunlcm4e7azwwes43mn6mx4ddamhp48nmth9qha4w4puxlll58lahvnkn4l45u8lhlyll95xr8f5ptn5lql7p3wac7dhax4d464n2tpt24wnna6tzfu0f9vprspfhlwha705dfvcfgzcudm40y9k3mxu7zllxncak69ullmxzheyh7n37mlj8ejhy3dnx7ln4v8unl726n70ndefl8smys49k2njnuaxar5etplfemaua4wj8lmd7sh8caq8zn7qczqg37k3nxtdrxvkmpnpn8tfnydr8tgsgteuzlf25ds0maa08cu80xsnahf3mf8hthruufl5g7vyfzkmm480uvf9uml8lpd2gkwgy7elge2ace76ldyddwlc8l4wma7e7lx84ldj8vlzjmeln9frzc8aslf22ncqsjp7jhmj3htfekj7vaj4whr49faa6y5te07fd89aaqa396s4wfsnrw3lxwar7va68apa5lywhqtj6krq8wlncn9lusy3mq7phm0marwv09kjey4tj6jekt8mu8spljeulyg2cuwmlflup4jxf9y5zvg4hmldycj6v4fx5ej2wf4yzknfw9j4u5jfg4m9vtn379c4ulspd8chy3twfg48r7gkw8ukynnjv448yktkyculnenpvea95u33p80wvs26x90dukjfw9yhu5tz0g49annxtcmrau7xvc398622ffjnczdek80vzhn6gdlllxvttf8p07yhc7mw2thd6vv7dvm4ml92lmp8jghcjnj58dfrt6k4a4a5ml0l07m729h7h7gymfm8v52kwea4l3lal9xnlflupzjddn6vnnxwf54uu4ftepysxk2krsre5w3v4cq3zwve3dv9d8c0wzgdz40vuzwprdez73ah6na7e7rshrle65rpmryc2lltasczvs7gepd98ue9u78007jkqg4xdmejkcu2chukdlal9f2rtanlee9nsv4yat76ztrw0l6lj7ef4largj6hlmutnwwp65m5l82ekhk76c5fkenjm4awfu70f0gmedqnkwsmpqffl3h8qdrkwqc3qm9udecrqaqwegpydjam57k4u4adtrzjy84809uu8rhhf2jlvt4ckax9flxc0q52akmd65u8zaecsvvpxs8w469vmt52ek4qnl26xau8dnq4z6htea8t62v77m586hxh8l8629mlhz35wga7cana5neecr93h264g9flhgqv4vply8njwnux3x9q0ccuqursy9g07la07qkdk6pkgh6heql3dj4tttnntemhszlk7tlfxwccxm2h8lu8ez7ud80lj7j82eavm6vd6w3lfuldxazrw2hfw6wwxk7tlt963nh0cdsja66at5uy5u29qh9x0sx8rl87l0hkxlqkt5e7tl42kremfalkj244xkvhn6k7cw9cuaulm0u2z70c8z7ygmh6qrtq086gtpnuzw9va4dmfzddhuc5whmte4kl5aa7hca0lwmugaky4she3vlf9m59upp6xq50j4ry7d97azhf5fjkz6a40sdu54t8l3vldzxl6k65we7rfs9ge6llvz3kzryyqqs3kct22rf9t6xv7t5veuhgenew3n8jarx096xv72qvetxrrfyxv7a75u7xhmf5tcmtwvn2gfhprkxxwkq7vu7hqmrxmr8vttw6x2c9me5hy8l3c96x9c6l0ea69eme62mye2l9jmwwe3aj3dpl4zllfdefu0ktx0mak6k3rd8zxv74hnuf5s2sjmng9s6p0ur3n5c6nuvalez3zn5tc79flnnhaftm3vkf686vfa0zeuc65l3tlygn9ssfdrmpv5sqkx6ealjnln7h9v5n59r7azdp7skrgpdm3yxjjw67kmccwpk9zhaeau83xkhhmdtmlhjxvlkdxkajkr6lwklk25kcvqh0n3zf6elsdktm3knf4zs8fe86nl89jglmxwxg5cs7lema2hhnlsz5k4vy6v35lnmks6ukemgrqqknd8jzq539wmq
    
  2. Make an offer to buy the same Chia Buddy for 100 XCS:

    Offer 2: 100 XCS for the same Chia Buddy
    offer1qqr83wcuu2ryhmvu0v6fg6guclrj2amf4424vfx323rusef8p39y59ahf2sjfn99nxg6rhsevezg6j7gkh2w4qn62vder5fxgskj496uugv0wavttf9w25n5tn9txe6xa355lfue84nm0hw67l7788tl0menenlmn0mhmhk07d7ma06p59g29zhx7t3nte76cl8k9xj2tvwqm5ufgjr46psdzurv55t0tvtg6kcd9ue9hh98gs88k2w6uwjsc2x2wy472558dux3z93farzwg6zawjcydg2s9zgscjltt980mkhhhpus6hel08w747uyuzep6kcd36v33e7gmadt3w43ew0655e9ty06za62euhupp8r620fccrlarv0xgr6wks2zth04w7qdqf7sruj29vy3tyxw44nqqcqwu9tny4nvv3z8je5hpyw5d640jxxh6c5erd42eghj24l39xnmcqxkvqdsq2z9yz72aa8u0njsccrktce4yg35lx5k7a68x8m586t3k5lty4yp04fw0aexrwwdsgzcqqgfgkwy3c27egdsqmjh220fxzwq2scduux2ce89znngk6xkfjr8kekxj50tz6uxlxmm3ktam8ecy593df0sf4lucnv7qu9767mkcfvl8gh0ndycc6xjd4kr5n7d38k2d6uqhg3ckhs6f98paxuf80g9qm4n406ufgc3tx2j4rrhntgaewj9enullcqwacfc2yqsgez3jyrqgkz3qyz8l3t3ny8h9wrnrpdwmsrg7plguunn4m3mae2my3egev0f88de656hu6lya5qeg9daxs3prqmktlfm0y822w5emza70ql9zkegmft0f8vyezy9c692fkkaps5wk8q0030slmp6rghetpvk0g5jhdefqx9hyurf4am20p68ugn7euafn8yfqanlnq2mj5srukmv22waxvnrwlrastg4n72cnwqvenf4zm23ua93d654faf7j595lk4jc7f4vtpy2nlwcjyt4f9l47mch6h6gzy9zxgkwvzs3epk9sgkrzfc43dqh7q8h6r6frdtvhed0t0tjpygw8x4purznr82cevt29nhft9edrjkaax93x3d85x0cqxkzx8mk5e09cufqymkglf72x0d8zgjqjymwq9jmmrg6w98uvcthsztp6xu3lqt9mm8f4ygkfy5ug0ylvpcxvv64qpuyvamvw0ak4m6aup9pr25zt36espmpttzgr2gx4hdcpe7kg5jrlpstymyfftgjp293l8utjg74xg48627pcquerk2q8xcajjp9ztvhwfru7rgrqh9n6dqwleva89ug8av89j2nqesj8ccezrl4mp47wp38hgdfgmecu5c4yg4mecsfa87g9v3r5gnzy73r5gnzy73r5gjhe49v3p4ed8gren6tgztqfe9x8hewczhg6t92euwaxtw6sdjdhgm8ly88p4883dty4zpgrsvq8scxx2dhk2z7r39mn9ykhhlaum2t493dd2psavjw3phfn2k5wt24x4wypv049cpv8frczuq2a06xeqye6vntvr2a8u4dawvwln7z6c20d24f75v7ft2dp94p5lwfn0pz3lt5ysyc8ucqylendrucngkfcj0leqsjyu4l49m4xurqfuh4h0tc4sa8d2zqhsse608xaz7rt6kdayxhw6gacsavtzydtwekf4943vtlhz4nvd8me0lpyt4hhk528t53ua2kfxyd768kkxa975htznjx0vn9haepnm0ge7qqhlfyd3u89z4dhmyaety6l0r44mwchltv93r8j97xw9hew8030k7tpmhfjh8u32kgqxwrzqprsccrle5qtunf9krxct36l8vr5hehtws9jwm2cgf20pww2d9tlqpnvx8wnk0dzyah5mmyw60qmzp2y5duup336hg0xu39xq7qse3u3lsy8fzzf75str0pfteapp3r4w6ccguyv9u7yzhc05th7005lcujwql9afyvgc7gqly03zpcedqfhmzqsk6253qmgmcra6au3k3me82v7ht453erhpvapun6uawq9r4lk58hw8q2qqfp7k6tk0easy0djcv6f9vpe6dt5w3qluqud78xcmgfpjh5lnp094zzqxqte69rm0cnxr5fqdu4qkqrqlh6dqrrlclatffhrpr2qsg8dswnlywcr9rww7g6yvv0tmj9strrrj57xs52uhguyf6u2dmwghwepu7m2hm2rvxdek0p0al8cyr5szamcysvrd3n6xyj63lu7yk3f57mtc3pk40mv4xccv7je2946yk7rvhl5rqan8a7afeqkmd6y5neendeh9xhyahlxm924sku3n9uwu0456t3xj2uu65vlr95fzu5pp70z0fzusq53wxqx5gj9eczt4yt3s8hme2eh5xweay5w2pm5za7zp44z6tal269gxt3nc767kmyxe348rgmu7ld3m0mz0pd2uga50mrh0luakl08m6g8r976lrs5zsk6xvppugnc3fa80p2zxdf7gny8spcrcg6x7zp4hrrmaxd28fkvfaef2fyf8helvfk9vhtw2kzmkmu5d4ytxqtumf0mqymrckdvd0jyd2tkrhfha5dr0x2fwuplv5nukawwmhpajxkprdr2crjf6r2p9wmf7wu4g7dc02ckavcdvmtnema4q70gtg693q3tk4cdpfadwxzl3zq3aj64rahd2jmq75a0djhw2zl2fdwc84x93xquze54s4s564cmh4udtdtqx468z5
    
  3. 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
    offer1qqr83wcuu2ryhmvm092984ckcmpjqv6jtyy9y358rr5eys2qyvgqvyd49235tqfzpqjvfpzqgzfsqsv3xqygss92s5uprw2snusppg9zypyvzqdpyrpqyxftwvk455gy2n98excyw95w6j462m4hhmfmlsg4se88n67uu008anhlaundqspzrzgs7p36qu8jvrpjzxcdkjv4guzju2g8a0hw6n59gmu8w2ukkstgmty7azy9hqmn59250ag06s04ql2374rl2r77rl6k7hn3lmssht3gt4ls69y2xc4kmwt47p250xypgw9zmywz0n6j57zwhpe48c9hk80xl6xer82z4afwxckujhe7rv42ghmwyea9e30z43ryeyjehh8t3cxh7hf2sz3ppch3taj0u8tgm5fq027n4nyyzaynhr8t9j9xkxgd9js48kvjdxl6fesch4y8ujuwkpwjkfv0ujy4rra5clpqgqxlv5mrag4xfr27r0ca25kjf7at5dkrmxty77gyslcsv9pyvg8f7573pjkxcm6dft8r06kly3y5n2v49j9uh8mn790p6wd7w9448as8g5xtqzcqh0qfkwc0aspqhlluclvqtekhxc2fzclcg4jhw56leawknzwtt5ldhtjhx4arzhvd842yc8mqra0nn8qwgdyg44p8m7xh4phkefcqdnrh0cqjx8euya6k6cx6zt3uw4jk6cagwsk0ga5zhd3m0twuprc8rtndmj36vf4yzzdt8w08lg0f5t7c27ehe5hm6heqvps9978mzcqrq20u0dxs4lq9paqhd6fwcy8vu9ev4sxz4fhdrz3wfwz94gu30vwxlyfsvrs7vf0a7kkczu603psls99qmg5cat4lk78rayltu3qu6cmssnar6ryna4zsjf0cqyv9gg8qckj0cxrqdcru34p0erzzhn7mzazldscgnzzxzv83palrky2xx0tzzz02y37x2v5afsyrmmahma0u39jf9lnxqtgahx5xc0glwxdr3h6g9j8mh8v3rgycg64y8zz82sqzwa3dpdehtmkjf0y7lvwdfd82etj2epu3y8khtyqux5gpmj6ys8ef95l55gyendteg0mhfnwd7d4x95g7kjaxlr4qtc0shnqjsex77ah3g2h0xkqqqj0y66u8at4y4vfj7uk6xdahwjwyvnedsm22758048j6clehrncfeduwjyfqagzwr6wwegnelkpg5vcrl39hdsxznx5mppz6m2tus8mk4wgs7m7vps4ana9as6v0egwrs6m8t8fuphs476s2a04vnreu64ucg6e5kkwyummwx8ecdzq6tedal5zzctwq6g3zq8cctpdgf020rlrayvnl7mwujl2z9lyyr2pe5da2rmcm4h4syda73fgsh8fvh26ckcn3hyrm2737dns3e77dsv4tppsqd0y0l0p4qnlvsrmy6rqaedkph2ncwk9hptv9kenl2txe42xhj3p6tkdjzgdecnp69csnme7r5fzdvtnq90q0nls5uaa779cvgvn5xx7kdl654u3764cdfrzw4tg53y76ka597x4nlph2agmatdc990fs6p85gj8yg7ex8hzwku2n6d9hdv4vfkaf6n9wj0ufl5eqg4e3hlrwhtq57av5wswhursnuv635wmlawnl0dhjal7zs403wmhmjs24t0wme8uf3x0f2hzkh6smtdau8j86w7j7l3cm5cvgv8pkd00dh3tx8y0r5ds5fx4s7hm0crljscm7uy9hramzfjykcpphmsfgdjh33zxpl8xc340d8zgdplteu6vf0a68gnwvkdetjzrejdp0m8x5z9hdgm3cc6h6ee38ae0uvuqvug6gh9a9z5xrmms7hq7tkgfhnzmzaxyx93j4lu238x27629a5ny2rgyyykekfdcv0l09qksmm4yszt3najrtk9hm7c87uuy72tamr39pdfpch6j3zge7q5946c29zeaj8v4t58atm5wxe8t68ez57tmy3lh0jhmy4pxyu3wxvr2khchmhd9q4yp0gzlrqs97qrus9eqtjqh5p0sqlypwgzus9aqtamull5q0nlk3u5q0ypwg7lkku7mn70046zqqly9qupwgzurv3aaryd0uqq8lnqqhuy8cdc8uakrsqhva3f6zpt9mxjgm3fm43x3y87h8cyulhgw3uu0mem5psr8exr7h80m457jextvffv9mxynvtzdu8ed8fsfscw67nz4ztx0ta8cr8c739plvy0xxmpvdnumv4akkdraeas47m54m8d963fhteu6s7ekyx7jnm65at9l07ytlsg3vw7glurhhcjzssl90clhps9za2w7xg5l37rm99yzmkuxk8qcx32xnx0zsxzjmjrltdptmkvyk6ucttdgaj5n3ktske4lyr96dh8gwjl6nj79u7wtuvt0fcv6c3xgw4hykmg7zkpjmzhqmtwc2aljjkck2dj9fvlcdju48s5njvgf73mfjk3kr2takhevncakm9yndx56pu2mweun3luyzrq9ke70x0n7zydwvuy3pcv3pl62wt8vplmntalslv5ntgpcfkfjlwag7568naurpljrypm6st5cjw9vd45x36mawncuwr9uza3u9jgx00xct230m39v2z7t3relhd0md4efv4yxpkkzmfdu3pe2rsr46gvvx9h7jwepxhdnv86nqk2lpng0696vvh55gakc5fmxpnxtznm52uv2m6lumgt66tm3k92crx2gmwj3ym3hp77t0jkqvu2ws8yzn4f6quu2w38qzn4f7l8qlaulz908atgf0uem4fm7elj9q5wj3s7t6404s4d22p9xvx42v76vyuchf88w29ntj27n44hq5s7ha23d8tth4983nkjs0wvca6sjalu2nlsv328pdzt9exwwzssepsss29syawadtske9luz96tne44f226ds0exczst76gu3f2fsvjwkn5mh5e89dd0dtmwftnx3vlddhws4tgkv7tac45krnxc7t5nm07nqacta55y4xhhpwurd44570nug5vsrg03ghw7reau9hd99d9y6kvh0fhjn0japkuxe0cz3chkhztczqax60cvc2k5r23ccxpj27hrk03v6whhcmy2jlfmagn96d26h0cnpdxfchyv2kxz34ws0zeqtwu6yr2svsczwjvhw83c92xf0wdqny59ch8n5mqaq9wqju7aqzkq4vpz6z95ytgq4vxsptq0m265zu77pmzwpkg5s3prujhsnyfk8u3ppu32up2l2xgmhfdjyrm4wfkral2kxth9x3gq5mn28ycjm4xw3rk3clxchuj2kqaf93sqpxfyuuzlns677vsc3s8yfhja267jvy3kcku7zk0pdvucy3anah9a8tfve4kv9m8tnrput73x4vvk67dt8t7vtm6002jzk7afdtxzxefu3c79uwkeph5de8s380z62hk3uwmyz6n5996cevwlk3a0nth4earc43p3cua8vz9kl7305lzhu5y7p2v440hfy
    

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

  • Coin selection (select_spends_excluding in crates/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 in spends. Coin selection now skips DIDs, NFTs, and options that are already in spends. This also stops the offer's NFT from being overwritten by a database copy.
  • Royalties (take_offer in crates/sage-wallet/src/wallet/offer/take_offer.rs): with only the first fix, the chain rejected the take with AssertPuzzleAnnouncementFailed. 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 in make_offer. This is identical for single offers, also fixes aggregates whose legs net XCH or CATs against each other, and removes the requested_nfts / NftOfferInfo rebuild. 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

  • New 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, then AssertPuzzleAnnouncementFailed).
  • All sage-wallet tests pass (50).
  • cargo fmt --all -- --files-with-diff --check
  • cargo clippy --workspace --all-features --all-targets
  • Manually verified against the combined offer above.

Made with Cursor


let arbitrage = offer.arbitrage();

let mut requested_nfts = IndexMap::new();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

judeallred and others added 3 commits September 28, 2026 13:29
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>
@judeallred
judeallred force-pushed the take-aggregate-offer-pass-through-nft branch from 35b5737 to 775be32 Compare September 28, 2026 17:30
@judeallred
judeallred marked this pull request as ready for review September 28, 2026 23:01
@judeallred

Copy link
Copy Markdown
Contributor Author

@Rigidity for your consideration

@judeallred
judeallred marked this pull request as draft September 29, 2026 16:55
@judeallred

Copy link
Copy Markdown
Contributor Author

found a regression; need to fix first. ignore me :-)

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.

1 participant