Files
rippled/src/test/rpc/Book_test.cpp
2026-03-06 12:24:45 +01:00

1809 lines
73 KiB
C++

#include <test/jtx.h>
#include <test/jtx/WSClient.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
namespace test {
class Book_test : public beast::unit_test::suite
{
std::string
getBookDir(
jtx::Env& env,
Issue const& in,
Issue const& out,
std::optional<uint256> const& domain = std::nullopt)
{
std::string dir;
auto uBookBase = getBookBase({in, out, domain});
auto uBookEnd = getQualityNext(uBookBase);
auto view = env.closed();
auto key = view->succ(uBookBase, uBookEnd);
if (key)
{
auto sleOfferDir = view->read(keylet::page(key.value()));
uint256 offerIndex;
unsigned int bookEntry;
cdirFirst(*view, sleOfferDir->key(), sleOfferDir, bookEntry, offerIndex);
auto sleOffer = view->read(keylet::offer(offerIndex));
dir = to_string(sleOffer->getFieldH256(sfBookDirectory));
}
return dir;
}
public:
void
testOneSideEmptyBook()
{
testcase("One Side Empty Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 0);
BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 1)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
env.close();
BEAST_EXPECT(!wsc->getMsg(10ms));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testOneSideOffersInBook()
{
testcase("One Side Offers In Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
// Create an ask: TakerPays 500, TakerGets 100/USD
env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
// Create a bid: TakerPays 100/USD, TakerGets 200
env(offer("alice", USD(100), XRP(200)), require(owners("alice", 2)));
env.close();
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 1);
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerGets] ==
XRP(200).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerPays] ==
USD(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 3)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 4)));
env.close();
BEAST_EXPECT(!wsc->getMsg(10ms));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testBothSidesEmptyBook()
{
testcase("Both Sides Empty Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::both] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::asks) && jv[jss::result][jss::asks].size() == 0);
BEAST_EXPECT(
jv[jss::result].isMember(jss::bids) && jv[jss::result][jss::bids].size() == 0);
BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 1)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == XRP(75).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == USD(100).value().getJson(JsonOptions::none);
}));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testBothSidesOffersInBook()
{
testcase("Both Sides Offers In Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
// Create an ask: TakerPays 500, TakerGets 100/USD
env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
// Create a bid: TakerPays 100/USD, TakerGets 200
env(offer("alice", USD(100), XRP(200)), require(owners("alice", 2)));
env.close();
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::both] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::asks) && jv[jss::result][jss::asks].size() == 1);
BEAST_EXPECT(
jv[jss::result].isMember(jss::bids) && jv[jss::result][jss::bids].size() == 1);
BEAST_EXPECT(
jv[jss::result][jss::asks][0u][jss::TakerGets] ==
USD(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::asks][0u][jss::TakerPays] ==
XRP(500).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::bids][0u][jss::TakerGets] ==
XRP(200).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::bids][0u][jss::TakerPays] ==
USD(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 3)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 4)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == XRP(75).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == USD(100).value().getJson(JsonOptions::none);
}));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testMultipleBooksOneSideEmptyBook()
{
testcase("Multiple Books, One Side Empty");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto CNY = Account("alice")["CNY"];
auto JPY = Account("alice")["JPY"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "CNY";
j[jss::taker_gets][jss::issuer] = Account("alice").human();
j[jss::taker_pays][jss::currency] = "JPY";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 0);
BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 1)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
env.close();
BEAST_EXPECT(!wsc->getMsg(10ms));
}
{
// Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
env(offer("alice", CNY(700), JPY(100)), require(owners("alice", 3)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == JPY(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == CNY(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 4)));
env.close();
BEAST_EXPECT(!wsc->getMsg(10ms));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testMultipleBooksOneSideOffersInBook()
{
testcase("Multiple Books, One Side Offers In Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto CNY = Account("alice")["CNY"];
auto JPY = Account("alice")["JPY"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
// Create an ask: TakerPays 500, TakerGets 100/USD
env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
// Create an ask: TakerPays 500/CNY, TakerGets 100/JPY
env(offer("alice", CNY(500), JPY(100)), require(owners("alice", 2)));
// Create a bid: TakerPays 100/USD, TakerGets 200
env(offer("alice", USD(100), XRP(200)), require(owners("alice", 3)));
// Create a bid: TakerPays 100/JPY, TakerGets 200/CNY
env(offer("alice", JPY(100), CNY(200)), require(owners("alice", 4)));
env.close();
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "CNY";
j[jss::taker_gets][jss::issuer] = Account("alice").human();
j[jss::taker_pays][jss::currency] = "JPY";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 2);
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerGets] ==
XRP(200).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerPays] ==
USD(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][1u][jss::TakerGets] ==
CNY(200).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][1u][jss::TakerPays] ==
JPY(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 5)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 6)));
env.close();
BEAST_EXPECT(!wsc->getMsg(10ms));
}
{
// Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
env(offer("alice", CNY(700), JPY(100)), require(owners("alice", 7)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == JPY(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == CNY(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 8)));
env.close();
BEAST_EXPECT(!wsc->getMsg(10ms));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testMultipleBooksBothSidesEmptyBook()
{
testcase("Multiple Books, Both Sides Empty Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto CNY = Account("alice")["CNY"];
auto JPY = Account("alice")["JPY"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::both] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::both] = true;
j[jss::taker_gets][jss::currency] = "CNY";
j[jss::taker_gets][jss::issuer] = Account("alice").human();
j[jss::taker_pays][jss::currency] = "JPY";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::asks) && jv[jss::result][jss::asks].size() == 0);
BEAST_EXPECT(
jv[jss::result].isMember(jss::bids) && jv[jss::result][jss::bids].size() == 0);
BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 1)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 2)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == XRP(75).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == USD(100).value().getJson(JsonOptions::none);
}));
}
{
// Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
env(offer("alice", CNY(700), JPY(100)), require(owners("alice", 3)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == JPY(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == CNY(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 4)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == CNY(75).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == JPY(100).value().getJson(JsonOptions::none);
}));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testMultipleBooksBothSidesOffersInBook()
{
testcase("Multiple Books, Both Sides Offers In Book");
using namespace std::chrono_literals;
using namespace jtx;
Env env(*this);
env.fund(XRP(10000), "alice");
auto USD = Account("alice")["USD"];
auto CNY = Account("alice")["CNY"];
auto JPY = Account("alice")["JPY"];
auto wsc = makeWSClient(env.app().config());
Json::Value books;
// Create an ask: TakerPays 500, TakerGets 100/USD
env(offer("alice", XRP(500), USD(100)), require(owners("alice", 1)));
// Create an ask: TakerPays 500/CNY, TakerGets 100/JPY
env(offer("alice", CNY(500), JPY(100)), require(owners("alice", 2)));
// Create a bid: TakerPays 100/USD, TakerGets 200
env(offer("alice", USD(100), XRP(200)), require(owners("alice", 3)));
// Create a bid: TakerPays 100/JPY, TakerGets 200/CNY
env(offer("alice", JPY(100), CNY(200)), require(owners("alice", 4)));
env.close();
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::both] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
// RPC subscribe to books stream
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::both] = true;
j[jss::taker_gets][jss::currency] = "CNY";
j[jss::taker_gets][jss::issuer] = Account("alice").human();
j[jss::taker_pays][jss::currency] = "JPY";
j[jss::taker_pays][jss::issuer] = Account("alice").human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::asks) && jv[jss::result][jss::asks].size() == 2);
BEAST_EXPECT(
jv[jss::result].isMember(jss::bids) && jv[jss::result][jss::bids].size() == 2);
BEAST_EXPECT(
jv[jss::result][jss::asks][0u][jss::TakerGets] ==
USD(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::asks][0u][jss::TakerPays] ==
XRP(500).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::asks][1u][jss::TakerGets] ==
JPY(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::asks][1u][jss::TakerPays] ==
CNY(500).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::bids][0u][jss::TakerGets] ==
XRP(200).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::bids][0u][jss::TakerPays] ==
USD(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::bids][1u][jss::TakerGets] ==
CNY(200).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::bids][1u][jss::TakerPays] ==
JPY(100).value().getJson(JsonOptions::none));
BEAST_EXPECT(!jv[jss::result].isMember(jss::offers));
}
{
// Create an ask: TakerPays 700, TakerGets 100/USD
env(offer("alice", XRP(700), USD(100)), require(owners("alice", 5)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == XRP(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/USD, TakerGets 75
env(offer("alice", USD(100), XRP(75)), require(owners("alice", 6)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == XRP(75).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == USD(100).value().getJson(JsonOptions::none);
}));
}
{
// Create an ask: TakerPays 700/CNY, TakerGets 100/JPY
env(offer("alice", CNY(700), JPY(100)), require(owners("alice", 7)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == JPY(100).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == CNY(700).value().getJson(JsonOptions::none);
}));
}
{
// Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
env(offer("alice", JPY(100), CNY(75)), require(owners("alice", 8)));
env.close();
// Check stream update
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
auto const& t = jv[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == CNY(75).value().getJson(JsonOptions::none) &&
t[jss::TakerPays] == JPY(100).value().getJson(JsonOptions::none);
}));
}
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
}
void
testTrackOffers()
{
testcase("TrackOffers");
using namespace jtx;
Env env(*this);
Account gw{"gw"};
Account alice{"alice"};
Account bob{"bob"};
auto wsc = makeWSClient(env.app().config());
env.fund(XRP(20000), alice, bob, gw);
env.close();
auto USD = gw["USD"];
Json::Value books;
{
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = gw.human();
}
auto jv = wsc->invoke("subscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 0);
BEAST_EXPECT(!jv[jss::result].isMember(jss::asks));
BEAST_EXPECT(!jv[jss::result].isMember(jss::bids));
}
env(rate(gw, 1.1));
env.close();
env.trust(USD(1000), alice);
env.trust(USD(1000), bob);
env(pay(gw, alice, USD(100)));
env(pay(gw, bob, USD(50)));
env(offer(alice, XRP(4000), USD(10)));
env.close();
Json::Value jvParams;
jvParams[jss::taker] = env.master.human();
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto jv = wsc->invoke("book_offers", jvParams);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
auto jrr = jv[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 1);
auto const jrOffer = jrr[jss::offers][0u];
BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
BEAST_EXPECT(jrOffer[sfBookDirectory.fieldName] == getBookDir(env, XRP, USD.issue()));
BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
BEAST_EXPECT(jrOffer[jss::Flags] == 0);
BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
BEAST_EXPECT(jrOffer[sfSequence.fieldName] == 5);
BEAST_EXPECT(jrOffer[jss::TakerGets] == USD(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrOffer[jss::TakerPays] == XRP(4000).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrOffer[jss::owner_funds] == "100");
BEAST_EXPECT(jrOffer[jss::quality] == "400000000");
using namespace std::chrono_literals;
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jval) {
auto const& t = jval[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(10).value().getJson(JsonOptions::none) &&
t[jss::owner_funds] == "100" &&
t[jss::TakerPays] == XRP(4000).value().getJson(JsonOptions::none);
}));
env(offer(bob, XRP(2000), USD(5)));
env.close();
BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jval) {
auto const& t = jval[jss::transaction];
return t[jss::TransactionType] == jss::OfferCreate &&
t[jss::TakerGets] == USD(5).value().getJson(JsonOptions::none) &&
t[jss::owner_funds] == "50" &&
t[jss::TakerPays] == XRP(2000).value().getJson(JsonOptions::none);
}));
jv = wsc->invoke("book_offers", jvParams);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
jrr = jv[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 2);
auto const jrNextOffer = jrr[jss::offers][1u];
BEAST_EXPECT(jrNextOffer[sfAccount.fieldName] == bob.human());
BEAST_EXPECT(jrNextOffer[sfBookDirectory.fieldName] == getBookDir(env, XRP, USD.issue()));
BEAST_EXPECT(jrNextOffer[sfBookNode.fieldName] == "0");
BEAST_EXPECT(jrNextOffer[jss::Flags] == 0);
BEAST_EXPECT(jrNextOffer[sfLedgerEntryType.fieldName] == jss::Offer);
BEAST_EXPECT(jrNextOffer[sfOwnerNode.fieldName] == "0");
BEAST_EXPECT(jrNextOffer[sfSequence.fieldName] == 5);
BEAST_EXPECT(jrNextOffer[jss::TakerGets] == USD(5).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrNextOffer[jss::TakerPays] == XRP(2000).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrNextOffer[jss::owner_funds] == "50");
BEAST_EXPECT(jrNextOffer[jss::quality] == "400000000");
jv = wsc->invoke("unsubscribe", books);
if (wsc->version() == 2)
{
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::status] == "success");
}
// Check that a stream only sees the given OfferCreate once
static bool
offerOnlyOnceInStream(
std::unique_ptr<WSClient> const& wsc,
std::chrono::milliseconds const& timeout,
jtx::PrettyAmount const& takerGets,
jtx::PrettyAmount const& takerPays)
{
auto maybeJv = wsc->getMsg(timeout);
// No message
if (!maybeJv)
return false;
// wrong message
if (!(*maybeJv).isMember(jss::transaction))
return false;
auto const& t = (*maybeJv)[jss::transaction];
if (t[jss::TransactionType] != jss::OfferCreate ||
t[jss::TakerGets] != takerGets.value().getJson(JsonOptions::none) ||
t[jss::TakerPays] != takerPays.value().getJson(JsonOptions::none))
return false;
// Make sure no other message is waiting
return wsc->getMsg(timeout) == std::nullopt;
}
void
testCrossingSingleBookOffer()
{
testcase("Crossing single book offer");
// This was added to check that an OfferCreate transaction is only
// published once in a stream, even if it updates multiple offer
// ledger entries
using namespace jtx;
Env env(*this);
// Scenario is:
// - Alice and Bob place identical offers for USD -> XRP
// - Charlie places a crossing order that takes both Alice and Bob's
auto const gw = Account("gateway");
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const charlie = Account("charlie");
auto const USD = gw["USD"];
env.fund(XRP(1000000), gw, alice, bob, charlie);
env.close();
env(trust(alice, USD(500)));
env(trust(bob, USD(500)));
env.close();
env(pay(gw, alice, USD(500)));
env(pay(gw, bob, USD(500)));
env.close();
// Alice and Bob offer $500 for 500 XRP
env(offer(alice, XRP(500), USD(500)));
env(offer(bob, XRP(500), USD(500)));
env.close();
auto wsc = makeWSClient(env.app().config());
Json::Value books;
{
// RPC subscribe to books stream
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = false;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = gw.human();
}
auto jv = wsc->invoke("subscribe", books);
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
}
// Charlie places an offer that crosses Alice and Charlie's offers
env(offer(charlie, USD(1000), XRP(1000)));
env.close();
env.require(offers(alice, 0), offers(bob, 0), offers(charlie, 0));
using namespace std::chrono_literals;
BEAST_EXPECT(offerOnlyOnceInStream(wsc, 1s, XRP(1000), USD(1000)));
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
}
void
testCrossingMultiBookOffer()
{
testcase("Crossing multi-book offer");
// This was added to check that an OfferCreate transaction is only
// published once in a stream, even if it auto-bridges across several
// books that are under subscription
using namespace jtx;
Env env(*this);
// Scenario is:
// - Alice has 1 USD and wants 100 XRP
// - Bob has 100 XRP and wants 1 EUR
// - Charlie has 1 EUR and wants 1 USD and should auto-bridge through
// Alice and Bob
auto const gw = Account("gateway");
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const charlie = Account("charlie");
auto const USD = gw["USD"];
auto const EUR = gw["EUR"];
env.fund(XRP(1000000), gw, alice, bob, charlie);
env.close();
for (auto const& account : {alice, bob, charlie})
{
for (auto const& iou : {USD, EUR})
{
env(trust(account, iou(1)));
}
}
env.close();
env(pay(gw, alice, USD(1)));
env(pay(gw, charlie, EUR(1)));
env.close();
env(offer(alice, XRP(100), USD(1)));
env(offer(bob, EUR(1), XRP(100)));
env.close();
auto wsc = makeWSClient(env.app().config());
Json::Value books;
{
// RPC subscribe to multiple book streams
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = false;
j[jss::taker_gets][jss::currency] = "XRP";
j[jss::taker_pays][jss::currency] = "USD";
j[jss::taker_pays][jss::issuer] = gw.human();
}
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = false;
j[jss::taker_gets][jss::currency] = "EUR";
j[jss::taker_gets][jss::issuer] = gw.human();
j[jss::taker_pays][jss::currency] = "XRP";
}
auto jv = wsc->invoke("subscribe", books);
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
}
// Charlies places an on offer for EUR -> USD that should auto-bridge
env(offer(charlie, USD(1), EUR(1)));
env.close();
using namespace std::chrono_literals;
BEAST_EXPECT(offerOnlyOnceInStream(wsc, 1s, EUR(1), USD(1)));
// RPC unsubscribe
auto jv = wsc->invoke("unsubscribe", books);
BEAST_EXPECT(jv[jss::status] == "success");
}
void
testBookOfferErrors()
{
testcase("BookOffersRPC Errors");
using namespace jtx;
Env env(*this);
Account gw{"gw"};
Account alice{"alice"};
env.fund(XRP(10000), alice, gw);
env.close();
auto USD = gw["USD"];
{
Json::Value jvParams;
jvParams[jss::ledger_index] = 10u;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "lgrNotFound");
BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_pays'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays] = Json::objectValue;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_gets'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays] = "not an object";
jvParams[jss::taker_gets] = Json::objectValue;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker_pays', not object.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays] = Json::objectValue;
jvParams[jss::taker_gets] = "not an object";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker_gets', not object.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays] = Json::objectValue;
jvParams[jss::taker_gets] = Json::objectValue;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_pays.currency'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = 1;
jvParams[jss::taker_gets] = Json::objectValue;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_pays.currency', not string.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets] = Json::objectValue;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Missing field 'taker_gets.currency'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = 1;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_gets.currency', not string.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "NOT_VALID";
jvParams[jss::taker_gets][jss::currency] = "XRP";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "srcCurMalformed");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_pays.currency', bad currency.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "NOT_VALID";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "dstAmtMalformed");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_gets.currency', bad currency.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = 1;
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_gets.issuer', not string.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_pays][jss::issuer] = 1;
jvParams[jss::taker_gets][jss::currency] = "USD";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_pays.issuer', not string.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_pays][jss::issuer] = gw.human() + "DEAD";
jvParams[jss::taker_gets][jss::currency] = "USD";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_pays.issuer', bad issuer.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_pays][jss::issuer] = toBase58(noAccount());
jvParams[jss::taker_gets][jss::currency] = "USD";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Invalid field 'taker_pays.issuer', bad issuer account one.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human() + "DEAD";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] == "Invalid field 'taker_gets.issuer', bad issuer.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = toBase58(noAccount());
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Invalid field 'taker_gets.issuer', bad issuer account one.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_pays][jss::issuer] = alice.human();
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Unneeded field 'taker_pays.issuer' "
"for XRP currency specification.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "USD";
jvParams[jss::taker_pays][jss::issuer] = toBase58(xrpAccount());
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "srcIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Invalid field 'taker_pays.issuer', expected non-XRP issuer.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker] = 1;
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker', not string.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker] = env.master.human() + "DEAD";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'taker'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker] = env.master.human();
jvParams[jss::taker_pays][jss::currency] = "USD";
jvParams[jss::taker_pays][jss::issuer] = gw.human();
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "badMarket");
BEAST_EXPECT(jrr[jss::error_message] == "No such market.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker] = env.master.human();
jvParams[jss::limit] = "0"; // NOT an integer
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit', not unsigned integer.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker] = env.master.human();
jvParams[jss::limit] = 0; // must be > 0
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "USD";
jvParams[jss::taker_pays][jss::issuer] = gw.human();
jvParams[jss::taker_gets][jss::currency] = "USD";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Invalid field 'taker_gets.issuer', "
"expected non-XRP issuer.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "USD";
jvParams[jss::taker_pays][jss::issuer] = gw.human();
jvParams[jss::taker_gets][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "dstIsrMalformed");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Unneeded field 'taker_gets.issuer' "
"for XRP currency specification.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "USD";
jvParams[jss::taker_pays][jss::issuer] = gw.human();
jvParams[jss::taker_gets][jss::currency] = "EUR";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
jvParams[jss::domain] = "badString";
auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "domainMalformed");
BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain.");
}
}
void
testBookOfferLimits(bool asAdmin)
{
testcase("BookOffer Limits");
using namespace jtx;
Env env{*this, asAdmin ? envconfig() : envconfig(no_admin)};
Account gw{"gw"};
env.fund(XRP(200000), gw);
// Note that calls to env.close() fail without admin permission.
if (asAdmin)
env.close();
auto USD = gw["USD"];
for (auto i = 0; i <= RPC::Tuning::bookOffers.rmax; i++)
env(offer(gw, XRP(50 + 1 * i), USD(1.0 + 0.1 * i)));
if (asAdmin)
env.close();
Json::Value jvParams;
jvParams[jss::limit] = 1;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u));
// NOTE - a marker field is not returned for this method
jvParams[jss::limit] = RPC::Tuning::bookOffers.rmax + 1;
jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? RPC::Tuning::bookOffers.rmax + 1 : 0u));
jvParams[jss::limit] = Json::nullValue;
jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? RPC::Tuning::bookOffers.rDefault : 0u));
}
void
testTrackDomainOffer()
{
testcase("TrackDomainOffer");
using namespace jtx;
FeatureBitset const all{
jtx::testable_amendments() | featurePermissionedDomains | featureCredentials |
featurePermissionedDEX};
Env env(*this, all);
PermissionedDEX permDex(env);
auto const alice = permDex.alice;
auto const bob = permDex.bob;
auto const carol = permDex.carol;
auto const domainID = permDex.domainID;
auto const gw = permDex.gw;
auto const USD = permDex.USD;
auto wsc = makeWSClient(env.app().config());
env(offer(alice, XRP(10), USD(10)), domain(domainID));
env.close();
auto checkBookOffers = [&](Json::Value const& jrr) {
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 1);
auto const jrOffer = jrr[jss::offers][0u];
BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
BEAST_EXPECT(
jrOffer[sfBookDirectory.fieldName] == getBookDir(env, XRP, USD.issue(), domainID));
BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
BEAST_EXPECT(jrOffer[jss::Flags] == 0);
BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
BEAST_EXPECT(jrOffer[jss::TakerGets] == USD(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrOffer[jss::TakerPays] == XRP(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrOffer[sfDomainID.jsonName].asString() == to_string(domainID));
};
// book_offers: open book doesn't return offer
{
Json::Value jvParams;
jvParams[jss::taker] = env.master.human();
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto jv = wsc->invoke("book_offers", jvParams);
auto jrr = jv[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 0);
}
auto checkSubBooks = [&](Json::Value const& jv) {
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 1);
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerGets] ==
USD(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerPays] ==
XRP(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][sfDomainID.jsonName].asString() ==
to_string(domainID));
};
// book_offers: requesting domain book returns hybrid offer
{
Json::Value jvParams;
jvParams[jss::taker] = env.master.human();
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
jvParams[jss::domain] = to_string(domainID);
auto jv = wsc->invoke("book_offers", jvParams);
auto jrr = jv[jss::result];
checkBookOffers(jrr);
}
// subscribe to domain book should return domain offer
{
Json::Value books;
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_pays][jss::currency] = "XRP";
j[jss::taker_gets][jss::currency] = "USD";
j[jss::taker_gets][jss::issuer] = gw.human();
j[jss::domain] = to_string(domainID);
}
auto jv = wsc->invoke("subscribe", books);
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
checkSubBooks(jv);
}
// subscribe to open book should not return domain offer
{
Json::Value books;
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_pays][jss::currency] = "XRP";
j[jss::taker_gets][jss::currency] = "USD";
j[jss::taker_gets][jss::issuer] = gw.human();
}
auto jv = wsc->invoke("subscribe", books);
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 0);
}
}
void
testTrackHybridOffer()
{
testcase("TrackHybridOffer");
using namespace jtx;
FeatureBitset const all{
jtx::testable_amendments() | featurePermissionedDomains | featureCredentials |
featurePermissionedDEX};
Env env(*this, all);
PermissionedDEX permDex(env);
auto const alice = permDex.alice;
auto const bob = permDex.bob;
auto const carol = permDex.carol;
auto const domainID = permDex.domainID;
auto const gw = permDex.gw;
auto const USD = permDex.USD;
auto wsc = makeWSClient(env.app().config());
env(offer(alice, XRP(10), USD(10)), domain(domainID), txflags(tfHybrid));
env.close();
auto checkBookOffers = [&](Json::Value const& jrr) {
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 1);
auto const jrOffer = jrr[jss::offers][0u];
BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human());
BEAST_EXPECT(
jrOffer[sfBookDirectory.fieldName] == getBookDir(env, XRP, USD.issue(), domainID));
BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0");
BEAST_EXPECT(jrOffer[jss::Flags] == lsfHybrid);
BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0");
BEAST_EXPECT(jrOffer[jss::TakerGets] == USD(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrOffer[jss::TakerPays] == XRP(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(jrOffer[sfDomainID.jsonName].asString() == to_string(domainID));
BEAST_EXPECT(jrOffer[sfAdditionalBooks.jsonName].size() == 1);
};
// book_offers: open book returns hybrid offer
{
Json::Value jvParams;
jvParams[jss::taker] = env.master.human();
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto jv = wsc->invoke("book_offers", jvParams);
auto jrr = jv[jss::result];
checkBookOffers(jrr);
}
auto checkSubBooks = [&](Json::Value const& jv) {
BEAST_EXPECT(
jv[jss::result].isMember(jss::offers) && jv[jss::result][jss::offers].size() == 1);
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerGets] ==
USD(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][jss::TakerPays] ==
XRP(10).value().getJson(JsonOptions::none));
BEAST_EXPECT(
jv[jss::result][jss::offers][0u][sfDomainID.jsonName].asString() ==
to_string(domainID));
};
// book_offers: requesting domain book returns hybrid offer
{
Json::Value jvParams;
jvParams[jss::taker] = env.master.human();
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
jvParams[jss::domain] = to_string(domainID);
auto jv = wsc->invoke("book_offers", jvParams);
auto jrr = jv[jss::result];
checkBookOffers(jrr);
}
// subscribe to domain book should return hybrid offer
{
Json::Value books;
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_pays][jss::currency] = "XRP";
j[jss::taker_gets][jss::currency] = "USD";
j[jss::taker_gets][jss::issuer] = gw.human();
j[jss::domain] = to_string(domainID);
}
auto jv = wsc->invoke("subscribe", books);
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
checkSubBooks(jv);
// RPC unsubscribe
auto unsubJv = wsc->invoke("unsubscribe", books);
if (wsc->version() == 2)
BEAST_EXPECT(unsubJv[jss::status] == "success");
}
// subscribe to open book should return hybrid offer
{
Json::Value books;
books[jss::books] = Json::arrayValue;
{
auto& j = books[jss::books].append(Json::objectValue);
j[jss::snapshot] = true;
j[jss::taker_pays][jss::currency] = "XRP";
j[jss::taker_gets][jss::currency] = "USD";
j[jss::taker_gets][jss::issuer] = gw.human();
}
auto jv = wsc->invoke("subscribe", books);
if (!BEAST_EXPECT(jv[jss::status] == "success"))
return;
checkSubBooks(jv);
}
}
void
run() override
{
testOneSideEmptyBook();
testOneSideOffersInBook();
testBothSidesEmptyBook();
testBothSidesOffersInBook();
testMultipleBooksOneSideEmptyBook();
testMultipleBooksOneSideOffersInBook();
testMultipleBooksBothSidesEmptyBook();
testMultipleBooksBothSidesOffersInBook();
testTrackOffers();
testCrossingSingleBookOffer();
testCrossingMultiBookOffer();
testBookOfferErrors();
testBookOfferLimits(true);
testBookOfferLimits(false);
testTrackDomainOffer();
testTrackHybridOffer();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(Book, rpc, xrpl, 1);
} // namespace test
} // namespace xrpl