mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
The XRP Ledger utilizes an account model. Unlike systems based on a UTXO model, XRP Ledger accounts are first-class objects. This design choice allows the XRP Ledger to offer rich functionality, including the ability to own objects (offers, escrows, checks, signer lists) as well as other advanced features, such as key rotation and configurable multi-signing without needing to change a destination address. The trade-off is that accounts must be stored on ledger. The XRP Ledger applies reserve requirements, in XRP, to protect the shared global ledger from growing excessively large as the result of spam or malicious usage. Prior to this commit, accounts had been permanent objects; once created, they could never be deleted. This commit introduces a new amendment "DeletableAccounts" which, if enabled, will allow account objects to be deleted by executing the new "AccountDelete" transaction. Any funds remaining in the account will be transferred to an account specified in the deletion transaction. The amendment changes the mechanics of account creation; previously a new account would have an initial sequence number of 1. Accounts created after the amendment will have an initial sequence number that is equal to the ledger in which the account was created. Accounts can only be deleted if they are not associated with any obligations (like RippleStates, Escrows, or PayChannels) and if the current ledger sequence number exceeds the account's sequence number by at least 256 so that, if recreated, the account can be protected from transaction replay.
1702 lines
67 KiB
C++
1702 lines
67 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
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 <ripple/protocol/jss.h>
|
|
#include <ripple/protocol/Indexes.h>
|
|
#include <test/jtx/WSClient.h>
|
|
#include <test/jtx.h>
|
|
#include <ripple/beast/unit_test.h>
|
|
#include <ripple/rpc/impl/Tuning.h>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
class Book_test : public beast::unit_test::suite
|
|
{
|
|
std::string getBookDir(jtx::Env & env, Issue const& in, Issue const& out)
|
|
{
|
|
std::string dir;
|
|
auto uBookBase = getBookBase({in, out});
|
|
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, env.journal);
|
|
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] == "0000000000000000");
|
|
BEAST_EXPECT(jrOffer[jss::Flags] == 0);
|
|
BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer);
|
|
BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0000000000000000");
|
|
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] == "0000000000000000");
|
|
BEAST_EXPECT(jrNextOffer[jss::Flags] == 0);
|
|
BEAST_EXPECT(jrNextOffer[sfLedgerEntryType.fieldName] == jss::Offer);
|
|
BEAST_EXPECT(jrNextOffer[sfOwnerNode.fieldName] == "0000000000000000");
|
|
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<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) == boost::none;
|
|
}
|
|
|
|
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_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.");
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
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)));
|
|
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] = 0u;
|
|
jrr =
|
|
env.rpc("json", "book_offers", to_string(jvParams)) [jss::result];
|
|
BEAST_EXPECT(jrr[jss::offers].isArray());
|
|
BEAST_EXPECT(jrr[jss::offers].size() == 0u);
|
|
|
|
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
|
|
run() override
|
|
{
|
|
testOneSideEmptyBook();
|
|
testOneSideOffersInBook();
|
|
testBothSidesEmptyBook();
|
|
testBothSidesOffersInBook();
|
|
testMultipleBooksOneSideEmptyBook();
|
|
testMultipleBooksOneSideOffersInBook();
|
|
testMultipleBooksBothSidesEmptyBook();
|
|
testMultipleBooksBothSidesOffersInBook();
|
|
testTrackOffers();
|
|
testCrossingSingleBookOffer();
|
|
testCrossingMultiBookOffer();
|
|
testBookOfferErrors();
|
|
testBookOfferLimits(true);
|
|
testBookOfferLimits(false);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE_PRIO(Book,app,ripple,1);
|
|
|
|
} // test
|
|
} // ripple
|
|
|