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:
CJ Cobb
2019-10-16 12:44:25 -07:00
parent 761bb5744e
commit 7d867b806d
112 changed files with 4428 additions and 296 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View 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

View 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
View 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

View File

@@ -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>