feat: Add Support Credentials for Clio (#1712)

Rippled PR: [here](https://github.com/XRPLF/rippled/pull/5103)
This commit is contained in:
Peter Chen
2024-11-14 14:52:03 -05:00
committed by GitHub
parent 0e25c0cabc
commit 67d99457f2
23 changed files with 1560 additions and 99 deletions

View File

@@ -24,6 +24,7 @@
#include "etl/NetworkValidatedLedgersInterface.hpp"
#include "etl/Source.hpp"
#include "feed/SubscriptionManagerInterface.hpp"
#include "rpc/Errors.hpp"
#include "util/Mutex.hpp"
#include "util/ResponseExpirationCache.hpp"
#include "util/config/Config.hpp"

View File

@@ -6,6 +6,7 @@ target_sources(
Factories.cpp
AMMHelpers.cpp
RPCHelpers.cpp
CredentialHelpers.cpp
Counters.cpp
WorkQueue.cpp
common/Specs.cpp

View File

@@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <expected>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
namespace rpc::credentials {
bool
checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger)
{
if (sleCred.isFieldPresent(ripple::sfExpiration)) {
std::uint32_t const exp = sleCred.getFieldU32(ripple::sfExpiration);
std::uint32_t const now = ledger.parentCloseTime.time_since_epoch().count();
return now > exp;
}
return false;
}
std::set<std::pair<ripple::AccountID, ripple::Slice>>
createAuthCredentials(ripple::STArray const& in)
{
std::set<std::pair<ripple::AccountID, ripple::Slice>> out;
for (auto const& cred : in)
out.insert({cred[ripple::sfIssuer], cred[ripple::sfCredentialType]});
return out;
}
ripple::STArray
parseAuthorizeCredentials(boost::json::array const& jv)
{
ripple::STArray arr;
for (auto const& jo : jv) {
ASSERT(
jo.at(JS(issuer)).is_string(),
"issuer must be string, should already be checked in AuthorizeCredentialValidator"
);
auto const issuer =
ripple::parseBase58<ripple::AccountID>(static_cast<std::string>(jo.at(JS(issuer)).as_string()));
ASSERT(
issuer.has_value(), "issuer must be present, should already be checked in AuthorizeCredentialValidator."
);
ASSERT(
jo.at(JS(credential_type)).is_string(),
"credential_type must be string, should already be checked in AuthorizeCredentialValidator"
);
auto const credentialType = ripple::strUnHex(static_cast<std::string>(jo.at(JS(credential_type)).as_string()));
ASSERT(
credentialType.has_value(),
"credential_type must be present, should already be checked in AuthorizeCredentialValidator."
);
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
credential.setAccountID(ripple::sfIssuer, *issuer);
credential.setFieldVL(ripple::sfCredentialType, *credentialType);
arr.push_back(std::move(credential));
}
return arr;
}
std::expected<ripple::STArray, Status>
fetchCredentialArray(
std::optional<boost::json::array> const& credID,
ripple::AccountID const& srcAcc,
BackendInterface const& backend,
ripple::LedgerHeader const& info,
boost::asio::yield_context const& yield
)
{
ripple::STArray authCreds;
std::unordered_set<std::string_view> elems;
for (auto const& elem : credID.value()) {
ASSERT(elem.is_string(), "should already be checked in validators.hpp that elem is a string.");
if (elems.contains(elem.as_string()))
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "duplicates in credentials."}};
elems.insert(elem.as_string());
ripple::uint256 credHash;
ASSERT(
credHash.parseHex(boost::json::value_to<std::string>(elem)),
"should already be checked in validators.hpp that elem is a uint256 hex"
);
auto const credKeylet = ripple::keylet::credential(credHash).key;
auto const credLedgerObject = backend.fetchLedgerObject(credKeylet, info.seq, yield);
if (!credLedgerObject)
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't exist."}};
auto credIt = ripple::SerialIter{credLedgerObject->data(), credLedgerObject->size()};
auto const sleCred = ripple::SLE{credIt, credKeylet};
if ((sleCred.getType() != ripple::ltCREDENTIAL) ||
((sleCred.getFieldU32(ripple::sfFlags) & ripple::lsfAccepted) == 0u))
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials aren't accepted"}};
if (credentials::checkExpired(sleCred, info))
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials are expired"}};
if (sleCred.getAccountID(ripple::sfSubject) != srcAcc)
return Error{Status{RippledError::rpcBAD_CREDENTIALS, "credentials don't belong to the root account"}};
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
credential.setAccountID(ripple::sfIssuer, sleCred.getAccountID(ripple::sfIssuer));
credential.setFieldVL(ripple::sfCredentialType, sleCred.getFieldVL(ripple::sfCredentialType));
authCreds.push_back(std::move(credential));
}
return authCreds;
}
} // namespace rpc::credentials

View File

@@ -0,0 +1,89 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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 "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <expected>
#include <optional>
#include <set>
#include <utility>
namespace rpc::credentials {
/**
* @brief Check if credential is expired
*
* @param sleCred The credential to check
* @param ledger The ledger to check the closed time of
* @return true if credential not expired, false otherwise
*/
bool
checkExpired(ripple::SLE const& sleCred, ripple::LedgerHeader const& ledger);
/**
* @brief Creates authentication credential field (which is a set of pairs of AccountID and Credential ID)
*
* @param in The array of Credential objects to check
* @return Auth Credential array
*/
std::set<std::pair<ripple::AccountID, ripple::Slice>>
createAuthCredentials(ripple::STArray const& in);
/**
* @brief Parses each credential object and makes sure the credential type and values are correct
*
* @param jv The boost json array of credentials to parse
* @return Array of credentials after parsing
*/
ripple::STArray
parseAuthorizeCredentials(boost::json::array const& jv);
/**
* @brief Get Array of Credential objects
*
* @param credID Array of CredentialID's to parse
* @param srcAcc The Source Account
* @param backend backend interface
* @param info The ledger header
* @param yield The coroutine context
* @return Array of credential objects, error if failed otherwise
*/
std::expected<ripple::STArray, Status>
fetchCredentialArray(
std::optional<boost::json::array> const& credID,
ripple::AccountID const& srcAcc,
BackendInterface const& backend,
ripple::LedgerHeader const& info,
boost::asio::yield_context const& yield
);
} // namespace rpc::credentials

View File

@@ -83,6 +83,9 @@ getErrorInfo(ClioError code)
{ClioError::rpcUNKNOWN_OPTION, "unknownOption", "Unknown option."},
{ClioError::rpcFIELD_NOT_FOUND_TRANSACTION, "fieldNotFoundTransaction", "Missing field."},
{ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID, "malformedDocumentID", "Malformed oracle_document_id."},
{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
"malformedAuthorizedCredentials",
"Malformed authorized credentials."},
// special system errors
{ClioError::rpcINVALID_API_VERSION, JS(invalid_API_version), "Invalid API version."},
{ClioError::rpcCOMMAND_IS_MISSING, JS(missingCommand), "Method is not specified or is not a string."},

View File

@@ -43,6 +43,7 @@ enum class ClioError {
rpcUNKNOWN_OPTION = 5005,
rpcFIELD_NOT_FOUND_TRANSACTION = 5006,
rpcMALFORMED_ORACLE_DOCUMENT_ID = 5007,
rpcMALFORMED_AUTHORIZED_CREDENTIALS = 5008,
// special system errors start with 6000
rpcINVALID_API_VERSION = 6000,

View File

@@ -29,8 +29,10 @@
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <fmt/core.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/UintTypes.h>
#include <charconv>
@@ -253,4 +255,67 @@ CustomValidator CustomValidators::CurrencyIssueValidator =
return MaybeError{};
}};
CustomValidator CustomValidators::CredentialTypeValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (not value.is_string())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + " NotString"}};
auto const& credTypeHex = ripple::strViewUnHex(value.as_string());
if (!credTypeHex.has_value())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " NotHexString"}};
if (credTypeHex->empty())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " is empty"}};
if (credTypeHex->size() > ripple::maxCredentialTypeLength)
return Error{
Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, std::string(key) + " greater than max length"}
};
return MaybeError{};
}};
CustomValidator CustomValidators::AuthorizeCredentialValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (not value.is_array())
return Error{Status{ClioError::rpcMALFORMED_REQUEST, std::string(key) + " not array"}};
auto const& authCred = value.as_array();
if (authCred.size() == 0) {
return Error{Status{
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
fmt::format("Requires at least one element in authorized_credentials array")
}};
}
if (authCred.size() > ripple::maxCredentialsArraySize) {
return Error{Status{
ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS,
fmt::format(
"Max {} number of credentials in authorized_credentials array", ripple::maxCredentialsArraySize
)
}};
}
for (auto const& credObj : value.as_array()) {
auto const& obj = credObj.as_object();
if (!obj.contains("issuer"))
return Error{Status{ClioError::rpcMALFORMED_REQUEST, "Field 'Issuer' is required but missing."}};
if (auto const err = IssuerValidator.verify(credObj, "issuer"); !err)
return err;
if (!obj.contains("credential_type")) {
return Error{Status{ClioError::rpcMALFORMED_REQUEST, "Field 'CredentialType' is required but missing."}
};
}
if (auto const err = CredentialTypeValidator.verify(credObj, "credential_type"); !err)
return err;
}
return MaybeError{};
}};
} // namespace rpc::validation

View File

@@ -27,15 +27,13 @@
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <concepts>
#include <cstdint>
#include <ctime>
#include <functional>
#include <initializer_list>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
@@ -153,7 +151,7 @@ struct Type final {
verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail instead
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
auto const& res = value.as_object().at(key.data());
auto const convertible = (checkType<Types>(res) || ...);
@@ -559,6 +557,51 @@ struct CustomValidators final {
* Used by amm_info.
*/
static CustomValidator CurrencyIssueValidator;
/**
* @brief Provides a validator for validating authorized_credentials json array.
*
* Used by deposit_preauth.
*/
static CustomValidator AuthorizeCredentialValidator;
/**
* @brief Provides a validator for validating credential_type.
*
* Used by AuthorizeCredentialValidator in deposit_preauth.
*/
static CustomValidator CredentialTypeValidator;
};
/**
* @brief Validates that the elements of the array is of type Hex256 uint
*/
struct Hex256ItemType final {
/**
* @brief Validates given the prerequisite that the type of the json value is an array,
* verifies all values within the array is of uint256 hash
*
* @param value the value to verify
* @param key The key used to retrieve the tested value from the outer object
* @return `RippledError::rpcINVALID_PARAMS` if validation failed; otherwise no error is returned
*/
[[nodiscard]] static MaybeError
verify(boost::json::value const& value, std::string_view key)
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
auto const& res = value.as_object().at(key.data());
// loop through each item in the array and make sure it is uint256 hex string
for (auto const& elem : res.as_array()) {
ripple::uint256 num;
if (!elem.is_string() || !num.parseHex(elem.as_string())) {
return Error{Status{RippledError::rpcINVALID_PARAMS, "Item is not a valid uint256 type."}};
}
}
return {};
}
};
} // namespace rpc::validation

View File

@@ -19,25 +19,34 @@
#include "rpc/handlers/DepositAuthorized.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <memory>
#include <string>
#include <utility>
#include <variant>
namespace rpc {
@@ -71,26 +80,55 @@ DepositAuthorizedHandler::process(DepositAuthorizedHandler::Input input, Context
Output response;
auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()};
auto const sleDest = ripple::SLE{it, dstKeylet};
bool const reqAuth = sleDest.isFlag(ripple::lsfDepositAuth) && (sourceAccountID != destinationAccountID);
auto const& creds = input.credentials;
bool const credentialsPresent = creds.has_value();
ripple::STArray authCreds;
if (credentialsPresent) {
if (creds.value().empty()) {
return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array has no elements."}};
}
if (creds.value().size() > ripple::maxCredentialsArraySize) {
return Error{Status{RippledError::rpcINVALID_PARAMS, "credential array too long."}};
}
auto const credArray = credentials::fetchCredentialArray(
input.credentials, *sourceAccountID, *sharedPtrBackend_, lgrInfo, ctx.yield
);
if (!credArray.has_value())
return Error{std::move(credArray).error()};
authCreds = std::move(credArray).value();
}
// If the two accounts are the same OR if that flag is
// not set, then the deposit should be fine.
bool depositAuthorized = true;
if (reqAuth) {
ripple::uint256 hashKey;
if (credentialsPresent) {
auto const sortedAuthCreds = credentials::createAuthCredentials(authCreds);
ASSERT(
sortedAuthCreds.size() == authCreds.size(), "should already be checked above that there is no duplicate"
);
hashKey = ripple::keylet::depositPreauth(*destinationAccountID, sortedAuthCreds).key;
} else {
hashKey = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID).key;
}
depositAuthorized = sharedPtrBackend_->fetchLedgerObject(hashKey, lgrInfo.seq, ctx.yield).has_value();
}
response.sourceAccount = input.sourceAccount;
response.destinationAccount = input.destinationAccount;
response.ledgerHash = ripple::strHex(lgrInfo.hash);
response.ledgerIndex = lgrInfo.seq;
// If the two accounts are the same, then the deposit should be fine.
if (sourceAccountID != destinationAccountID) {
auto it = ripple::SerialIter{dstAccountLedgerObject->data(), dstAccountLedgerObject->size()};
auto sle = ripple::SLE{it, dstKeylet};
// Check destination for the DepositAuth flag.
// If that flag is not set then a deposit should be just fine.
if ((sle.getFieldU32(ripple::sfFlags) & ripple::lsfDepositAuth) != 0u) {
// See if a preauthorization entry is in the ledger.
auto const depositPreauthKeylet = ripple::keylet::depositPreauth(*destinationAccountID, *sourceAccountID);
auto const sleDepositAuth =
sharedPtrBackend_->fetchLedgerObject(depositPreauthKeylet.key, lgrInfo.seq, ctx.yield);
response.depositAuthorized = static_cast<bool>(sleDepositAuth);
}
}
response.depositAuthorized = depositAuthorized;
if (credentialsPresent)
response.credentials = input.credentials.value();
return response;
}
@@ -115,6 +153,10 @@ tag_invoke(boost::json::value_to_tag<DepositAuthorizedHandler::Input>, boost::js
}
}
if (jsonObject.contains(JS(credentials))) {
input.credentials = boost::json::value_to<boost::json::array>(jv.at(JS(credentials)));
}
return input;
}
@@ -127,8 +169,10 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, DepositAuthorize
{JS(destination_account), output.destinationAccount},
{JS(ledger_hash), output.ledgerHash},
{JS(ledger_index), output.ledgerIndex},
{JS(validated), output.validated},
{JS(validated), output.validated}
};
if (output.credentials)
jv.as_object()[JS(credentials)] = *output.credentials;
}
} // namespace rpc

View File

@@ -25,8 +25,10 @@
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/value.hpp>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
@@ -59,6 +61,8 @@ public:
std::string destinationAccount;
std::string ledgerHash;
uint32_t ledgerIndex{};
std::optional<boost::json::array> credentials;
// validated should be sent via framework
bool validated = true;
};
@@ -71,6 +75,7 @@ public:
std::string destinationAccount;
std::optional<std::string> ledgerHash;
std::optional<uint32_t> ledgerIndex;
std::optional<boost::json::array> credentials;
};
using Result = HandlerReturnType<Output>;
@@ -99,6 +104,7 @@ public:
{JS(destination_account), validation::Required{}, validation::CustomValidators::AccountValidator},
{JS(ledger_hash), validation::CustomValidators::Uint256HexStringValidator},
{JS(ledger_index), validation::CustomValidators::LedgerIndexValidator},
{JS(credentials), validation::Type<boost::json::array>{}, validation::Hex256ItemType()}
};
return rpcSpec;

View File

@@ -19,6 +19,7 @@
#include "rpc/handlers/LedgerEntry.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"
@@ -30,6 +31,8 @@
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/json_value.h>
@@ -38,6 +41,7 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STXChainBridge.h>
@@ -97,11 +101,30 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
auto const owner = util::parseBase58Wrapper<ripple::AccountID>(
boost::json::value_to<std::string>(input.depositPreauth->at(JS(owner)))
);
// Only one of authorize or authorized_credentials MUST exist;
if (input.depositPreauth->contains(JS(authorized)) ==
input.depositPreauth->contains(JS(authorized_credentials))) {
return Error{
Status{ClioError::rpcMALFORMED_REQUEST, "Must have one of authorized or authorized_credentials."}
};
}
if (input.depositPreauth->contains(JS(authorized))) {
auto const authorized = util::parseBase58Wrapper<ripple::AccountID>(
boost::json::value_to<std::string>(input.depositPreauth->at(JS(authorized)))
);
key = ripple::keylet::depositPreauth(*owner, *authorized).key;
} else {
auto const authorizedCredentials = rpc::credentials::parseAuthorizeCredentials(
input.depositPreauth->at(JS(authorized_credentials)).as_array()
);
auto const authCreds = credentials::createAuthCredentials(authorizedCredentials);
if (authCreds.size() != authorizedCredentials.size())
return Error{Status{ClioError::rpcMALFORMED_AUTHORIZED_CREDENTIALS, "duplicates in credentials."}};
key = ripple::keylet::depositPreauth(owner.value(), authCreds).key;
}
} else if (input.ticket) {
auto const id =
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(input.ticket->at(JS(account))
@@ -145,6 +168,8 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
}
} else if (input.oracleNode) {
key = input.oracleNode.value();
} else if (input.credential) {
key = input.credential.value();
} else if (input.mptIssuance) {
auto const mptIssuanceID = ripple::uint192{std::string_view(*(input.mptIssuance))};
key = ripple::keylet::mptIssuance(mptIssuanceID).key;
@@ -287,6 +312,7 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
{JS(xchain_owned_create_account_claim_id), ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
{JS(xchain_owned_claim_id), ripple::ltXCHAIN_OWNED_CLAIM_ID},
{JS(oracle), ripple::ltORACLE},
{JS(credential), ripple::ltCREDENTIAL},
{JS(mptoken), ripple::ltMPTOKEN},
};
@@ -313,6 +339,16 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
return ripple::keylet::oracle(*account, documentId).key;
};
auto const parseCredentialFromJson = [](boost::json::value const& json) {
auto const subject =
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(json.at(JS(subject))));
auto const issuer =
util::parseBase58Wrapper<ripple::AccountID>(boost::json::value_to<std::string>(json.at(JS(issuer))));
auto const credType = ripple::strUnHex(boost::json::value_to<std::string>(json.at(JS(credential_type))));
return ripple::keylet::credential(*subject, *issuer, ripple::Slice(credType->data(), credType->size())).key;
};
auto const indexFieldType =
std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) {
auto const& [field, _] = pair;
@@ -361,6 +397,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
);
} else if (jsonObject.contains(JS(oracle))) {
input.oracleNode = parseOracleFromJson(jv.at(JS(oracle)));
} else if (jsonObject.contains(JS(credential))) {
input.credential = parseCredentialFromJson(jv.at(JS(credential)));
} else if (jsonObject.contains(JS(mptoken))) {
input.mptoken = jv.at(JS(mptoken)).as_object();
}

View File

@@ -30,6 +30,7 @@
#include "rpc/common/Validators.hpp"
#include "util/AccountUtils.hpp"
#include <boost/json/array.hpp>
#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
@@ -107,6 +108,7 @@ public:
std::optional<uint32_t> chainClaimId;
std::optional<uint32_t> createAccountClaimId;
std::optional<ripple::uint256> oracleNode;
std::optional<ripple::uint256> credential;
bool includeDeleted = false;
};
@@ -197,7 +199,8 @@ public:
meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_OWNER)
}},
{JS(authorized), validation::Required{}, validation::CustomValidators::AccountBase58Validator},
{JS(authorized), validation::CustomValidators::AccountBase58Validator},
{JS(authorized_credentials), validation::CustomValidators::AuthorizeCredentialValidator}
},
}},
{JS(directory),
@@ -318,6 +321,30 @@ public:
},
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
}}},
{JS(credential),
meta::WithCustomError{
validation::Type<std::string, boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
},
meta::IfType<std::string>{
meta::WithCustomError{malformedRequestHexStringValidator, Status(ClioError::rpcMALFORMED_ADDRESS)}
},
meta::IfType<boost::json::object>{meta::Section{
{JS(subject),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)
}},
{JS(issuer),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::CustomValidators::AccountBase58Validator, Status(ClioError::rpcMALFORMED_ADDRESS)
}},
{
JS(credential_type),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{validation::Type<std::string>{}, Status(ClioError::rpcMALFORMED_REQUEST)},
},
}}},
{JS(mpt_issuance),
meta::WithCustomError{
validation::CustomValidators::Uint192HexStringValidator, Status(ClioError::rpcMALFORMED_REQUEST)

View File

@@ -19,6 +19,8 @@
#pragma once
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/tokens.h>
#include <cctype>

View File

@@ -112,6 +112,7 @@ class LedgerTypes {
),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(did), ripple::ltDID),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(oracle), ripple::ltORACLE),
LedgerTypeAttribute::AccountOwnedLedgerType(JS(credential), ripple::ltCREDENTIAL),
LedgerTypeAttribute::ChainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL),
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE),
LedgerTypeAttribute::DeletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN),

View File

@@ -90,6 +90,7 @@ public:
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:
case rpc::ClioError::etlCONNECTION_ERROR:
case rpc::ClioError::etlREQUEST_ERROR:
case rpc::ClioError::etlREQUEST_TIMEOUT:

View File

@@ -26,6 +26,7 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/json/json_value.h>
@@ -48,6 +49,7 @@
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
@@ -540,7 +542,7 @@ CreateCheckLedgerObject(std::string_view account, std::string_view dest)
}
ripple::STObject
CreateDepositPreauthLedgerObject(std::string_view account, std::string_view auth)
CreateDepositPreauthLedgerObjectByAuth(std::string_view account, std::string_view auth)
{
ripple::STObject depositPreauth(ripple::sfLedgerEntry);
depositPreauth.setFieldU16(ripple::sfLedgerEntryType, ripple::ltDEPOSIT_PREAUTH);
@@ -553,6 +555,27 @@ CreateDepositPreauthLedgerObject(std::string_view account, std::string_view auth
return depositPreauth;
}
ripple::STObject
CreateDepositPreauthLedgerObjectByAuthCredentials(
std::string_view account,
std::string_view issuer,
std::string_view credType
)
{
ripple::STObject depositPreauth(ripple::sfLedgerEntry);
depositPreauth.setFieldU16(ripple::sfLedgerEntryType, ripple::ltDEPOSIT_PREAUTH);
depositPreauth.setAccountID(ripple::sfAccount, GetAccountIDWithString(account));
depositPreauth.setFieldArray(
ripple::sfAuthorizeCredentials,
CreateAuthCredentialArray(std::vector<std::string_view>{issuer}, std::vector<std::string_view>{credType})
);
depositPreauth.setFieldU32(ripple::sfFlags, 0);
depositPreauth.setFieldU64(ripple::sfOwnerNode, 0);
depositPreauth.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{});
depositPreauth.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0);
return depositPreauth;
}
data::NFT
CreateNFT(std::string_view tokenID, std::string_view account, ripple::LedgerIndex seq, ripple::Blob uri, bool isBurned)
{
@@ -1192,3 +1215,47 @@ CreateOracleObject(
return ledgerObject;
}
// acc2 issue credential for acc1 so acc2 is issuer
ripple::STObject
CreateCredentialObject(
std::string_view acc1,
std::string_view acc2,
std::string_view credType,
bool accept,
std::optional<uint32_t> expiration
)
{
ripple::STObject credObj(ripple::sfCredential);
credObj.setFieldU16(ripple::sfLedgerEntryType, ripple::ltCREDENTIAL);
credObj.setFieldVL(ripple::sfCredentialType, ripple::Blob{credType.begin(), credType.end()});
credObj.setAccountID(ripple::sfSubject, GetAccountIDWithString(acc1));
credObj.setAccountID(ripple::sfIssuer, GetAccountIDWithString(acc2));
if (expiration.has_value())
credObj.setFieldU32(ripple::sfExpiration, expiration.value());
if (accept) {
credObj.setFieldU32(ripple::sfFlags, ripple::lsfAccepted);
} else {
credObj.setFieldU32(ripple::sfFlags, 0);
}
credObj.setFieldU64(ripple::sfSubjectNode, 0);
credObj.setFieldU64(ripple::sfIssuerNode, 0);
credObj.setFieldH256(ripple::sfPreviousTxnID, ripple::uint256{});
credObj.setFieldU32(ripple::sfPreviousTxnLgrSeq, 0);
return credObj;
}
ripple::STArray
CreateAuthCredentialArray(std::vector<std::string_view> issuer, std::vector<std::string_view> credType)
{
ripple::STArray arr;
ASSERT(issuer.size() == credType.size(), "issuer and credtype vector must be same length");
for (std::size_t i = 0; i < issuer.size(); ++i) {
auto credential = ripple::STObject::makeInnerObject(ripple::sfCredential);
credential.setAccountID(ripple::sfIssuer, GetAccountIDWithString(issuer[i]));
credential.setFieldVL(ripple::sfCredentialType, ripple::strUnHex(std::string(credType[i])).value());
arr.push_back(credential);
}
return arr;
}

View File

@@ -22,6 +22,7 @@
#include "data/Types.hpp"
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
@@ -268,7 +269,14 @@ CreateEscrowLedgerObject(std::string_view account, std::string_view dest);
CreateCheckLedgerObject(std::string_view account, std::string_view dest);
[[nodiscard]] ripple::STObject
CreateDepositPreauthLedgerObject(std::string_view account, std::string_view auth);
CreateDepositPreauthLedgerObjectByAuth(std::string_view account, std::string_view auth);
[[nodiscard]] ripple::STObject
CreateDepositPreauthLedgerObjectByAuthCredentials(
std::string_view account,
std::string_view issuer,
std::string_view credType
);
[[nodiscard]] data::NFT
CreateNFT(
@@ -435,3 +443,15 @@ CreateOracleSetTxWithMetadata(
bool created,
std::string_view previousTxnId
);
[[nodiscard]] ripple::STObject
CreateCredentialObject(
std::string_view acc1,
std::string_view acc2,
std::string_view credType,
bool accept = true,
std::optional<uint32_t> expiration = std::nullopt
);
[[nodiscard]] ripple::STArray
CreateAuthCredentialArray(std::vector<std::string_view> issuer, std::vector<std::string_view> credType);

View File

@@ -67,6 +67,7 @@ target_sources(
rpc/handlers/AMMInfoTests.cpp
rpc/handlers/BookChangesTests.cpp
rpc/handlers/BookOffersTests.cpp
rpc/handlers/CredentialHelpersTests.cpp
rpc/handlers/DefaultProcessorTests.cpp
rpc/handlers/DepositAuthorizedTests.cpp
rpc/handlers/FeatureTests.cpp

View File

@@ -30,10 +30,12 @@
#include <boost/asio/impl/spawn.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/parse.hpp>
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
@@ -47,6 +49,7 @@
#include <cstdint>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <variant>
#include <vector>

View File

@@ -0,0 +1,153 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2024, 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/CredentialHelpers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/MockBackendTestFixture.hpp"
#include "util/MockPrometheus.hpp"
#include "util/TestObject.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/jss.h>
#include <string>
#include <string_view>
#include <utility>
using namespace rpc;
using namespace testing;
constexpr static auto Account = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto Account2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto Index1 = "E6DBAFC99223B42257915A63DFC6B0C032D4070F9A574B255AD97466726FC321";
constexpr static auto CredentialID = "c7a14f6b9d5d4a9cb9c223a61b8e5c7df58e8b7ad1c6b4f8e7a321fa4e5b4c9d";
constexpr static std::string_view CredentialType = "credType";
TEST(CreateAuthCredentialsTest, UniqueCredentials)
{
ripple::STArray credentials;
auto const cred1 = CreateCredentialObject(Account, Account2, CredentialType);
auto const cred2 = CreateCredentialObject(Account2, Account, CredentialType);
credentials.push_back(cred1);
credentials.push_back(cred2);
auto const result = credentials::createAuthCredentials(credentials);
// Validate that the result contains the correct set of credentials
ASSERT_EQ(result.size(), 2);
auto const cred1Type = cred1.getFieldVL(ripple::sfCredentialType);
auto const cred2Type = cred2.getFieldVL(ripple::sfCredentialType);
auto const expected_cred1 =
std::make_pair(cred1.getAccountID(ripple::sfIssuer), ripple::Slice{cred1Type.data(), cred1Type.size()});
auto const expected_cred2 =
std::make_pair(cred2.getAccountID(ripple::sfIssuer), ripple::Slice{cred2Type.data(), cred2Type.size()});
EXPECT_TRUE(result.count(expected_cred1));
EXPECT_TRUE(result.count(expected_cred2));
}
TEST(ParseAuthorizeCredentialsTest, ValidCredentialsArray)
{
boost::json::array credentials;
boost::json::object credential1;
credential1[JS(issuer)] = Account;
credential1[JS(credential_type)] = ripple::strHex(CredentialType);
credentials.push_back(credential1);
ripple::STArray const parsedCredentials = credentials::parseAuthorizeCredentials(credentials);
ASSERT_EQ(parsedCredentials.size(), 1);
ripple::STObject const& cred = parsedCredentials[0];
ASSERT_TRUE(cred.isFieldPresent(ripple::sfIssuer));
ASSERT_TRUE(cred.isFieldPresent(ripple::sfCredentialType));
auto const expectedIssuer =
*ripple::parseBase58<ripple::AccountID>(static_cast<std::string>(credential1[JS(issuer)].as_string()));
auto const expectedCredentialType =
ripple::strUnHex(static_cast<std::string>(credential1[JS(credential_type)].as_string())).value();
EXPECT_EQ(cred.getAccountID(ripple::sfIssuer), expectedIssuer);
EXPECT_EQ(cred.getFieldVL(ripple::sfCredentialType), expectedCredentialType);
}
class CredentialHelperTest : public util::prometheus::WithPrometheus,
public MockBackendTest,
public SyncAsioContextTest {};
TEST_F(CredentialHelperTest, GetInvalidCredentialArray)
{
boost::json::array credentialsArray = {CredentialID};
auto const info = CreateLedgerHeader(Index1, 30);
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const ret =
credentials::fetchCredentialArray(credentialsArray, GetAccountIDWithString(Account), *backend, info, yield);
ASSERT_FALSE(ret.has_value());
auto const status = ret.error();
EXPECT_EQ(status, RippledError::rpcBAD_CREDENTIALS);
EXPECT_EQ(status.message, "credentials don't exist.");
});
ctx.run();
}
TEST_F(CredentialHelperTest, GetValidCredentialArray)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(Index1, 30);
auto const credLedgerObject = CreateCredentialObject(Account, Account2, CredentialType, true);
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(credLedgerObject.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(1);
boost::json::array credentialsArray = {CredentialID};
ripple::STArray expectedAuthCreds;
ripple::STObject credential(ripple::sfCredential);
credential.setAccountID(ripple::sfIssuer, GetAccountIDWithString(Account2));
credential.setFieldVL(ripple::sfCredentialType, ripple::Blob{std::begin(CredentialType), std::end(CredentialType)});
expectedAuthCreds.push_back(std::move(credential));
boost::asio::spawn(ctx, [&](boost::asio::yield_context yield) {
auto const result = credentials::fetchCredentialArray(
credentialsArray, GetAccountIDWithString(Account), *backend, ledgerHeader, yield
);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), expectedAuthCreds);
});
ctx.run();
}

View File

@@ -28,24 +28,34 @@
#include <boost/json/parse.hpp>
#include <fmt/core.h>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STArray.h>
#include <chrono>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
constexpr static auto ACCOUNT = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto ACCOUNT2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr static auto INDEX2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B1";
constexpr static auto Account = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn";
constexpr static auto Account2 = "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun";
constexpr static auto LedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr static auto Index1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
constexpr static auto Index2 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B1";
constexpr static std::string_view CredentialType = "credType";
constexpr static auto CredentialHash = "F245428267E6177AEEFDD4FEA3533285712A4B1091CF82A7EA7BC39A62C3FB1A";
constexpr static auto RANGEMIN = 10;
constexpr static auto RANGEMAX = 30;
constexpr static auto RangeMin = 10;
constexpr static auto RangeMax = 30;
using namespace rpc;
namespace json = boost::json;
@@ -156,6 +166,38 @@ generateTestValuesForParametersTest()
"invalidParams",
"ledgerIndexMalformed",
},
{
"CredentialsNotArray",
R"({
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"credentials": "x"
})",
"invalidParams",
"Invalid parameters.",
},
{
"CredentialsNotStringsInArray",
R"({
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"credentials": [123]
})",
"invalidParams",
"Item is not a valid uint256 type.",
},
{
"CredentialsNotHexedStringInArray",
R"({
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"credentials": ["234", "432"]
})",
"invalidParams",
"Item is not a valid uint256 type.",
}
};
}
@@ -184,10 +226,10 @@ TEST_P(DepositAuthorizedParameterTest, InvalidParams)
TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaIntSequence)
{
backend->setRange(RANGEMIN, RANGEMAX);
backend->setRange(RangeMin, RangeMax);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt));
ON_CALL(*backend, fetchLedgerBySequence(RangeMax, _)).WillByDefault(Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
@@ -197,9 +239,9 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaIntSequence)
"destination_account": "{}",
"ledger_index": {}
}})",
ACCOUNT,
ACCOUNT2,
RANGEMAX
Account,
Account2,
RangeMax
));
auto const output = handler.process(req, Context{yield});
@@ -213,10 +255,10 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaIntSequence)
TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaStringSequence)
{
backend->setRange(RANGEMIN, RANGEMAX);
backend->setRange(RangeMin, RangeMax);
EXPECT_CALL(*backend, fetchLedgerBySequence).Times(1);
ON_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _)).WillByDefault(Return(std::nullopt));
ON_CALL(*backend, fetchLedgerBySequence(RangeMax, _)).WillByDefault(Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
@@ -226,9 +268,9 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaStringSequence)
"destination_account": "{}",
"ledger_index": "{}"
}})",
ACCOUNT,
ACCOUNT2,
RANGEMAX
Account,
Account2,
RangeMax
));
auto const output = handler.process(req, Context{yield});
@@ -242,10 +284,10 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaStringSequence)
TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaHash)
{
backend->setRange(RANGEMIN, RANGEMAX);
backend->setRange(RangeMin, RangeMax);
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(std::nullopt));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(std::nullopt));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
@@ -255,9 +297,9 @@ TEST_F(RPCDepositAuthorizedTest, LedgerNotExistViaHash)
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT2,
LEDGERHASH
Account,
Account2,
LedgerHash
));
auto const output = handler.process(req, Context{yield});
@@ -273,9 +315,9 @@ TEST_F(RPCDepositAuthorizedTest, SourceAccountDoesNotExist)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(std::optional<Blob>{}));
@@ -287,9 +329,9 @@ TEST_F(RPCDepositAuthorizedTest, SourceAccountDoesNotExist)
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT2,
LEDGERHASH
Account,
Account2,
LedgerHash
));
runSpawn([&, this](auto yield) {
@@ -308,14 +350,14 @@ TEST_F(RPCDepositAuthorizedTest, DestinationAccountDoesNotExist)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
auto const accountRoot = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2);
auto const accountRoot = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(accountRoot.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(std::optional<Blob>{}));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
@@ -326,9 +368,9 @@ TEST_F(RPCDepositAuthorizedTest, DestinationAccountDoesNotExist)
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT2,
LEDGERHASH
Account,
Account2,
LedgerHash
));
runSpawn([&, this](auto yield) {
@@ -357,12 +399,12 @@ TEST_F(RPCDepositAuthorizedTest, AccountsAreEqual)
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
auto const accountRoot = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2);
auto const accountRoot = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
ON_CALL(*backend, doFetchLedgerObject).WillByDefault(Return(accountRoot.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
@@ -372,9 +414,9 @@ TEST_F(RPCDepositAuthorizedTest, AccountsAreEqual)
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT,
LEDGERHASH
Account,
Account,
LedgerHash
));
runSpawn([&, this](auto yield) {
@@ -400,17 +442,17 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsNoDepositAuthFlag)
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
auto const account1Root = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2);
auto const account2Root = CreateAccountRootObject(ACCOUNT2, 0, 2, 200, 2, INDEX2, 2);
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, 0, 2, 200, 2, Index2, 2);
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
@@ -420,9 +462,9 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsNoDepositAuthFlag)
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT2,
LEDGERHASH
Account,
Account2,
LedgerHash
));
runSpawn([&, this](auto yield) {
@@ -448,18 +490,18 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsFals
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
auto const account1Root = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2);
auto const account2Root = CreateAccountRootObject(ACCOUNT2, ripple::lsfDepositAuth, 2, 200, 2, INDEX2, 2);
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::nullopt));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
@@ -469,9 +511,9 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsFals
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT2,
LEDGERHASH
Account,
Account2,
LedgerHash
));
runSpawn([&, this](auto yield) {
@@ -497,18 +539,18 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsTrue
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LEDGERHASH, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LEDGERHASH}, _)).WillByDefault(Return(ledgerHeader));
ON_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillByDefault(Return(ledgerHeader));
EXPECT_CALL(*backend, fetchLedgerByHash).Times(1);
auto const account1Root = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2);
auto const account2Root = CreateAccountRootObject(ACCOUNT2, ripple::lsfDepositAuth, 2, 200, 2, INDEX2, 2);
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(ACCOUNT2)).key, _, _))
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
@@ -518,9 +560,9 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsTrue
"destination_account": "{}",
"ledger_hash": "{}"
}})",
ACCOUNT,
ACCOUNT2,
LEDGERHASH
Account,
Account2,
LedgerHash
));
runSpawn([&, this](auto yield) {
@@ -531,3 +573,380 @@ TEST_F(RPCDepositAuthorizedTest, DifferentAccountsWithDepositAuthFlagReturnsTrue
EXPECT_EQ(*output.result, json::parse(expectedOut));
});
}
TEST_F(RPCDepositAuthorizedTest, CredentialAcceptedAndNotExpiredReturnsTrue)
{
static auto const expectedOut = fmt::format(
R"({{
"ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652",
"ledger_index": 30,
"validated": true,
"deposit_authorized": true,
"source_account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"destination_account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun",
"credentials": ["{}"]
}})",
CredentialHash // CREDENTIALHASH should match credentialIndex
);
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
auto const credential = CreateCredentialObject(Account, Account2, CredentialType);
auto const credentialIndex = ripple::keylet::credential(
GetAccountIDWithString(Account),
GetAccountIDWithString(Account2),
ripple::Slice(CredentialType.data(), CredentialType.size())
)
.key;
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _))
.WillByDefault(Return(credential.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(4);
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": ["{}"]
}})",
Account,
Account2,
LedgerHash,
ripple::strHex(credentialIndex)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(*output.result, json::parse(expectedOut));
});
}
TEST_F(RPCDepositAuthorizedTest, CredentialNotAuthorizedReturnsFalse)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
auto const credential = CreateCredentialObject(Account, Account2, CredentialType, false);
auto const credentialIndex = ripple::keylet::credential(
GetAccountIDWithString(Account),
GetAccountIDWithString(Account2),
ripple::Slice(CredentialType.data(), CredentialType.size())
)
.key;
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _))
.WillByDefault(Return(credential.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": ["{}"]
}})",
Account,
Account2,
LedgerHash,
ripple::strHex(credentialIndex)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "badCredentials");
EXPECT_EQ(err.at("error_message").as_string(), "credentials aren't accepted");
});
}
TEST_F(RPCDepositAuthorizedTest, CredentialExpiredReturnsFalse)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 100);
// set parent close time to 500 seconds
ledgerHeader.parentCloseTime = ripple::NetClock::time_point{std::chrono::seconds{500}};
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
// credential expire time is 23 seconds, so credential will fail
auto const expiredCredential = CreateCredentialObject(Account, Account2, CredentialType, true, 23);
auto const credentialIndex = ripple::keylet::credential(
GetAccountIDWithString(Account),
GetAccountIDWithString(Account2),
ripple::Slice(CredentialType.data(), CredentialType.size())
)
.key;
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _))
.WillByDefault(Return(expiredCredential.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": ["{}"]
}})",
Account,
Account2,
LedgerHash,
ripple::strHex(credentialIndex)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "badCredentials");
EXPECT_EQ(err.at("error_message").as_string(), "credentials are expired");
});
}
TEST_F(RPCDepositAuthorizedTest, DuplicateCredentialsReturnsFalse)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 34);
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
auto const credential = CreateCredentialObject(Account, Account2, CredentialType);
auto const credentialIndex = ripple::keylet::credential(
GetAccountIDWithString(Account),
GetAccountIDWithString(Account2),
ripple::Slice(CredentialType.data(), CredentialType.size())
)
.key;
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _))
.WillByDefault(Return(credential.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": ["{}", "{}"]
}})",
Account,
Account2,
LedgerHash,
ripple::strHex(credentialIndex),
ripple::strHex(credentialIndex)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "badCredentials");
EXPECT_EQ(err.at("error_message").as_string(), "duplicates in credentials.");
});
}
TEST_F(RPCDepositAuthorizedTest, NoElementsInCredentialsReturnsFalse)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 34);
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": []
}})",
Account,
Account2,
LedgerHash
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "credential array has no elements.");
});
}
TEST_F(RPCDepositAuthorizedTest, MoreThanMaxNumberOfCredentialsReturnsFalse)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30, 34);
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
auto const credential = CreateCredentialObject(Account, Account2, CredentialType);
auto const credentialIndex = ripple::keylet::credential(
GetAccountIDWithString(Account),
GetAccountIDWithString(Account2),
ripple::Slice(CredentialType.data(), CredentialType.size())
)
.key;
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _))
.WillByDefault(Return(credential.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(2);
std::vector<std::string> credentials(9, ripple::strHex(credentialIndex));
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": [{}]
}})",
Account,
Account2,
LedgerHash,
fmt::join(
credentials | std::views::transform([](std::string const& cred) { return fmt::format("\"{}\"", cred); }),
", "
)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "credential array too long.");
});
}
TEST_F(RPCDepositAuthorizedTest, DifferenSubjectAccountForCredentialReturnsFalse)
{
backend->setRange(10, 30);
auto ledgerHeader = CreateLedgerHeader(LedgerHash, 30);
EXPECT_CALL(*backend, fetchLedgerByHash(ripple::uint256{LedgerHash}, _)).WillOnce(Return(ledgerHeader));
auto const account1Root = CreateAccountRootObject(Account, 0, 2, 200, 2, Index1, 2);
auto const account2Root = CreateAccountRootObject(Account2, ripple::lsfDepositAuth, 2, 200, 2, Index2, 2);
// reverse the subject and issuer account. Now subject is ACCOUNT2
auto const credential = CreateCredentialObject(Account2, Account, CredentialType);
auto const credentialIndex = ripple::keylet::credential(
GetAccountIDWithString(Account2),
GetAccountIDWithString(Account),
ripple::Slice(CredentialType.data(), CredentialType.size())
)
.key;
ON_CALL(*backend, doFetchLedgerObject(_, _, _)).WillByDefault(Return(std::optional<Blob>{{1, 2, 3}}));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account)).key, _, _))
.WillByDefault(Return(account1Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(ripple::keylet::account(GetAccountIDWithString(Account2)).key, _, _))
.WillByDefault(Return(account2Root.getSerializer().peekData()));
ON_CALL(*backend, doFetchLedgerObject(credentialIndex, _, _))
.WillByDefault(Return(credential.getSerializer().peekData()));
EXPECT_CALL(*backend, doFetchLedgerObject).Times(3);
auto const input = json::parse(fmt::format(
R"({{
"source_account": "{}",
"destination_account": "{}",
"ledger_hash": "{}",
"credentials": ["{}"]
}})",
Account,
Account2,
LedgerHash,
ripple::strHex(credentialIndex)
));
runSpawn([&, this](auto yield) {
auto const handler = AnyHandler{DepositAuthorizedHandler{backend}};
auto const output = handler.process(input, Context{yield});
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "badCredentials");
EXPECT_EQ(err.at("error_message").as_string(), "credentials don't belong to the root account");
});
}

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include "data/Types.hpp"
#include "rpc/CredentialHelpers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/common/AnyHandler.hpp"
#include "rpc/common/Types.hpp"
@@ -29,6 +30,8 @@
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_to.hpp>
@@ -36,6 +39,8 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AccountID.h>
@@ -49,6 +54,7 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
@@ -66,6 +72,7 @@ constexpr static auto LEDGERHASH = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A
constexpr static auto TOKENID = "000827103B94ECBB7BF0A0A6ED62B3607801A27B65F4679F4AD1D4850000C0EA";
constexpr static auto NFTID = "00010000A7CAD27B688D14BA1A9FA5366554D6ADCF9CE0875B974D9F00000004";
constexpr static auto TXNID = "05FB0EB4B899F056FA095537C5817163801F544BAFCEA39C995D76DB4D16F9DD";
constexpr static auto CREDENTIALTYPE = "4B5943";
class RPCLedgerEntryTest : public HandlerBaseTest {};
@@ -200,6 +207,206 @@ generateTestValuesForParametersTest()
"authorizedNotString"
},
ParamTestCaseBundle{
"InvalidDepositPreauthJsonAuthorizeCredentialsNotArray",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": "asdf"
}}
}})",
ACCOUNT
),
"malformedRequest",
"authorized_credentials not array"
},
ParamTestCaseBundle{
"DepositPreauthBothAuthAndAuthCredentialsDoesNotExists",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}"
}}
}})",
ACCOUNT
),
"malformedRequest",
"Must have one of authorized or authorized_credentials."
},
ParamTestCaseBundle{
"DepositPreauthBothAuthAndAuthCredentialsExists",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized": "{}",
"authorized_credentials": [
{{
"issuer": "{}",
"credential_type": "{}"
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2,
ACCOUNT3,
CREDENTIALTYPE
),
"malformedRequest",
"Must have one of authorized or authorized_credentials."
},
ParamTestCaseBundle{
"DepositPreauthEmptyAuthorizeCredentials",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
]
}}
}})",
ACCOUNT
),
"malformedAuthorizedCredentials",
"Requires at least one element in authorized_credentials array"
},
ParamTestCaseBundle{
"DepositPreauthAuthorizeCredentialsMissingCredentialType",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"issuer": "{}"
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2
),
"malformedRequest",
"Field 'CredentialType' is required but missing."
},
ParamTestCaseBundle{
"DepositPreauthAuthorizeCredentialsMissingIssuer",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"credential_type": "{}"
}}
]
}}
}})",
ACCOUNT,
CREDENTIALTYPE
),
"malformedRequest",
"Field 'Issuer' is required but missing."
},
ParamTestCaseBundle{
"DepositPreauthAuthorizeCredentialsIncorrectCredentialType",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"issuer": "{}",
"credential_type": 432
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2
),
"invalidParams",
"credential_type NotString"
},
ParamTestCaseBundle{
"DepositPreauthAuthorizeCredentialsCredentialTypeNotHex",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"issuer": "{}",
"credential_type": "hello world"
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2
),
"malformedAuthorizedCredentials",
"credential_type NotHexString"
},
ParamTestCaseBundle{
"DepositPreauthAuthorizeCredentialsCredentialTypeEmpty",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"issuer": "{}",
"credential_type": ""
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2
),
"malformedAuthorizedCredentials",
"credential_type is empty"
},
ParamTestCaseBundle{
"DepositPreauthDuplicateAuthorizeCredentials",
fmt::format(
R"({{
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"issuer": "{}",
"credential_type": "{}"
}},
{{
"issuer": "{}",
"credential_type": "{}"
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2,
CREDENTIALTYPE,
ACCOUNT2,
CREDENTIALTYPE
),
"malformedAuthorizedCredentials",
"duplicates in credentials."
},
ParamTestCaseBundle{
"InvalidTicketType",
R"({
@@ -1759,6 +1966,29 @@ generateTestValuesForParametersTest()
"malformedRequest",
"Malformed request."
},
ParamTestCaseBundle{
"CredentialInvalidSubjectType",
R"({
"credential": {
"subject": 123
}
})",
"malformedAddress",
"Malformed address."
},
ParamTestCaseBundle{
"CredentialInvalidIssuerType",
fmt::format(
R"({{
"credential": {{
"issuer": ["{}"]
}}
}})",
ACCOUNT
),
"malformedRequest",
"Malformed request."
},
ParamTestCaseBundle{
"InvalidMPTIssuanceStringIndex",
R"({
@@ -1806,6 +2036,37 @@ generateTestValuesForParametersTest()
"malformedRequest",
"Malformed request."
},
ParamTestCaseBundle{
"CredentialInvalidCredentialType",
fmt::format(
R"({{
"credential": {{
"subject": "{}",
"issuer": "{}",
"credential_type": 1234
}}
}})",
ACCOUNT,
ACCOUNT2
),
"malformedRequest",
"Malformed request."
},
ParamTestCaseBundle{
"CredentialMissingIssuerField",
fmt::format(
R"({{
"credential": {{
"subject": "{}",
"credential_type": "1234"
}}
}})",
ACCOUNT,
ACCOUNT2
),
"malformedRequest",
"Malformed request."
},
ParamTestCaseBundle{
"InvalidMPTokenAccount",
fmt::format(
@@ -1828,7 +2089,7 @@ generateTestValuesForParametersTest()
),
"malformedRequest",
"Malformed request."
},
}
};
}
@@ -2068,7 +2329,7 @@ generateTestValuesForNormalPathTest()
INDEX1
),
ripple::uint256{INDEX1},
CreateDepositPreauthLedgerObject(ACCOUNT, ACCOUNT2)
CreateDepositPreauthLedgerObjectByAuth(ACCOUNT, ACCOUNT2)
},
NormalPathTestBundle{
"AccountRoot",
@@ -2155,7 +2416,7 @@ generateTestValuesForNormalPathTest()
CreateEscrowLedgerObject(ACCOUNT, ACCOUNT2)
},
NormalPathTestBundle{
"DepositPreauth",
"DepositPreauthByAuth",
fmt::format(
R"({{
"binary": true,
@@ -2168,7 +2429,58 @@ generateTestValuesForNormalPathTest()
ACCOUNT2
),
ripple::keylet::depositPreauth(account1, account2).key,
CreateDepositPreauthLedgerObject(ACCOUNT, ACCOUNT2)
CreateDepositPreauthLedgerObjectByAuth(ACCOUNT, ACCOUNT2)
},
NormalPathTestBundle{
"DepositPreauthByAuthCredentials",
fmt::format(
R"({{
"binary": true,
"deposit_preauth": {{
"owner": "{}",
"authorized_credentials": [
{{
"issuer": "{}",
"credential_type": "{}"
}}
]
}}
}})",
ACCOUNT,
ACCOUNT2,
CREDENTIALTYPE
),
ripple::keylet::depositPreauth(
account1,
credentials::createAuthCredentials(CreateAuthCredentialArray(
std::vector<std::string_view>{ACCOUNT2}, std::vector<std::string_view>{CREDENTIALTYPE}
))
)
.key,
CreateDepositPreauthLedgerObjectByAuthCredentials(ACCOUNT, ACCOUNT2, CREDENTIALTYPE)
},
NormalPathTestBundle{
"Credentials",
fmt::format(
R"({{
"binary": true,
"credential": {{
"subject": "{}",
"issuer": "{}",
"credential_type": "{}"
}}
}})",
ACCOUNT,
ACCOUNT2,
CREDENTIALTYPE
),
ripple::keylet::credential(
account1,
account2,
ripple::Slice(ripple::strUnHex(CREDENTIALTYPE)->data(), ripple::strUnHex(CREDENTIALTYPE)->size())
)
.key,
CreateCredentialObject(ACCOUNT, ACCOUNT2, CREDENTIALTYPE)
},
NormalPathTestBundle{
"RippleState",

View File

@@ -55,6 +55,7 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList)
JS(mpt_issuance),
JS(mptoken),
JS(oracle),
JS(credential),
JS(nunl)
};
@@ -86,6 +87,7 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList)
JS(xchain_owned_create_account_claim_id),
JS(did),
JS(oracle),
JS(credential),
JS(mpt_issuance),
JS(mptoken)
};