mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
Merge branch 'develop' into mvadari/refactor-tx-settings
This commit is contained in:
@@ -137,7 +137,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
AMM amm(env, gw, btc(100), usd(100));
|
||||
env.close();
|
||||
amm.deposit(alice, 1'000);
|
||||
env.close();
|
||||
|
||||
// can not clawback when tfMPTCanClawback is not enabled
|
||||
env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
|
||||
@@ -503,6 +502,150 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testAMMClawbackAmountRoundsToZero(FeatureBitset features)
|
||||
{
|
||||
// Ensure a clawback that rounds down to zero MPT fails with
|
||||
// tecAMM_FAILED instead of silently burning the holder's LP.
|
||||
testcase("test AMMClawback amount that rounds down to zero");
|
||||
using namespace jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10'000'000), gw, alice, bob);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
// The clawed asset (amountRounded) rounds to zero while its XRP
|
||||
// counterpart is always large.
|
||||
{
|
||||
MPTTester const mptBtc(
|
||||
{.env = env,
|
||||
.issuer = gw,
|
||||
.holders = {alice, bob},
|
||||
.pay = 1'000,
|
||||
.flags = tfMPTCanClawback | kMptDexFlags});
|
||||
MPT const btc = mptBtc;
|
||||
|
||||
AMM amm(env, alice, btc(3), XRP(333'000));
|
||||
amm.deposit(bob, btc(3), XRP(333'000));
|
||||
|
||||
[[maybe_unused]] auto const [poolBtcBefore, poolXrpBefore, lptBefore] = amm.balances();
|
||||
BEAST_EXPECT(poolBtcBefore == btc(6));
|
||||
|
||||
auto const issuerOABefore = mptBtc.getBalance(gw);
|
||||
auto const aliceLpBefore = amm.getLPTokensBalance(alice.id());
|
||||
auto const bobLpBefore = amm.getLPTokensBalance(bob.id());
|
||||
|
||||
// Attempt to clawback 1/6th of the BTC pool. When the zero-rounding
|
||||
// guard is active (gated by fixCleanup3_4_0) the rounded amount
|
||||
// drops to 0 and should trigger tecAMM_FAILED.
|
||||
env(amm::ammClawback(gw, alice, btc, XRP, btc(1)),
|
||||
Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
|
||||
env.close();
|
||||
|
||||
[[maybe_unused]] auto const [poolBtcAfter, poolXrpAfter, lptAfter] = amm.balances();
|
||||
auto const issuerOAAfter = mptBtc.getBalance(gw);
|
||||
auto const aliceLpAfter = amm.getLPTokensBalance(alice.id());
|
||||
auto const bobLpAfter = amm.getLPTokensBalance(bob.id());
|
||||
|
||||
if (features[fixCleanup3_4_0])
|
||||
{
|
||||
// Post-fixCleanup3_4_0: Clawback fails because the BTC balance
|
||||
// would round to zero. All balances must remain untouched.
|
||||
BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
|
||||
BEAST_EXPECT(poolXrpAfter == poolXrpBefore);
|
||||
BEAST_EXPECT(issuerOAAfter == issuerOABefore);
|
||||
BEAST_EXPECT(aliceLpAfter == aliceLpBefore);
|
||||
BEAST_EXPECT(bobLpAfter == bobLpBefore);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-fixCleanup3_4_0: BTC rounds to zero and the clawback
|
||||
// silently burns alice's LP without clawing back any BTC.
|
||||
BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
|
||||
BEAST_EXPECT(poolXrpAfter < poolXrpBefore);
|
||||
BEAST_EXPECT(issuerOAAfter == issuerOABefore);
|
||||
BEAST_EXPECT(aliceLpAfter < aliceLpBefore);
|
||||
BEAST_EXPECT(bobLpAfter == bobLpBefore);
|
||||
}
|
||||
}
|
||||
|
||||
// The pool above only ever rounds the clawed asset (amountRounded) to
|
||||
// zero; its XRP counterpart is always large. Exercise the other operand
|
||||
// of the guard (amount2Rounded == 0) with an MPT/MPT pool where the
|
||||
// *paired* asset is the tiny integer that floors to zero while the
|
||||
// clawed asset still rounds non-zero.
|
||||
{
|
||||
Account const carol{"carol"};
|
||||
Account const dan{"dan"};
|
||||
env.fund(XRP(10'000'000), carol, dan);
|
||||
env.close();
|
||||
|
||||
MPTTester const mptBtc(
|
||||
{.env = env,
|
||||
.issuer = gw,
|
||||
.holders = {carol, dan},
|
||||
.pay = 100'000,
|
||||
.flags = tfMPTCanClawback | kMptDexFlags});
|
||||
MPT const btc = mptBtc;
|
||||
|
||||
MPTTester const mptEth(
|
||||
{.env = env,
|
||||
.issuer = gw,
|
||||
.holders = {carol, dan},
|
||||
.pay = 1'000,
|
||||
.flags = tfMPTCanClawback | kMptDexFlags});
|
||||
MPT const eth = mptEth;
|
||||
|
||||
// btc pool dwarfs the eth pool, so a ~1/12th claw withdraws a
|
||||
// non-zero btc amount while the eth counterpart rounds to zero.
|
||||
AMM amm(env, carol, btc(3'000), eth(3));
|
||||
amm.deposit(dan, btc(3'000), eth(3));
|
||||
|
||||
[[maybe_unused]] auto const [poolBtcBefore, poolEthBefore, lptBefore] = amm.balances();
|
||||
BEAST_EXPECT(poolBtcBefore == btc(6'000));
|
||||
BEAST_EXPECT(poolEthBefore == eth(6));
|
||||
|
||||
auto const carolLpBefore = amm.getLPTokensBalance(carol.id());
|
||||
auto const danLpBefore = amm.getLPTokensBalance(dan.id());
|
||||
|
||||
env(amm::ammClawback(gw, carol, btc, eth, btc(500)),
|
||||
Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
|
||||
env.close();
|
||||
|
||||
[[maybe_unused]] auto const [poolBtcAfter, poolEthAfter, lptAfter] = amm.balances();
|
||||
auto const carolLpAfter = amm.getLPTokensBalance(carol.id());
|
||||
auto const danLpAfter = amm.getLPTokensBalance(dan.id());
|
||||
|
||||
if (features[fixCleanup3_4_0])
|
||||
{
|
||||
// Post-fixCleanup3_4_0: clawback fails because the ETH (Asset2)
|
||||
// balance would round to zero (guard fires via
|
||||
// amount2Rounded == 0). All balances must remain untouched.
|
||||
BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
|
||||
BEAST_EXPECT(poolEthAfter == poolEthBefore);
|
||||
BEAST_EXPECT(carolLpAfter == carolLpBefore);
|
||||
BEAST_EXPECT(danLpAfter == danLpBefore);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-fixCleanup3_4_0: the asymmetric round-off goes through.
|
||||
// btc is clawed (non-zero) but eth rounds to zero, so the eth
|
||||
// pool is untouched while carol's LP is burned. This asymmetry
|
||||
// proves amount2Rounded == 0 is the trigger.
|
||||
BEAST_EXPECT(poolBtcAfter < poolBtcBefore);
|
||||
BEAST_EXPECT(poolEthAfter == poolEthBefore);
|
||||
BEAST_EXPECT(carolLpAfter < carolLpBefore);
|
||||
BEAST_EXPECT(danLpAfter == danLpBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testAMMClawbackAll(FeatureBitset features)
|
||||
{
|
||||
@@ -543,7 +686,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
|
||||
// gw clawback all BTC from alice
|
||||
amm.deposit(bob, btc(1'000'000000), usd(2000));
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(3000), IOUAmount(3000000)));
|
||||
|
||||
auto aliceBTC = env.balance(alice, btc);
|
||||
@@ -921,7 +1063,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(8'000), IOUAmount(4'000'000)));
|
||||
|
||||
amm.deposit(bob, btc(1'000'000000), usd(4'000));
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(12'000), IOUAmount(6'000'000)));
|
||||
|
||||
auto aliceBTC = env.balance(alice, btc);
|
||||
@@ -1361,7 +1502,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(XRP(100), btc(400), IOUAmount(200000)));
|
||||
amm.deposit(alice, btc(400));
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(XRP(100), btc(800), IOUAmount{282842'712474619, -9}));
|
||||
|
||||
auto aliceBTC = env.balance(alice, MPT(btc));
|
||||
@@ -1407,7 +1547,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
|
||||
amm.deposit(alice, btc(400));
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
|
||||
|
||||
auto aliceBTC = env.balance(alice, MPT(btc));
|
||||
@@ -1462,7 +1601,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
|
||||
amm.deposit(alice, btc(400));
|
||||
env.close();
|
||||
BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
|
||||
|
||||
auto aliceBTC = env.balance(alice, MPT(btc));
|
||||
@@ -1669,7 +1807,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
|
||||
|
||||
// Although USD is clawable with asfAllowTrustLineClawback.
|
||||
// When tfClawTwoAssets is set, we will claw Asser2 as well.
|
||||
// When tfClawTwoAssets is set, we will claw Asset2 as well.
|
||||
// But Asset2 is not clawable. tfMPTCanClawback was not set for BTC.
|
||||
env(amm::ammClawback(gw, alice, usd, btc, std::nullopt),
|
||||
Txflags(tfClawTwoAssets),
|
||||
@@ -1819,6 +1957,9 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
testInvalidRequest(all);
|
||||
testFeatureDisabled(all);
|
||||
testAMMClawbackAmount(all);
|
||||
testAMMClawbackAmount(all - fixCleanup3_4_0);
|
||||
testAMMClawbackAmountRoundsToZero(all);
|
||||
testAMMClawbackAmountRoundsToZero(all - fixCleanup3_4_0);
|
||||
testAMMClawbackAll(all);
|
||||
testAMMClawbackAmountSameIssuer(all);
|
||||
testAMMClawbackAllSameIssuer(all);
|
||||
|
||||
@@ -2155,6 +2155,209 @@ class AMMClawback_test : public beast::unit_test::Suite
|
||||
}
|
||||
BEAST_EXPECT(env.balance(carol, eur) == eur(7750));
|
||||
}
|
||||
|
||||
// gw (USD issuer) individually freezes the AMM-USD trust line.
|
||||
// AMMClawback must still succeed because the freeze invariant
|
||||
// short-circuits before reaching the AMM line check (no receivers in
|
||||
// the USD issuer's change set). Behavior is identical with or without
|
||||
// fixCleanup3_4_0.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"};
|
||||
Account const gw2{"gateway2"};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000000), gw, gw2, alice);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(Flags(gw, asfAllowTrustLineClawback));
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
env.trust(usd(100000), alice);
|
||||
env(pay(gw, alice, usd(3000)));
|
||||
env.close();
|
||||
|
||||
auto const eur = gw2["EUR"];
|
||||
env.trust(eur(100000), alice);
|
||||
env(pay(gw2, alice, eur(3000)));
|
||||
env.close();
|
||||
|
||||
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
|
||||
|
||||
// gw individually freezes the AMM-USD trust line (AMM pseudo-account
|
||||
// <-> gw), not alice's trust line.
|
||||
env(trust(gw, STAmount{Issue{usd.currency, amm.ammAccount()}, 0}, tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, usd(1000)));
|
||||
env.require(Balance(alice, eur(2500)));
|
||||
BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
|
||||
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
|
||||
}
|
||||
|
||||
// gw2 (EUR issuer) individually freezes the AMM-EUR trust line.
|
||||
// The EUR flow (AMM → alice) is a genuine P2P transfer checked by the
|
||||
// freeze invariant. Pre-fixCleanup3_4_0 the isAMMNode guard incorrectly
|
||||
// blocked AMMClawback's overrideFreeze privilege on that trust line.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"};
|
||||
Account const gw2{"gateway2"};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000000), gw, gw2, alice);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(Flags(gw, asfAllowTrustLineClawback));
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
env.trust(usd(100000), alice);
|
||||
env(pay(gw, alice, usd(3000)));
|
||||
env.close();
|
||||
|
||||
auto const eur = gw2["EUR"];
|
||||
env.trust(eur(100000), alice);
|
||||
env(pay(gw2, alice, eur(3000)));
|
||||
env.close();
|
||||
|
||||
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
|
||||
|
||||
// gw2 individually freezes the AMM-EUR trust line.
|
||||
env(trust(gw2, STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
if (features[fixCleanup3_4_0])
|
||||
{
|
||||
// Post-fixCleanup3_4_0: overrideFreeze privilege applies to
|
||||
// all freeze types on AMM trust lines.
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, usd(1000)));
|
||||
env.require(Balance(alice, eur(2500)));
|
||||
BEAST_EXPECT(
|
||||
amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
|
||||
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-fixCleanup3_4_0: the isAMMNode guard prevents the
|
||||
// overrideFreeze privilege from applying to individually-frozen
|
||||
// AMM trust lines, so the invariant blocks the clawback.
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED));
|
||||
}
|
||||
}
|
||||
|
||||
// gw2 (EUR issuer) globally freezes its issued assets. AMMClawback
|
||||
// must still be able to return EUR from the AMM to alice.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"};
|
||||
Account const gw2{"gateway2"};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000000), gw, gw2, alice);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(Flags(gw, asfAllowTrustLineClawback));
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
env.trust(usd(100000), alice);
|
||||
env(pay(gw, alice, usd(3000)));
|
||||
env.close();
|
||||
|
||||
auto const eur = gw2["EUR"];
|
||||
env.trust(eur(100000), alice);
|
||||
env(pay(gw2, alice, eur(3000)));
|
||||
env.close();
|
||||
|
||||
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
|
||||
|
||||
env(fset(gw2, asfGlobalFreeze));
|
||||
env.close();
|
||||
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, usd(1000)));
|
||||
env.require(Balance(alice, eur(2500)));
|
||||
BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
|
||||
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
|
||||
}
|
||||
|
||||
// Same as above but gw2 deep-freezes the AMM-EUR trust line.
|
||||
if (features[featureDeepFreeze])
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account const gw{"gateway"};
|
||||
Account const gw2{"gateway2"};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000000), gw, gw2, alice);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(Flags(gw, asfAllowTrustLineClawback));
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
env.trust(usd(100000), alice);
|
||||
env(pay(gw, alice, usd(3000)));
|
||||
env.close();
|
||||
|
||||
auto const eur = gw2["EUR"];
|
||||
env.trust(eur(100000), alice);
|
||||
env(pay(gw2, alice, eur(3000)));
|
||||
env.close();
|
||||
|
||||
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
|
||||
|
||||
// gw2 deep-freezes the AMM-EUR trust line.
|
||||
env(trust(
|
||||
gw2,
|
||||
STAmount{Issue{eur.currency, amm.ammAccount()}, 0},
|
||||
tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
|
||||
if (features[fixCleanup3_4_0])
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, usd(1000)));
|
||||
env.require(Balance(alice, eur(2500)));
|
||||
BEAST_EXPECT(
|
||||
amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
|
||||
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-fixCleanup3_4_0: same isAMMNode guard issue blocks the
|
||||
// clawback on deep-frozen AMM trust lines.
|
||||
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2530,6 +2733,7 @@ class AMMClawback_test : public beast::unit_test::Suite
|
||||
// precision loss caught in transaction layer -> tecPRECISION_LOSS
|
||||
all - fixAMMClawbackRounding - featureMPTokensV2,
|
||||
all - featureMPTokensV2,
|
||||
all - fixCleanup3_4_0,
|
||||
all})
|
||||
{
|
||||
testAMMClawbackSpecificAmount(features);
|
||||
|
||||
@@ -4790,6 +4790,87 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
|
||||
checkOffers("nft_buy_offers", 501, 2, __LINE__);
|
||||
}
|
||||
|
||||
void
|
||||
testNftXxxOffersMarkerWrongSide(FeatureBitset features)
|
||||
{
|
||||
// A pagination marker passed to nft_buy_offers / nft_sell_offers must
|
||||
// reference an offer on the same side (buy vs. sell) as the directory
|
||||
// being enumerated. A wrong-side marker is rejected with invalidParams.
|
||||
//
|
||||
// Note: the pre-fix code also returned invalidParams for a wrong-side
|
||||
// marker, but only after scanning the entire target directory (an
|
||||
// O(directory size) walk usable to burn CPU). The fix short-circuits
|
||||
// that scan. The scan-avoidance is not observable from the RPC
|
||||
// response, so this test locks the rejection contract (wrong-side ->
|
||||
// error, same-side -> success) rather than the performance property.
|
||||
testcase("nft_buy_offers and nft_sell_offers wrong-side marker");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const buyer{"buyer"};
|
||||
|
||||
env.fund(XRP(10000), issuer, buyer);
|
||||
env.close();
|
||||
|
||||
// Mint a transferable NFT.
|
||||
uint256 const nftID{token::getNextID(env, issuer, 0u, tfTransferable)};
|
||||
env(token::mint(issuer, 0), Txflags(tfTransferable));
|
||||
env.close();
|
||||
|
||||
// Create one sell offer (from the issuer, who owns the NFT) and one
|
||||
// buy offer (from the buyer) for the same NFT.
|
||||
env(token::createOffer(issuer, nftID, XRP(100)), Txflags(tfSellNFToken));
|
||||
env(token::createOffer(buyer, nftID, XRP(50)), token::Owner(issuer));
|
||||
env.close();
|
||||
|
||||
// Grab the index of the single offer on each side from the RPC
|
||||
// response so we can use it as a marker.
|
||||
auto firstOfferIndex = [this, &env, &nftID](char const* request) {
|
||||
json::Value params;
|
||||
params[jss::nft_id] = to_string(nftID);
|
||||
json::Value const result = env.rpc("json", request, to_string(params))[jss::result];
|
||||
BEAST_EXPECT(result.isMember(jss::offers) && result[jss::offers].size() == 1);
|
||||
return result[jss::offers][0u][jss::nft_offer_index].asString();
|
||||
};
|
||||
|
||||
std::string const sellOfferIndex = firstOfferIndex("nft_sell_offers");
|
||||
std::string const buyOfferIndex = firstOfferIndex("nft_buy_offers");
|
||||
|
||||
auto queryWithMarker = [&env, &nftID](char const* request, std::string const& marker) {
|
||||
json::Value params;
|
||||
params[jss::nft_id] = to_string(nftID);
|
||||
params[jss::marker] = marker;
|
||||
return env.rpc("json", request, to_string(params))[jss::result];
|
||||
};
|
||||
|
||||
// A marker referencing an offer on the wrong side is rejected with
|
||||
// invalidParams.
|
||||
{
|
||||
// Sell-side marker passed to nft_buy_offers.
|
||||
json::Value const result = queryWithMarker("nft_buy_offers", sellOfferIndex);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
|
||||
}
|
||||
{
|
||||
// Buy-side marker passed to nft_sell_offers.
|
||||
json::Value const result = queryWithMarker("nft_sell_offers", buyOfferIndex);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
|
||||
}
|
||||
|
||||
// A same-side marker is still accepted. With a single offer on each
|
||||
// side, resuming after it simply yields no further offers.
|
||||
{
|
||||
json::Value const result = queryWithMarker("nft_buy_offers", buyOfferIndex);
|
||||
BEAST_EXPECT(!result.isMember(jss::error));
|
||||
}
|
||||
{
|
||||
json::Value const result = queryWithMarker("nft_sell_offers", sellOfferIndex);
|
||||
BEAST_EXPECT(!result.isMember(jss::error));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testNFTokenNegOffer(FeatureBitset features)
|
||||
{
|
||||
@@ -7305,6 +7386,7 @@ protected:
|
||||
testNFTokenWithTickets(features);
|
||||
testNFTokenDeleteAccount(features);
|
||||
testNftXxxOffers(features);
|
||||
testNftXxxOffersMarkerWrongSide(features);
|
||||
testNFTokenNegOffer(features);
|
||||
testIOUWithTransferFee(features);
|
||||
testBrokeredSaleToSelf(features);
|
||||
|
||||
@@ -94,6 +94,24 @@ public:
|
||||
LedgerHeader const ledger3Info = env.closed()->header();
|
||||
BEAST_EXPECT(ledger3Info.seq == 3);
|
||||
|
||||
{
|
||||
// test peer non-string
|
||||
auto testInvalidPeerParam = [&](auto const& param) {
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::peer] = param;
|
||||
auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
|
||||
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'.");
|
||||
};
|
||||
|
||||
testInvalidPeerParam(1);
|
||||
testInvalidPeerParam(1.1);
|
||||
testInvalidPeerParam(true);
|
||||
testInvalidPeerParam(json::Value(json::ValueType::Null));
|
||||
testInvalidPeerParam(json::Value(json::ValueType::Object));
|
||||
testInvalidPeerParam(json::Value(json::ValueType::Array));
|
||||
}
|
||||
{
|
||||
// alice is funded but has no lines. An empty array is returned.
|
||||
json::Value params;
|
||||
@@ -775,6 +793,35 @@ public:
|
||||
LedgerHeader const ledger3Info = env.closed()->header();
|
||||
BEAST_EXPECT(ledger3Info.seq == 3);
|
||||
|
||||
{
|
||||
// test peer non-string
|
||||
auto testInvalidPeerParam = [&](auto const& param) {
|
||||
json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::peer] = param;
|
||||
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
};
|
||||
|
||||
testInvalidPeerParam(1);
|
||||
testInvalidPeerParam(1.1);
|
||||
testInvalidPeerParam(true);
|
||||
testInvalidPeerParam(json::Value(json::ValueType::Null));
|
||||
testInvalidPeerParam(json::Value(json::ValueType::Object));
|
||||
testInvalidPeerParam(json::Value(json::ValueType::Array));
|
||||
}
|
||||
{
|
||||
// alice is funded but has no lines. An empty array is returned.
|
||||
json::Value params;
|
||||
|
||||
@@ -176,6 +176,45 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testGWBInvalidAccount(FeatureBitset features)
|
||||
{
|
||||
testcase("Gateway Balances with non-string account/ident");
|
||||
using namespace std::chrono_literals;
|
||||
using namespace jtx;
|
||||
Env env(*this, features);
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
// A non-string "account" must be rejected cleanly with invalidParams
|
||||
// rather than throwing a Json::LogicError that surfaces as internal.
|
||||
json::Value qry;
|
||||
qry[jss::account] = 42;
|
||||
qry[jss::hotwallet] = alice.human();
|
||||
|
||||
forAllApiVersions([&, this](unsigned apiVersion) {
|
||||
qry[jss::api_version] = apiVersion;
|
||||
auto jv = wsc->invoke("gateway_balances", qry);
|
||||
expect(jv[jss::status] == "error");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams");
|
||||
});
|
||||
|
||||
// The same applies to a non-string "ident".
|
||||
json::Value qry2;
|
||||
qry2[jss::ident] = 42;
|
||||
|
||||
forAllApiVersions([&, this](unsigned apiVersion) {
|
||||
qry2[jss::api_version] = apiVersion;
|
||||
auto jv = wsc->invoke("gateway_balances", qry2);
|
||||
expect(jv[jss::status] == "error");
|
||||
BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams");
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testGWBOverflow()
|
||||
{
|
||||
@@ -280,6 +319,7 @@ public:
|
||||
{
|
||||
testGWB(feature);
|
||||
testGWBApiVersions(feature);
|
||||
testGWBInvalidAccount(feature);
|
||||
}
|
||||
testGWBWithMPT();
|
||||
testGWBOverflow();
|
||||
|
||||
Reference in New Issue
Block a user