fix: Serialize MPToken UInt64 amounts as base-10 strings (#3139)

This commit is contained in:
Chenna Keshava B S
2026-07-22 05:31:21 -07:00
committed by GitHub
parent c073c4dfd3
commit 30f8a227b1
4 changed files with 233 additions and 41 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(

View File

@@ -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));
});
}