#include #include #include #include #include #include #include #include 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 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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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::ripplerpc) && jv[jss::ripplerpc] == "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 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