mirror of
https://github.com/XRPLF/clio.git
synced 2026-07-23 15:10:23 +00:00
fix: Serialize MPToken UInt64 amounts as base-10 strings (#3139)
This commit is contained in:
committed by
GitHub
parent
c073c4dfd3
commit
30f8a227b1
@@ -17,6 +17,7 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STInteger.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -236,11 +237,23 @@ tag_invoke(
|
||||
}
|
||||
};
|
||||
|
||||
// UInt64 amount fields must be serialized as base-10 strings (matching rippled's
|
||||
// STUInt64::getJson) so that JSON parsers using IEEE-754 doubles do not silently lose
|
||||
// precision for values greater than 2^53.
|
||||
auto const setUint64IfPresent =
|
||||
[&](boost::json::string_view field, xrpl::SField const& sField, auto const& value) {
|
||||
if (value.has_value()) {
|
||||
obj[field] = toBoostJson(
|
||||
xrpl::STUInt64{sField, *value}.getJson(xrpl::JsonOptions::Values::None)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setIfPresent("transfer_fee", issuance.transferFee);
|
||||
setIfPresent("asset_scale", issuance.assetScale);
|
||||
setIfPresent("maximum_amount", issuance.maximumAmount);
|
||||
setIfPresent("outstanding_amount", issuance.outstandingAmount);
|
||||
setIfPresent("locked_amount", issuance.lockedAmount);
|
||||
setUint64IfPresent("maximum_amount", xrpl::sfMaximumAmount, issuance.maximumAmount);
|
||||
setUint64IfPresent("outstanding_amount", xrpl::sfOutstandingAmount, issuance.outstandingAmount);
|
||||
setUint64IfPresent("locked_amount", xrpl::sfLockedAmount, issuance.lockedAmount);
|
||||
setIfPresent("mptoken_metadata", issuance.mptokenMetadata);
|
||||
setIfPresent("domain_id", issuance.domainID);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/LedgerHeader.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STInteger.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -176,20 +177,29 @@ tag_invoke(
|
||||
AccountMPTokensHandler::MPTokenResponse const& mptoken
|
||||
)
|
||||
{
|
||||
// UInt64 amount fields must be serialized as base-10 strings (matching rippled's
|
||||
// STUInt64::getJson) so that JSON parsers using IEEE-754 doubles do not silently lose
|
||||
// precision for values greater than 2^53.
|
||||
auto const uint64ToString = [](xrpl::SField const& field, std::uint64_t value) {
|
||||
return toBoostJson(xrpl::STUInt64{field, value}.getJson(xrpl::JsonOptions::Values::None));
|
||||
};
|
||||
|
||||
auto obj = boost::json::object{
|
||||
{"mpt_id", mptoken.mpTokenId},
|
||||
{JS(account), mptoken.account},
|
||||
{JS(mpt_issuance_id), mptoken.mpTokenIssuanceId},
|
||||
{JS(mpt_amount), mptoken.mptAmount},
|
||||
{JS(mpt_amount), uint64ToString(xrpl::sfMPTAmount, mptoken.mptAmount)},
|
||||
};
|
||||
|
||||
if (mptoken.lockedAmount.has_value())
|
||||
obj["locked_amount"] = uint64ToString(xrpl::sfLockedAmount, *mptoken.lockedAmount);
|
||||
|
||||
auto const setIfPresent = [&](boost::json::string_view field, auto const& value) {
|
||||
if (value.has_value()) {
|
||||
obj[field] = *value;
|
||||
}
|
||||
};
|
||||
|
||||
setIfPresent("locked_amount", mptoken.lockedAmount);
|
||||
setIfPresent("mpt_locked", mptoken.mptLocked);
|
||||
setIfPresent("mpt_authorized", mptoken.mptAuthorized);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -60,8 +61,8 @@ auto const kIssuanceOuT1 = fmt::format(
|
||||
"mpt_issuance_id": "{}",
|
||||
"issuer": "{}",
|
||||
"sequence": 1,
|
||||
"maximum_amount": {},
|
||||
"outstanding_amount": {},
|
||||
"maximum_amount": "{}",
|
||||
"outstanding_amount": "{}",
|
||||
"asset_scale": {},
|
||||
"mpt_can_escrow": true,
|
||||
"mpt_can_trade": true,
|
||||
@@ -80,9 +81,9 @@ auto const kIssuanceOuT2 = fmt::format(
|
||||
"mpt_issuance_id": "{}",
|
||||
"issuer": "{}",
|
||||
"sequence": 2,
|
||||
"maximum_amount": {},
|
||||
"outstanding_amount": {},
|
||||
"locked_amount": {},
|
||||
"maximum_amount": "{}",
|
||||
"outstanding_amount": "{}",
|
||||
"locked_amount": "{}",
|
||||
"transfer_fee": {},
|
||||
"mptoken_metadata": "{}",
|
||||
"domain_id": "{}",
|
||||
@@ -370,7 +371,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
|
||||
// return non-empty account
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
@@ -378,7 +379,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kIssuanceIndeX1}, xrpl::uint256{kIssuanceIndeX2}}, kIssuanceIndeX1
|
||||
);
|
||||
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillByDefault(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
// mocking mptoken issuance ledger objects
|
||||
@@ -452,7 +453,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, UseLimit)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
@@ -468,7 +469,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, UseLimit)
|
||||
|
||||
xrpl::STObject ownerDir = createOwnerDirLedgerObject(indexes, kIssuanceIndeX1);
|
||||
ownerDir.setFieldU64(xrpl::sfIndexNext, 99);
|
||||
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillByDefault(Return(ownerDir.getSerializer().peekData()));
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(7);
|
||||
|
||||
@@ -662,14 +663,14 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitLessThanMin)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kIssuanceIndeX1}, xrpl::uint256{kIssuanceIndeX2}}, kIssuanceIndeX1
|
||||
);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{
|
||||
@@ -750,14 +751,14 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitMoreThanMax)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kIssuanceIndeX1}, xrpl::uint256{kIssuanceIndeX2}}, kIssuanceIndeX1
|
||||
);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{
|
||||
@@ -838,12 +839,12 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, EmptyResult)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject({}, kIssuanceIndeX1);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
runSpawn([this](auto yield) {
|
||||
@@ -862,6 +863,97 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, EmptyResult)
|
||||
});
|
||||
}
|
||||
|
||||
// Regression test: UInt64 amount fields must be serialized as base-10 JSON strings (as xrpld
|
||||
// does) so that values greater than 2^53 are not silently rounded by JSON parsers backed by
|
||||
// IEEE-754 doubles. 2^53 itself is still exactly representable as a double, but it must be emitted
|
||||
// as a string like every other amount so the wire format stays consistent regardless of magnitude.
|
||||
struct AccountMPTokenIssuancesAmountSerializationTestCaseBundle {
|
||||
std::string testName;
|
||||
uint64_t maxAmount;
|
||||
uint64_t outstandingAmount;
|
||||
uint64_t lockedAmount;
|
||||
};
|
||||
|
||||
struct AccountMPTokenIssuancesAmountSerializationTest
|
||||
: RPCAccountMPTokenIssuancesHandlerTest,
|
||||
WithParamInterface<AccountMPTokenIssuancesAmountSerializationTestCaseBundle> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
RPCAccountMPTokenIssuancesAmountSerializationGroup,
|
||||
AccountMPTokenIssuancesAmountSerializationTest,
|
||||
ValuesIn(
|
||||
std::vector<AccountMPTokenIssuancesAmountSerializationTestCaseBundle>{
|
||||
{.testName = "LargeAmounts",
|
||||
.maxAmount = static_cast<uint64_t>(std::pow(2, 63)) - 1, // max MPT amount
|
||||
.outstandingAmount = static_cast<uint64_t>(std::pow(2, 53)) + 1,
|
||||
.lockedAmount = static_cast<uint64_t>(std::pow(2, 53)) +
|
||||
12345}, // odd value above 2^53
|
||||
{.testName = "ExactDoubleBoundary",
|
||||
.maxAmount = static_cast<uint64_t>(std::pow(2, 53)),
|
||||
.outstandingAmount = static_cast<uint64_t>(std::pow(2, 53)),
|
||||
.lockedAmount = static_cast<uint64_t>(std::pow(2, 53))}
|
||||
}
|
||||
),
|
||||
tests::util::kNameGenerator
|
||||
);
|
||||
|
||||
TEST_P(AccountMPTokenIssuancesAmountSerializationTest, SerializedAsStrings)
|
||||
{
|
||||
auto const testBundle = GetParam();
|
||||
|
||||
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir =
|
||||
createOwnerDirLedgerObject({xrpl::uint256{kIssuanceIndeX1}}, kIssuanceIndeX1);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
xrpl::STObject const mptIssuance = createMptIssuanceObject(
|
||||
kAccount,
|
||||
1,
|
||||
std::nullopt,
|
||||
xrpl::lsfMPTCanLock,
|
||||
testBundle.outstandingAmount,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
testBundle.maxAmount,
|
||||
testBundle.lockedAmount
|
||||
);
|
||||
auto const bbs = std::vector<Blob>{mptIssuance.getSerializer().peekData()};
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const input =
|
||||
boost::json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kAccount));
|
||||
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
|
||||
auto const& issuances = output.result->as_object().at("mpt_issuances").as_array();
|
||||
ASSERT_EQ(issuances.size(), 1);
|
||||
auto const& issuance = issuances[0].as_object();
|
||||
|
||||
ASSERT_TRUE(issuance.at("maximum_amount").is_string());
|
||||
EXPECT_EQ(issuance.at("maximum_amount").as_string(), std::to_string(testBundle.maxAmount));
|
||||
ASSERT_TRUE(issuance.at("outstanding_amount").is_string());
|
||||
EXPECT_EQ(
|
||||
issuance.at("outstanding_amount").as_string(),
|
||||
std::to_string(testBundle.outstandingAmount)
|
||||
);
|
||||
ASSERT_TRUE(issuance.at("locked_amount").is_string());
|
||||
EXPECT_EQ(
|
||||
issuance.at("locked_amount").as_string(), std::to_string(testBundle.lockedAmount)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MutableFlags)
|
||||
{
|
||||
uint32_t const mutableFlags1 = xrpl::lsmfMPTCanEnableCanLock |
|
||||
@@ -877,14 +969,14 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MutableFlags)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kIssuanceIndeX1}, xrpl::uint256{kIssuanceIndeX2}}, kIssuanceIndeX1
|
||||
);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{
|
||||
@@ -945,7 +1037,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MutableFlags)
|
||||
"mpt_issuance_id": "{}",
|
||||
"issuer": "{}",
|
||||
"sequence": 3,
|
||||
"outstanding_amount": {},
|
||||
"outstanding_amount": "{}",
|
||||
"transfer_fee": {},
|
||||
"mpt_can_transfer": true,
|
||||
"mpt_can_mutate_can_lock": true,
|
||||
@@ -957,7 +1049,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MutableFlags)
|
||||
"mpt_issuance_id": "{}",
|
||||
"issuer": "{}",
|
||||
"sequence": 5,
|
||||
"outstanding_amount": {},
|
||||
"outstanding_amount": "{}",
|
||||
"transfer_fee": {},
|
||||
"mptoken_metadata": "{}",
|
||||
"mpt_can_transfer": true,
|
||||
@@ -1035,13 +1127,13 @@ TEST_P(AccountMPTokenIssuancesImmutableFlagsTest, SingleFlag)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir =
|
||||
createOwnerDirLedgerObject({xrpl::uint256{kIssuanceIndeX1}}, kIssuanceIndeX1);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs =
|
||||
@@ -1130,13 +1222,13 @@ TEST_P(AccountMPTokenIssuancesMutableFlagsTest, SingleMutableFlag)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir =
|
||||
createOwnerDirLedgerObject({xrpl::uint256{kIssuanceIndeX1}}, kIssuanceIndeX1);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{createMptIssuanceObject(
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -47,8 +48,8 @@ auto const kTokenOuT1 = fmt::format(
|
||||
"mpt_id": "{}",
|
||||
"account": "{}",
|
||||
"mpt_issuance_id": "{}",
|
||||
"mpt_amount": {},
|
||||
"locked_amount": {},
|
||||
"mpt_amount": "{}",
|
||||
"locked_amount": "{}",
|
||||
"mpt_locked": true
|
||||
}})JSON",
|
||||
kTokenIndeX1,
|
||||
@@ -63,7 +64,7 @@ auto const kTokenOuT2 = fmt::format(
|
||||
"mpt_id": "{}",
|
||||
"account": "{}",
|
||||
"mpt_issuance_id": "{}",
|
||||
"mpt_amount": {},
|
||||
"mpt_amount": "{}",
|
||||
"mpt_authorized": true
|
||||
}})JSON",
|
||||
kTokenIndeX2,
|
||||
@@ -337,14 +338,14 @@ TEST_F(RPCAccountMPTokensHandlerTest, DefaultParameters)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kTokenIndeX1}, xrpl::uint256{kTokenIndeX2}}, kTokenIndeX1
|
||||
);
|
||||
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillByDefault(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{
|
||||
@@ -407,7 +408,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
@@ -427,7 +428,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
|
||||
|
||||
xrpl::STObject ownerDir = createOwnerDirLedgerObject(indexes, kTokenIndeX1);
|
||||
ownerDir.setFieldU64(xrpl::sfIndexNext, 99);
|
||||
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
ON_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillByDefault(Return(ownerDir.getSerializer().peekData()));
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(7);
|
||||
|
||||
@@ -630,14 +631,14 @@ TEST_F(RPCAccountMPTokensHandlerTest, LimitLessThanMin)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kTokenIndeX1}, xrpl::uint256{kTokenIndeX2}}, kTokenIndeX1
|
||||
);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{
|
||||
@@ -709,14 +710,14 @@ TEST_F(RPCAccountMPTokensHandlerTest, LimitMoreThanMax)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject(
|
||||
{xrpl::uint256{kTokenIndeX1}, xrpl::uint256{kTokenIndeX2}}, kTokenIndeX1
|
||||
);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
auto const bbs = std::vector<Blob>{
|
||||
@@ -788,12 +789,12 @@ TEST_F(RPCAccountMPTokensHandlerTest, EmptyResult)
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const owneDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir = createOwnerDirLedgerObject({}, kTokenIndeX1);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _))
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
runSpawn([this](auto yield) {
|
||||
@@ -811,3 +812,79 @@ TEST_F(RPCAccountMPTokensHandlerTest, EmptyResult)
|
||||
EXPECT_EQ(output.result->as_object().at("mptokens").as_array().size(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Regression test: UInt64 amount fields must be serialized as base-10 JSON strings (as xrpld
|
||||
// does) so that values greater than 2^53 are not silently rounded by JSON parsers backed by
|
||||
// IEEE-754 doubles. 2^53 itself is still exactly representable as a double, but it must be emitted
|
||||
// as a string like every other amount so the wire format stays consistent regardless of magnitude.
|
||||
struct AccountMPTokensAmountSerializationTestCaseBundle {
|
||||
std::string testName;
|
||||
uint64_t mptAmount;
|
||||
uint64_t lockedAmount;
|
||||
};
|
||||
|
||||
struct AccountMPTokensAmountSerializationTest
|
||||
: RPCAccountMPTokensHandlerTest,
|
||||
WithParamInterface<AccountMPTokensAmountSerializationTestCaseBundle> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
RPCAccountMPTokensAmountSerializationGroup,
|
||||
AccountMPTokensAmountSerializationTest,
|
||||
ValuesIn(
|
||||
std::vector<AccountMPTokensAmountSerializationTestCaseBundle>{
|
||||
{.testName = "LargeAmounts",
|
||||
.mptAmount = static_cast<uint64_t>(std::pow(2, 63)) - 1, // max MPT amount
|
||||
.lockedAmount = static_cast<uint64_t>(std::pow(2, 53)) + 1},
|
||||
{.testName = "ExactDoubleBoundary",
|
||||
.mptAmount = static_cast<uint64_t>(std::pow(2, 53)),
|
||||
.lockedAmount = static_cast<uint64_t>(std::pow(2, 53))}
|
||||
}
|
||||
),
|
||||
tests::util::kNameGenerator
|
||||
);
|
||||
|
||||
TEST_P(AccountMPTokensAmountSerializationTest, SerializedAsStrings)
|
||||
{
|
||||
auto const testBundle = GetParam();
|
||||
|
||||
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30);
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
|
||||
|
||||
auto const account = getAccountIdWithString(kAccount);
|
||||
auto const accountKk = xrpl::keylet::account(account).key;
|
||||
auto const ownerDirKk = xrpl::keylet::ownerDir(account).key;
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _))
|
||||
.WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
|
||||
|
||||
xrpl::STObject const ownerDir =
|
||||
createOwnerDirLedgerObject({xrpl::uint256{kTokenIndeX1}}, kTokenIndeX1);
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObject(ownerDirKk, _, _))
|
||||
.WillOnce(Return(ownerDir.getSerializer().peekData()));
|
||||
|
||||
xrpl::STObject const mptoken = createMpTokenObject(
|
||||
kAccount,
|
||||
xrpl::uint192(kIssuanceIdHex),
|
||||
testBundle.mptAmount,
|
||||
xrpl::lsfMPTLocked,
|
||||
testBundle.lockedAmount
|
||||
);
|
||||
auto const bbs = std::vector<Blob>{mptoken.getSerializer().peekData()};
|
||||
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
auto const input =
|
||||
boost::json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kAccount));
|
||||
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
|
||||
auto const output = handler.process(input, Context{yield});
|
||||
ASSERT_TRUE(output);
|
||||
|
||||
auto const& mptokens = output.result->as_object().at("mptokens").as_array();
|
||||
ASSERT_EQ(mptokens.size(), 1);
|
||||
auto const& token = mptokens[0].as_object();
|
||||
|
||||
ASSERT_TRUE(token.at("mpt_amount").is_string());
|
||||
EXPECT_EQ(token.at("mpt_amount").as_string(), std::to_string(testBundle.mptAmount));
|
||||
ASSERT_TRUE(token.at("locked_amount").is_string());
|
||||
EXPECT_EQ(token.at("locked_amount").as_string(), std::to_string(testBundle.lockedAmount));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user