Cut NFT out of scope (#7616)

This commit is contained in:
yinyiqian1
2026-06-25 12:30:04 -04:00
committed by GitHub
parent 6ecf413c64
commit 9a7d1a114c
6 changed files with 46 additions and 450 deletions

View File

@@ -39,7 +39,7 @@ findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftoken
/** Insert the token in the owner's token directory. */
TER
insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft);
insertToken(ApplyView& view, AccountID owner, STObject&& nft);
/** Remove the token from the owner's token directory. */
TER
@@ -107,7 +107,6 @@ tokenOfferCreatePreclaim(
TER
tokenOfferCreateApply(
ApplyView& view,
STTx const& tx,
AccountID const& acctID,
STAmount const& amount,
std::optional<AccountID> const& dest,

View File

@@ -4,14 +4,12 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -23,7 +21,6 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -35,7 +32,7 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <functional>
#include <iterator>
#include <memory>
#include <optional>
@@ -70,13 +67,12 @@ locatePage(ApplyView& view, AccountID const& owner, uint256 const& id)
Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
}
static std::expected<SLE::pointer, TER>
static SLE::pointer
getPageForToken(
ApplyView& view,
STTx const& tx,
AccountID const& owner,
SLE::ref sponsorSle,
uint256 const& id)
uint256 const& id,
std::function<void(ApplyView&, AccountID const&)> const& createCallback)
{
auto const base = keylet::nftpageMin(owner);
auto const first = keylet::nftpage(base, id);
@@ -88,21 +84,6 @@ getPageForToken(
auto cp =
view.peek(Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key)));
auto const onNewPage = [&](SLE::ref newPage) -> TER {
if (isReserveSponsored(tx))
{
auto const ownerSle = view.read(keylet::account(owner));
auto const ownerBalance = ownerSle->getFieldAmount(sfBalance);
if (auto const ret =
checkInsufficientReserve(view, tx, ownerSle, ownerBalance, sponsorSle, 1);
!isTesSuccess(ret))
return ret;
}
adjustOwnerCount(view, view.peek(keylet::account(owner)), sponsorSle, 1);
addSponsorToLedgerEntry(newPage, sponsorSle);
return tesSUCCESS;
};
// A suitable page doesn't exist; we'll have to create one.
if (!cp)
{
@@ -110,9 +91,7 @@ getPageForToken(
cp = std::make_shared<SLE>(last);
cp->setFieldArray(sfNFTokens, arr);
view.insert(cp);
if (auto const ret = onNewPage(cp); !isTesSuccess(ret))
return std::unexpected(ret);
createCallback(view, owner);
return cp;
}
@@ -225,8 +204,7 @@ getPageForToken(
cp->setFieldH256(sfPreviousPageMin, np->key());
view.update(cp);
if (auto const ret = onNewPage(np); !isTesSuccess(ret))
return std::unexpected(ret);
createCallback(view, owner);
return (first.key < np->key()) ? np : cp;
}
@@ -282,33 +260,33 @@ changeTokenURI(
/** Insert the token in the owner's token directory. */
TER
insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft)
insertToken(ApplyView& view, AccountID owner, STObject&& nft)
{
XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token");
// First, we need to locate the page the NFT belongs to, creating it
// if necessary. This operation may fail if it is impossible to insert
// the NFT.
auto const page = getPageForToken(view, tx, owner, sponsorSle, nft[sfNFTokenID]);
SLE::pointer const page =
getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) {
adjustOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()});
});
if (!page.has_value())
return page.error();
if (!(*page))
if (!page)
return tecNO_SUITABLE_NFTOKEN_PAGE;
{
auto arr = (*page)->getFieldArray(sfNFTokens);
auto arr = page->getFieldArray(sfNFTokens);
arr.pushBack(std::move(nft));
arr.sort([](STObject const& o1, STObject const& o2) {
return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID));
});
(*page)->setFieldArray(sfNFTokens, arr);
page->setFieldArray(sfNFTokens, arr);
}
view.update((*page));
view.update(page);
return tesSUCCESS;
}
@@ -429,11 +407,18 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
curr->setFieldArray(sfNFTokens, arr);
view.update(curr);
int cnt = 0;
if (prev && mergePages(view, prev, curr))
adjustOwnerCountObj(view, owner, prev, -1);
cnt--;
if (next && mergePages(view, curr, next))
adjustOwnerCountObj(view, owner, curr, -1);
cnt--;
if (cnt != 0)
{
adjustOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()});
}
return tesSUCCESS;
}
@@ -467,7 +452,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
curr->makeFieldAbsent(sfPreviousPageMin);
}
adjustOwnerCountObj(view, owner, prev, -1);
adjustOwnerCount(view, owner, {}, -1, beast::Journal{beast::Journal::getNullSink()});
view.update(curr);
view.erase(prev);
@@ -503,10 +488,10 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
view.update(next);
}
adjustOwnerCountObj(view, owner, curr, -1);
view.erase(curr);
int cnt = 1;
// Since we're here, try to consolidate the previous and current pages
// of the page we removed (if any) into one. mergePages() _should_
// always return false. Since tokens are burned one at a time, there
@@ -520,9 +505,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S
view,
view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())),
view.peek(Keylet(ltNFTOKEN_PAGE, next->key()))))
{
adjustOwnerCountObj(view, owner, prev, -1);
}
cnt++;
adjustOwnerCount(view, owner, {}, -1 * cnt, beast::Journal{beast::Journal::getNullSink()});
return tesSUCCESS;
}
@@ -638,7 +623,7 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer)
false))
return false;
adjustOwnerCountObj(view, owner, offer, -1);
adjustOwnerCount(view, owner, {}, -1, beast::Journal{beast::Journal::getNullSink()});
view.erase(offer);
return true;
@@ -917,7 +902,6 @@ tokenOfferCreatePreclaim(
TER
tokenOfferCreateApply(
ApplyView& view,
STTx const& tx,
AccountID const& acctID,
STAmount const& amount,
std::optional<AccountID> const& dest,
@@ -929,14 +913,8 @@ tokenOfferCreateApply(
std::uint32_t txFlags)
{
Keylet const acctKeylet = keylet::account(acctID);
auto const acct = view.read(acctKeylet);
auto const sponsorSle = getTxReserveSponsor(view, tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (auto const ret =
checkInsufficientReserve(view, tx, acct, priorBalance, *sponsorSle, 1, 0, j);
!isTesSuccess(ret))
return ret;
if (auto const acct = view.read(acctKeylet); priorBalance < accountReserve(view, acct, j, 1))
return tecINSUFFICIENT_RESERVE;
auto const offerID = keylet::nftoffer(acctID, seqProxy.value());
@@ -983,13 +961,11 @@ tokenOfferCreateApply(
if (dest)
(*offer)[sfDestination] = *dest;
addSponsorToLedgerEntry(offer, *sponsorSle);
view.insert(offer);
}
// Update owner count.
adjustOwnerCount(view, view.peek(acctKeylet), *sponsorSle, 1, j);
adjustOwnerCount(view, acctID, {}, 1, j);
return tesSUCCESS;
}

View File

@@ -7,7 +7,6 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -374,12 +373,7 @@ NFTokenAcceptOffer::transferNFToken(
std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount);
auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto const insertRet =
nft::insertToken(view(), ctx_.tx, buyer, *sponsorSle, std::move(tokenAndPage->token));
auto const insertRet = nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
// if fixNFTokenReserve is enabled, check if the buyer has sufficient
// reserve to own a new object, if their OwnerCount changed.
@@ -399,13 +393,8 @@ NFTokenAcceptOffer::transferNFToken(
auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount);
if (buyerOwnerCountAfter > buyerOwnerCountBefore)
{
SLE::const_pointer buyerSponsorSle;
if (accountID_ == buyer)
buyerSponsorSle = *sponsorSle;
if (auto const ret = checkInsufficientReserve(
ctx_.view(), ctx_.tx, sleBuyer, buyerBalance, buyerSponsorSle, 0, 0, j_);
!isTesSuccess(ret))
return ret;
if (buyerBalance < accountReserve(view(), sleBuyer, j_))
return tecINSUFFICIENT_RESERVE;
}
}

View File

@@ -77,7 +77,6 @@ NFTokenCreateOffer::doApply()
// Use implementation shared with NFTokenMint
return nft::tokenOfferCreateApply(
view(),
ctx_.tx,
ctx_.tx[sfAccount],
ctx_.tx[sfAmount],
ctx_.tx[~sfDestination],

View File

@@ -5,7 +5,6 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -306,12 +305,7 @@ NFTokenMint::doApply()
object.setFieldVL(sfURI, *uri);
});
auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (TER const ret =
nft::insertToken(ctx_.view(), ctx_.tx, accountID_, *sponsorSle, std::move(newToken));
if (TER const ret = nft::insertToken(ctx_.view(), accountID_, std::move(newToken));
!isTesSuccess(ret))
return ret;
@@ -322,7 +316,6 @@ NFTokenMint::doApply()
// because a Mint is only allowed to create a sell offer.
if (TER const ter = nft::tokenOfferCreateApply(
view(),
ctx_.tx,
ctx_.tx[sfAccount],
ctx_.tx[sfAmount],
ctx_.tx[~sfDestination],
@@ -339,21 +332,15 @@ NFTokenMint::doApply()
// allows NFTs to be added to the page (and burn fees) without
// requiring the reserve to be met each time. The reserve is
// only managed when a new NFT page or sell offer is added.
if (auto const ownerCountAfter =
view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount);
auto const sleAccount = view().read(keylet::account(accountID_));
if (!sleAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto const ownerCountAfter = sleAccount->getFieldU32(sfOwnerCount);
ownerCountAfter > ownerCountBefore)
{
if (auto const ret = checkInsufficientReserve(
ctx_.view(),
ctx_.tx,
view().read(keylet::account(accountID_)),
preFeeBalance_,
*sponsorSle,
0,
0,
j_);
!isTesSuccess(ret))
return ret;
if (preFeeBalance_ < accountReserve(view(), sleAccount, j_))
return tecINSUFFICIENT_RESERVE;
}
return tesSUCCESS;
}

View File

@@ -29,7 +29,6 @@
#include <test/jtx/sponsor.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/token.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
@@ -3733,357 +3732,6 @@ public:
}
}
void
testNFToken(bool cosigning)
{
testcase("NFToken");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const sponsor("sponsor");
Account const sponsor2("sponsor2");
{
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
env.close();
// NFTokenMint
uint256 nftId;
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
nftId = token::getNextID(env, alice, 0);
submit(token::mint(alice));
});
// transfer sponsor
auto const keylet = keylet::nftpageMax(alice);
if (cosigning)
{
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
sponsor::As(sponsor2, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor2));
env.close();
}
else
{
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
env.close();
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
sponsor::As(sponsor2, spfSponsorReserve));
}
// 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);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
// NFTokenMintOffer
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
2,
2,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
submit(token::mint(alice), token::Amount(XRP(100)));
});
}
{
// multiple nft page process
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor);
env.close();
auto const nftCount = 200;
// NFTokenMint
if (cosigning)
{
for (auto i = 0; i < nftCount; i++)
{
env(token::mint(alice),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
}
}
else
{
env(sponsor::set_reserve(sponsor, 0, 8), sponsor::SponseeAcc(alice));
env.close();
for (auto i = 0; i < nftCount; i++)
{
env(token::mint(alice), sponsor::As(sponsor, spfSponsorReserve));
}
}
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
testNFTokenOffer(bool cosigning)
{
testcase("NFTokenOffer");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const broker("broker");
Account const sponsor("sponsor");
Account const sponsor2("sponsor2");
auto const taxon = 0u;
{
// Mint + CreateOffer + CancelOffer
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
env.close();
// Mint
uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)};
env(token::mint(alice, taxon), Txflags(tfTransferable));
env.close();
// NFTokenOfferCreate
uint256 offerIndex1;
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
offerIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
submit(
token::createOffer(alice, nftId, XRP(1)),
token::Destination(bob),
Txflags(tfSellNFToken));
});
uint256 offerIndex2;
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
offerIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
submit(
token::createOffer(alice, nftId, XRP(1)),
token::Destination(bob),
Txflags(tfSellNFToken));
});
// transfer sponsor
if (cosigning)
{
env(sponsor::transfer(alice, tfSponsorshipReassign, offerIndex1),
sponsor::As(sponsor2, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor2));
env.close();
}
else
{
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
env.close();
env(sponsor::transfer(alice, tfSponsorshipReassign, offerIndex1),
sponsor::As(sponsor2, spfSponsorReserve));
env.close();
}
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, {offerIndex1, offerIndex2}));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
}
{
// Mint + CreateSellOffer + AcceptSellOffer
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor);
env.close();
// Mint
uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)};
env(token::mint(alice, taxon), Txflags(tfTransferable));
env.close();
// NFTokenOfferCreate
uint256 offerIndex;
testEachSponsorship(
env,
cosigning,
sponsor,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
offerIndex = keylet::nftoffer(alice, env.seq(alice)).key;
submit(
token::createOffer(alice, nftId, XRP(1)),
token::Destination(bob),
Txflags(tfSellNFToken));
});
// NFTokenOfferAccept
env(token::acceptSellOffer(bob, offerIndex));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
}
{
// Mint + CreateBuyOffer + AcceptBuyOffer
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, sponsor);
env.close();
// Mint
uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)};
env(token::mint(alice, taxon), Txflags(tfTransferable));
env.close();
// NFTokenOfferCreate
uint256 offerIndex;
testEachSponsorship(
env,
cosigning,
sponsor,
bob,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
offerIndex = keylet::nftoffer(bob, env.seq(bob)).key;
submit(
token::createOffer(bob, nftId, XRP(1)),
token::Owner(alice),
token::Destination(alice));
});
// NFTokenOfferAccept
env(token::acceptBuyOffer(alice, offerIndex));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
}
{
// Broker
Env env{*this, testableAmendments()};
env.fund(XRP(1000000), alice, bob, broker, sponsor, sponsor2);
env.close();
// Mint
uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)};
env(token::mint(alice, taxon), Txflags(tfTransferable));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
// NFTokenOfferCreate (BuyOffer)
uint256 buyOfferIndex;
testEachSponsorship(
env,
cosigning,
sponsor,
bob,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
buyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
submit(
token::createOffer(bob, nftId, XRP(1)),
token::Owner(alice),
token::Destination(broker));
});
// NFTokenOfferCreate (SellOffer)
uint256 sellOfferIndex;
testEachSponsorship(
env,
cosigning,
sponsor2,
alice,
1,
1,
tecINSUFFICIENT_RESERVE,
[&](Env& env, auto const& submit) {
sellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
submit(
token::createOffer(alice, nftId, XRP(1)),
Txflags(tfSellNFToken),
token::Destination(broker));
});
// NFTokenOfferAccept
env(token::brokerOffers(broker, buyOfferIndex, sellOfferIndex));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, bob) == 1);
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
}
}
void
testPayChan(bool cosigning)
{
@@ -6062,8 +5710,6 @@ public:
testDID(cosigning);
testEscrow(cosigning);
testMPToken(cosigning);
testNFToken(cosigning);
testNFTokenOffer(cosigning);
testPayChan(cosigning);
testPermissionedDomain(cosigning);
testOracle(cosigning);