Files
clio/tests/unit/rpc/handlers/BookOffersTests.cpp
Ayaz Salikhov 9c92a2b51b style: Use pre-commit tool and add simple config (#2029)
I started with really simple pre-commit hooks and will add more on top.

Important files:
- `.pre-commit-config.yaml` - the config for pre-commit
- `.github/workflows/pre-commit.yml` - runs pre-commit hooks in branches
and `develop`
- `.github/workflows/pre-commit-autoupdate.yml` - autoupdates pre-commit
hooks once in a month
2025-04-24 17:59:43 +01:00

1586 lines
63 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#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/core.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/Book.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 kLEDGER_HASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr auto kINDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr auto kINDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
// 20 USD : 10 XRP
constexpr auto kPAYS20_USD_GETS10_XRP_BOOK_DIR = "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000";
// 20 XRP : 10 USD
constexpr auto kPAYS20_XRP_GETS10_USD_BOOK_DIR = "7B1767D41DBCE79D9585CF9D0262A5FEC45E5206FF524F8B55071AFD498D0000";
// transfer rate x2
constexpr auto kTRANSFER_RATE_X2 = 2000000000;
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"({
"taker_pays" :
{
"currency" : "USD",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'taker_gets' missing"
},
ParameterTestBundle{
.testName = "MissingTakerPays",
.testJson = R"({
"taker_gets" :
{
"currency" : "USD",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'taker_pays' missing"
},
ParameterTestBundle{
.testName = "WrongTypeTakerPays",
.testJson = R"({
"taker_pays" : "wrong",
"taker_gets" :
{
"currency" : "XRP"
}
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "WrongTypeTakerGets",
.testJson = R"({
"taker_gets" : "wrong",
"taker_pays" :
{
"currency" : "XRP"
}
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "TakerPaysMissingCurrency",
.testJson = R"({
"taker_pays" : {},
"taker_gets" :
{
"currency" : "XRP"
}
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'currency' missing"
},
ParameterTestBundle{
.testName = "TakerGetsMissingCurrency",
.testJson = R"({
"taker_gets" : {},
"taker_pays" :
{
"currency" : "XRP"
}
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Required field 'currency' missing"
},
ParameterTestBundle{
.testName = "TakerGetsWrongCurrency",
.testJson = R"({
"taker_gets" :
{
"currency" : "CNYY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_pays" :
{
"currency" : "XRP"
}
})",
.expectedError = "dstAmtMalformed",
.expectedErrorMessage = "Destination amount/currency/issuer is malformed."
},
ParameterTestBundle{
.testName = "TakerPaysWrongCurrency",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNYY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
}
})",
.expectedError = "srcCurMalformed",
.expectedErrorMessage = "Source currency is malformed."
},
ParameterTestBundle{
.testName = "TakerGetsCurrencyNotString",
.testJson = R"({
"taker_gets" :
{
"currency" : 123,
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_pays" :
{
"currency" : "XRP"
}
})",
.expectedError = "dstAmtMalformed",
.expectedErrorMessage = "Destination amount/currency/issuer is malformed."
},
ParameterTestBundle{
.testName = "TakerPaysCurrencyNotString",
.testJson = R"({
"taker_pays" :
{
"currency" : 123,
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
}
})",
.expectedError = "srcCurMalformed",
.expectedErrorMessage = "Source currency is malformed."
},
ParameterTestBundle{
.testName = "TakerGetsWrongIssuer",
.testJson = R"({
"taker_gets" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs5"
},
"taker_pays" :
{
"currency" : "XRP"
}
})",
.expectedError = "dstIsrMalformed",
.expectedErrorMessage = "Destination issuer is malformed."
},
ParameterTestBundle{
.testName = "TakerPaysWrongIssuer",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs5"
},
"taker_gets" :
{
"currency" : "XRP"
}
})",
.expectedError = "srcIsrMalformed",
.expectedErrorMessage = "Source issuer is malformed."
},
ParameterTestBundle{
.testName = "InvalidTaker",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"taker": "123"
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid field 'taker'."
},
ParameterTestBundle{
.testName = "TakerNotString",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"taker": 123
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid field 'taker'."
},
ParameterTestBundle{
.testName = "LimitNotInt",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"limit": "123"
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "LimitNagetive",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"limit": -1
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "LimitZero",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"limit": 0
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "Invalid parameters."
},
ParameterTestBundle{
.testName = "LedgerIndexInvalid",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"ledger_index": "xxx"
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "ledgerIndexMalformed"
},
ParameterTestBundle{
.testName = "LedgerHashInvalid",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"ledger_hash": "xxx"
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "ledger_hashMalformed"
},
ParameterTestBundle{
.testName = "LedgerHashNotString",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP"
},
"ledger_hash": 123
})",
.expectedError = "invalidParams",
.expectedErrorMessage = "ledger_hashNotString"
},
ParameterTestBundle{
.testName = "GetsPaysXRPWithIssuer",
.testJson = R"({
"taker_pays" :
{
"currency" : "XRP",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "CNY",
"issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
}
})",
.expectedError = "srcIsrMalformed",
.expectedErrorMessage = "Unneeded field 'taker_pays.issuer' for XRP currency specification."
},
ParameterTestBundle{
.testName = "PaysCurrencyWithXRPIssuer",
.testJson = R"({
"taker_pays" :
{
"currency" : "JPY"
},
"taker_gets" :
{
"currency" : "CNY",
"issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
}
})",
.expectedError = "srcIsrMalformed",
.expectedErrorMessage = "Invalid field 'taker_pays.issuer', expected non-XRP issuer."
},
ParameterTestBundle{
.testName = "GetsCurrencyWithXRPIssuer",
.testJson = R"({
"taker_pays" :
{
"currency" : "XRP"
},
"taker_gets" :
{
"currency" : "CNY"
}
})",
.expectedError = "dstIsrMalformed",
.expectedErrorMessage = "Invalid field 'taker_gets.issuer', expected non-XRP issuer."
},
ParameterTestBundle{
.testName = "GetsXRPWithIssuer",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "XRP",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})",
.expectedError = "dstIsrMalformed",
.expectedErrorMessage = "Unneeded field 'taker_gets.issuer' for XRP currency specification."
},
ParameterTestBundle{
.testName = "BadMarket",
.testJson = R"({
"taker_pays" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
},
"taker_gets" :
{
"currency" : "CNY",
"issuer" : "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
}
})",
.expectedError = "badMarket",
.expectedErrorMessage = "badMarket"
}
};
}
INSTANTIATE_TEST_SUITE_P(
RPCBookOffersHandler,
RPCBookOffersParameterTest,
testing::ValuesIn(generateParameterBookOffersTestBundles()),
tests::util::kNAME_GENERATOR
);
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(kLEDGER_HASH, 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()),
kPAYS20_XRP_GETS10_USD_BOOK_DIR
);
auto const gets10USDPays20XRPOwnerOffer = createOfferLedgerObject(
kACCOUNT,
10,
20,
ripple::to_string(ripple::to_currency("USD")),
ripple::to_string(ripple::xrpCurrency()),
kACCOUNT,
toBase58(ripple::xrpAccount()),
kPAYS20_XRP_GETS10_USD_BOOK_DIR
);
auto const gets10XRPPays20USDOffer = createOfferLedgerObject(
kACCOUNT2,
10,
20,
ripple::to_string(ripple::xrpCurrency()),
ripple::to_string(ripple::to_currency("USD")),
toBase58(ripple::xrpAccount()),
kACCOUNT,
kPAYS20_USD_GETS10_XRP_BOOK_DIR
);
auto const getsXRPPaysUSDBook = getBookBase(std::get<ripple::Book>(
rpc::parseBook(ripple::to_currency("USD"), account, ripple::xrpCurrency(), ripple::xrpAccount())
));
auto const getsUSDPaysXRPBook = getBookBase(std::get<ripple::Book>(
rpc::parseBook(ripple::xrpCurrency(), ripple::xrpAccount(), ripple::to_currency("USD"), account)
));
auto const getsXRPPaysUSDInputJson = fmt::format(
R"({{
"taker_gets":
{{
"currency": "XRP"
}},
"taker_pays":
{{
"currency": "USD",
"issuer": "{}"
}}
}})",
kACCOUNT
);
auto const paysXRPGetsUSDInputJson = fmt::format(
R"({{
"taker_pays":
{{
"currency": "XRP"
}},
"taker_gets":
{{
"currency": "USD",
"issuer": "{}"
}}
}})",
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{kPAYS20_USD_GETS10_XRP_BOOK_DIR}},
{ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR},
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"({{
"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":"{}"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
193,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysUSDGetsXRPNoFrozenOwnerFundNotEnough",
.inputJson = getsXRPPaysUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsXRPPaysUSDBook, ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}},
{ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR},
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"({{
"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"
}}
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
5,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysUSDGetsXRPFrozen",
.inputJson = getsXRPPaysUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsXRPPaysUSDBook, ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}},
{ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR},
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"({{
"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"
}}
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "GetsUSDPaysXRPFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2
)
.getSerializer()
.peekData()}
},
.ledgerObjectCalls = 3,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"({{
"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"
}}
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDWithTransferFee",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2)
.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"({{
"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"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
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{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2)
.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"({{
"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":"{}"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
30,
2,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
2
),
.amendmentIsEnabledCalls = 1,
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDSellingOwnCurrency",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2)
.getSerializer()
.peekData()},
},
.ledgerObjectCalls = 3,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOwnerOffer},
.expectedJson = fmt::format(
R"({{
"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":"{}"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
10,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDTrustLineFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2)
.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"({{
"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"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
0,
2
),
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDIsDeepFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2
)
.getSerializer()
.peekData()},
},
.ledgerObjectCalls = 4,
.mockedOffers = std::vector<ripple::STObject>{gets10USDPays20XRPOffer},
.expectedJson = fmt::format(
R"({{
"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"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
0,
2
)
},
BookOffersNormalTestBundle{
.testName = "PaysXRPGetsUSDTrustLineFrozenAndIsDeepFrozen",
.inputJson = paysXRPGetsUSDInputJson,
// prepare offer dir index
.mockedSuccessors =
std::map<ripple::uint256, std::optional<ripple::uint256>>{
{getsUSDPaysXRPBook, ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}},
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR}, std::optional<ripple::uint256>{}}
},
.mockedLedgerObjects =
std::map<ripple::uint256, ripple::Blob>{
// book dir object
{ripple::uint256{kPAYS20_XRP_GETS10_USD_BOOK_DIR},
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, kTRANSFER_RATE_X2
)
.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"({{
"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"
}}
]
}})",
kLEDGER_HASH,
kACCOUNT2,
kPAYS20_XRP_GETS10_USD_BOOK_DIR,
0,
2
)
}
};
}
INSTANTIATE_TEST_SUITE_P(
RPCBookOffersHandler,
RPCBookOffersNormalPathTest,
testing::ValuesIn(generateNormalPathBookOffersTestBundles()),
tests::util::kNAME_GENERATOR
);
// 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>{}));
auto static const kINPUT = json::parse(fmt::format(
R"({{
"ledger_index": 30,
"taker_gets":
{{
"currency": "XRP"
}},
"taker_pays":
{{
"currency": "USD",
"issuer": "{}"
}}
}})",
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>{}));
auto static const kINPUT = json::parse(fmt::format(
R"({{
"ledger_index": "30",
"taker_gets":
{{
"currency": "XRP"
}},
"taker_pays":
{{
"currency": "USD",
"issuer": "{}"
}}
}})",
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{kLEDGER_HASH}, _))
.WillByDefault(Return(std::optional<ripple::LedgerHeader>{}));
auto static const kINPUT = json::parse(fmt::format(
R"({{
"ledger_hash": "{}",
"taker_gets":
{{
"currency": "XRP"
}},
"taker_pays":
{{
"currency": "USD",
"issuer": "{}"
}}
}})",
kLEDGER_HASH,
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(kLEDGER_HASH, 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(std::get<ripple::Book>(
rpc::parseBook(ripple::to_currency("USD"), issuer, ripple::xrpCurrency(), ripple::xrpAccount())
));
ON_CALL(*backend_, doFetchSuccessorKey(getsXRPPaysUSDBook, seq, _))
.WillByDefault(Return(ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(5);
auto const indexes = std::vector<ripple::uint256>(10, ripple::uint256{kINDEX2});
ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}, 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, kTRANSFER_RATE_X2).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,
kPAYS20_USD_GETS10_XRP_BOOK_DIR
);
std::vector<Blob> const bbs(10, gets10XRPPays20USDOffer.getSerializer().peekData());
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
auto static const kINPUT = json::parse(fmt::format(
R"({{
"taker_gets":
{{
"currency": "XRP"
}},
"taker_pays":
{{
"currency": "USD",
"issuer": "{}"
}},
"limit": 5
}})",
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(kLEDGER_HASH, 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(std::get<ripple::Book>(
rpc::parseBook(ripple::to_currency("USD"), issuer, ripple::xrpCurrency(), ripple::xrpAccount())
));
ON_CALL(*backend_, doFetchSuccessorKey(getsXRPPaysUSDBook, seq, _))
.WillByDefault(Return(ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(5);
auto const indexes = std::vector<ripple::uint256>(BookOffersHandler::kLIMIT_MAX + 1, ripple::uint256{kINDEX2});
ON_CALL(*backend_, doFetchLedgerObject(ripple::uint256{kPAYS20_USD_GETS10_XRP_BOOK_DIR}, 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, kTRANSFER_RATE_X2).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,
kPAYS20_USD_GETS10_XRP_BOOK_DIR
);
std::vector<Blob> const bbs(BookOffersHandler::kLIMIT_MAX + 1, gets10XRPPays20USDOffer.getSerializer().peekData());
ON_CALL(*backend_, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*backend_, doFetchLedgerObjects).Times(1);
auto static const kINPUT = json::parse(fmt::format(
R"({{
"taker_gets":
{{
"currency": "XRP"
}},
"taker_pays":
{{
"currency": "USD",
"issuer": "{}"
}},
"limit": {}
}})",
kACCOUNT,
BookOffersHandler::kLIMIT_MAX + 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::kLIMIT_MAX);
});
}