Compare commits

...

7 Commits
2.3.0 ... 2.3.1

Author SHA1 Message Date
Sergey Kuznetsov
3555b7d998 fix: gateway_balance discrepancy (#1839) (#1874)
Port of #1839 into 2.3.1.
Fixes #1832.

rippled code:


https://github.com/XRPLF/rippled/blob/develop/src/xrpld/rpc/handlers/GatewayBalances.cpp#L129
2025-02-04 17:10:12 +00:00
Sergey Kuznetsov
8a418bfe00 Merge branch 'release/2.3.1' into Port_gateway_balance_discrepancy 2025-02-04 16:39:18 +00:00
cyan317
dc1b146729 fix: gateway_balance discrepancy (#1839)
Fix https://github.com/XRPLF/clio/issues/1832

rippled code:

https://github.com/XRPLF/rippled/blob/develop/src/xrpld/rpc/handlers/GatewayBalances.cpp#L129
2025-02-04 16:36:09 +00:00
Sergey Kuznetsov
47c0a6a297 fix: Remove InvalidHotWallet Error from gateway_balances RPC handler … (#1873)
Port of #1830 into release/2.3.1.

Fixes #1825 by removing the check in the gateway_balances RPC handler
that returns the RpcInvalidHotWallet error code if one of the addresses
supplied in the request's `hotwallet` array does not have a trustline
with the `account` from the request.

As stated in the original ticket, this change fixes a discrepancy in
behavior between Clio and rippled, as rippled does not check for
trustline existence when handling gateway_balances RPCs
2025-02-04 15:37:49 +00:00
Sergey Kuznetsov
44df7bf966 Merge branch 'release/2.3.1' into Commits_for_2.3.1 2025-02-04 13:32:08 +00:00
Sergey Kuznetsov
8a5a984d51 chore: Update libxrpl to 2.3.1 (#1866) 2025-02-04 13:31:34 +00:00
nkramer44
9ca4c7afd3 fix: Remove InvalidHotWallet Error from gateway_balances RPC handler (#1830)
Fixes #1825 by removing the check in the gateway_balances RPC handler
that returns the RpcInvalidHotWallet error code if one of the addresses
supplied in the request's `hotwallet` array does not have a trustline
with the `account` from the request.

As stated in the original ticket, this change fixes a discrepancy in
behavior between Clio and rippled, as rippled does not check for
trustline existence when handling gateway_balances RPCs

Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2025-02-04 10:37:50 +00:00
9 changed files with 146 additions and 135 deletions

View File

@@ -74,7 +74,7 @@ jobs:
conan_profile: clang
code_coverage: false
static: true
- os: macos14
- os: macos15
build_type: Release
code_coverage: false
static: false
@@ -197,7 +197,7 @@ jobs:
image: rippleci/clio_ci:latest
conan_profile: clang
build_type: Debug
- os: macos14
- os: macos15
conan_profile: apple_clang_15
build_type: Release
runs-on: [self-hosted, "${{ matrix.os }}"]

View File

@@ -28,7 +28,7 @@ class Clio(ConanFile):
'protobuf/3.21.9',
'grpc/1.50.1',
'openssl/1.1.1u',
'xrpl/2.3.0',
'xrpl/2.3.1',
'zlib/1.3.1',
'libbacktrace/cci.20210118'
]

View File

@@ -79,7 +79,6 @@ getErrorInfo(ClioError code)
{ClioError::rpcMALFORMED_REQUEST, "malformedRequest", "Malformed request."},
{ClioError::rpcMALFORMED_OWNER, "malformedOwner", "Malformed owner."},
{ClioError::rpcMALFORMED_ADDRESS, "malformedAddress", "Malformed address."},
{ClioError::rpcINVALID_HOT_WALLET, "invalidHotWallet", "Invalid hot wallet."},
{ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."},
{ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."},
{ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."},

View File

@@ -39,7 +39,6 @@ enum class ClioError {
rpcMALFORMED_REQUEST = 5001,
rpcMALFORMED_OWNER = 5002,
rpcMALFORMED_ADDRESS = 5003,
rpcINVALID_HOT_WALLET = 5004,
rpcUNKNOWN_OPTION = 5005,
rpcFIELD_NOT_FOUND_TRANSACTION = 5006,
rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007,

View File

@@ -142,10 +142,6 @@ GatewayBalancesHandler::process(GatewayBalancesHandler::Input input, Context con
if (auto status = std::get_if<Status>(&ret))
return Error{*status};
auto inHotbalances = [&](auto const& hw) { return output.hotBalances.contains(hw); };
if (not std::all_of(input.hotWallets.begin(), input.hotWallets.end(), inHotbalances))
return Error{Status{ClioError::rpcINVALID_HOT_WALLET}};
output.accountID = input.account;
output.ledgerHash = ripple::strHex(lgrInfo.hash);
output.ledgerIndex = lgrInfo.seq;

View File

@@ -108,44 +108,51 @@ public:
static RpcSpecConstRef
spec([[maybe_unused]] uint32_t apiVersion)
{
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{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotStringOrArray"}};
auto const getHotWalletValidator = [](RippledError errCode) {
return validation::CustomValidator{
[errCode](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_string() && !value.is_array())
return Error{Status{errCode, 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 = util::parseBase58Wrapper<ripple::PublicKey>(
ripple::TokenType::AccountPublic, boost::json::value_to<std::string>(j)
);
// 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 = util::parseBase58Wrapper<ripple::PublicKey>(
ripple::TokenType::AccountPublic, boost::json::value_to<std::string>(j)
);
if (pk)
return ripple::calcAccountID(*pk);
if (pk)
return ripple::calcAccountID(*pk);
return util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(j));
return util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(j));
}
return {};
};
for (auto const& wallet : wallets) {
if (!getAccountID(wallet))
return Error{Status{errCode, std::string(key) + "Malformed"}};
}
return {};
};
for (auto const& wallet : wallets) {
if (!getAccountID(wallet))
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "Malformed"}};
return MaybeError{};
}
return MaybeError{};
}};
static auto const rpcSpec = RpcSpec{
{JS(account), validation::Required{}, validation::CustomValidators::AccountValidator},
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
{JS(hotwallet), hotWalletValidator}
};
};
return rpcSpec;
static auto const kSPEC_COMMON = RpcSpec{
{JS(account), validation::Required{}, validation::CustomValidators::AccountValidator},
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator}
};
auto static const kSPEC_V1 =
RpcSpec{kSPEC_COMMON, {{JS(hotwallet), getHotWalletValidator(ripple::rpcINVALID_HOTWALLET)}}};
auto static const kSPEC_V2 =
RpcSpec{kSPEC_COMMON, {{JS(hotwallet), getHotWalletValidator(ripple::rpcINVALID_PARAMS)}}};
return apiVersion == 1 ? kSPEC_V1 : kSPEC_V2;
}
/**

View File

@@ -87,7 +87,6 @@ public:
case rpc::ClioError::rpcMALFORMED_REQUEST:
case rpc::ClioError::rpcMALFORMED_OWNER:
case rpc::ClioError::rpcMALFORMED_ADDRESS:
case rpc::ClioError::rpcINVALID_HOT_WALLET:
case rpc::ClioError::rpcFIELD_NOT_FOUND_TRANSACTION:
case rpc::ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID:
case rpc::ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS:

View File

@@ -50,24 +50,19 @@
#include <string>
#include <vector>
constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
namespace {
constexpr auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr static auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
constexpr auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
// 20 USD : 10 XRP
constexpr static auto PAYS20USDGETS10XRPBOOKDIR = "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000";
constexpr auto PAYS20USDGETS10XRPBOOKDIR = "43B83ADC452B85FCBADA6CAEAC5181C255A213630D58FFD455071AFD498D0000";
// 20 XRP : 10 USD
constexpr static auto PAYS20XRPGETS10USDBOOKDIR = "7B1767D41DBCE79D9585CF9D0262A5FEC45E5206FF524F8B55071AFD498D0000";
constexpr auto PAYS20XRPGETS10USDBOOKDIR = "7B1767D41DBCE79D9585CF9D0262A5FEC45E5206FF524F8B55071AFD498D0000";
// transfer rate x2
constexpr static auto TRANSFERRATEX2 = 2000000000;
using namespace rpc;
namespace json = boost::json;
using namespace testing;
class RPCBookOffersHandlerTest : public HandlerBaseTest {};
constexpr auto TRANSFERRATEX2 = 2000000000;
struct ParameterTestBundle {
std::string testName;
@@ -76,7 +71,15 @@ struct ParameterTestBundle {
std::string expectedErrorMessage;
};
struct RPCBookOffersParameterTest : public RPCBookOffersHandlerTest, public WithParamInterface<ParameterTestBundle> {};
} // namespace
using namespace rpc;
namespace json = boost::json;
using namespace testing;
class RPCBookOffersHandlerTest : public HandlerBaseTest {};
struct RPCBookOffersParameterTest : RPCBookOffersHandlerTest, WithParamInterface<ParameterTestBundle> {};
TEST_P(RPCBookOffersParameterTest, CheckError)
{

View File

@@ -49,22 +49,30 @@ using namespace rpc;
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 {};
namespace {
constexpr auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr auto ACCOUNT3 = "raHGBERMka3KZsfpTQUAtumxmvpqhFLyrk";
constexpr auto ISSUER = "rK9DrarGKnVEo2nYp5MfVRXRYf5yRX3mwD";
constexpr auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr auto INDEX2 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
constexpr auto TXNID = "E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879";
struct ParameterTestBundle {
std::string testName;
std::string testJson;
std::string expectedError;
std::string expectedErrorMessage;
std::uint32_t apiVersion = 1u;
};
} // namespace
struct RPCGatewayBalancesHandlerTest : HandlerBaseTest {
RPCGatewayBalancesHandlerTest()
{
backend->setRange(10, 300);
}
};
struct ParameterTest : public RPCGatewayBalancesHandlerTest, public WithParamInterface<ParameterTestBundle> {};
@@ -74,7 +82,8 @@ TEST_P(ParameterTest, CheckError)
auto bundle = GetParam();
auto const handler = AnyHandler{GatewayBalancesHandler{backend}};
runSpawn([&](auto yield) {
auto const output = handler.process(json::parse(bundle.testJson), Context{yield});
auto const output =
handler.process(json::parse(bundle.testJson), Context{.yield = yield, .apiVersion = bundle.apiVersion});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), bundle.expectedError);
@@ -146,52 +155,104 @@ generateParameterTestBundles()
"ledger_hashNotString"
},
ParameterTestBundle{
"WalletsNotStringOrArray",
fmt::format(
.testName = "WalletsNotStringOrArrayV1",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": 12
}})",
ACCOUNT
),
"invalidParams",
"hotwalletNotStringOrArray"
.expectedError = "invalidHotWallet",
.expectedErrorMessage = "hotwalletNotStringOrArray"
},
ParameterTestBundle{
"WalletsNotStringAccount",
fmt::format(
.testName = "WalletsNotStringAccountV1",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": [12]
}})",
ACCOUNT
),
"invalidParams",
"hotwalletMalformed"
.expectedError = "invalidHotWallet",
.expectedErrorMessage = "hotwalletMalformed"
},
ParameterTestBundle{
"WalletsInvalidAccount",
fmt::format(
.testName = "WalletsInvalidAccountV1",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": ["12"]
}})",
ACCOUNT
),
"invalidParams",
"hotwalletMalformed"
.expectedError = "invalidHotWallet",
.expectedErrorMessage = "hotwalletMalformed"
},
ParameterTestBundle{
"WalletInvalidAccount",
fmt::format(
.testName = "WalletInvalidAccountV1",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": "12"
}})",
ACCOUNT
),
"invalidParams",
"hotwalletMalformed"
.expectedError = "invalidHotWallet",
.expectedErrorMessage = "hotwalletMalformed"
},
ParameterTestBundle{
.testName = "WalletsNotStringOrArrayV2",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": 12
}})",
ACCOUNT
),
.expectedError = "invalidParams",
.expectedErrorMessage = "hotwalletNotStringOrArray",
.apiVersion = 2u
},
ParameterTestBundle{
.testName = "WalletsNotStringAccountV2",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": [12]
}})",
ACCOUNT
),
.expectedError = "invalidParams",
.expectedErrorMessage = "hotwalletMalformed",
.apiVersion = 2u
},
ParameterTestBundle{
.testName = "WalletsInvalidAccountV2",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": ["12"]
}})",
ACCOUNT
),
.expectedError = "invalidParams",
.expectedErrorMessage = "hotwalletMalformed",
.apiVersion = 2u
},
ParameterTestBundle{
.testName = "WalletInvalidAccountV2",
.testJson = fmt::format(
R"({{
"account": "{}",
"hotwallet": "12"
}})",
ACCOUNT
),
.expectedError = "invalidParams",
.expectedErrorMessage = "hotwalletMalformed",
.apiVersion = 2u
},
};
}
@@ -207,7 +268,6 @@ TEST_F(RPCGatewayBalancesHandlerTest, LedgerNotFoundViaStringIndex)
{
auto const seq = 123;
backend->setRange(10, 300);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// return empty ledgerHeader
ON_CALL(*backend, fetchLedgerBySequence(seq, _)).WillByDefault(Return(std::optional<ripple::LedgerHeader>{}));
@@ -236,7 +296,6 @@ TEST_F(RPCGatewayBalancesHandlerTest, LedgerNotFoundViaIntIndex)
{
auto const seq = 123;
backend->setRange(10, 300);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// return empty ledgerHeader
ON_CALL(*backend, fetchLedgerBySequence(seq, _)).WillByDefault(Return(std::optional<ripple::LedgerHeader>{}));
@@ -263,7 +322,6 @@ TEST_F(RPCGatewayBalancesHandlerTest, LedgerNotFoundViaIntIndex)
TEST_F(RPCGatewayBalancesHandlerTest, LedgerNotFoundViaHash)
{
backend->setRange(10, 300);
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
// return empty ledgerHeader
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _))
@@ -293,7 +351,6 @@ TEST_F(RPCGatewayBalancesHandlerTest, AccountNotFound)
{
auto const seq = 300;
backend->setRange(10, seq);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// return valid ledgerHeader
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, seq);
@@ -322,54 +379,6 @@ TEST_F(RPCGatewayBalancesHandlerTest, AccountNotFound)
});
}
TEST_F(RPCGatewayBalancesHandlerTest, InvalidHotWallet)
{
auto const seq = 300;
backend->setRange(10, seq);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// return valid ledgerHeader
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, seq);
ON_CALL(*backend, fetchLedgerBySequence(seq, _)).WillByDefault(Return(ledgerHeader));
// return valid account
auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key;
ON_CALL(*backend, 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(*backend, doFetchLedgerObject(ownerDirKk, seq, _))
.WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
// create a valid line, balance is 0
auto const line1 = CreateRippleStateLedgerObject("USD", ISSUER, 0, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123);
std::vector<Blob> bbs;
bbs.push_back(line1.getSerializer().peekData());
ON_CALL(*backend, doFetchLedgerObjects).WillByDefault(Return(bbs));
EXPECT_CALL(*backend, doFetchLedgerObjects).Times(1);
auto const handler = AnyHandler{GatewayBalancesHandler{backend}};
runSpawn([&](auto yield) {
auto const output = handler.process(
json::parse(fmt::format(
R"({{
"account": "{}",
"hotwallet": "{}"
}})",
ACCOUNT,
ACCOUNT2
)),
Context{yield}
);
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "invalidHotWallet");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid hot wallet.");
});
}
struct NormalTestBundle {
std::string testName;
ripple::STObject mockedDir;
@@ -385,7 +394,6 @@ TEST_P(NormalPathTest, CheckOutput)
auto const& bundle = GetParam();
auto const seq = 300;
backend->setRange(10, seq);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
// return valid ledgerHeader
auto const ledgerHeader = CreateLedgerHeader(LEDGERHASH, seq);