Compare commits

...

26 Commits

Author SHA1 Message Date
Mayukha Vadari
53f57cad83 fix typos 2025-10-09 16:39:33 -04:00
Mayukha Vadari
df8768d24a Merge branch 'develop' into copilot/fix-f350b804-905b-4a06-ab84-d0f12e5b0dd1 2025-10-09 16:05:54 -04:00
Mayukha Vadari
9e4bc87146 clean up 2025-10-09 16:05:45 -04:00
Mayukha Vadari
3f5c350c5a refactor source code 2025-10-07 14:09:40 -04:00
Mayukha Vadari
5b22d7a574 fix tests 2025-10-06 16:50:46 -04:00
Mayukha Vadari
9cc2e2cc16 clean up testCreateOfferExpiration 2025-10-06 16:50:46 -04:00
Mayukha Vadari
7e0d5ad933 roll back unnecessary changes 2025-10-06 16:50:46 -04:00
Mayukha Vadari
1425345fed clean up testAcceptOfferInvalid 2025-10-06 16:50:46 -04:00
Mayukha Vadari
5524610c82 exclude tecINTERNALs from codecov 2025-10-06 16:50:46 -04:00
Mayukha Vadari
11de1418e6 [Claude] clean up 2025-10-06 16:50:46 -04:00
Mayukha Vadari
36f27a63fc [manual] fix all the tests 2025-10-06 16:50:46 -04:00
Mayukha Vadari
b5a94445aa [Claude] make progress 2025-10-06 16:50:46 -04:00
Mayukha Vadari
ab71cfb206 [WIP] fix some stuff, hit the limit before I could continue 2025-10-06 16:50:46 -04:00
Mayukha Vadari
85120ed502 [Claude] Temporarily disable new tests to isolate amendment impact 2025-10-06 16:50:45 -04:00
Mayukha Vadari
9f7e9e4197 [Claude] Make existing NFToken tests amendment-aware for fixExpiredNFTokenOfferRemoval 2025-10-06 16:50:45 -04:00
Mayukha Vadari
55b3bc2332 [Claude] Fix NFToken expired offer test timing logic 2025-10-06 16:50:45 -04:00
Mayukha Vadari
cbac095cf2 [Claude] attempt fix 2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
b589551a5d Fix test logic: use robust timing pattern for expired offer tests
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
0fd1dc12a5 Fix test logic: use fresh expiration times for buy offer and brokered scenarios
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
858dc2b63b Fix test logic: create offers with future expiration times, then advance ledger time
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
41bfa3819c Fix test logic error: buy offer should exist after creation regardless of amendment
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
be5d105f07 Simplify amendment check using tweakedFeatures[] operator instead of count()
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
0493180658 Fix build error: use bo->key() instead of getIndex() and resolve clang-format issues
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
76a1831c6d Improve test coverage for brokered expired offers
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:45 -04:00
copilot-swe-agent[bot]
98e9fce6ca Add fixExpiredNFTokenOfferRemoval amendment and implementation
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2025-10-06 16:50:43 -04:00
copilot-swe-agent[bot]
35eaf4fe8a Initial plan 2025-10-06 16:50:16 -04:00
3 changed files with 353 additions and 144 deletions

View File

@@ -32,6 +32,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -1045,23 +1045,25 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env.fund(XRP(1000), alice, buyer, gw);
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == 0);
uint256 const nftAlice0ID =
token::getNextID(env, alice, 0, tfTransferable);
env(token::mint(alice, 0u), txflags(tfTransferable));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
uint8_t aliceCount = 1;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
uint256 const nftXrpOnlyID =
token::getNextID(env, alice, 0, tfOnlyXRP | tfTransferable);
env(token::mint(alice, 0), txflags(tfOnlyXRP | tfTransferable));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
uint256 nftNoXferID = token::getNextID(env, alice, 0);
env(token::mint(alice, 0));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 1);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
// alice creates sell offers for her nfts.
uint256 const plainOfferIndex =
@@ -1069,28 +1071,32 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(alice, nftAlice0ID, XRP(10)),
txflags(tfSellNFToken));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 2);
aliceCount++;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
uint256 const audOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftAlice0ID, gwAUD(30)),
txflags(tfSellNFToken));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 3);
aliceCount++;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
uint256 const xrpOnlyOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftXrpOnlyID, XRP(20)),
txflags(tfSellNFToken));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 4);
aliceCount++;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
uint256 const noXferOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, nftNoXferID, XRP(30)),
txflags(tfSellNFToken));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 5);
aliceCount++;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
// alice creates a sell offer that will expire soon.
uint256 const aliceExpOfferIndex =
@@ -1099,7 +1105,18 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
txflags(tfSellNFToken),
token::expiration(lastClose(env) + 5));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 6);
aliceCount++;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
// buyer creates a Buy offer that will expire soon.
uint256 const buyerExpOfferIndex =
keylet::nftoffer(buyer, env.seq(buyer)).key;
env(token::createOffer(buyer, nftAlice0ID, XRP(40)),
token::owner(alice),
token::expiration(lastClose(env) + 5));
env.close();
uint8_t buyerCount = 1;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
//----------------------------------------------------------------------
// preflight
@@ -1109,14 +1126,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
fee(STAmount(10ull, true)),
ter(temBAD_FEE));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Set an invalid flag.
env(token::acceptSellOffer(buyer, noXferOfferIndex),
txflags(0x00008000),
ter(temINVALID_FLAG));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Supply nether an sfNFTokenBuyOffer nor an sfNFTokenSellOffer field.
{
@@ -1124,7 +1141,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
jv.removeMember(sfNFTokenSellOffer.jsonName);
env(jv, ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// A buy offer may not contain a sfNFTokenBrokerFee field.
@@ -1134,7 +1151,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
STAmount(500000).getJson(JsonOptions::none);
env(jv, ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// A sell offer may not contain a sfNFTokenBrokerFee field.
@@ -1144,7 +1161,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
STAmount(500000).getJson(JsonOptions::none);
env(jv, ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// A brokered offer may not contain a negative or zero brokerFee.
@@ -1152,7 +1169,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
token::brokerFee(gwAUD(0)),
ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
//----------------------------------------------------------------------
// preclaim
@@ -1161,36 +1178,51 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::acceptBuyOffer(buyer, beast::zero),
ter(tecOBJECT_NOT_FOUND));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The buy offer must be present in the ledger.
uint256 const missingOfferIndex = keylet::nftoffer(alice, 1).key;
env(token::acceptBuyOffer(buyer, missingOfferIndex),
ter(tecOBJECT_NOT_FOUND));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The buy offer must not have expired.
env(token::acceptBuyOffer(buyer, aliceExpOfferIndex), ter(tecEXPIRED));
// NOTE: this is only a preclaim check with the
// fixExpiredNFTokenOfferRemoval amendment disabled.
env(token::acceptBuyOffer(alice, buyerExpOfferIndex), ter(tecEXPIRED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
if (features[fixExpiredNFTokenOfferRemoval])
{
buyerCount--;
}
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The sell offer must be non-zero.
env(token::acceptSellOffer(buyer, beast::zero),
ter(tecOBJECT_NOT_FOUND));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The sell offer must be present in the ledger.
env(token::acceptSellOffer(buyer, missingOfferIndex),
ter(tecOBJECT_NOT_FOUND));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The sell offer must not have expired.
// NOTE: this is only a preclaim check with the
// fixExpiredNFTokenOfferRemoval amendment disabled.
env(token::acceptSellOffer(buyer, aliceExpOfferIndex), ter(tecEXPIRED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 0);
// Alice's count is decremented by one when the expired offer is
// removed.
if (features[fixExpiredNFTokenOfferRemoval])
{
aliceCount--;
}
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
//----------------------------------------------------------------------
// preclaim brokered
@@ -1202,8 +1234,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env.close();
env(pay(gw, buyer, gwAUD(30)));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 7);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
aliceCount++;
buyerCount++;
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// We're about to exercise offer brokering, so we need
// corresponding buy and sell offers.
@@ -1214,31 +1251,33 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(buyer, nftAlice0ID, gwAUD(29)),
token::owner(alice));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
buyerCount++;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// gw attempts to broker offers that are not for the same token.
env(token::brokerOffers(gw, buyerOfferIndex, xrpOnlyOfferIndex),
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// gw attempts to broker offers that are not for the same currency.
env(token::brokerOffers(gw, buyerOfferIndex, plainOfferIndex),
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// In a brokered offer, the buyer must offer greater than or
// equal to the selling price.
env(token::brokerOffers(gw, buyerOfferIndex, audOfferIndex),
ter(tecINSUFFICIENT_PAYMENT));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Remove buyer's offer.
env(token::cancelOffer(buyer, {buyerOfferIndex}));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 1);
buyerCount--;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
{
// buyer creates a buy offer for one of alice's nfts.
@@ -1247,7 +1286,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(buyer, nftAlice0ID, gwAUD(31)),
token::owner(alice));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
buyerCount++;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Broker sets their fee in a denomination other than the one
// used by the offers
@@ -1255,14 +1295,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
token::brokerFee(XRP(40)),
ter(tecNFTOKEN_BUY_SELL_MISMATCH));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Broker fee way too big.
env(token::brokerOffers(gw, buyerOfferIndex, audOfferIndex),
token::brokerFee(gwAUD(31)),
ter(tecINSUFFICIENT_PAYMENT));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Broker fee is smaller, but still too big once the offer
// seller's minimum is taken into account.
@@ -1270,12 +1310,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
token::brokerFee(gwAUD(1.5)),
ter(tecINSUFFICIENT_PAYMENT));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Remove buyer's offer.
env(token::cancelOffer(buyer, {buyerOfferIndex}));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 1);
buyerCount--;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
//----------------------------------------------------------------------
// preclaim buy
@@ -1286,19 +1327,20 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(buyer, nftAlice0ID, gwAUD(30)),
token::owner(alice));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
buyerCount++;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Don't accept a buy offer if the sell flag is set.
env(token::acceptBuyOffer(buyer, plainOfferIndex),
ter(tecNFTOKEN_OFFER_TYPE_MISMATCH));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 7);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
// An account can't accept its own offer.
env(token::acceptBuyOffer(buyer, buyerOfferIndex),
ter(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// An offer acceptor must have enough funds to pay for the offer.
env(pay(buyer, gw, gwAUD(30)));
@@ -1307,7 +1349,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::acceptBuyOffer(alice, buyerOfferIndex),
ter(tecINSUFFICIENT_FUNDS));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// alice gives her NFT to gw, so alice no longer owns nftAlice0.
{
@@ -1318,7 +1360,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env.close();
env(token::acceptSellOffer(gw, offerIndex));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 7);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
}
env(pay(gw, buyer, gwAUD(30)));
env.close();
@@ -1327,12 +1369,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::acceptBuyOffer(alice, buyerOfferIndex),
ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Remove buyer's offer.
env(token::cancelOffer(buyer, {buyerOfferIndex}));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 1);
buyerCount--;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
//----------------------------------------------------------------------
// preclaim sell
@@ -1343,26 +1386,27 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(buyer, nftXrpOnlyID, XRP(30)),
token::owner(alice));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
buyerCount++;
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Don't accept a sell offer without the sell flag set.
env(token::acceptSellOffer(alice, buyerOfferIndex),
ter(tecNFTOKEN_OFFER_TYPE_MISMATCH));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 7);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
// An account can't accept its own offer.
env(token::acceptSellOffer(alice, plainOfferIndex),
ter(tecCANT_ACCEPT_OWN_NFTOKEN_OFFER));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The seller must currently be in possession of the token they
// are selling. alice gave nftAlice0ID to gw.
env(token::acceptSellOffer(buyer, plainOfferIndex),
ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// gw gives nftAlice0ID back to alice. That allows us to check
// buyer attempting to accept one of alice's offers with
@@ -1375,7 +1419,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env.close();
env(token::acceptSellOffer(alice, offerIndex));
env.close();
BEAST_EXPECT(ownerCount(env, alice) == 7);
BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
}
env(pay(buyer, gw, gwAUD(30)));
env.close();
@@ -1383,7 +1427,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::acceptSellOffer(buyer, audOfferIndex),
ter(tecINSUFFICIENT_FUNDS));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
//----------------------------------------------------------------------
@@ -3226,6 +3270,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
token::issuer(issuer),
txflags(tfTransferable));
env.close();
uint8_t issuerCount, minterCount, buyerCount;
// Test how adding an Expiration field to an offer affects permissions
// for cancelling offers.
@@ -3257,9 +3302,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
token::owner(minter),
token::expiration(expiration));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(ownerCount(env, minter) == 3);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
issuerCount = 1;
minterCount = 3;
buyerCount = 1;
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Test who gets to cancel the offers. Anyone outside of the
// offer-owner/destination pair should not be able to cancel
@@ -3273,32 +3321,36 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(lastClose(env) < expiration);
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(ownerCount(env, minter) == 3);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// The offer creator can cancel their own unexpired offer.
env(token::cancelOffer(minter, {offerMinterToAnyone}));
minterCount--;
// The destination of a sell offer can cancel the NFT owner's
// unexpired offer.
env(token::cancelOffer(issuer, {offerMinterToIssuer}));
minterCount--;
// Close enough ledgers to get past the expiration.
while (lastClose(env) < expiration)
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Anyone can cancel expired offers.
env(token::cancelOffer(issuer, {offerBuyerToMinter}));
buyerCount--;
env(token::cancelOffer(buyer, {offerIssuerToMinter}));
issuerCount--;
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// Show that:
// 1. An unexpired sell offer with an expiration can be accepted.
@@ -3312,45 +3364,72 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(minter, nftokenID0, drops(1)),
token::expiration(expiration),
txflags(tfSellNFToken));
minterCount++;
uint256 const offer1 =
keylet::nftoffer(minter, env.seq(minter)).key;
env(token::createOffer(minter, nftokenID1, drops(1)),
token::expiration(expiration),
txflags(tfSellNFToken));
minterCount++;
env.close();
BEAST_EXPECT(lastClose(env) < expiration);
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 3);
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Anyone can accept an unexpired sell offer.
env(token::acceptSellOffer(buyer, offer0));
minterCount--;
buyerCount++;
// Close enough ledgers to get past the expiration.
while (lastClose(env) < expiration)
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// No one can accept an expired sell offer.
env(token::acceptSellOffer(buyer, offer1), ter(tecEXPIRED));
// With fixExpiredNFTokenOfferRemoval amendment, the first accept
// attempt deletes the expired offer. Without the amendment,
// the offer remains and we can try to accept it again.
if (features[fixExpiredNFTokenOfferRemoval])
{
// After amendment: offer was deleted by first accept attempt
minterCount--;
env(token::acceptSellOffer(issuer, offer1),
ter(tecOBJECT_NOT_FOUND));
}
else
{
// Before amendment: offer still exists, second accept also
// fails
env(token::acceptSellOffer(issuer, offer1), ter(tecEXPIRED));
}
env.close();
// The expired sell offer is still in the ledger.
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
// Check if the expired sell offer behavior matches amendment status
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Anyone can cancel the expired sell offer.
if (!features[fixExpiredNFTokenOfferRemoval])
{
// Before amendment: expired offer still exists and needs to be
// cancelled
env(token::cancelOffer(issuer, {offer1}));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
minterCount--;
}
// Ensure that owner counts are correct with and without the
// amendment
BEAST_EXPECT(ownerCount(env, issuer) == 0 && issuerCount == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1 && minterCount == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1 && buyerCount == 1);
// Transfer nftokenID0 back to minter so we start the next test in
// a simple place.
@@ -3361,10 +3440,11 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
token::destination(minter));
env.close();
env(token::acceptSellOffer(minter, offerSellBack));
buyerCount--;
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 0);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// Show that:
// 1. An unexpired buy offer with an expiration can be accepted.
@@ -3377,16 +3457,18 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(buyer, nftokenID0, drops(1)),
token::owner(minter),
token::expiration(expiration));
buyerCount++;
uint256 const offer1 = keylet::nftoffer(buyer, env.seq(buyer)).key;
env(token::createOffer(buyer, nftokenID1, drops(1)),
token::owner(minter),
token::expiration(expiration));
buyerCount++;
env.close();
BEAST_EXPECT(lastClose(env) < expiration);
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// An unexpired buy offer can be accepted.
env(token::acceptBuyOffer(minter, offer0));
@@ -3395,26 +3477,49 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
while (lastClose(env) < expiration)
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// An expired buy offer cannot be accepted.
env(token::acceptBuyOffer(minter, offer1), ter(tecEXPIRED));
// With fixExpiredNFTokenOfferRemoval amendment, the first accept
// attempt deletes the expired offer. Without the amendment,
// the offer remains and we can try to accept it again.
if (features[fixExpiredNFTokenOfferRemoval])
{
// After amendment: offer was deleted by first accept attempt
buyerCount--;
env(token::acceptBuyOffer(issuer, offer1),
ter(tecOBJECT_NOT_FOUND));
}
else
{
// Before amendment: offer still exists, second accept also
// fails
env(token::acceptBuyOffer(issuer, offer1), ter(tecEXPIRED));
}
env.close();
// The expired buy offer is still in the ledger.
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// Check if the expired buy offer behavior matches amendment status
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// Anyone can cancel the expired buy offer.
if (!features[fixExpiredNFTokenOfferRemoval])
{
// Before amendment: expired offer still exists and can be
// cancelled
env(token::cancelOffer(issuer, {offer1}));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
buyerCount--;
}
// Ensure that owner counts are the same with and without the
// amendment
BEAST_EXPECT(ownerCount(env, issuer) == 0 && issuerCount == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1 && minterCount == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1 && buyerCount == 1);
// Transfer nftokenID0 back to minter so we start the next test in
// a simple place.
@@ -3426,9 +3531,10 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env.close();
env(token::acceptSellOffer(minter, offerSellBack));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 0);
buyerCount--;
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// Show that in brokered mode:
// 1. An unexpired sell offer with an expiration can be accepted.
@@ -3442,56 +3548,80 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env(token::createOffer(minter, nftokenID0, drops(1)),
token::expiration(expiration),
txflags(tfSellNFToken));
minterCount++;
uint256 const sellOffer1 =
keylet::nftoffer(minter, env.seq(minter)).key;
env(token::createOffer(minter, nftokenID1, drops(1)),
token::expiration(expiration),
txflags(tfSellNFToken));
minterCount++;
uint256 const buyOffer0 =
keylet::nftoffer(buyer, env.seq(buyer)).key;
env(token::createOffer(buyer, nftokenID0, drops(1)),
token::owner(minter));
buyerCount++;
uint256 const buyOffer1 =
keylet::nftoffer(buyer, env.seq(buyer)).key;
env(token::createOffer(buyer, nftokenID1, drops(1)),
token::owner(minter));
buyerCount++;
env.close();
BEAST_EXPECT(lastClose(env) < expiration);
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 3);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// An unexpired offer can be brokered.
env(token::brokerOffers(issuer, buyOffer0, sellOffer0));
minterCount--;
// Close enough ledgers to get past the expiration.
while (lastClose(env) < expiration)
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
// If the sell offer is expired it cannot be brokered.
env(token::brokerOffers(issuer, buyOffer1, sellOffer1),
ter(tecEXPIRED));
env.close();
// The expired sell offer is still in the ledger.
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
if (features[fixExpiredNFTokenOfferRemoval])
{
// With amendment: expired offers are deleted
minterCount--;
}
// Anyone can cancel the expired sell offer.
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
if (features[fixExpiredNFTokenOfferRemoval])
{
// The buy offer was deleted, so no need to cancel it
// The sell offer still exists, so we can cancel it
env(token::cancelOffer(buyer, {buyOffer1}));
buyerCount--;
}
else
{
// Anyone can cancel the expired offers
env(token::cancelOffer(buyer, {buyOffer1, sellOffer1}));
minterCount--;
buyerCount--;
}
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
// Ensure that owner counts are the same with and without the
// amendment
BEAST_EXPECT(ownerCount(env, issuer) == 0 && issuerCount == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1 && minterCount == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 1 && buyerCount == 1);
// Transfer nftokenID0 back to minter so we start the next test in
// a simple place.
@@ -3503,9 +3633,10 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
env.close();
env(token::acceptSellOffer(minter, offerSellBack));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
BEAST_EXPECT(ownerCount(env, buyer) == 0);
buyerCount--;
BEAST_EXPECT(ownerCount(env, issuer) == issuerCount);
BEAST_EXPECT(ownerCount(env, minter) == minterCount);
BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
}
// Show that in brokered mode:
// 1. An unexpired buy offer with an expiration can be accepted.
@@ -3553,18 +3684,29 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// If the buy offer is expired it cannot be brokered.
env(token::brokerOffers(issuer, buyOffer1, sellOffer1),
ter(tecEXPIRED));
env.close();
// The expired buy offer is still in the ledger.
BEAST_EXPECT(ownerCount(env, issuer) == 0);
if (features[fixExpiredNFTokenOfferRemoval])
{
// After amendment: expired offers were deleted during broker
// attempt
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 1);
// The buy offer was deleted, so no need to cancel it
// The sell offer still exists, so we can cancel it
env(token::cancelOffer(minter, {sellOffer1}));
}
else
{
// Before amendment: expired offers still exist in ledger
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// Anyone can cancel the expired buy offer.
// Anyone can cancel the expired offers
env(token::cancelOffer(minter, {buyOffer1, sellOffer1}));
}
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
@@ -3633,18 +3775,20 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// If the offers are expired they cannot be brokered.
env(token::brokerOffers(issuer, buyOffer1, sellOffer1),
ter(tecEXPIRED));
env.close();
// The expired offers are still in the ledger.
BEAST_EXPECT(ownerCount(env, issuer) == 0);
if (!features[fixExpiredNFTokenOfferRemoval])
{
// Before amendment: expired offers still exist in ledger
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// Anyone can cancel the expired offers.
// Anyone can cancel the expired offers
env(token::cancelOffer(issuer, {buyOffer1, sellOffer1}));
}
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, minter) == 1);
@@ -8078,20 +8222,25 @@ public:
static FeatureBitset const all{testable_amendments()};
static FeatureBitset const fixNFTDir{fixNFTokenDirV1};
static std::array<FeatureBitset, 8> const feats{
static std::array<FeatureBitset, 9> const feats{
all - fixNFTDir - fixNonFungibleTokensV1_2 - fixNFTokenRemint -
fixNFTokenReserve - featureNFTokenMintOffer - featureDynamicNFT,
fixNFTokenReserve - featureNFTokenMintOffer -
featureDynamicNFT - fixExpiredNFTokenOfferRemoval,
all - disallowIncoming - fixNonFungibleTokensV1_2 -
fixNFTokenRemint - fixNFTokenReserve - featureNFTokenMintOffer -
featureDynamicNFT,
featureDynamicNFT - fixExpiredNFTokenOfferRemoval,
all - fixNonFungibleTokensV1_2 - fixNFTokenRemint -
fixNFTokenReserve - featureNFTokenMintOffer - featureDynamicNFT,
fixNFTokenReserve - featureNFTokenMintOffer -
featureDynamicNFT - fixExpiredNFTokenOfferRemoval,
all - fixNFTokenRemint - fixNFTokenReserve -
featureNFTokenMintOffer - featureDynamicNFT,
featureNFTokenMintOffer - featureDynamicNFT -
fixExpiredNFTokenOfferRemoval,
all - fixNFTokenReserve - featureNFTokenMintOffer -
featureDynamicNFT,
all - featureNFTokenMintOffer - featureDynamicNFT,
all - featureDynamicNFT,
featureDynamicNFT - fixExpiredNFTokenOfferRemoval,
all - featureNFTokenMintOffer - featureDynamicNFT -
fixExpiredNFTokenOfferRemoval,
all - featureDynamicNFT - fixExpiredNFTokenOfferRemoval,
all - fixExpiredNFTokenOfferRemoval,
all};
if (BEAST_EXPECT(instance < feats.size()))
@@ -8162,12 +8311,21 @@ class NFTokenWOModify_test : public NFTokenBaseUtil_test
}
};
class NFTokenWOExpiredOfferRemoval_test : public NFTokenBaseUtil_test
{
void
run() override
{
NFTokenBaseUtil_test::run(7);
}
};
class NFTokenAllFeatures_test : public NFTokenBaseUtil_test
{
void
run() override
{
NFTokenBaseUtil_test::run(7, true);
NFTokenBaseUtil_test::run(8, true);
}
};
@@ -8178,6 +8336,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOTokenRemint, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOTokenReserve, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOMintOffer, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOModify, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOExpiredOfferRemoval, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAllFeatures, app, ripple, 2);
} // namespace ripple

View File

@@ -73,7 +73,17 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
return {nullptr, tecOBJECT_NOT_FOUND};
if (hasExpired(ctx.view, (*offerSLE)[~sfExpiration]))
{
// Before fixExpiredNFTokenOfferRemoval amendment, expired
// offers caused tecEXPIRED in preclaim, leaving them on ledger
// forever. After the amendment, we allow expired offers to
// reach doApply() where they get deleted and tecEXPIRED is
// returned.
if (!ctx.view.rules().enabled(fixExpiredNFTokenOfferRemoval))
return {nullptr, tecEXPIRED};
// Amendment enabled: return the expired offer to be handled in
// doApply
}
// The initial implementation had a bug that allowed a negative
// amount. The fixNFTokenNegOffer amendment fixes that.
@@ -356,7 +366,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
auto const& offer = bo ? bo : so;
if (!offer)
// Purely defensive, should be caught in preflight.
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
auto const& tokenID = offer->at(sfNFTokenID);
auto const& amount = offer->at(sfAmount);
@@ -399,7 +409,7 @@ NFTokenAcceptOffer::pay(
{
// This should never happen, but it's easy and quick to check.
if (amount < beast::zero)
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
auto const result = accountSend(view(), from, to, amount, j_);
@@ -428,7 +438,7 @@ NFTokenAcceptOffer::transferNFToken(
auto tokenAndPage = nft::findTokenAndPage(view(), seller, nftokenID);
if (!tokenAndPage)
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto const ret = nft::removeToken(
view(), seller, nftokenID, std::move(tokenAndPage->page));
@@ -437,7 +447,7 @@ NFTokenAcceptOffer::transferNFToken(
auto const sleBuyer = view().read(keylet::account(buyer));
if (!sleBuyer)
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
std::uint32_t const buyerOwnerCountBefore =
sleBuyer->getFieldU32(sfOwnerCount);
@@ -521,18 +531,57 @@ NFTokenAcceptOffer::doApply()
auto bo = loadToken(ctx_.tx[~sfNFTokenBuyOffer]);
auto so = loadToken(ctx_.tx[~sfNFTokenSellOffer]);
// With fixExpiredNFTokenOfferRemoval amendment, check for expired offers
// and delete them, returning tecEXPIRED. This ensures expired offers
// are properly cleaned up from the ledger.
if (view().rules().enabled(fixExpiredNFTokenOfferRemoval))
{
bool foundExpired = false;
auto const deleteOfferIfExpired =
[this, &foundExpired](std::shared_ptr<SLE> const& offer) -> TER {
if (offer && hasExpired(view(), (*offer)[~sfExpiration]))
{
JLOG(j_.trace())
<< "Offer is expired, deleting: " << offer->key();
if (!nft::deleteTokenOffer(view(), offer))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete expired offer '"
<< offer->key() << "': ignoring";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
foundExpired = true;
}
return tesSUCCESS;
};
if (auto const r = deleteOfferIfExpired(bo); !isTesSuccess(r))
return r;
if (auto const r = deleteOfferIfExpired(so); !isTesSuccess(r))
return r;
if (foundExpired)
return tecEXPIRED;
}
if (bo && !nft::deleteTokenOffer(view(), bo))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete buy offer '"
<< to_string(bo->key()) << "': ignoring";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (so && !nft::deleteTokenOffer(view(), so))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete sell offer '"
<< to_string(so->key()) << "': ignoring";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Bridging two different offers
@@ -603,7 +652,7 @@ NFTokenAcceptOffer::doApply()
if (so)
return acceptOffer(so);
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
}
} // namespace ripple