Files
clio/tests/unit/rpc/handlers/BookOffersTests.cpp
2026-05-13 12:09:06 +01:00

1748 lines
69 KiB
C++

#include "data/AmendmentCenter.hpp"
#include "data/Types.hpp"
#include "rpc/Errors.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/AnyHandler.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/handlers/BookOffers.hpp"
#include "util/HandlerBaseTestFixture.hpp"
#include "util/MockAmendmentCenter.hpp"
#include "util/NameGenerator.hpp"
#include "util/TestObject.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/parse.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/UintTypes.h>
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace {
constexpr auto kAccount = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr auto kAccount2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr auto kIndex1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr auto kIndex2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
constexpr auto kPayS20UsdGetS10XrpBookDir =
"43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000";
constexpr auto kPayS20XrpGetS10UsdBookDir =
"7B1767D41DBCE79D9585CF9D0262A5FEC45E5206FF524F8B55071AFD498D0000";
constexpr auto kTransferRateX2 = 2000000000;
constexpr auto kDomain = "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134E5";
struct ParameterTestBundle {
std::string testName;
std::string testJson;
std::string expectedError;
std::string expectedErrorMessage;
};
} // namespace
using namespace rpc;
using namespace data;
namespace json = boost::json;
using namespace testing;
struct RPCBookOffersHandlerTest : HandlerBaseTest {
RPCBookOffersHandlerTest()
{
backend_->setRange(10, 300);
}
protected:
StrictMockAmendmentCenterSharedPtr mockAmendmentCenterPtr_;
};
struct RPCBookOffersParameterTest : RPCBookOffersHandlerTest,
WithParamInterface<ParameterTestBundle> {};
TEST_P(RPCBookOffersParameterTest, CheckError)
{
auto bundle = GetParam();
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(json::parse(bundle.testJson), Context{.yield = yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), bundle.expectedError);
EXPECT_EQ(err.at("error_message").as_string(), bundle.expectedErrorMessage);
});
}
static auto
generateParameterBookOffersTestBundles()
{
return std::vector<ParameterTestBundle>{
ParameterTestBundle{
.testName = "MissingTakerGets",
.testJson = R"JSON({
"taker_pays": {
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'taker_gets' missing"
},
ParameterTestBundle{
.testName = "MissingTakerPays",
.testJson = R"JSON({
"taker_gets": {
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'taker_pays' missing"
},
ParameterTestBundle{
.testName = "WrongTypeTakerPays",
.testJson = R"JSON({
"taker_pays": "wrong",
"taker_gets": {
"currency": "XRP"
}
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "WrongTypeTakerGets",
.testJson = R"JSON({
"taker_gets": "wrong",
"taker_pays": {
"currency": "XRP"
}
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "TakerPaysMissingCurrency",
.testJson = R"JSON({
"taker_pays": {},
"taker_gets": {
"currency": "XRP"
}
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'currency' missing"
},
ParameterTestBundle{
.testName = "TakerGetsMissingCurrency",
.testJson = R"JSON({
"taker_gets": {},
"taker_pays": {
"currency": "XRP"
}
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'currency' missing"
},
ParameterTestBundle{
.testName = "TakerGetsWrongCurrency",
.testJson = R"JSON({
"taker_gets": {
"currency": "CNYY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_pays": {
"currency": "XRP"
}
})JSON",
.expectedError = "dstAmtMalformed",
.expectedErrorMessage = "Destination amount/currency/issuer is malformed."
},
ParameterTestBundle{
.testName = "TakerPaysWrongCurrency",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNYY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
}
})JSON",
.expectedError = "srcCurMalformed",
.expectedErrorMessage = "Source currency is malformed."
},
ParameterTestBundle{
.testName = "TakerGetsCurrencyNotString",
.testJson = R"JSON({
"taker_gets": {
"currency": 123,
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_pays": {
"currency": "XRP"
}
})JSON",
.expectedError = "dstAmtMalformed",
.expectedErrorMessage = "Destination amount/currency/issuer is malformed."
},
ParameterTestBundle{
.testName = "TakerPaysCurrencyNotString",
.testJson = R"JSON({
"taker_pays": {
"currency": 123,
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
}
})JSON",
.expectedError = "srcCurMalformed",
.expectedErrorMessage = "Source currency is malformed."
},
ParameterTestBundle{
.testName = "TakerGetsWrongIssuer",
.testJson = R"JSON({
"taker_gets": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs5"
},
"taker_pays": {
"currency": "XRP"
}
})JSON",
.expectedError = "dstIsrMalformed",
.expectedErrorMessage = "Destination issuer is malformed."
},
ParameterTestBundle{
.testName = "TakerPaysWrongIssuer",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs5"
},
"taker_gets": {
"currency": "XRP"
}
})JSON",
.expectedError = "srcIsrMalformed",
.expectedErrorMessage = "Source issuer is malformed."
},
ParameterTestBundle{
.testName = "InvalidTaker",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"taker": "123"
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid field 'taker'."
},
ParameterTestBundle{
.testName = "TakerNotString",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"taker": 123
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid field 'taker'."
},
ParameterTestBundle{
.testName = "Domain_InvalidType",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"domain": 0
})JSON",
.expectedError = "domainMalformed",
.expectedErrorMessage = "Unable to parse domain."
},
ParameterTestBundle{
.testName = "Domain_InvalidInt",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"domain": "123"
})JSON",
.expectedError = "domainMalformed",
.expectedErrorMessage = "Unable to parse domain."
},
ParameterTestBundle{
.testName = "Domain_InvalidObject",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"domain": {}
})JSON",
.expectedError = "domainMalformed",
.expectedErrorMessage = "Unable to parse domain."
},
ParameterTestBundle{
.testName = "LimitNotInt",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"limit": "123"
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "LimitNegative",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"limit": -1
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "LimitZero",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"limit": 0
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "LedgerIndexInvalid",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"ledger_index": "xxx"
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "ledgerIndexMalformed"
},
ParameterTestBundle{
.testName = "LedgerHashInvalid",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"ledger_hash": "xxx"
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "ledger_hashMalformed"
},
ParameterTestBundle{
.testName = "LedgerHashNotString",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP"
},
"ledger_hash": 123
})JSON",
.expectedError = "invalidParams",
.expectedErrorMessage = "ledger_hashNotString"
},
ParameterTestBundle{
.testName = "GetsPaysXRPWithIssuer",
.testJson = R"JSON({
"taker_pays": {
"currency": "XRP",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "CNY",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
}
})JSON",
.expectedError = "srcIsrMalformed",
.expectedErrorMessage =
"Unneeded field 'taker_pays.issuer' for XRP currency specification."
},
ParameterTestBundle{
.testName = "PaysCurrencyWithXRPIssuer",
.testJson = R"JSON({
"taker_pays": {
"currency": "JPY"
},
"taker_gets": {
"currency": "CNY",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
}
})JSON",
.expectedError = "srcIsrMalformed",
.expectedErrorMessage = "Invalid field 'taker_pays.issuer', expected non-XRP issuer."
},
ParameterTestBundle{
.testName = "GetsCurrencyWithXRPIssuer",
.testJson = R"JSON({
"taker_pays": {
"currency": "XRP"
},
"taker_gets": {
"currency": "CNY"
}
})JSON",
.expectedError = "dstIsrMalformed",
.expectedErrorMessage = "Invalid field 'taker_gets.issuer', expected non-XRP issuer."
},
ParameterTestBundle{
.testName = "GetsXRPWithIssuer",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "XRP",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})JSON",
.expectedError = "dstIsrMalformed",
.expectedErrorMessage =
"Unneeded field 'taker_gets.issuer' for XRP currency specification."
},
ParameterTestBundle{
.testName = "BadMarket",
.testJson = R"JSON({
"taker_pays": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets": {
"currency": "CNY",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})JSON",
.expectedError = "badMarket",
.expectedErrorMessage = "badMarket"
}
};
}
INSTANTIATE_TEST_SUITE_P(
RPCBookOffersHandler,
RPCBookOffersParameterTest,
testing::ValuesIn(generateParameterBookOffersTestBundles()),
tests::util::kNameGenerator
);
struct BookOffersNormalTestBundle {
std::string testName;
std::string inputJson;
std::map<ripple::uint256, std::optional<ripple::uint256>> mockedSuccessors;
std::map<ripple::uint256, Blob> mockedLedgerObjects;
uint32_t ledgerObjectCalls;
std::vector<ripple::STObject> mockedOffers;
std::string expectedJson;
uint32_t amendmentIsEnabledCalls = 0;
};
struct RPCBookOffersNormalPathTest : public RPCBookOffersHandlerTest,
public WithParamInterface<BookOffersNormalTestBundle> {};
TEST_P(RPCBookOffersNormalPathTest, CheckOutput)
{
auto const& bundle = GetParam();
auto const seq = 300;
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1);
// return valid ledgerHeader
auto const ledgerHeader = createLedgerHeader(kLedgerHash, seq);
ON_CALL(*backend_, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::fixFrozenLPTokenTransfer, _))
.Times(bundle.amendmentIsEnabledCalls);
ON_CALL(*mockAmendmentCenterPtr_, isEnabled(_, Amendments::fixFrozenLPTokenTransfer, _))
.WillByDefault(Return(false));
// return valid book dir
EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(bundle.mockedSuccessors.size());
for (auto const& [key, value] : bundle.mockedSuccessors) {
ON_CALL(*backend_, doFetchSuccessorKey(key, seq, _)).WillByDefault(Return(value));
}
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(bundle.ledgerObjectCalls);
for (auto const& [key, value] : bundle.mockedLedgerObjects) {
ON_CALL(*backend_, doFetchLedgerObject(key, seq, _)).WillByDefault(Return(value));
}
std::vector<Blob> bbs;
std::ranges::transform(
bundle.mockedOffers,
std::back_inserter(bbs),
[](auto const& obj) { return obj.getSerializer().peekData(); }
);
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(json::parse(bundle.inputJson), Context{.yield = yield});
ASSERT_TRUE(output);
EXPECT_EQ(output.result.value(), json::parse(bundle.expectedJson));
});
}
static auto
generateNormalPathBookOffersTestBundles()
{
auto const account = getAccountIdWithString(kAccount);
auto const account2 = getAccountIdWithString(kAccount2);
auto const frozenTrustLine = createRippleStateLedgerObject(
"USD", kAccount, -8, kAccount2, 1000, kAccount, 2000, kIndex1, 2, ripple::lsfLowFreeze
);
auto const gets10USDPays20XRPOffer = createOfferLedgerObject(
kAccount2,
10,
20,
ripple::to_string(ripple::to_currency("USD")),
ripple::to_string(ripple::xrpCurrency()),
kAccount,
toBase58(ripple::xrpAccount()),
kPayS20XrpGetS10UsdBookDir
);
auto const gets10USDPays20XRPOwnerOffer = createOfferLedgerObject(
kAccount,
10,
20,
ripple::to_string(ripple::to_currency("USD")),
ripple::to_string(ripple::xrpCurrency()),
kAccount,
toBase58(ripple::xrpAccount()),
kPayS20XrpGetS10UsdBookDir
);
auto const gets10XRPPays20USDOffer = createOfferLedgerObject(
kAccount2,
10,
20,
ripple::to_string(ripple::xrpCurrency()),
ripple::to_string(ripple::to_currency("USD")),
toBase58(ripple::xrpAccount()),
kAccount,
kPayS20UsdGetS10XrpBookDir
);
auto const gets10XRPPays20USDOfferWithDomain = createOfferLedgerObject(
kAccount2,
10,
20,
ripple::to_string(ripple::xrpCurrency()),
ripple::to_string(ripple::to_currency("USD")),
toBase58(ripple::xrpAccount()),
kAccount,
kPayS20UsdGetS10XrpBookDir,
kDomain
);
auto const getsXRPPaysUSDBook = getBookBase(
rpc::parseBook(
ripple::to_currency("USD"),
account,
ripple::xrpCurrency(),
ripple::xrpAccount(),
std::nullopt
)
.value()
);
auto const getsXRPPaysUSDBookWithDomain = getBookBase(
rpc::parseBook(
ripple::to_currency("USD"),
account,
ripple::xrpCurrency(),
ripple::xrpAccount(),
kDomain
)
.value()
);
auto const getsUSDPaysXRPBook = getBookBase(
rpc::parseBook(
ripple::xrpCurrency(),
ripple::xrpAccount(),
ripple::to_currency("USD"),
account,
std::nullopt
)
.value()
);
auto const getsXRPPaysUSDInputJson = fmt::format(
R"JSON({{
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}}
}})JSON",
kAccount
);
auto const getsXRPPaysUSDInputJsonWithDomain = fmt::format(
R"JSON({{
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}},
"domain": "{}"
}})JSON",
kAccount,
kDomain
);
auto const paysXRPGetsUSDInputJson = fmt::format(
R"JSON({{
"taker_pays": {{
"currency": "XRP"
}},
"taker_gets": {{
"currency": "USD",
"issuer": "{}"
}}
}})JSON",
kAccount
);
auto const feeLedgerObject = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
auto const trustline30Balance = createRippleStateLedgerObject(
"USD", kAccount, -30, kAccount2, 1000, kAccount, 2000, kIndex1, 2, 0
);
auto const trustline8Balance = createRippleStateLedgerObject(
"USD", kAccount, -8, kAccount2, 1000, kAccount, 2000, kIndex1, 2, 0
);
return std::vector<BookOffersNormalTestBundle>{
BookOffersNormalTestBundle{
.testName = "PaysUSDGetsXRPNoFrozenOwnerFundEnough",
.inputJson = getsXRPPaysUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsXRPPaysUSDBook, ripple::uint256{kPayS20UsdGetS10XrpBookDir}},
{ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20UsdGetS10XrpBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// pays issuer account object
{ripple::keylet::account(account).key,
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2)
.getSerializer()
.peekData()},
// owner account object
{ripple::keylet::account(account2).key,
createAccountRootObject(kAccount2, 0, 2, 200, 2, kIndex1, 2)
.getSerializer()
.peekData()},
// fee settings: base ->3 inc->2, account2 has 2 objects ,total
// reserve ->7
// owner_funds should be 193
{ripple::keylet::fees().key, feeLedgerObject}
},
.ledgerObjectCalls = 5,
.mockedOffers = std::vector<ripple::STObject>{gets10XRPPays20USDOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerGets": "10",
"TakerPays": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "20"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}"
}}
]
}})JSON",
kLedgerHash,
kAccount2,
193,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysUSDGetsXRPNoFrozenOwnerFundNotEnough",
.inputJson = getsXRPPaysUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsXRPPaysUSDBook, ripple::uint256{kPayS20UsdGetS10XrpBookDir}},
{ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20UsdGetS10XrpBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// pays issuer account object
{ripple::keylet::account(account).key,
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2)
.getSerializer()
.peekData()},
// owner account object, hold
{ripple::keylet::account(account2).key,
createAccountRootObject(kAccount2, 0, 2, 5 + 7, 2, kIndex1, 2)
.getSerializer()
.peekData()},
// fee settings: base ->3 inc->2, account2 has 2 objects
// ,total
// reserve ->7
{ripple::keylet::fees().key, feeLedgerObject}
},
.ledgerObjectCalls = 5,
.mockedOffers = std::vector<ripple::STObject>{gets10XRPPays20USDOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerGets": "10",
"TakerPays": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "20"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": "5",
"taker_pays_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}}
}}
]
}})JSON",
kLedgerHash,
kAccount2,
5,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysUSDGetsXRPFrozen",
.inputJson = getsXRPPaysUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsXRPPaysUSDBook, ripple::uint256{kPayS20UsdGetS10XrpBookDir}},
{ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20UsdGetS10XrpBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// pays issuer account object
{ripple::keylet::account(account).key,
createAccountRootObject(
kAccount, ripple::lsfGlobalFreeze, 2, 200, 2, kIndex1, 2
)
.getSerializer()
.peekData()}
},
.ledgerObjectCalls = 3,
.mockedOffers = std::vector<ripple::STObject>{gets10XRPPays20USDOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerGets": "10",
"TakerPays": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "20"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": "0",
"taker_pays_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "0"
}}
}}
]
}})JSON",
kLedgerHash,
kAccount2,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysUSDGetsXRPFrozenWithDomain",
.inputJson = getsXRPPaysUSDInputJsonWithDomain,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsXRPPaysUSDBookWithDomain, ripple::uint256{kPayS20UsdGetS10XrpBookDir}},
{ripple::uint256{kPayS20UsdGetS10XrpBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20UsdGetS10XrpBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// pays issuer account object
{ripple::keylet::account(account).key,
createAccountRootObject(
kAccount, ripple::lsfGlobalFreeze, 2, 200, 2, kIndex1, 2
)
.getSerializer()
.peekData()}
},
.ledgerObjectCalls = 3,
.mockedOffers = std::vector<ripple::STObject>{gets10XRPPays20USDOfferWithDomain},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000",
"BookNode": "0",
"DomainID": "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134E5",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerGets": "10",
"TakerPays": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "20"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": "0",
"taker_pays_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "0"
}}
}}
]
}})JSON",
kLedgerHash,
kAccount2,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "GetsUSDPaysXRPFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// gets issuer account object
{ripple::keylet::account(account).key,
createAccountRootObject(
kAccount, ripple::lsfGlobalFreeze, 2, 200, 2, kIndex1, 2, kTransferRateX2
)
.getSerializer()
.peekData()}
},
.ledgerObjectCalls = 3,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_pays_funded": "0",
"taker_gets_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "0"
}}
}}
]
}})JSON",
kLedgerHash,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDWithTransferFee",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// gets issuer account object, rate is 1/2
{ripple::keylet::account(account).key,
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2)
.getSerializer()
.peekData()},
// trust line between gets issuer and owner,owner has 8 USD
{ripple::keylet::line(account2, account, ripple::to_currency("USD")).key,
trustline8Balance.getSerializer().peekData()},
},
.ledgerObjectCalls = 6,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "4"
}},
"taker_pays_funded": "8"
}}
]
}})JSON",
kLedgerHash,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
8,
2
),
.amendmentIsEnabledCalls = 1,
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDWithMultipleOffers",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject(
{ripple::uint256{kIndex2}, ripple::uint256{kIndex2}}, kIndex1
)
.getSerializer()
.peekData()},
// gets issuer account object
{ripple::keylet::account(account).key,
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2)
.getSerializer()
.peekData()},
// trust line between gets issuer and owner,owner has 30 USD
{ripple::keylet::line(account2, account, ripple::to_currency("USD")).key,
trustline30Balance.getSerializer().peekData()},
},
.ledgerObjectCalls = 6,
.mockedOffers =
std::vector<ripple::STObject>{
// After offer1, balance is 30 - 2*10 = 10
gets10USDPays20XRPOffer,
// offer2 not fully funded, balance is 10, rate is 2, so only
// gets 5
gets10USDPays20XRPOffer
},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}"
}},
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"taker_gets_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "5"
}},
"taker_pays_funded": "10",
"quality": "{}"
}}
]
}})JSON",
kLedgerHash,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
30,
2,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
2
),
.amendmentIsEnabledCalls = 1,
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDSellingOwnCurrency",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// gets issuer account object, rate is 1/2
{ripple::keylet::account(account).key,
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2)
.getSerializer()
.peekData()},
},
.ledgerObjectCalls = 3,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOwnerOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}"
}}
]
}})JSON",
kLedgerHash,
kAccount,
kPayS20XrpGetS10UsdBookDir,
10,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDTrustLineFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// gets issuer account object, rate is 1/2
{ripple::keylet::account(account).key,
createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2)
.getSerializer()
.peekData()},
// trust line between gets issuer and owner,owner has 8 USD
{ripple::keylet::line(account2, account, ripple::to_currency("USD")).key,
frozenTrustLine.getSerializer().peekData()},
},
.ledgerObjectCalls = 6,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "0"
}},
"taker_pays_funded": "0"
}}
]
}})JSON",
kLedgerHash,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
0,
2
),
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDIsDeepFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// gets issuer account object, is deep frozen so unfunded
{ripple::keylet::account(account).key,
createAccountRootObject(
kAccount, ripple::lsfLowDeepFreeze, 2, 200, 2, kIndex1, 2, kTransferRateX2
)
.getSerializer()
.peekData()},
},
.ledgerObjectCalls = 4,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "0"
}},
"taker_pays_funded": "0"
}}
]
}})JSON",
kLedgerHash,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDTrustLineFrozenAndIsDeepFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPayS20XrpGetS10UsdBookDir}},
{ripple::uint256{kPayS20XrpGetS10UsdBookDir}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPayS20XrpGetS10UsdBookDir},
createOwnerDirLedgerObject({ripple::uint256{kIndex2}}, kIndex1)
.getSerializer()
.peekData()},
// gets issuer account object, is deep frozen so unfunded
{ripple::keylet::account(account).key,
createAccountRootObject(
kAccount, ripple::lsfLowDeepFreeze, 2, 200, 2, kIndex1, 2, kTransferRateX2
)
.getSerializer()
.peekData()},
{ripple::keylet::line(account2, account, ripple::to_currency("USD")).key,
frozenTrustLine.getSerializer().peekData()},
},
.ledgerObjectCalls = 6,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"JSON({{
"ledger_hash": "{}",
"ledger_index": 300,
"offers": [
{{
"Account": "{}",
"BookDirectory": "{}",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000000",
"PreviousTxnLgrSeq": 0,
"Sequence": 0,
"TakerPays": "20",
"TakerGets": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "10"
}},
"index": "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321",
"owner_funds": "{}",
"quality": "{}",
"taker_gets_funded": {{
"currency": "USD",
"issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"value": "0"
}},
"taker_pays_funded": "0"
}}
]
}})JSON",
kLedgerHash,
kAccount2,
kPayS20XrpGetS10UsdBookDir,
0,
2
)
}
};
}
INSTANTIATE_TEST_SUITE_P(
RPCBookOffersHandler,
RPCBookOffersNormalPathTest,
testing::ValuesIn(generateNormalPathBookOffersTestBundles()),
tests::util::kNameGenerator
);
// ledger not exist
TEST_F(RPCBookOffersHandlerTest, LedgerNonExistViaIntSequence)
{
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1);
// return empty ledgerHeader
ON_CALL(*backend_, fetchLedgerBySequence(30, _))
.WillByDefault(Return(std::optional<ripple::LedgerHeader>{}));
static auto const kInput = json::parse(
fmt::format(
R"JSON({{
"ledger_index": 30,
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}}
}})JSON",
kAccount
)
);
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(kInput, Context{.yield = yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCBookOffersHandlerTest, LedgerNonExistViaSequence)
{
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1);
// return empty ledgerHeader
ON_CALL(*backend_, fetchLedgerBySequence(30, _))
.WillByDefault(Return(std::optional<ripple::LedgerHeader>{}));
static auto const kInput = json::parse(
fmt::format(
R"JSON({{
"ledger_index": "30",
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}}
}})JSON",
kAccount
)
);
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(kInput, Context{.yield = yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCBookOffersHandlerTest, LedgerNonExistViaHash)
{
EXPECT_CALL(*backend_, fetchLedgerByHash).Times(1);
// return empty ledgerHeader
ON_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLedgerHash}, _))
.WillByDefault(Return(std::optional<ripple::LedgerHeader>{}));
static auto const kInput = json::parse(
fmt::format(
R"JSON({{
"ledger_hash": "{}",
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}}
}})JSON",
kLedgerHash,
kAccount
)
);
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(kInput, Context{.yield = yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
}
TEST_F(RPCBookOffersHandlerTest, Limit)
{
auto const seq = 300;
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1);
// return valid ledgerHeader
auto const ledgerHeader = createLedgerHeader(kLedgerHash, seq);
ON_CALL(*backend_, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader));
auto const issuer = getAccountIdWithString(kAccount);
// return valid book dir
EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(1);
auto const getsXRPPaysUSDBook = getBookBase(
rpc::parseBook(
ripple::to_currency("USD"),
issuer,
ripple::xrpCurrency(),
ripple::xrpAccount(),
std::nullopt
)
.value()
);
ON_CALL(*backend_, doFetchSuccessorKey(getsXRPPaysUSDBook, seq, _))
.WillByDefault(Return(ripple::uint256{kPayS20UsdGetS10XrpBookDir}));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(5);
auto const indexes = std::vector<ripple::uint256>(10, ripple::uint256{kIndex2});
ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kPayS20UsdGetS10XrpBookDir}, seq, _))
.WillByDefault(
Return(createOwnerDirLedgerObject(indexes, kIndex1).getSerializer().peekData())
);
ON_CALL(
*backend_,
doFetchLedgerObject(ripple::keylet::account(getAccountIdWithString(kAccount2)).key, seq, _)
)
.WillByDefault(Return(
createAccountRootObject(kAccount2, 0, 2, 200, 2, kIndex1, 2).getSerializer().peekData()
));
ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::fees().key, seq, _))
.WillByDefault(Return(createLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::account(issuer).key, seq, _))
.WillByDefault(
Return(createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2)
.getSerializer()
.peekData())
);
auto const gets10XRPPays20USDOffer = createOfferLedgerObject(
kAccount2,
10,
20,
ripple::to_string(ripple::xrpCurrency()),
ripple::to_string(ripple::to_currency("USD")),
toBase58(ripple::xrpAccount()),
kAccount,
kPayS20UsdGetS10XrpBookDir
);
std::vector<Blob> const bbs(10, gets10XRPPays20USDOffer.getSerializer().peekData());
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
static auto const kInput = json::parse(
fmt::format(
R"JSON({{
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}},
"limit": 5
}})JSON",
kAccount
)
);
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(kInput, Context{.yield = yield});
ASSERT_TRUE(output);
EXPECT_EQ(output.result.value().as_object().at("offers").as_array().size(), 5);
});
}
TEST_F(RPCBookOffersHandlerTest, LimitMoreThanMax)
{
auto const seq = 300;
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(1);
// return valid ledgerHeader
auto const ledgerHeader = createLedgerHeader(kLedgerHash, seq);
ON_CALL(*backend_, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader));
auto const issuer = getAccountIdWithString(kAccount);
// return valid book dir
EXPECT_CALL(*backend_, doFetchSuccessorKey).Times(1);
auto const getsXRPPaysUSDBook = getBookBase(
rpc::parseBook(
ripple::to_currency("USD"),
issuer,
ripple::xrpCurrency(),
ripple::xrpAccount(),
std::nullopt
)
.value()
);
ON_CALL(*backend_, doFetchSuccessorKey(getsXRPPaysUSDBook, seq, _))
.WillByDefault(Return(ripple::uint256{kPayS20UsdGetS10XrpBookDir}));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(5);
auto const indexes =
std::vector<ripple::uint256>(BookOffersHandler::kLimitMax + 1, ripple::uint256{kIndex2});
ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kPayS20UsdGetS10XrpBookDir}, seq, _))
.WillByDefault(
Return(createOwnerDirLedgerObject(indexes, kIndex1).getSerializer().peekData())
);
ON_CALL(
*backend_,
doFetchLedgerObject(ripple::keylet::account(getAccountIdWithString(kAccount2)).key, seq, _)
)
.WillByDefault(Return(
createAccountRootObject(kAccount2, 0, 2, 200, 2, kIndex1, 2).getSerializer().peekData()
));
ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::fees().key, seq, _))
.WillByDefault(Return(createLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
ON_CALL(*backend_, doFetchLedgerObject(ripple::keylet::account(issuer).key, seq, _))
.WillByDefault(
Return(createAccountRootObject(kAccount, 0, 2, 200, 2, kIndex1, 2, kTransferRateX2)
.getSerializer()
.peekData())
);
auto const gets10XRPPays20USDOffer = createOfferLedgerObject(
kAccount2,
10,
20,
ripple::to_string(ripple::xrpCurrency()),
ripple::to_string(ripple::to_currency("USD")),
toBase58(ripple::xrpAccount()),
kAccount,
kPayS20UsdGetS10XrpBookDir
);
std::vector<Blob> const bbs(
BookOffersHandler::kLimitMax + 1, gets10XRPPays20USDOffer.getSerializer().peekData()
);
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
static auto const kInput = json::parse(
fmt::format(
R"JSON({{
"taker_gets": {{
"currency": "XRP"
}},
"taker_pays": {{
"currency": "USD",
"issuer": "{}"
}},
"limit": {}
}})JSON",
kAccount,
BookOffersHandler::kLimitMax + 1
)
);
auto const handler = AnyHandler{BookOffersHandler{backend_, mockAmendmentCenterPtr_}};
runSpawn([&](boost::asio::yield_context yield) {
auto const output = handler.process(kInput, Context{.yield = yield});
ASSERT_TRUE(output);
EXPECT_EQ(
output.result.value().as_object().at("offers").as_array().size(),
BookOffersHandler::kLimitMax
);
});
}