diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index c521794157..a739952c9e 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -3267,6 +3267,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index ac016d9fb7..35c0ed33a2 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -3753,6 +3753,9 @@
ripple\rpc\tests
+
+ ripple\rpc\tests
+
ripple\rpc\tests
diff --git a/src/ripple/rpc/tests/Book.test.cpp b/src/ripple/rpc/tests/Book.test.cpp
new file mode 100644
index 0000000000..8960a01134
--- /dev/null
+++ b/src/ripple/rpc/tests/Book.test.cpp
@@ -0,0 +1,823 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 Ripple Labs Inc.
+ Permission to use, copy, modify, and/or 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
+#include
+#include
+#include
+#include
+
+namespace ripple {
+namespace test {
+
+class Book_test : public beast::unit_test::suite
+{
+public:
+ void
+ testOneSideEmptyBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::offers) &&
+ jv[jss::result][jss::offers].size() == 0);
+ expect(! jv[jss::result].isMember(jss::asks));
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 2)));
+ env.close();
+ expect(! wsc->getMsg(10ms));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testOneSideOffersInBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::offers) &&
+ jv[jss::result][jss::offers].size() == 1);
+ expect(jv[jss::result][jss::offers][0u][jss::TakerGets] ==
+ XRP(200).value().getJson(0));
+ expect(jv[jss::result][jss::offers][0u][jss::TakerPays] ==
+ USD(100).value().getJson(0));
+ expect(! jv[jss::result].isMember(jss::asks));
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 4)));
+ env.close();
+ expect(! wsc->getMsg(10ms));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testBothSidesEmptyBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::asks) &&
+ jv[jss::result][jss::asks].size() == 0);
+ expect(jv[jss::result].isMember(jss::bids) &&
+ jv[jss::result][jss::bids].size() == 0);
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 2)));
+ env.close();
+
+ // Check stream update
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == XRP(75).value().getJson(0) &&
+ t[jss::TakerPays] == USD(100).value().getJson(0);
+ }));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testBothSidesOffersInBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::asks) &&
+ jv[jss::result][jss::asks].size() == 1);
+ expect(jv[jss::result].isMember(jss::bids) &&
+ jv[jss::result][jss::bids].size() == 1);
+ expect(jv[jss::result][jss::asks][0u][jss::TakerGets] ==
+ USD(100).value().getJson(0));
+ expect(jv[jss::result][jss::asks][0u][jss::TakerPays] ==
+ XRP(500).value().getJson(0));
+ expect(jv[jss::result][jss::bids][0u][jss::TakerGets] ==
+ XRP(200).value().getJson(0));
+ expect(jv[jss::result][jss::bids][0u][jss::TakerPays] ==
+ USD(100).value().getJson(0));
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 4)));
+ env.close();
+
+ // Check stream update
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == XRP(75).value().getJson(0) &&
+ t[jss::TakerPays] == USD(100).value().getJson(0);
+ }));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testMultipleBooksOneSideEmptyBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::offers) &&
+ jv[jss::result][jss::offers].size() == 0);
+ expect(! jv[jss::result].isMember(jss::asks));
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 2)));
+ env.close();
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == JPY(100).value().getJson(0) &&
+ t[jss::TakerPays] == CNY(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
+ env(offer("alice", JPY(100), CNY(75)),
+ require(owners("alice", 4)));
+ env.close();
+ expect(! wsc->getMsg(10ms));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testMultipleBooksOneSideOffersInBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::offers) &&
+ jv[jss::result][jss::offers].size() == 2);
+ expect(jv[jss::result][jss::offers][0u][jss::TakerGets] ==
+ XRP(200).value().getJson(0));
+ expect(jv[jss::result][jss::offers][0u][jss::TakerPays] ==
+ USD(100).value().getJson(0));
+ expect(jv[jss::result][jss::offers][1u][jss::TakerGets] ==
+ CNY(200).value().getJson(0));
+ expect(jv[jss::result][jss::offers][1u][jss::TakerPays] ==
+ JPY(100).value().getJson(0));
+ expect(! jv[jss::result].isMember(jss::asks));
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 6)));
+ env.close();
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == JPY(100).value().getJson(0) &&
+ t[jss::TakerPays] == CNY(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/JPY, TakerGets 75/CNY
+ env(offer("alice", JPY(100), CNY(75)),
+ require(owners("alice", 8)));
+ env.close();
+ expect(! wsc->getMsg(10ms));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testMultipleBooksBothSidesEmptyBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::asks) &&
+ jv[jss::result][jss::asks].size() == 0);
+ expect(jv[jss::result].isMember(jss::bids) &&
+ jv[jss::result][jss::bids].size() == 0);
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 2)));
+ env.close();
+
+ // Check stream update
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == XRP(75).value().getJson(0) &&
+ t[jss::TakerPays] == USD(100).value().getJson(0);
+ }));
+ }
+
+ {
+ // 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == JPY(100).value().getJson(0) &&
+ t[jss::TakerPays] == CNY(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == CNY(75).value().getJson(0) &&
+ t[jss::TakerPays] == JPY(100).value().getJson(0);
+ }));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ testMultipleBooksBothSidesOffersInBook()
+ {
+ 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);
+ expect(jv[jss::status] == "success");
+ expect(jv[jss::result].isMember(jss::asks) &&
+ jv[jss::result][jss::asks].size() == 2);
+ expect(jv[jss::result].isMember(jss::bids) &&
+ jv[jss::result][jss::bids].size() == 2);
+ expect(jv[jss::result][jss::asks][0u][jss::TakerGets] ==
+ USD(100).value().getJson(0));
+ expect(jv[jss::result][jss::asks][0u][jss::TakerPays] ==
+ XRP(500).value().getJson(0));
+ expect(jv[jss::result][jss::asks][1u][jss::TakerGets] ==
+ JPY(100).value().getJson(0));
+ expect(jv[jss::result][jss::asks][1u][jss::TakerPays] ==
+ CNY(500).value().getJson(0));
+ expect(jv[jss::result][jss::bids][0u][jss::TakerGets] ==
+ XRP(200).value().getJson(0));
+ expect(jv[jss::result][jss::bids][0u][jss::TakerPays] ==
+ USD(100).value().getJson(0));
+ expect(jv[jss::result][jss::bids][1u][jss::TakerGets] ==
+ CNY(200).value().getJson(0));
+ expect(jv[jss::result][jss::bids][1u][jss::TakerPays] ==
+ JPY(100).value().getJson(0));
+ 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == USD(100).value().getJson(0) &&
+ t[jss::TakerPays] == XRP(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // Create a bid: TakerPays 100/USD, TakerGets 75
+ env(offer("alice", USD(100), XRP(75)),
+ require(owners("alice", 6)));
+ env.close();
+
+ // Check stream update
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == XRP(75).value().getJson(0) &&
+ t[jss::TakerPays] == USD(100).value().getJson(0);
+ }));
+ }
+
+ {
+ // 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == JPY(100).value().getJson(0) &&
+ t[jss::TakerPays] == CNY(700).value().getJson(0);
+ }));
+ }
+
+ {
+ // 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
+ expect(wsc->findMsg(5s,
+ [&](auto const& jv)
+ {
+ auto const& t = jv[jss::transaction];
+ return t[jss::TransactionType] == "OfferCreate" &&
+ t[jss::TakerGets] == CNY(75).value().getJson(0) &&
+ t[jss::TakerPays] == JPY(100).value().getJson(0);
+ }));
+ }
+
+ // RPC unsubscribe
+ expect(wsc->invoke("unsubscribe",
+ books)[jss::status] == "success");
+ }
+
+ void
+ run() override
+ {
+ testOneSideEmptyBook();
+ testOneSideOffersInBook();
+
+ testBothSidesEmptyBook();
+ testBothSidesOffersInBook();
+
+ testMultipleBooksOneSideEmptyBook();
+ testMultipleBooksOneSideOffersInBook();
+
+ testMultipleBooksBothSidesEmptyBook();
+ testMultipleBooksBothSidesOffersInBook();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE(Book,app,ripple);
+
+} // test
+} // ripple
diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp
index fdf41f0ca5..e226ce3d63 100644
--- a/src/ripple/unity/rpcx.cpp
+++ b/src/ripple/unity/rpcx.cpp
@@ -101,6 +101,7 @@
#include
#include
+#include
#include
#include
#include
diff --git a/test/subscribe-book-test.js b/test/subscribe-book-test.js
index a63780a785..c4b77c2425 100644
--- a/test/subscribe-book-test.js
+++ b/test/subscribe-book-test.js
@@ -650,4 +650,3 @@ suite("Subscribe book tests", function() {
});
});
-