mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Add gRPC support (#3127):
* add support for AccountInfo, Fee and Submit RPCs * add partial support for Tx RPC (only supports Payments)
This commit is contained in:
@@ -221,9 +221,17 @@ public:
|
||||
auto& app = env.app();
|
||||
Resource::Charge loadType = Resource::feeReferenceRPC;
|
||||
Resource::Consumer c;
|
||||
RPC::Context context { env.journal, {}, app, loadType,
|
||||
app.getOPs(), app.getLedgerMaster(), c, Role::USER,
|
||||
RPC::APIVersionIfUnspecified};
|
||||
|
||||
RPC::JsonContext context{{env.journal,
|
||||
app,
|
||||
loadType,
|
||||
app.getOPs(),
|
||||
app.getLedgerMaster(),
|
||||
c,
|
||||
Role::USER},
|
||||
{},
|
||||
RPC::APIVersionIfUnspecified,
|
||||
{}};
|
||||
|
||||
Json::Value params = Json::objectValue;
|
||||
params[jss::command] = "ripple_path_find";
|
||||
@@ -321,9 +329,17 @@ public:
|
||||
auto& app = env.app();
|
||||
Resource::Charge loadType = Resource::feeReferenceRPC;
|
||||
Resource::Consumer c;
|
||||
RPC::Context context {env.journal, {}, app, loadType,
|
||||
app.getOPs(), app.getLedgerMaster(), c, Role::USER,
|
||||
RPC::APIVersionIfUnspecified};
|
||||
|
||||
RPC::JsonContext context{{env.journal,
|
||||
app,
|
||||
loadType,
|
||||
app.getOPs(),
|
||||
app.getLedgerMaster(),
|
||||
c,
|
||||
Role::USER},
|
||||
{},
|
||||
RPC::APIVersionIfUnspecified,
|
||||
{}};
|
||||
Json::Value result;
|
||||
gate g;
|
||||
// Test RPC::Tuning::max_src_cur source currencies.
|
||||
|
||||
@@ -115,9 +115,18 @@ validator(std::unique_ptr<Config>, std::string const&);
|
||||
std::unique_ptr<Config>
|
||||
port_increment(std::unique_ptr<Config>, int);
|
||||
|
||||
} // jtx
|
||||
} // test
|
||||
} // ripple
|
||||
/// @brief add a grpc address and port to config
|
||||
///
|
||||
/// This is intended for use with envconfig, for tests that require a grpc
|
||||
/// server. If this function is not called, grpc server will not start
|
||||
///
|
||||
///
|
||||
/// @param cfg config instance to be modified
|
||||
std::unique_ptr<Config> addGrpcConfig(std::unique_ptr<Config>);
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -108,6 +108,15 @@ port_increment(std::unique_ptr<Config> cfg, int increment)
|
||||
return cfg;
|
||||
}
|
||||
|
||||
} // jtx
|
||||
} // test
|
||||
} // ripple
|
||||
std::unique_ptr<Config>
|
||||
addGrpcConfig(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
std::string port_grpc = std::to_string(port_base + 3);
|
||||
(*cfg)["port_grpc"].set("ip", getEnvLocalhostAddr());
|
||||
(*cfg)["port_grpc"].set("port", port_grpc);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <ripple/resource/Charge.h>
|
||||
#include <ripple/resource/Fees.h>
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
@@ -314,16 +320,232 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void run() override
|
||||
// gRPC stuff
|
||||
class GetAccountInfoClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
rpc::v1::GetAccountInfoRequest request;
|
||||
rpc::v1::GetAccountInfoResponse reply;
|
||||
|
||||
explicit GetAccountInfoClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
GetAccountInfo()
|
||||
{
|
||||
status = stub_->GetAccountInfo(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testSimpleGrpc()
|
||||
{
|
||||
testcase("gRPC simple");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
Account const alice{"alice"};
|
||||
env.fund(drops(1000 * 1000 * 1000), alice);
|
||||
|
||||
{
|
||||
// most simple case
|
||||
GetAccountInfoClient client(grpcPort);
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
std::cout << client.reply.DebugString() << std::endl;
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().account().address() ==
|
||||
alice.human());
|
||||
}
|
||||
{
|
||||
GetAccountInfoClient client(grpcPort);
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_queue(true);
|
||||
client.request.mutable_ledger()->set_sequence(3);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().balance().drops() ==
|
||||
1000 * 1000 * 1000);
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().account().address() ==
|
||||
alice.human());
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().sequence() == env.seq(alice));
|
||||
BEAST_EXPECT(client.reply.queue_data().txn_count() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testErrorsGrpc()
|
||||
{
|
||||
testcase("gRPC errors");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
auto getClient = [&grpcPort]() {
|
||||
return GetAccountInfoClient(grpcPort);
|
||||
};
|
||||
Account const alice{"alice"};
|
||||
env.fund(drops(1000 * 1000 * 1000), alice);
|
||||
|
||||
{
|
||||
// bad address
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address("deadbeef");
|
||||
client.GetAccountInfo();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
{
|
||||
// no account
|
||||
Account const bogie{"bogie"};
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(bogie.human());
|
||||
client.GetAccountInfo();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
{
|
||||
// bad ledger_index
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.mutable_ledger()->set_sequence(0);
|
||||
client.GetAccountInfo();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSignerListsGrpc()
|
||||
{
|
||||
testcase("gRPC singer lists");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
auto getClient = [&grpcPort]() {
|
||||
return GetAccountInfoClient(grpcPort);
|
||||
};
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(drops(1000 * 1000 * 1000), alice);
|
||||
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(true);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
return;
|
||||
BEAST_EXPECT(client.reply.signer_list().signer_entries_size() == 0);
|
||||
}
|
||||
|
||||
// Give alice a SignerList.
|
||||
Account const bogie{"bogie"};
|
||||
Json::Value const smallSigners = signers(alice, 2, {{bogie, 3}});
|
||||
env(smallSigners);
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(false);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
return;
|
||||
BEAST_EXPECT(client.reply.signer_list().signer_entries_size() == 0);
|
||||
}
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(true);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.account_data().owner_count() == 1);
|
||||
BEAST_EXPECT(client.reply.signer_list().signer_entries_size() == 1);
|
||||
}
|
||||
|
||||
// Give alice a big signer list
|
||||
Account const demon{"demon"};
|
||||
Account const ghost{"ghost"};
|
||||
Account const haunt{"haunt"};
|
||||
Account const jinni{"jinni"};
|
||||
Account const phase{"phase"};
|
||||
Account const shade{"shade"};
|
||||
Account const spook{"spook"};
|
||||
Json::Value const bigSigners = signers(
|
||||
alice,
|
||||
4,
|
||||
{
|
||||
{bogie, 1},
|
||||
{demon, 1},
|
||||
{ghost, 1},
|
||||
{haunt, 1},
|
||||
{jinni, 1},
|
||||
{phase, 1},
|
||||
{shade, 1},
|
||||
{spook, 1},
|
||||
});
|
||||
env(bigSigners);
|
||||
|
||||
std::set<std::string> accounts;
|
||||
accounts.insert(bogie.human());
|
||||
accounts.insert(demon.human());
|
||||
accounts.insert(ghost.human());
|
||||
accounts.insert(haunt.human());
|
||||
accounts.insert(jinni.human());
|
||||
accounts.insert(phase.human());
|
||||
accounts.insert(shade.human());
|
||||
accounts.insert(spook.human());
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(true);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.account_data().owner_count() == 1);
|
||||
auto& signerList = client.reply.signer_list();
|
||||
BEAST_EXPECT(signerList.signer_quorum() == 4);
|
||||
BEAST_EXPECT(signerList.signer_entries_size() == 8);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
BEAST_EXPECT(signerList.signer_entries(i).signer_weight() == 1);
|
||||
BEAST_EXPECT(
|
||||
accounts.erase(
|
||||
signerList.signer_entries(i).account().address()) == 1);
|
||||
}
|
||||
BEAST_EXPECT(accounts.size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testErrors();
|
||||
testSignerLists();
|
||||
testSignerListsV2();
|
||||
testSimpleGrpc();
|
||||
testErrorsGrpc();
|
||||
testSignerListsGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(AccountInfo,app,ripple);
|
||||
|
||||
}
|
||||
}
|
||||
BEAST_DEFINE_TESTSUITE(AccountInfo, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
143
src/test/rpc/Fee_test.cpp
Normal file
143
src/test/rpc/Fee_test.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 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/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/basics/mulDiv.h>
|
||||
#include <ripple/core/DatabaseCon.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Fee_test : public beast::unit_test::suite
|
||||
{
|
||||
class GrpcFeeClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
rpc::v1::GetFeeRequest request;
|
||||
rpc::v1::GetFeeResponse reply;
|
||||
|
||||
explicit GrpcFeeClient(std::string const& grpcPort)
|
||||
: GRPCTestClientBase(grpcPort)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
GetFee()
|
||||
{
|
||||
status = stub_->GetFee(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
std::pair<bool, rpc::v1::GetFeeResponse>
|
||||
grpcGetFee(std::string const& grpcPort)
|
||||
{
|
||||
GrpcFeeClient client(grpcPort);
|
||||
client.GetFee();
|
||||
return std::pair<bool, rpc::v1::GetFeeResponse>(
|
||||
client.status.ok(), client.reply);
|
||||
}
|
||||
|
||||
void
|
||||
testFeeGrpc()
|
||||
{
|
||||
testcase("Test Fee Grpc");
|
||||
|
||||
using namespace test::jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
env.fund(XRP(10000), A1);
|
||||
env.fund(XRP(10000), A2);
|
||||
env.close();
|
||||
env.trust(A2["USD"](1000), A1);
|
||||
env.close();
|
||||
for (int i = 0; i < 7; ++i)
|
||||
{
|
||||
env(pay(A2, A1, A2["USD"](100)));
|
||||
if (i == 4)
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto view = env.current();
|
||||
|
||||
auto const metrics = env.app().getTxQ().getMetrics(*env.current());
|
||||
|
||||
auto const result = grpcGetFee(grpcPort);
|
||||
|
||||
BEAST_EXPECT(result.first == true);
|
||||
|
||||
auto reply = result.second;
|
||||
|
||||
// current ledger data
|
||||
BEAST_EXPECT(reply.current_ledger_size() == metrics.txInLedger);
|
||||
BEAST_EXPECT(reply.current_queue_size() == metrics.txCount);
|
||||
BEAST_EXPECT(reply.expected_ledger_size() == metrics.txPerLedger);
|
||||
BEAST_EXPECT(reply.ledger_current_index() == view->info().seq);
|
||||
BEAST_EXPECT(reply.max_queue_size() == *metrics.txQMaxSize);
|
||||
|
||||
// fee levels data
|
||||
rpc::v1::FeeLevels& levels = *reply.mutable_levels();
|
||||
BEAST_EXPECT(levels.median_level() == metrics.medFeeLevel);
|
||||
BEAST_EXPECT(levels.minimum_level() == metrics.minProcessingFeeLevel);
|
||||
BEAST_EXPECT(levels.open_ledger_level() == metrics.openLedgerFeeLevel);
|
||||
BEAST_EXPECT(levels.reference_level() == metrics.referenceFeeLevel);
|
||||
|
||||
// fee data
|
||||
rpc::v1::Fee& drops = *reply.mutable_drops();
|
||||
auto const baseFee = view->fees().base;
|
||||
BEAST_EXPECT(
|
||||
drops.base_fee().drops() ==
|
||||
toDrops(metrics.referenceFeeLevel, baseFee).second);
|
||||
BEAST_EXPECT(
|
||||
drops.minimum_fee().drops() ==
|
||||
toDrops(metrics.minProcessingFeeLevel, baseFee).second);
|
||||
BEAST_EXPECT(
|
||||
drops.median_fee().drops() ==
|
||||
toDrops(metrics.medFeeLevel, baseFee).second);
|
||||
auto openLedgerFee =
|
||||
toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee)
|
||||
.second +
|
||||
1;
|
||||
BEAST_EXPECT(drops.open_ledger_fee().drops() == openLedgerFee.drops());
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testFeeGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Fee, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
48
src/test/rpc/GRPCTestClientBase.h
Normal file
48
src/test/rpc/GRPCTestClientBase.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLED_GRPCTESTCLIENTBASE_H
|
||||
#define RIPPLED_GRPCTESTCLIENTBASE_H
|
||||
|
||||
#include <rpc/v1/xrp_ledger.grpc.pb.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
struct GRPCTestClientBase
|
||||
{
|
||||
explicit GRPCTestClientBase(std::string const& port)
|
||||
: stub_(rpc::v1::XRPLedgerAPIService::NewStub(grpc::CreateChannel(
|
||||
beast::IP::Endpoint(
|
||||
boost::asio::ip::make_address(getEnvLocalhostAddr()),
|
||||
std::stoi(port))
|
||||
.to_string(),
|
||||
grpc::InsecureChannelCredentials())))
|
||||
{
|
||||
}
|
||||
|
||||
grpc::Status status;
|
||||
grpc::ClientContext context;
|
||||
std::unique_ptr<rpc::v1::XRPLedgerAPIService::Stub> stub_;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
#endif // RIPPLED_GRPCTESTCLIENTBASE_H
|
||||
281
src/test/rpc/Submit_test.cpp
Normal file
281
src/test/rpc/Submit_test.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 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/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
|
||||
#include <ripple/resource/Charge.h>
|
||||
#include <ripple/resource/Fees.h>
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Submit_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
class SubmitClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
rpc::v1::SubmitTransactionRequest request;
|
||||
rpc::v1::SubmitTransactionResponse reply;
|
||||
|
||||
explicit SubmitClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
SubmitTransaction()
|
||||
{
|
||||
status = stub_->SubmitTransaction(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
struct TestData
|
||||
{
|
||||
std::string xrpTxBlob;
|
||||
std::string xrpTxHash;
|
||||
std::string usdTxBlob;
|
||||
std::string usdTxHash;
|
||||
const static int fund = 10000;
|
||||
} testData;
|
||||
|
||||
void
|
||||
fillTestData()
|
||||
{
|
||||
testcase("fill test data");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(addGrpcConfig));
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
env.fund(XRP(TestData::fund), "alice", "bob");
|
||||
env.trust(bob["USD"](TestData::fund), alice);
|
||||
env.close();
|
||||
|
||||
auto toBinary = [](std::string const& text) {
|
||||
std::string binary;
|
||||
for (size_t i = 0; i < text.size(); ++i)
|
||||
{
|
||||
unsigned int c = charUnHex(text[i]);
|
||||
c = c << 4;
|
||||
++i;
|
||||
c = c | charUnHex(text[i]);
|
||||
binary.push_back(c);
|
||||
}
|
||||
|
||||
return binary;
|
||||
};
|
||||
|
||||
// use a websocket client to fill transaction blobs
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
{
|
||||
Json::Value jrequestXrp;
|
||||
jrequestXrp[jss::secret] = toBase58(generateSeed("alice"));
|
||||
jrequestXrp[jss::tx_json] =
|
||||
pay("alice", "bob", XRP(TestData::fund / 2));
|
||||
Json::Value jreply_xrp = wsc->invoke("sign", jrequestXrp);
|
||||
|
||||
if (!BEAST_EXPECT(jreply_xrp.isMember(jss::result)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(jreply_xrp[jss::result].isMember(jss::tx_blob)))
|
||||
return;
|
||||
testData.xrpTxBlob =
|
||||
toBinary(jreply_xrp[jss::result][jss::tx_blob].asString());
|
||||
if (!BEAST_EXPECT(jreply_xrp[jss::result].isMember(jss::tx_json)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(
|
||||
jreply_xrp[jss::result][jss::tx_json].isMember(jss::hash)))
|
||||
return;
|
||||
testData.xrpTxHash = toBinary(
|
||||
jreply_xrp[jss::result][jss::tx_json][jss::hash].asString());
|
||||
}
|
||||
{
|
||||
Json::Value jrequestUsd;
|
||||
jrequestUsd[jss::secret] = toBase58(generateSeed("bob"));
|
||||
jrequestUsd[jss::tx_json] =
|
||||
pay("bob", "alice", bob["USD"](TestData::fund / 2));
|
||||
Json::Value jreply_usd = wsc->invoke("sign", jrequestUsd);
|
||||
|
||||
if (!BEAST_EXPECT(jreply_usd.isMember(jss::result)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(jreply_usd[jss::result].isMember(jss::tx_blob)))
|
||||
return;
|
||||
testData.usdTxBlob =
|
||||
toBinary(jreply_usd[jss::result][jss::tx_blob].asString());
|
||||
if (!BEAST_EXPECT(jreply_usd[jss::result].isMember(jss::tx_json)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(
|
||||
jreply_usd[jss::result][jss::tx_json].isMember(jss::hash)))
|
||||
return;
|
||||
testData.usdTxHash = toBinary(
|
||||
jreply_usd[jss::result][jss::tx_json][jss::hash].asString());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubmitGoodBlobGrpc()
|
||||
{
|
||||
testcase("Submit good blobs, XRP, USD, and same transaction twice");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
env.fund(XRP(TestData::fund), "alice", "bob");
|
||||
env.trust(bob["USD"](TestData::fund), alice);
|
||||
env.close();
|
||||
|
||||
auto getClient = [&grpcPort]() { return SubmitClient(grpcPort); };
|
||||
|
||||
// XRP
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.xrpTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.engine_result().result() == "tesSUCCESS");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == 0);
|
||||
BEAST_EXPECT(client.reply.hash() == testData.xrpTxHash);
|
||||
}
|
||||
// USD
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.usdTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.engine_result().result() == "tesSUCCESS");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == 0);
|
||||
BEAST_EXPECT(client.reply.hash() == testData.usdTxHash);
|
||||
}
|
||||
// USD, error, same transaction again
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.usdTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.engine_result().result() == "tefALREADY");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == -198);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubmitErrorBlobGrpc()
|
||||
{
|
||||
testcase("Submit error, bad blob, no account");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
|
||||
auto getClient = [&grpcPort]() { return SubmitClient(grpcPort); };
|
||||
|
||||
// short transaction blob, cannot parse
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction("deadbeef");
|
||||
client.SubmitTransaction();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
// bad blob with correct length, cannot parse
|
||||
{
|
||||
auto client = getClient();
|
||||
auto xrpTxBlobCopy(testData.xrpTxBlob);
|
||||
std::reverse(xrpTxBlobCopy.begin(), xrpTxBlobCopy.end());
|
||||
client.request.set_signed_transaction(xrpTxBlobCopy);
|
||||
client.SubmitTransaction();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
// good blob, can parse but no account
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.xrpTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.engine_result().result() == "terNO_ACCOUNT");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == -96);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubmitInsufficientFundsGrpc()
|
||||
{
|
||||
testcase("Submit good blobs but insufficient funds");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
// fund 1000 (TestData::fund/10) XRP, the transaction sends 5000
|
||||
// (TestData::fund/2) XRP, so insufficient funds
|
||||
env.fund(XRP(TestData::fund / 10), "alice", "bob");
|
||||
env.trust(bob["USD"](TestData::fund), alice);
|
||||
env.close();
|
||||
|
||||
{
|
||||
SubmitClient client(grpcPort);
|
||||
client.request.set_signed_transaction(testData.xrpTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.engine_result().result() == "tecUNFUNDED_PAYMENT");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == 104);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
fillTestData();
|
||||
testSubmitGoodBlobGrpc();
|
||||
testSubmitErrorBlobGrpc();
|
||||
testSubmitInsufficientFundsGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Submit, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
314
src/test/rpc/Tx_test.cpp
Normal file
314
src/test/rpc/Tx_test.cpp
Normal file
@@ -0,0 +1,314 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 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/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/basics/mulDiv.h>
|
||||
#include <ripple/core/DatabaseCon.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Tx_test : public beast::unit_test::suite
|
||||
{
|
||||
template <class T>
|
||||
std::string
|
||||
toByteString(T const& data)
|
||||
{
|
||||
const char* bytes = reinterpret_cast<const char*>(data.data());
|
||||
return {bytes, data.size()};
|
||||
}
|
||||
|
||||
void
|
||||
cmpAmount(const rpc::v1::CurrencyAmount& proto_amount, STAmount amount)
|
||||
{
|
||||
if (amount.native())
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
proto_amount.xrp_amount().drops() == amount.xrp().drops());
|
||||
}
|
||||
else
|
||||
{
|
||||
rpc::v1::IssuedCurrencyAmount issuedCurrency =
|
||||
proto_amount.issued_currency_amount();
|
||||
Issue const& issue = amount.issue();
|
||||
Currency currency = issue.currency;
|
||||
BEAST_EXPECT(
|
||||
issuedCurrency.currency().name() == to_string(currency));
|
||||
BEAST_EXPECT(
|
||||
issuedCurrency.currency().code() == toByteString(currency));
|
||||
BEAST_EXPECT(issuedCurrency.value() == to_string(amount.iou()));
|
||||
BEAST_EXPECT(
|
||||
issuedCurrency.issuer().address() == toBase58(issue.account));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
cmpTx(const rpc::v1::Transaction& proto, std::shared_ptr<STTx const> txnSt)
|
||||
{
|
||||
AccountID account = txnSt->getAccountID(sfAccount);
|
||||
BEAST_EXPECT(proto.account().address() == toBase58(account));
|
||||
|
||||
STAmount amount = txnSt->getFieldAmount(sfAmount);
|
||||
cmpAmount(proto.payment().amount(), amount);
|
||||
|
||||
AccountID accountDest = txnSt->getAccountID(sfDestination);
|
||||
BEAST_EXPECT(
|
||||
proto.payment().destination().address() == toBase58(accountDest));
|
||||
|
||||
STAmount fee = txnSt->getFieldAmount(sfFee);
|
||||
BEAST_EXPECT(proto.fee().drops() == fee.xrp().drops());
|
||||
|
||||
BEAST_EXPECT(proto.sequence() == txnSt->getFieldU32(sfSequence));
|
||||
|
||||
Blob signingPubKey = txnSt->getFieldVL(sfSigningPubKey);
|
||||
BEAST_EXPECT(proto.signing_public_key() == toByteString(signingPubKey));
|
||||
|
||||
BEAST_EXPECT(proto.flags() == txnSt->getFieldU32(sfFlags));
|
||||
|
||||
BEAST_EXPECT(
|
||||
proto.last_ledger_sequence() ==
|
||||
txnSt->getFieldU32(sfLastLedgerSequence));
|
||||
|
||||
Blob blob = txnSt->getFieldVL(sfTxnSignature);
|
||||
BEAST_EXPECT(proto.signature() == toByteString(blob));
|
||||
|
||||
if (txnSt->isFieldPresent(sfSendMax))
|
||||
{
|
||||
STAmount const& send_max = txnSt->getFieldAmount(sfSendMax);
|
||||
cmpAmount(proto.payment().send_max(), send_max);
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfAccountTxnID))
|
||||
{
|
||||
auto field = txnSt->getFieldH256(sfAccountTxnID);
|
||||
BEAST_EXPECT(proto.account_transaction_id() == toByteString(field));
|
||||
}
|
||||
|
||||
// populate path data
|
||||
STPathSet const& pathset = txnSt->getFieldPathSet(sfPaths);
|
||||
int ind = 0;
|
||||
for (auto it = pathset.begin(); it < pathset.end(); ++it)
|
||||
{
|
||||
STPath const& path = *it;
|
||||
|
||||
const rpc::v1::Path& protoPath = proto.payment().paths(ind++);
|
||||
|
||||
int ind2 = 0;
|
||||
for (auto it2 = path.begin(); it2 != path.end(); ++it2)
|
||||
{
|
||||
const rpc::v1::PathElement& protoElement =
|
||||
protoPath.elements(ind2++);
|
||||
STPathElement const& elt = *it2;
|
||||
|
||||
if (elt.isOffer())
|
||||
{
|
||||
if (elt.hasCurrency())
|
||||
{
|
||||
Currency const& currency = elt.getCurrency();
|
||||
BEAST_EXPECT(
|
||||
protoElement.currency().name() ==
|
||||
to_string(currency));
|
||||
}
|
||||
if (elt.hasIssuer())
|
||||
{
|
||||
AccountID const& issuer = elt.getIssuerID();
|
||||
BEAST_EXPECT(
|
||||
protoElement.issuer().address() ==
|
||||
toBase58(issuer));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AccountID const& path_account = elt.getAccountID();
|
||||
BEAST_EXPECT(
|
||||
protoElement.account().address() ==
|
||||
toBase58(path_account));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
cmpMeta(const rpc::v1::Meta& proto, std::shared_ptr<TxMeta> txMeta)
|
||||
{
|
||||
BEAST_EXPECT(proto.transaction_index() == txMeta->getIndex());
|
||||
BEAST_EXPECT(
|
||||
proto.transaction_result().result() ==
|
||||
transToken(txMeta->getResultTER()));
|
||||
|
||||
rpc::v1::TransactionResult r;
|
||||
|
||||
RPC::populateTransactionResultType(r, txMeta->getResultTER());
|
||||
|
||||
BEAST_EXPECT(
|
||||
proto.transaction_result().result_type() == r.result_type());
|
||||
|
||||
if (txMeta->hasDeliveredAmount())
|
||||
{
|
||||
cmpAmount(proto.delivered_amount(), txMeta->getDeliveredAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// gRPC stuff
|
||||
class GrpcTxClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
rpc::v1::GetTxRequest request;
|
||||
rpc::v1::GetTxResponse reply;
|
||||
|
||||
explicit GrpcTxClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Tx()
|
||||
{
|
||||
status = stub_->GetTx(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testTxGrpc()
|
||||
{
|
||||
testcase("Test Tx Grpc");
|
||||
|
||||
using namespace test::jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
|
||||
auto grpcTx = [&grpcPort](auto hash, auto binary) {
|
||||
GrpcTxClient client(grpcPort);
|
||||
client.request.set_hash(&hash, sizeof(hash));
|
||||
client.request.set_binary(binary);
|
||||
client.Tx();
|
||||
return std::pair<bool, rpc::v1::GetTxResponse>(
|
||||
client.status.ok(), client.reply);
|
||||
};
|
||||
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
env.fund(XRP(10000), A1);
|
||||
env.fund(XRP(10000), A2);
|
||||
env.close();
|
||||
env.trust(A2["USD"](1000), A1);
|
||||
env.close();
|
||||
std::vector<std::shared_ptr<STTx const>> txns;
|
||||
auto const startLegSeq = env.current()->info().seq;
|
||||
for (int i = 0; i < 14; ++i)
|
||||
{
|
||||
if (i & 1)
|
||||
env(pay(A2, A1, A2["USD"](100)));
|
||||
else
|
||||
env(pay(A2, A1, A2["XRP"](200)));
|
||||
txns.emplace_back(env.tx());
|
||||
env.close();
|
||||
}
|
||||
auto const endLegSeq = env.closed()->info().seq;
|
||||
|
||||
// Find the existing transactions
|
||||
auto& ledgerMaster = env.app().getLedgerMaster();
|
||||
int index = startLegSeq;
|
||||
for (auto&& tx : txns)
|
||||
{
|
||||
auto id = tx->getTransactionID();
|
||||
auto ledger = ledgerMaster.getLedgerBySeq(index);
|
||||
|
||||
for (bool b : {false, true})
|
||||
{
|
||||
auto const result = grpcTx(id, b);
|
||||
|
||||
BEAST_EXPECT(result.first == true);
|
||||
BEAST_EXPECT(result.second.ledger_index() == index);
|
||||
BEAST_EXPECT(result.second.validated() == true);
|
||||
if (b)
|
||||
{
|
||||
Serializer s = tx->getSerializer();
|
||||
BEAST_EXPECT(
|
||||
result.second.transaction_binary() == toByteString(s));
|
||||
}
|
||||
else
|
||||
{
|
||||
cmpTx(result.second.transaction(), tx);
|
||||
}
|
||||
|
||||
if (ledger && !b)
|
||||
{
|
||||
auto rawMeta = ledger->txRead(id).second;
|
||||
if (rawMeta)
|
||||
{
|
||||
auto txMeta = std::make_shared<TxMeta>(
|
||||
id, ledger->seq(), *rawMeta);
|
||||
|
||||
cmpMeta(result.second.meta(), txMeta);
|
||||
}
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// Find not existing transaction
|
||||
auto const tx = env.jt(noop(A1), seq(env.seq(A1))).stx;
|
||||
for (bool b : {false, true})
|
||||
{
|
||||
auto const result = grpcTx(tx->getTransactionID(), b);
|
||||
|
||||
BEAST_EXPECT(result.first == false);
|
||||
}
|
||||
|
||||
// Delete one transaction
|
||||
const auto deletedLedger = (startLegSeq + endLegSeq) / 2;
|
||||
{
|
||||
// Remove one of the ledgers from the database directly
|
||||
auto db = env.app().getTxnDB().checkoutDb();
|
||||
*db << "DELETE FROM Transactions WHERE LedgerSeq == "
|
||||
<< deletedLedger << ";";
|
||||
}
|
||||
|
||||
for (bool b : {false, true})
|
||||
{
|
||||
auto const result = grpcTx(tx->getTransactionID(), b);
|
||||
|
||||
BEAST_EXPECT(result.first == false);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testTxGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Tx, app, ripple);
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <test/rpc/DepositAuthorized_test.cpp>
|
||||
#include <test/rpc/DeliveredAmount_test.cpp>
|
||||
#include <test/rpc/Feature_test.cpp>
|
||||
#include <test/rpc/Fee_test.cpp>
|
||||
#include <test/rpc/GatewayBalances_test.cpp>
|
||||
#include <test/rpc/GetCounts_test.cpp>
|
||||
#include <test/rpc/JSONRPC_test.cpp>
|
||||
@@ -48,9 +49,11 @@
|
||||
#include <test/rpc/RPCOverload_test.cpp>
|
||||
#include <test/rpc/ServerInfo_test.cpp>
|
||||
#include <test/rpc/Status_test.cpp>
|
||||
#include <test/rpc/Submit_test.cpp>
|
||||
#include <test/rpc/Subscribe_test.cpp>
|
||||
#include <test/rpc/Transaction_test.cpp>
|
||||
#include <test/rpc/TransactionEntry_test.cpp>
|
||||
#include <test/rpc/TransactionHistory_test.cpp>
|
||||
#include <test/rpc/Tx_test.cpp>
|
||||
#include <test/rpc/ValidatorRPC_test.cpp>
|
||||
#include <test/rpc/Version_test.cpp>
|
||||
|
||||
Reference in New Issue
Block a user