Gateway balance (#536)

Fixes #538
This commit is contained in:
cyan317
2023-03-14 14:21:28 +00:00
committed by GitHub
parent 7a819f4955
commit b99a68e55f
4 changed files with 1104 additions and 1 deletions

View File

@@ -60,6 +60,7 @@ target_sources(clio PRIVATE
src/rpc/ngHandlers/AccountChannels.cpp
src/rpc/ngHandlers/AccountCurrencies.cpp
src/rpc/ngHandlers/Tx.cpp
src/rpc/ngHandlers/GatewayBalances.cpp
## RPC Methods
# Account
src/rpc/handlers/AccountChannels.cpp
@@ -122,7 +123,8 @@ if(BUILD_TESTS)
unittests/rpc/handlers/DefaultProcessorTests.cpp
unittests/rpc/handlers/PingTest.cpp
unittests/rpc/handlers/AccountChannelsTest.cpp
unittests/rpc/handlers/TxTest.cpp)
unittests/rpc/handlers/TxTest.cpp
unittests/rpc/handlers/GatewayBalancesTest.cpp)
include(CMake/deps/gtest.cmake)
# if CODE_COVERAGE enable, add clio_test-ccov

View File

@@ -0,0 +1,246 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and 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 <rpc/RPCHelpers.h>
#include <rpc/ngHandlers/GatewayBalances.h>
namespace RPCng {
GatewayBalancesHandler::Result
GatewayBalancesHandler::process(
GatewayBalancesHandler::Input input,
boost::asio::yield_context& yield) const
{
// check ledger
auto const range = sharedPtrBackend_->fetchLedgerRange();
auto const lgrInfoOrStatus = RPC::getLedgerInfoFromHashOrSeq(
*sharedPtrBackend_,
yield,
input.ledgerHash,
input.ledgerIndex,
range->maxSequence);
if (auto const status = std::get_if<RPC::Status>(&lgrInfoOrStatus))
return Error{*status};
// check account
auto const lgrInfo = std::get<ripple::LedgerInfo>(lgrInfoOrStatus);
auto const accountID = RPC::accountFromStringStrict(input.account);
auto const accountLedgerObject = sharedPtrBackend_->fetchLedgerObject(
ripple::keylet::account(*accountID).key, lgrInfo.seq, yield);
if (!accountLedgerObject)
return Error{RPC::Status{
RPC::RippledError::rpcACT_NOT_FOUND, "accountNotFound"}};
GatewayBalancesHandler::Output output;
auto const addToResponse = [&](ripple::SLE&& sle) {
if (sle.getType() == ripple::ltRIPPLE_STATE)
{
ripple::STAmount balance = sle.getFieldAmount(ripple::sfBalance);
auto const lowLimit = sle.getFieldAmount(ripple::sfLowLimit);
auto const highLimit = sle.getFieldAmount(ripple::sfHighLimit);
auto const lowID = lowLimit.getIssuer();
auto const highID = highLimit.getIssuer();
auto const viewLowest = (lowLimit.getIssuer() == accountID);
auto const flags = sle.getFieldU32(ripple::sfFlags);
auto const freeze = flags &
(viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze);
if (!viewLowest)
balance.negate();
auto const balSign = balance.signum();
if (balSign == 0)
return true;
auto const& peer = !viewLowest ? lowID : highID;
// Here, a negative balance means the cold wallet owes (normal)
// A positive balance means the cold wallet has an asset
// (unusual)
if (input.hotWallets.count(peer) > 0)
{
// This is a specified hot wallet
output.hotBalances[peer].push_back(-balance);
}
else if (balSign > 0)
{
// This is a gateway asset
output.assets[peer].push_back(balance);
}
else if (freeze)
{
// An obligation the gateway has frozen
output.frozenBalances[peer].push_back(-balance);
}
else
{
// normal negative balance, obligation to customer
auto& bal = output.sums[balance.getCurrency()];
if (bal == beast::zero)
{
// This is needed to set the currency code correctly
bal = -balance;
}
else
{
try
{
bal -= balance;
}
catch (std::runtime_error const& e)
{
output.overflow = true;
}
}
}
}
return true;
};
// traverse all owned nodes, limit->max, marker->empty
auto const ret = RPC::ngTraverseOwnedNodes(
*sharedPtrBackend_,
*accountID,
lgrInfo.seq,
std::numeric_limits<std::uint32_t>::max(),
{},
yield,
addToResponse);
if (auto status = std::get_if<RPC::Status>(&ret))
return Error{*status};
if (not std::all_of(
input.hotWallets.begin(),
input.hotWallets.end(),
[&](auto const& hw) { return output.hotBalances.contains(hw); }))
return Error{RPC::Status{
RPC::RippledError::rpcINVALID_PARAMS, "invalidHotWallet"}};
output.accountID = input.account;
output.ledgerHash = ripple::strHex(lgrInfo.hash);
output.ledgerIndex = lgrInfo.seq;
return output;
}
void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
GatewayBalancesHandler::Output const& output)
{
boost::json::object obj;
if (!output.sums.empty())
{
boost::json::object obligations;
for (auto const& [k, v] : output.sums)
{
obligations[ripple::to_string(k)] = v.getText();
}
obj["obligations"] = std::move(obligations);
}
auto const toJson =
[](std::map<ripple::AccountID, std::vector<ripple::STAmount>> const&
balances) {
boost::json::object balancesObj;
if (!balances.empty())
{
for (auto const& [accId, accBalances] : balances)
{
boost::json::array arr;
for (auto const& balance : accBalances)
{
boost::json::object entry;
entry[JS(currency)] =
ripple::to_string(balance.issue().currency);
entry[JS(value)] = balance.getText();
arr.push_back(std::move(entry));
}
balancesObj[ripple::to_string(accId)] = std::move(arr);
}
}
return balancesObj;
};
if (auto balances = toJson(output.hotBalances); balances.size())
obj["balances"] = balances;
// we don't have frozen_balances field in the
// document:https://xrpl.org/gateway_balances.html#gateway_balances
if (auto balances = toJson(output.frozenBalances); balances.size())
obj["frozen_balances"] = balances;
if (auto balances = toJson(output.assets); balances.size())
obj["assets"] = balances;
obj["account"] = output.accountID;
obj["ledger_index"] = output.ledgerIndex;
obj["ledger_hash"] = output.ledgerHash;
if (output.overflow)
obj["overflow"] = true;
jv = std::move(obj);
}
GatewayBalancesHandler::Input
tag_invoke(
boost::json::value_to_tag<GatewayBalancesHandler::Input>,
boost::json::value const& jv)
{
auto const& jsonObject = jv.as_object();
GatewayBalancesHandler::Input input;
input.account = jv.at("account").as_string().c_str();
if (jsonObject.contains("ledger_hash"))
{
input.ledgerHash = jv.at("ledger_hash").as_string().c_str();
}
if (jsonObject.contains("ledger_index"))
{
if (!jsonObject.at("ledger_index").is_string())
{
input.ledgerIndex = jv.at("ledger_index").as_int64();
}
else if (jsonObject.at("ledger_index").as_string() != "validated")
{
input.ledgerIndex =
std::stoi(jv.at("ledger_index").as_string().c_str());
}
}
if (jsonObject.contains("hotwallet"))
{
if (jsonObject.at("hotwallet").is_string())
{
input.hotWallets.insert(*RPC::accountFromStringStrict(
jv.at("hotwallet").as_string().c_str()));
}
else
{
auto const& hotWallets = jv.at("hotwallet").as_array();
std::transform(
hotWallets.begin(),
hotWallets.end(),
std::inserter(input.hotWallets, input.hotWallets.begin()),
[](auto const& hotWallet) {
return *RPC::accountFromStringStrict(
hotWallet.as_string().c_str());
});
}
}
return input;
}
} // namespace RPCng

View File

@@ -0,0 +1,128 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and 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.
*/
//==============================================================================
#pragma once
#include <backend/BackendInterface.h>
#include <rpc/common/Types.h>
#include <rpc/common/Validators.h>
#include <boost/asio/spawn.hpp>
namespace RPCng {
class GatewayBalancesHandler
{
std::shared_ptr<BackendInterface> sharedPtrBackend_;
public:
struct Output
{
std::string ledgerHash;
uint32_t ledgerIndex;
std::string accountID;
bool overflow = false;
std::map<ripple::Currency, ripple::STAmount> sums;
std::map<ripple::AccountID, std::vector<ripple::STAmount>> hotBalances;
std::map<ripple::AccountID, std::vector<ripple::STAmount>> assets;
std::map<ripple::AccountID, std::vector<ripple::STAmount>>
frozenBalances;
// validated should be sent via framework
bool validated = true;
};
// TODO:we did not implement the "strict" field
struct Input
{
std::string account;
std::set<ripple::AccountID> hotWallets;
std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex;
};
using Result = RPCng::HandlerReturnType<Output>;
GatewayBalancesHandler(
std::shared_ptr<BackendInterface> const& sharedPtrBackend)
: sharedPtrBackend_(sharedPtrBackend)
{
}
RpcSpecConstRef
spec() const
{
static auto const hotWalletValidator = validation::CustomValidator{
[](boost::json::value const& value,
std::string_view key) -> MaybeError {
if (!value.is_string() && !value.is_array())
{
return Error{RPC::Status{
RPC::RippledError::rpcINVALID_PARAMS,
std::string(key) + "NotStringOrArray"}};
}
// wallet needs to be an valid accountID or public key
auto const wallets = value.is_array()
? value.as_array()
: boost::json::array{value};
auto const getAccountID =
[](auto const& j) -> std::optional<ripple::AccountID> {
if (j.is_string())
{
auto const pk = ripple::parseBase58<ripple::PublicKey>(
ripple::TokenType::AccountPublic,
j.as_string().c_str());
if (pk)
return ripple::calcAccountID(*pk);
return ripple::parseBase58<ripple::AccountID>(
j.as_string().c_str());
}
return {};
};
for (auto const& wallet : wallets)
{
if (!getAccountID(wallet))
return Error{RPC::Status{
RPC::RippledError::rpcINVALID_PARAMS,
std::string(key) + "Malformed"}};
}
return MaybeError{};
}};
static const RpcSpec rpcSpec = {
{"account", validation::Required{}, validation::AccountValidator},
{"ledger_hash", validation::LedgerHashValidator},
{"ledger_index", validation::LedgerIndexValidator},
{"hotwallet", hotWalletValidator}};
return rpcSpec;
}
Result
process(Input input, boost::asio::yield_context& yield) const;
};
void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
GatewayBalancesHandler::Output const& output);
GatewayBalancesHandler::Input
tag_invoke(
boost::json::value_to_tag<GatewayBalancesHandler::Input>,
boost::json::value const& jv);
} // namespace RPCng

View File

@@ -0,0 +1,727 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and 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 <rpc/common/AnyHandler.h>
#include <rpc/ngHandlers/GatewayBalances.h>
#include <util/Fixtures.h>
#include <util/TestObject.h>
#include <fmt/core.h>
using namespace RPCng;
namespace json = boost::json;
using namespace testing;
constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto ACCOUNT3 = "raHGBERMka3KZsfpTQUAtumxmvpqhFLyrk";
constexpr static auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD";
constexpr static auto LEDGERHASH =
"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto INDEX1 =
"1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr static auto INDEX2 =
"E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
constexpr static auto TXNID =
"E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879";
class RPCGatewayBalancesHandlerTest : public HandlerBaseTest
{
};
struct ParameterTestBundle
{
std::string testName;
std::string testJson;
std::string expectedError;
std::string expectedErrorMessage;
};
struct ParameterTest : public RPCGatewayBalancesHandlerTest,
public WithParamInterface<ParameterTestBundle>
{
struct NameGenerator
{
template <class ParamType>
std::string
operator()(const testing::TestParamInfo<ParamType>& info) const
{
auto bundle = static_cast<ParameterTestBundle>(info.param);
return bundle.testName;
}
};
};
TEST_P(ParameterTest, CheckError)
{
auto bundle = GetParam();
auto const handler = AnyHandler{GatewayBalancesHandler{mockBackendPtr}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output =
handler.process(json::parse(bundle.testJson), yield);
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), bundle.expectedError);
EXPECT_EQ(
err.at("error_message").as_string(), bundle.expectedErrorMessage);
});
ctx.run();
}
auto
generateParameterTestBundles()
{
return std::vector<ParameterTestBundle>{
ParameterTestBundle{
"AccountNotString",
R"({
"account": 1213
})",
"invalidParams",
"accountNotString"},
ParameterTestBundle{
"AccountMissing",
R"({
})",
"invalidParams",
"Required field 'account' missing"},
ParameterTestBundle{
"AccountInvalid",
R"({
"account": "1213"
})",
"invalidParams",
"accountMalformed"},
ParameterTestBundle{
"LedgerIndexInvalid",
fmt::format(
R"({{
"account": "{}",
"ledger_index": "meh"
}})",
ACCOUNT),
"invalidParams",
"ledgerIndexMalformed"},
ParameterTestBundle{
"LedgerHashInvalid",
fmt::format(
R"({{
"account": "{}",
"ledger_hash": "meh"
}})",
ACCOUNT),
"invalidParams",
"ledgerHashMalformed"},
ParameterTestBundle{
"LedgerHashNotString",
fmt::format(
R"({{
"account": "{}",
"ledger_hash": 12
}})",
ACCOUNT),
"invalidParams",
"ledgerHashNotString"},
ParameterTestBundle{
"WalletsNotStringOrArray",
fmt::format(
R"({{
"account": "{}",
"hotwallet": 12
}})",
ACCOUNT),
"invalidParams",
"hotwalletNotStringOrArray"},
ParameterTestBundle{
"WalletsNotStringAccount",
fmt::format(
R"({{
"account": "{}",
"hotwallet": [12]
}})",
ACCOUNT),
"invalidParams",
"hotwalletMalformed"},
ParameterTestBundle{
"WalletsInvalidAccount",
fmt::format(
R"({{
"account": "{}",
"hotwallet": ["12"]
}})",
ACCOUNT),
"invalidParams",
"hotwalletMalformed"},
ParameterTestBundle{
"WalletInvalidAccount",
fmt::format(
R"({{
"account": "{}",
"hotwallet": "12"
}})",
ACCOUNT),
"invalidParams",
"hotwalletMalformed"},
};
}
INSTANTIATE_TEST_SUITE_P(
RPCGatewayBalancesHandler,
ParameterTest,
testing::ValuesIn(generateParameterTestBundles()),
ParameterTest::NameGenerator());
TEST_F(RPCGatewayBalancesHandlerTest, LedgerNotFound)
{
auto const seq = 123;
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(300); // max
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
// return empty ledgerinfo
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(seq, _))
.WillByDefault(Return(std::optional<ripple::LedgerInfo>{}));
auto const handler = AnyHandler{GatewayBalancesHandler{mockBackendPtr}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output = handler.process(
json::parse(fmt::format(
R"({{
"account": "{}",
"ledger_index": "{}"
}})",
ACCOUNT,
seq)),
yield);
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "lgrNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "ledgerNotFound");
});
ctx.run();
}
TEST_F(RPCGatewayBalancesHandlerTest, AccountNotFound)
{
auto const seq = 300;
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(seq); // max
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
// return valid ledgerinfo
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq);
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(seq, _))
.WillByDefault(Return(ledgerinfo));
// return empty account
auto const accountKk =
ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, seq, _))
.WillByDefault(Return(std::optional<Blob>{}));
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1);
auto const handler = AnyHandler{GatewayBalancesHandler{mockBackendPtr}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output = handler.process(
json::parse(fmt::format(
R"({{
"account": "{}"
}})",
ACCOUNT)),
yield);
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "actNotFound");
EXPECT_EQ(err.at("error_message").as_string(), "accountNotFound");
});
ctx.run();
}
TEST_F(RPCGatewayBalancesHandlerTest, InvalidHotWallet)
{
auto const seq = 300;
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(seq); // max
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
// return valid ledgerinfo
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq);
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(seq, _))
.WillByDefault(Return(ledgerinfo));
// return valid account
auto const accountKk =
ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, seq, _))
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
// return valid owner dir
auto const ownerDir =
CreateOwnerDirLedgerObject({ripple::uint256{INDEX2}}, INDEX1);
auto const ownerDirKk =
ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _))
.WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2);
// create a valid line, balance is 0
auto const line1 = CreateRippleStateLedgerObject(
ACCOUNT, "USD", ISSUER, 0, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123);
std::vector<Blob> bbs;
bbs.push_back(line1.getSerializer().peekData());
ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1);
auto const handler = AnyHandler{GatewayBalancesHandler{mockBackendPtr}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output = handler.process(
json::parse(fmt::format(
R"({{
"account": "{}",
"hotwallet": "{}"
}})",
ACCOUNT,
ACCOUNT2)),
yield);
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "invalidHotWallet");
});
ctx.run();
}
struct NormalTestBundle
{
std::string testName;
ripple::STObject mockedDir;
std::vector<ripple::STObject> mockedObjects;
std::string expectedJson;
std::string hotwallet;
};
struct NormalPathTest : public RPCGatewayBalancesHandlerTest,
public WithParamInterface<NormalTestBundle>
{
struct NameGenerator
{
template <class ParamType>
std::string
operator()(const testing::TestParamInfo<ParamType>& info) const
{
auto bundle = static_cast<NormalTestBundle>(info.param);
return bundle.testName;
}
};
};
TEST_P(NormalPathTest, CheckOutput)
{
auto const& bundle = GetParam();
auto const seq = 300;
auto const rawBackendPtr = static_cast<MockBackend*>(mockBackendPtr.get());
mockBackendPtr->updateRange(10); // min
mockBackendPtr->updateRange(seq); // max
EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1);
// return valid ledgerinfo
auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, seq);
ON_CALL(*rawBackendPtr, fetchLedgerBySequence(seq, _))
.WillByDefault(Return(ledgerinfo));
// return valid account
auto const accountKk =
ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, seq, _))
.WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
// return valid owner dir
auto const ownerDir =
CreateOwnerDirLedgerObject({ripple::uint256{INDEX2}}, INDEX1);
auto const ownerDirKk =
ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, seq, _))
.WillByDefault(Return(bundle.mockedDir.getSerializer().peekData()));
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2);
std::vector<Blob> bbs;
std::transform(
bundle.mockedObjects.begin(),
bundle.mockedObjects.end(),
std::back_inserter(bbs),
[](auto const& obj) { return obj.getSerializer().peekData(); });
ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1);
auto const handler = AnyHandler{GatewayBalancesHandler{mockBackendPtr}};
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const output = handler.process(
json::parse(fmt::format(
R"({{
"account": "{}",
{}
}})",
ACCOUNT,
bundle.hotwallet)),
yield);
ASSERT_TRUE(output);
EXPECT_EQ(output.value(), json::parse(bundle.expectedJson));
});
ctx.run();
}
auto
generateNormalPathTestBundles()
{
auto frozenState = CreateRippleStateLedgerObject(
ACCOUNT, "JPY", ISSUER, -50, ACCOUNT, 10, ACCOUNT3, 20, TXNID, 123);
frozenState.setFieldU32(ripple::sfFlags, ripple::lsfLowFreeze);
auto overflowState = CreateRippleStateLedgerObject(
ACCOUNT, "JPY", ISSUER, 50, ACCOUNT, 10, ACCOUNT3, 20, TXNID, 123);
int64_t min64 = -9922966390934554;
overflowState.setFieldAmount(
ripple::sfBalance,
ripple::STAmount(GetIssue("JPY", ISSUER), min64, 80));
return std::vector<NormalTestBundle>{
NormalTestBundle{
"AllBranches",
CreateOwnerDirLedgerObject(
{ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2}},
INDEX1),
std::vector{// hotwallet
CreateRippleStateLedgerObject(
ACCOUNT,
"USD",
ISSUER,
-10,
ACCOUNT,
100,
ACCOUNT2,
200,
TXNID,
123),
// hotwallet
CreateRippleStateLedgerObject(
ACCOUNT,
"CNY",
ISSUER,
-20,
ACCOUNT,
100,
ACCOUNT2,
200,
TXNID,
123),
// positive balance -> asset
CreateRippleStateLedgerObject(
ACCOUNT,
"EUR",
ISSUER,
30,
ACCOUNT,
100,
ACCOUNT3,
200,
TXNID,
123),
// positive balance -> asset
CreateRippleStateLedgerObject(
ACCOUNT,
"JPY",
ISSUER,
40,
ACCOUNT,
100,
ACCOUNT3,
200,
TXNID,
123),
// obligation
CreateRippleStateLedgerObject(
ACCOUNT,
"JPY",
ISSUER,
-50,
ACCOUNT,
10,
ACCOUNT3,
20,
TXNID,
123),
frozenState
},
fmt::format(
R"({{
"obligations":{{
"JPY":"50"
}},
"balances":{{
"{}":[
{{
"currency":"USD",
"value":"10"
}},
{{
"currency":"CNY",
"value":"20"
}}
]
}},
"frozen_balances":{{
"{}":[
{{
"currency":"JPY",
"value":"50"
}}
]
}},
"assets":{{
"{}":[
{{
"currency":"EUR",
"value":"30"
}},
{{
"currency":"JPY",
"value":"40"
}}
]
}},
"account":"{}",
"ledger_index":300,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"
}})",
ACCOUNT2,
ACCOUNT3,
ACCOUNT3,
ACCOUNT),
fmt::format(R"("hotwallet": "{}")", ACCOUNT2)},
NormalTestBundle{
"NoHotwallet",
CreateOwnerDirLedgerObject({ripple::uint256{INDEX2}}, INDEX1),
std::vector{CreateRippleStateLedgerObject(
ACCOUNT,
"JPY",
ISSUER,
-50,
ACCOUNT,
10,
ACCOUNT3,
20,
TXNID,
123)},
fmt::format(
R"({{
"obligations":{{
"JPY":"50"
}},
"account":"{}",
"ledger_index":300,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"
}})",
ACCOUNT),
R"("ledger_index" : "validated")"},
NormalTestBundle{
"ObligationOverflow",
CreateOwnerDirLedgerObject(
{ripple::uint256{INDEX2}, ripple::uint256{INDEX2}}, INDEX1),
std::vector{overflowState, overflowState},
fmt::format(
R"({{
"obligations":{{
"JPY":"9922966390934554e80"
}},
"account":"{}",
"overflow":true,
"ledger_index":300,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"
}})",
ACCOUNT),
R"("ledger_index" : "validated")"},
NormalTestBundle{
"HighID",
CreateOwnerDirLedgerObject(
{ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2}},
INDEX1),
std::vector{// hotwallet
CreateRippleStateLedgerObject(
ACCOUNT,
"USD",
ISSUER,
10,
ACCOUNT2,
100,
ACCOUNT,
200,
TXNID,
123),
// hotwallet
CreateRippleStateLedgerObject(
ACCOUNT,
"CNY",
ISSUER,
20,
ACCOUNT2,
100,
ACCOUNT,
200,
TXNID,
123),
CreateRippleStateLedgerObject(
ACCOUNT,
"EUR",
ISSUER,
30,
ACCOUNT3,
100,
ACCOUNT,
200,
TXNID,
123),
CreateRippleStateLedgerObject(
ACCOUNT,
"JPY",
ISSUER,
-50,
ACCOUNT3,
10,
ACCOUNT,
20,
TXNID,
123)},
fmt::format(
R"({{
"obligations":{{
"EUR":"30"
}},
"balances":{{
"{}":[
{{
"currency":"USD",
"value":"10"
}},
{{
"currency":"CNY",
"value":"20"
}}
]
}},
"assets":{{
"{}":[
{{
"currency":"JPY",
"value":"50"
}}
]
}},
"account":"{}",
"ledger_index":300,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"
}})",
ACCOUNT2,
ACCOUNT3,
ACCOUNT),
fmt::format(R"("hotwallet": "{}")", ACCOUNT2)},
NormalTestBundle{
"HotWalletArray",
CreateOwnerDirLedgerObject(
{ripple::uint256{INDEX2},
ripple::uint256{INDEX2},
ripple::uint256{INDEX2}},
INDEX1),
std::vector{
CreateRippleStateLedgerObject(
ACCOUNT,
"USD",
ISSUER,
-10,
ACCOUNT,
100,
ACCOUNT2,
200,
TXNID,
123),
CreateRippleStateLedgerObject(
ACCOUNT,
"CNY",
ISSUER,
-20,
ACCOUNT,
100,
ACCOUNT2,
200,
TXNID,
123),
CreateRippleStateLedgerObject(
ACCOUNT,
"EUR",
ISSUER,
-30,
ACCOUNT,
100,
ACCOUNT3,
200,
TXNID,
123)
},
fmt::format(
R"({{
"balances":{{
"{}":[
{{
"currency":"EUR",
"value":"30"
}}
],
"{}":[
{{
"currency":"USD",
"value":"10"
}},
{{
"currency":"CNY",
"value":"20"
}}
]
}},
"account":"{}",
"ledger_index":300,
"ledger_hash":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"
}})",
ACCOUNT3,
ACCOUNT2,
ACCOUNT),
fmt::format(R"("hotwallet": ["{}", "{}"])", ACCOUNT2, ACCOUNT3)}};
}
INSTANTIATE_TEST_SUITE_P(
RPCGatewayBalancesHandler,
NormalPathTest,
testing::ValuesIn(generateNormalPathTestBundles()),
NormalPathTest::NameGenerator());