diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 7f07071c13..1ffcf23e8a 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1644,26 +1644,75 @@ public: void testNFToken() { - // testcase("NFToken"); - // using namespace test::jtx; - // Env env{*this, testable_amendments()}; - // Account const alice("alice"); - // Account const bob("bob"); - // Account const sponsor("sponsor"); - // Account const sponsor2("sponsor2"); + testcase("NFToken"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); - // env.fund(XRP(1000000), alice, bob, sponsor); - // env.close(); + { + Env env{*this, testable_amendments()}; - // // NFTokenMint - // env(token::mint(alice), - // sponsor::as(sponsor, tfSponsorReserve), - // sponsor::sig(sponsor)); - // env.close(); + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); - // BEAST_EXPECT(ownerCount(env, alice) == 1); - // BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - // BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + // NFTokenMint + uint256 const nftId{token::getNextID(env, alice, 0)}; + env(token::mint(alice), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::sig(sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // NFTokenBurn + env(token::burn(alice, nftId)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + + { + // multiple nft page process + Env env{*this, testable_amendments()}; + + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + auto const nftCount = 200; + + // NFTokenMint + for (auto i = 0; i < nftCount; i++) + { + env(token::mint(alice), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::sig(sponsor)); + } + env.close(); + + BEAST_EXPECT( + ownerCount(env, alice) == sponsoredOwnerCount(env, alice)); + BEAST_EXPECT( + sponsoredOwnerCount(env, alice) == + sponsoringOwnerCount(env, sponsor)); + + // NFTokenBurn + for (auto i = 0; i < nftCount; i++) + { + auto const nftId = token::getID(env, alice, 0, i, 0, 0); + env(token::burn(alice, nftId)); + } + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } } void @@ -1694,7 +1743,7 @@ public: BEAST_EXPECT(ownerCount(env, alice) == 1); // NFTokenOfferCreate - uint256 const offerIndex = + uint256 const offerIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; env(token::createOffer(alice, nftId, XRP(1)), token::destination(bob), @@ -1707,19 +1756,32 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + uint256 const offerIndex2 = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(alice, nftId, XRP(1)), + token::destination(bob), + txflags(tfSellNFToken), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::sig(sponsor)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); + // transfer sponsor - env(sponsor::transfer(alice, offerIndex), + env(sponsor::transfer(alice, offerIndex1), sponsor::as(sponsor2, tfSponsorReserve), sponsor::sig(sponsor2)); env.close(); - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); // NFTokenOfferCancel - env(token::cancelOffer(alice, {offerIndex})); + env(token::cancelOffer(alice, {offerIndex1, offerIndex2})); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 1); @@ -2546,7 +2608,7 @@ public: testDID(); testEscrow(); testMPToken(); - // testNFToken(); + testNFToken(); testNFTokenOffer(); testPayChan(); testPermissionedDomain(); diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index d145f4b5be..ba2d0769d6 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -445,8 +445,9 @@ NFTokenAcceptOffer::transferNFToken( std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount); - auto const insertRet = - nft::insertToken(view(), buyer, std::move(tokenAndPage->token)); + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + auto const insertRet = nft::insertToken( + view(), buyer, sponsor, std::move(tokenAndPage->token)); // if fixNFTokenReserve is enabled, check if the buyer has sufficient // reserve to own a new object, if their OwnerCount changed. diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 313111f08a..f14377f045 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -319,8 +319,9 @@ NFTokenMint::doApply() object.setFieldVL(sfURI, *uri); }); - if (TER const ret = - nft::insertToken(ctx_.view(), account_, std::move(newToken)); + auto const sponsor = getTxReserveSponsorAccountID(ctx_.tx); + if (TER const ret = nft::insertToken( + ctx_.view(), account_, sponsor, std::move(newToken)); ret != tesSUCCESS) return ret; diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 6df2a053b6..be5ed513ba 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -66,8 +66,13 @@ static std::shared_ptr getPageForToken( ApplyView& view, AccountID const& owner, + std::optional const& sponsor, uint256 const& id, - std::function const& createCallback) + std::function const&, + AccountID const&, + std::optional const&)> const& createCallback) { auto const base = keylet::nftpage_min(owner); auto const first = keylet::nftpage(base, id); @@ -87,7 +92,7 @@ getPageForToken( cp = std::make_shared(last); cp->setFieldArray(sfNFTokens, arr); view.insert(cp); - createCallback(view, owner); + createCallback(view, cp, owner, sponsor); return cp; } @@ -215,7 +220,7 @@ getPageForToken( cp->setFieldH256(sfPreviousPageMin, np->key()); view.update(cp); - createCallback(view, owner); + createCallback(view, np, owner, sponsor); // fixNFTokenDirV1 corrects a bug in the initial implementation that // would put an NFT in the wrong page. The problem was caused by an @@ -277,7 +282,11 @@ changeTokenURI( /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, AccountID owner, STObject&& nft) +insertToken( + ApplyView& view, + AccountID owner, + std::optional const& sponsor, + STObject&& nft) { XRPL_ASSERT( nft.isFieldPresent(sfNFTokenID), @@ -289,14 +298,22 @@ insertToken(ApplyView& view, AccountID owner, STObject&& nft) std::shared_ptr page = getPageForToken( view, owner, + sponsor, nft[sfNFTokenID], - [](ApplyView& view, AccountID const& owner) { + [](ApplyView& view, + std::shared_ptr const& newPage, + AccountID const& owner, + std::optional const& sponsor) { + std::optional> const sponsorSle = sponsor + ? view.peek(keylet::account(*sponsor)) + : std::optional>{std::nullopt}; adjustOwnerCount( view, view.peek(keylet::account(owner)), - std::nullopt, + sponsorSle, 1, beast::Journal{beast::Journal::getNullSink()}); + addSponsorToLedgerEntry(newPage, sponsorSle); }); if (!page) @@ -450,22 +467,25 @@ removeToken( curr->setFieldArray(sfNFTokens, arr); view.update(curr); - int cnt = 0; - if (prev && mergePages(view, prev, curr)) - cnt--; + { + auto const sponsor = getLedgerEntryReserveSponsor(view, prev); + adjustOwnerCount( + view, + view.peek(keylet::account(owner)), + sponsor, + -1, + beast::Journal{beast::Journal::getNullSink()}); + } if (next && mergePages(view, curr, next)) - cnt--; - - if (cnt != 0) { auto const sponsor = getLedgerEntryReserveSponsor(view, curr); adjustOwnerCount( view, view.peek(keylet::account(owner)), sponsor, - cnt, + -1, beast::Journal{beast::Journal::getNullSink()}); } @@ -501,7 +521,7 @@ removeToken( curr->makeFieldAbsent(sfPreviousPageMin); } - auto const sponsor = getLedgerEntryReserveSponsor(view, curr); + auto const sponsor = getLedgerEntryReserveSponsor(view, prev); adjustOwnerCount( view, view.peek(keylet::account(owner)), @@ -535,9 +555,15 @@ removeToken( view.update(next); } - view.erase(curr); + auto const sponsor = getLedgerEntryReserveSponsor(view, curr); + adjustOwnerCount( + view, + view.peek(keylet::account(owner)), + getLedgerEntryReserveSponsor(view, curr), + -1, + beast::Journal{beast::Journal::getNullSink()}); - int cnt = 1; + view.erase(curr); // Since we're here, try to consolidate the previous and current pages // of the page we removed (if any) into one. mergePages() _should_ @@ -552,14 +578,14 @@ removeToken( view, view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())), view.peek(Keylet(ltNFTOKEN_PAGE, next->key())))) - cnt++; - - adjustOwnerCount( - view, - view.peek(keylet::account(owner)), - std::nullopt, - -1 * cnt, - beast::Journal{beast::Journal::getNullSink()}); + { + adjustOwnerCount( + view, + view.peek(keylet::account(owner)), + getLedgerEntryReserveSponsor(view, prev), + -1, + beast::Journal{beast::Journal::getNullSink()}); + } return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.h b/src/xrpld/app/tx/detail/NFTokenUtils.h index e0e276db27..2b3a1e4797 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.h +++ b/src/xrpld/app/tx/detail/NFTokenUtils.h @@ -70,7 +70,11 @@ findTokenAndPage( /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, AccountID owner, STObject&& nft); +insertToken( + ApplyView& view, + AccountID owner, + std::optional const& sponsor, + STObject&& nft); /** Remove the token from the owner's token directory. */ TER