Compare commits

..

27 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
Bart
46ba8a28fe refactor: Update Conan dependencies: OpenSSL (#5873)
This change bumps OpenSSL from 1.1.1w to 3.6.0.
2025-10-09 13:27:26 -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
12 changed files with 426 additions and 435 deletions

View File

@@ -78,42 +78,42 @@
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "0ab1e4c"
"image_sha": "d133ce3"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "13",
"image_sha": "0ab1e4c"
"image_sha": "d133ce3"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "0ab1e4c"
"image_sha": "d133ce3"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "clang",
"compiler_version": "any",
"image_sha": "0ab1e4c"
"image_sha": "d133ce3"
},
{
"distro_name": "rhel",
"distro_version": "10",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "0ab1e4c"
"image_sha": "d133ce3"
},
{
"distro_name": "rhel",
"distro_version": "10",
"compiler_name": "clang",
"compiler_version": "any",
"image_sha": "0ab1e4c"
"image_sha": "d133ce3"
},
{
"distro_name": "ubuntu",

View File

@@ -16,13 +16,16 @@ set(CMAKE_CXX_EXTENSIONS OFF)
target_compile_definitions (common
INTERFACE
$<$<CONFIG:Debug>:DEBUG _DEBUG>
$<$<AND:$<BOOL:${profile}>,$<NOT:$<BOOL:${assert}>>>:NDEBUG>)
# ^^^^ NOTE: CMAKE release builds already have NDEBUG
# defined, so no need to add it explicitly except for
# this special case of (profile ON) and (assert OFF)
# -- presumably this is because we don't want profile
# builds asserting unless asserts were specifically
# requested
#[===[
NOTE: CMAKE release builds already have NDEBUG defined, so no need to add it
explicitly except for the special case of (profile ON) and (assert OFF).
Presumably this is because we don't want profile builds asserting unless
asserts were specifically requested.
]===]
$<$<AND:$<BOOL:${profile}>,$<NOT:$<BOOL:${assert}>>>:NDEBUG>
# TODO: Remove once we have migrated functions from OpenSSL 1.x to 3.x.
OPENSSL_SUPPRESS_DEPRECATED
)
if (MSVC)
# remove existing exception flag since we set it to -EHa

View File

@@ -9,7 +9,7 @@
"rocksdb/10.0.1#85537f46e538974d67da0c3977de48ac%1756234304.347",
"re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1756234257.976",
"protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614",
"openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729",
"openssl/3.6.0#89e8af1d4a21afcac0557079d23d8890%1759746682.365",
"nudb/2.0.9#c62cfd501e57055a7e0d8ee3d5e5427d%1756234237.107",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64",

View File

@@ -27,7 +27,7 @@ class Xrpl(ConanFile):
'grpc/1.50.1',
'libarchive/3.8.1',
'nudb/2.0.9',
'openssl/1.1.1w',
'openssl/3.6.0',
'soci/4.0.3',
'zlib/1.3.1',
]

View File

@@ -170,6 +170,9 @@ public:
bool
retrieve(key_type const& key, T& data);
mutex_type&
peekMutex();
std::vector<key_type>
getKeys() const;

View File

@@ -668,6 +668,29 @@ TaggedCache<
return true;
}
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
inline auto
TaggedCache<
Key,
T,
IsKeyCache,
SharedWeakUnionPointer,
SharedPointerType,
Hash,
KeyEqual,
Mutex>::peekMutex() -> mutex_type&
{
return m_mutex;
}
template <
class Key,
class T,

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));
env(token::acceptSellOffer(issuer, 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.
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);
if (!features[fixExpiredNFTokenOfferRemoval])
{
// Before amendment: expired offer still exists and needs to be
// cancelled
env(token::cancelOffer(issuer, {offer1}));
env.close();
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));
env(token::acceptBuyOffer(issuer, 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.
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);
if (!features[fixExpiredNFTokenOfferRemoval])
{
// Before amendment: expired offer still exists and can be
// cancelled
env(token::cancelOffer(issuer, {offer1}));
env.close();
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.
env(token::cancelOffer(buyer, {buyOffer1, sellOffer1}));
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);
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// Anyone can cancel the expired buy offer.
env(token::cancelOffer(minter, {buyOffer1, sellOffer1}));
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 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);
BEAST_EXPECT(ownerCount(env, minter) == 2);
BEAST_EXPECT(ownerCount(env, buyer) == 2);
// Anyone can cancel the expired offers.
env(token::cancelOffer(issuer, {buyOffer1, sellOffer1}));
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
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

@@ -27,6 +27,8 @@
namespace ripple {
// FIXME: Need to clean up ledgers by index at some point
LedgerHistory::LedgerHistory(
beast::insight::Collector::ptr const& collector,
Application& app)
@@ -45,7 +47,6 @@ LedgerHistory::LedgerHistory(
std::chrono::minutes{5},
stopwatch(),
app_.journal("TaggedCache"))
, mLedgersByIndex(256)
, j_(app.journal("LedgerHistory"))
{
}
@@ -62,16 +63,12 @@ LedgerHistory::insert(
ledger->stateMap().getHash().isNonZero(),
"ripple::LedgerHistory::insert : nonzero hash");
std::unique_lock sl(m_ledgers_by_hash.peekMutex());
bool const alreadyHad = m_ledgers_by_hash.canonicalize_replace_cache(
ledger->info().hash, ledger);
if (validated)
{
mLedgersByIndex[ledger->info().seq] = ledger->info().hash;
JLOG(j_.info()) << "LedgerHistory::insert: mLedgersByIndex size: "
<< mLedgersByIndex.size() << " , total size: "
<< mLedgersByIndex.size() *
(sizeof(LedgerIndex) + sizeof(LedgerHash));
}
return alreadyHad;
}
@@ -79,18 +76,25 @@ LedgerHistory::insert(
LedgerHash
LedgerHistory::getLedgerHash(LedgerIndex index)
{
if (auto p = mLedgersByIndex.get(index))
return *p;
std::unique_lock sl(m_ledgers_by_hash.peekMutex());
if (auto it = mLedgersByIndex.find(index); it != mLedgersByIndex.end())
return it->second;
return {};
}
std::shared_ptr<Ledger const>
LedgerHistory::getLedgerBySeq(LedgerIndex index)
{
if (auto p = mLedgersByIndex.get(index))
{
uint256 const hash = *p;
return getLedgerByHash(hash);
std::unique_lock sl(m_ledgers_by_hash.peekMutex());
auto it = mLedgersByIndex.find(index);
if (it != mLedgersByIndex.end())
{
uint256 hash = it->second;
sl.unlock();
return getLedgerByHash(hash);
}
}
std::shared_ptr<Ledger const> ret = loadByIndex(index, app_);
@@ -104,16 +108,13 @@ LedgerHistory::getLedgerBySeq(LedgerIndex index)
{
// Add this ledger to the local tracking by index
std::unique_lock sl(m_ledgers_by_hash.peekMutex());
XRPL_ASSERT(
ret->isImmutable(),
"ripple::LedgerHistory::getLedgerBySeq : immutable result ledger");
m_ledgers_by_hash.canonicalize_replace_client(ret->info().hash, ret);
mLedgersByIndex[ret->info().seq] = ret->info().hash;
JLOG(j_.info())
<< "LedgerHistory::getLedgerBySeq: mLedgersByIndex size: "
<< mLedgersByIndex.size() << " , total size: "
<< mLedgersByIndex.size() *
(sizeof(LedgerIndex) + sizeof(LedgerHash));
return (ret->info().seq == index) ? ret : nullptr;
}
}
@@ -457,6 +458,8 @@ LedgerHistory::builtLedger(
XRPL_ASSERT(
!hash.isZero(), "ripple::LedgerHistory::builtLedger : nonzero hash");
std::unique_lock sl(m_consensus_validated.peekMutex());
auto entry = std::make_shared<cv_entry>();
m_consensus_validated.canonicalize_replace_client(index, entry);
@@ -497,6 +500,8 @@ LedgerHistory::validatedLedger(
!hash.isZero(),
"ripple::LedgerHistory::validatedLedger : nonzero hash");
std::unique_lock sl(m_consensus_validated.peekMutex());
auto entry = std::make_shared<cv_entry>();
m_consensus_validated.canonicalize_replace_client(index, entry);
@@ -530,13 +535,13 @@ LedgerHistory::validatedLedger(
bool
LedgerHistory::fixIndex(LedgerIndex ledgerIndex, LedgerHash const& ledgerHash)
{
if (auto cur = mLedgersByIndex.get(ledgerIndex))
std::unique_lock sl(m_ledgers_by_hash.peekMutex());
auto it = mLedgersByIndex.find(ledgerIndex);
if ((it != mLedgersByIndex.end()) && (it->second != ledgerHash))
{
if (*cur != ledgerHash)
{
mLedgersByIndex.put(ledgerIndex, ledgerHash);
return false;
}
it->second = ledgerHash;
return false;
}
return true;
}

View File

@@ -22,7 +22,6 @@
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/core/detail/LRUCache.h>
#include <xrpl/beast/insight/Collector.h>
#include <xrpl/protocol/RippleLedgerHash.h>
@@ -150,8 +149,7 @@ private:
ConsensusValidated m_consensus_validated;
// Maps ledger indexes to the corresponding hash.
LRUCache<LedgerIndex, LedgerHash, concurrency::ExclusiveMutex>
mLedgersByIndex; // validated ledgers
std::map<LedgerIndex, LedgerHash> mLedgersByIndex; // validated ledgers
beast::Journal j_;
};

View File

@@ -73,7 +73,17 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
return {nullptr, tecOBJECT_NOT_FOUND};
if (hasExpired(ctx.view, (*offerSLE)[~sfExpiration]))
return {nullptr, tecEXPIRED};
{
// 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

View File

@@ -1,250 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_APP_LRU_CACHE_H_INCLUDED
#define RIPPLE_APP_LRU_CACHE_H_INCLUDED
#include <list>
#include <mutex>
#include <stdexcept>
#include <type_traits>
#include <unordered_map>
#include <utility>
namespace ripple {
namespace concurrency {
struct SingleThreaded
{
struct mutex_type
{
void
lock() noexcept
{
}
void
unlock() noexcept
{
}
};
using lock_guard = std::lock_guard<mutex_type>;
};
struct ExclusiveMutex
{
using mutex_type = std::mutex;
using lock_guard = std::lock_guard<mutex_type>;
};
} // namespace concurrency
template <
class Key,
class Value,
class Concurrency = concurrency::SingleThreaded>
class LRUCache
{
using List = std::list<Key>; // MRU .. LRU
using DataMap = std::unordered_map<Key, Value>; // Key -> Value
using PosMap =
std::unordered_map<Key, typename List::iterator>; // Key -> pos
// iterator in the
// list
public:
explicit LRUCache(std::size_t capacity) : capacity_(capacity)
{
if (capacity_ == 0)
throw std::invalid_argument("LRUCache capacity must be positive.");
data_.reserve(capacity_);
pos_.reserve(capacity_);
// TODO:
// static_assert(std::is_default_constructible_v<Value>,
// "LRUCache requires Value to be default-constructible for
// operator[]");
// static_assert(std::is_copy_constructible_v<Key> ||
// std::is_move_constructible_v<Key>,
// "LRUCache requires Key to be copy- or move-constructible");
}
LRUCache(LRUCache const&) = delete;
LRUCache&
operator=(LRUCache const&) = delete;
LRUCache(LRUCache&&) = delete;
LRUCache&
operator=(LRUCache&&) = delete;
Value&
operator[](Key const& key)
{
auto g = lock();
return insertOrpromote(key);
}
Value&
operator[](Key&& key)
{
auto g = lock();
auto it = data_.find(key);
if (it != data_.end())
{
promote(key);
return it->second;
}
evictIfFull();
usage_.emplace_front(std::move(key));
pos_.emplace(usage_.front(), usage_.begin());
auto d = data_.emplace(usage_.front(), Value{});
return d.first->second;
}
Value*
get(Key const& key)
{
auto g = lock();
auto it = data_.find(key);
if (it == data_.end())
return nullptr;
promote(key);
return &it->second;
}
template <class... Args>
Value&
put(Key const& key, Args&&... args)
{
auto g = lock();
if (auto it = data_.find(key); it != data_.end())
{
it->second = Value(std::forward<Args>(args)...);
promote(key);
return it->second;
}
evictIfFull();
usage_.emplace_front(key);
pos_.emplace(key, usage_.begin());
auto it = data_.emplace(key, Value(std::forward<Args>(args)...));
return it.first->second;
}
bool
erase(Key const& key)
{
auto g = lock();
auto it = data_.find(key);
if (it == data_.end())
return false;
usage_.erase(pos_.at(key));
pos_.erase(key);
data_.erase(it);
return true;
}
bool
contains(Key const& key) const
{
auto g = lock();
return data_.find(key) != data_.end();
}
std::size_t
size() const noexcept
{
auto g = lock();
return data_.size();
}
std::size_t
capacity() const noexcept
{
return capacity_;
}
bool
empty() const noexcept
{
auto g = lock();
return data_.empty();
}
void
clear()
{
auto g = lock();
data_.clear();
pos_.clear();
usage_.clear();
}
private:
Value&
insertOrpromote(Key const& key)
{
if (auto it = data_.find(key); it != data_.end())
{
promote(key);
return it->second;
}
evictIfFull();
usage_.emplace_front(key);
pos_.emplace(key, usage_.begin());
auto it = data_.emplace(key, Value{});
return it.first->second;
}
void
promote(Key const& key)
{
auto lit = pos_.at(key);
usage_.splice(usage_.begin(), usage_, lit); // O(1)
}
void
evictIfFull()
{
if (data_.size() < capacity_)
return;
auto const& k = usage_.back();
data_.erase(k);
pos_.erase(k);
usage_.pop_back();
}
typename Concurrency::lock_guard
lock() const
{
return typename Concurrency::lock_guard{mtx_};
}
private:
std::size_t const capacity_;
DataMap data_;
PosMap pos_;
List usage_;
mutable typename Concurrency::mutex_type mtx_;
};
} // namespace ripple
#endif