mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Remove gRPC code previously used for the xpring SDK
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,6 @@
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/beast/unit_test.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/rpc/impl/GRPCHelpers.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -1883,114 +1882,6 @@ class NegativeUNLVoteFilterValidations_test : public beast::unit_test::suite
|
||||
}
|
||||
};
|
||||
|
||||
class NegativeUNLgRPC_test : public beast::unit_test::suite
|
||||
{
|
||||
template <class T>
|
||||
std::string
|
||||
toByteString(T const& data)
|
||||
{
|
||||
const char* bytes = reinterpret_cast<const char*>(data.data());
|
||||
return {bytes, data.size()};
|
||||
}
|
||||
|
||||
void
|
||||
testGRPC()
|
||||
{
|
||||
testcase("gRPC test");
|
||||
|
||||
auto gRpcTest = [this](
|
||||
std::uint32_t negUnlSize,
|
||||
bool hasToDisable,
|
||||
bool hasToReEnable) -> bool {
|
||||
NetworkHistory history = {
|
||||
*this, {20, negUnlSize, hasToDisable, hasToReEnable, {}}};
|
||||
if (!history.goodHistory)
|
||||
return false;
|
||||
|
||||
auto const& negUnlObject =
|
||||
history.lastLedger()->read(keylet::negativeUNL());
|
||||
if (!negUnlSize && !hasToDisable && !hasToReEnable && !negUnlObject)
|
||||
return true;
|
||||
if (!negUnlObject)
|
||||
return false;
|
||||
|
||||
org::xrpl::rpc::v1::NegativeUNL to;
|
||||
ripple::RPC::convert(to, *negUnlObject);
|
||||
if (!to.has_flags() ||
|
||||
to.flags().value() != negUnlObject->getFlags())
|
||||
return false;
|
||||
|
||||
bool goodSize = to.disabled_validators_size() == negUnlSize &&
|
||||
to.has_validator_to_disable() == hasToDisable &&
|
||||
to.has_validator_to_re_enable() == hasToReEnable;
|
||||
if (!goodSize)
|
||||
return false;
|
||||
|
||||
if (negUnlSize)
|
||||
{
|
||||
if (!negUnlObject->isFieldPresent(sfDisabledValidators))
|
||||
return false;
|
||||
auto const& nUnlData =
|
||||
negUnlObject->getFieldArray(sfDisabledValidators);
|
||||
if (nUnlData.size() != negUnlSize)
|
||||
return false;
|
||||
int idx = 0;
|
||||
for (auto const& n : nUnlData)
|
||||
{
|
||||
if (!n.isFieldPresent(sfPublicKey) ||
|
||||
!n.isFieldPresent(sfFirstLedgerSequence))
|
||||
return false;
|
||||
|
||||
if (!to.disabled_validators(idx).has_ledger_sequence() ||
|
||||
!to.disabled_validators(idx).has_public_key())
|
||||
return false;
|
||||
|
||||
if (to.disabled_validators(idx).public_key().value() !=
|
||||
toByteString(n.getFieldVL(sfPublicKey)))
|
||||
return false;
|
||||
|
||||
if (to.disabled_validators(idx).ledger_sequence().value() !=
|
||||
n.getFieldU32(sfFirstLedgerSequence))
|
||||
return false;
|
||||
|
||||
++idx;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasToDisable)
|
||||
{
|
||||
if (!negUnlObject->isFieldPresent(sfValidatorToDisable))
|
||||
return false;
|
||||
if (to.validator_to_disable().value() !=
|
||||
toByteString(
|
||||
negUnlObject->getFieldVL(sfValidatorToDisable)))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasToReEnable)
|
||||
{
|
||||
if (!negUnlObject->isFieldPresent(sfValidatorToReEnable))
|
||||
return false;
|
||||
if (to.validator_to_re_enable().value() !=
|
||||
toByteString(
|
||||
negUnlObject->getFieldVL(sfValidatorToReEnable)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
BEAST_EXPECT(gRpcTest(0, false, false));
|
||||
BEAST_EXPECT(gRpcTest(2, true, true));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testGRPC();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(NegativeUNL, ledger, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(NegativeUNLNoAmendment, ledger, ripple);
|
||||
|
||||
@@ -2006,7 +1897,6 @@ BEAST_DEFINE_TESTSUITE_PRIO(
|
||||
1);
|
||||
BEAST_DEFINE_TESTSUITE(NegativeUNLVoteNewValidator, consensus, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(NegativeUNLVoteFilterValidations, consensus, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(NegativeUNLgRPC, ledger, ripple);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,975 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/basics/safe_cast.h>
|
||||
#include <ripple/beast/unit_test.h>
|
||||
#include <ripple/protocol/InnerObjectFormats.h>
|
||||
#include <ripple/protocol/LedgerFormats.h>
|
||||
#include <ripple/protocol/TxFormats.h>
|
||||
|
||||
#include "org/xrpl/rpc/v1/ledger_objects.pb.h"
|
||||
#include "org/xrpl/rpc/v1/transaction.pb.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// This test suite uses the google::protobuf::Descriptor class to do runtime
|
||||
// reflection on our gRPC stuff. At the time of this writing documentation
|
||||
// for Descriptor could be found here:
|
||||
//
|
||||
// https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor#Descriptor
|
||||
|
||||
class KnownFormatToGRPC_test : public beast::unit_test::suite
|
||||
{
|
||||
private:
|
||||
static constexpr auto fieldTYPE_UINT32 =
|
||||
google::protobuf::FieldDescriptor::Type::TYPE_UINT32;
|
||||
|
||||
static constexpr auto fieldTYPE_UINT64 =
|
||||
google::protobuf::FieldDescriptor::Type::TYPE_UINT64;
|
||||
|
||||
static constexpr auto fieldTYPE_BYTES =
|
||||
google::protobuf::FieldDescriptor::Type::TYPE_BYTES;
|
||||
|
||||
static constexpr auto fieldTYPE_STRING =
|
||||
google::protobuf::FieldDescriptor::Type::TYPE_STRING;
|
||||
|
||||
static constexpr auto fieldTYPE_MESSAGE =
|
||||
google::protobuf::FieldDescriptor::Type::TYPE_MESSAGE;
|
||||
|
||||
// Format names are CamelCase and FieldDescriptor names are snake_case.
|
||||
// Convert from CamelCase to snake_case. Do not be fooled by consecutive
|
||||
// capital letters like in NegativeUNL.
|
||||
static std::string
|
||||
formatNameToEntryTypeName(std::string const& fmtName)
|
||||
{
|
||||
std::string entryName;
|
||||
entryName.reserve(fmtName.size());
|
||||
bool prevUpper = false;
|
||||
for (std::size_t i = 0; i < fmtName.size(); i++)
|
||||
{
|
||||
char const ch = fmtName[i];
|
||||
bool const upper = std::isupper(ch);
|
||||
if (i > 0 && !prevUpper && upper)
|
||||
entryName.push_back('_');
|
||||
|
||||
prevUpper = upper;
|
||||
entryName.push_back(std::tolower(ch));
|
||||
}
|
||||
return entryName;
|
||||
};
|
||||
|
||||
// Create a map of (most) all the SFields in an SOTemplate. This map
|
||||
// can be used to correlate a gRPC Descriptor to its corresponding SField.
|
||||
template <typename KeyType>
|
||||
static std::map<std::string, SField const*>
|
||||
soTemplateToSFields(
|
||||
SOTemplate const& soTemplate,
|
||||
[[maybe_unused]] KeyType fmtId)
|
||||
{
|
||||
std::map<std::string, SField const*> sFields;
|
||||
for (SOElement const& element : soTemplate)
|
||||
{
|
||||
SField const& sField = element.sField();
|
||||
|
||||
// Fields that gRPC never includes.
|
||||
//
|
||||
// o sfLedgerIndex and
|
||||
// o sfLedgerEntryType are common to all ledger objects, so
|
||||
// gRPC includes them at a higher level than the ledger
|
||||
// object itself.
|
||||
//
|
||||
// o sfOperationLimit is an optional field in all transactions,
|
||||
// but no one knows what it was intended for.
|
||||
using FieldCode_t =
|
||||
std::remove_const<decltype(SField::fieldCode)>::type;
|
||||
static const std::set<FieldCode_t> excludedSFields{
|
||||
sfLedgerIndex.fieldCode,
|
||||
sfLedgerEntryType.fieldCode,
|
||||
sfOperationLimit.fieldCode};
|
||||
|
||||
if (excludedSFields.count(sField.fieldCode))
|
||||
continue;
|
||||
|
||||
// There are certain fields that gRPC never represents in
|
||||
// transactions. Exclude those.
|
||||
//
|
||||
// o sfPreviousTxnID is obsolete and was replaced by
|
||||
// sfAccountTxnID some time before November of 2014.
|
||||
//
|
||||
// o sfWalletLocator and
|
||||
// o sfWalletSize have been deprecated for six years or more.
|
||||
//
|
||||
// o sfTransactionType is not needed by gRPC, since the typing
|
||||
// is handled using protobuf message types.
|
||||
if constexpr (std::is_same_v<KeyType, TxType>)
|
||||
{
|
||||
static const std::set<FieldCode_t> excludedTxFields{
|
||||
sfPreviousTxnID.fieldCode,
|
||||
sfTransactionType.fieldCode,
|
||||
sfWalletLocator.fieldCode,
|
||||
sfWalletSize.fieldCode};
|
||||
|
||||
if (excludedTxFields.count(sField.fieldCode))
|
||||
continue;
|
||||
}
|
||||
|
||||
// If fmtId is a LedgerEntryType, exclude certain fields.
|
||||
if constexpr (std::is_same_v<KeyType, LedgerEntryType>)
|
||||
{
|
||||
// Fields that gRPC does not include in certain LedgerFormats.
|
||||
//
|
||||
// o sfWalletLocator,
|
||||
// o sfWalletSize,
|
||||
// o sfExchangeRate, and
|
||||
// o sfFirstLedgerSequence are all deprecated fields in
|
||||
// their respective ledger objects.
|
||||
static const std::
|
||||
map<LedgerEntryType, std::vector<SField const*>>
|
||||
gRPCOmitFields{
|
||||
{ltACCOUNT_ROOT, {&sfWalletLocator, &sfWalletSize}},
|
||||
{ltDIR_NODE, {&sfExchangeRate}},
|
||||
{ltLEDGER_HASHES, {&sfFirstLedgerSequence}},
|
||||
};
|
||||
|
||||
if (auto const iter = gRPCOmitFields.find(fmtId);
|
||||
iter != gRPCOmitFields.end())
|
||||
{
|
||||
std::vector<SField const*> const& omits = iter->second;
|
||||
|
||||
// Check for fields that gRPC omits from this type.
|
||||
if (std::find_if(
|
||||
omits.begin(),
|
||||
omits.end(),
|
||||
[&sField](SField const* const omit) {
|
||||
return *omit == sField;
|
||||
}) != omits.end())
|
||||
{
|
||||
// This is one of the fields that gRPC omits.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The SFields and gRPC disagree on the names of some fields.
|
||||
// Provide a mapping from SField names to gRPC names for the
|
||||
// known exceptions.
|
||||
//
|
||||
// clang-format off
|
||||
//
|
||||
// The implementers of the gRPC interface made the decision not
|
||||
// to abbreviate anything. This accounts for the following
|
||||
// field name differences:
|
||||
//
|
||||
// "AccountTxnID", "AccountTransactionID"
|
||||
// "PreviousTxnID", "PreviousTransactionID"
|
||||
// "PreviousTxnLgrSeq", "PreviousTransactionLedgerSequence"
|
||||
// "SigningPubKey", "SigningPublicKey"
|
||||
// "TxnSignature", "TransactionSignature"
|
||||
//
|
||||
// gRPC adds typing information for Fee, which accounts for
|
||||
// "Fee", "XRPDropsAmount"
|
||||
//
|
||||
// There's one misspelling which accounts for
|
||||
// "TakerGetsCurrency", "TakerGetsCurreny"
|
||||
//
|
||||
// The implementers of the gRPC interface observed that a
|
||||
// PaymentChannelClaim transaction has a TxnSignature field at the
|
||||
// upper level and a Signature field at the lever level. They
|
||||
// felt that was confusing, which is the reason for
|
||||
// "Signature", "PaymentChannelSignature"
|
||||
//
|
||||
static const std::map<std::string, std::string> sFieldToGRPC{
|
||||
{"AccountTxnID", "AccountTransactionID"},
|
||||
{"Fee", "XRPDropsAmount"},
|
||||
{"PreviousTxnID", "PreviousTransactionID"},
|
||||
{"PreviousTxnLgrSeq", "PreviousTransactionLedgerSequence"},
|
||||
{"Signature", "PaymentChannelSignature"},
|
||||
{"SigningPubKey", "SigningPublicKey"},
|
||||
{"TakerGetsCurrency", "TakerGetsCurreny"},
|
||||
{"TxnSignature", "TransactionSignature"},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
auto const iter = sFieldToGRPC.find(sField.getName());
|
||||
std::string gRPCName =
|
||||
iter != sFieldToGRPC.end() ? iter->second : sField.getName();
|
||||
|
||||
sFields.insert({std::move(gRPCName), &sField});
|
||||
}
|
||||
return sFields;
|
||||
}
|
||||
|
||||
// Given a Descriptor for a KnownFormat and a map of the SFields of that
|
||||
// KnownFormat, make sure the fields are aligned.
|
||||
void
|
||||
validateDescriptorAgainstSFields(
|
||||
google::protobuf::Descriptor const* const pbufDescriptor,
|
||||
google::protobuf::Descriptor const* const commonFields,
|
||||
std::string const& knownFormatName,
|
||||
std::map<std::string, SField const*>&& sFields)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
// We'll be running through two sets of pbuf::Descriptors: the ones in
|
||||
// the OneOf and the common fields. Here is a lambda that factors out
|
||||
// the common checking code for these two cases.
|
||||
auto checkFieldDesc = [this, &sFields, &knownFormatName](
|
||||
pbuf::FieldDescriptor const* const
|
||||
fieldDesc) {
|
||||
// gRPC has different handling for repeated vs non-repeated
|
||||
// types. So we need to do that too.
|
||||
std::string name;
|
||||
if (fieldDesc->is_repeated())
|
||||
{
|
||||
// Repeated-type handling.
|
||||
|
||||
// Munge the fieldDescriptor name so it looks like the
|
||||
// name in sFields.
|
||||
name = fieldDesc->camelcase_name();
|
||||
name[0] = toupper(name[0]);
|
||||
|
||||
// The ledger gives UNL all caps. Adapt to that.
|
||||
if (size_t const i = name.find("Unl"); i != std::string::npos)
|
||||
{
|
||||
name[i + 1] = 'N';
|
||||
name[i + 2] = 'L';
|
||||
}
|
||||
|
||||
// The ledger gives the NFT part of NFToken all caps.
|
||||
// Adapt to that.
|
||||
if (size_t const i = name.find("Nft"); i != std::string::npos)
|
||||
{
|
||||
name[i + 1] = 'F';
|
||||
name[i + 2] = 'T';
|
||||
}
|
||||
|
||||
if (!sFields.count(name))
|
||||
{
|
||||
fail(
|
||||
std::string("Repeated Protobuf Descriptor '") + name +
|
||||
"' expected in KnownFormat '" + knownFormatName +
|
||||
"' and not found",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
pass();
|
||||
|
||||
validateRepeatedField(fieldDesc, sFields.at(name));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-repeated handling.
|
||||
pbuf::Descriptor const* const entryDesc =
|
||||
fieldDesc->message_type();
|
||||
if (entryDesc == nullptr)
|
||||
return;
|
||||
|
||||
name = entryDesc->name();
|
||||
if (!sFields.count(name))
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") +
|
||||
entryDesc->name() + "' expected in KnownFormat '" +
|
||||
knownFormatName + "' and not found",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
pass();
|
||||
|
||||
validateDescriptor(entryDesc, sFields.at(entryDesc->name()));
|
||||
}
|
||||
// Remove the validated field from the map so we can tell if
|
||||
// there are left over fields at the end of all comparisons.
|
||||
sFields.erase(name);
|
||||
};
|
||||
|
||||
// Compare the SFields to the FieldDescriptor->Descriptors.
|
||||
for (int i = 0; i < pbufDescriptor->field_count(); ++i)
|
||||
{
|
||||
pbuf::FieldDescriptor const* const fieldDesc =
|
||||
pbufDescriptor->field(i);
|
||||
if (fieldDesc == nullptr || fieldDesc->type() != fieldTYPE_MESSAGE)
|
||||
continue;
|
||||
|
||||
checkFieldDesc(fieldDesc);
|
||||
}
|
||||
|
||||
// Now all of the OneOf-specific fields have been removed from
|
||||
// sFields. But there may be common fields left in there. Process
|
||||
// the commonFields next.
|
||||
if (commonFields)
|
||||
{
|
||||
for (int i = 0; i < commonFields->field_count(); ++i)
|
||||
{
|
||||
// If the field we picked up is a OneOf, skip it. Common
|
||||
// fields are never OneOfs.
|
||||
pbuf::FieldDescriptor const* const fieldDesc =
|
||||
commonFields->field(i);
|
||||
|
||||
if (fieldDesc == nullptr ||
|
||||
fieldDesc->containing_oneof() != nullptr ||
|
||||
fieldDesc->type() != fieldTYPE_MESSAGE)
|
||||
continue;
|
||||
|
||||
checkFieldDesc(fieldDesc);
|
||||
}
|
||||
}
|
||||
|
||||
// All SFields in the KnownFormat have corresponding gRPC fields
|
||||
// if the sFields map is now empty.
|
||||
if (!sFields.empty())
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") + pbufDescriptor->name() +
|
||||
"' did not account for all fields in KnownFormat '" +
|
||||
knownFormatName + "'. Left over field: `" +
|
||||
sFields.begin()->first + "'",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
pass();
|
||||
}
|
||||
|
||||
// Compare a protobuf descriptor with multiple oneOfFields to choose from
|
||||
// to an SField.
|
||||
void
|
||||
validateOneOfDescriptor(
|
||||
google::protobuf::Descriptor const* const entryDesc,
|
||||
SField const* const sField)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
// Note that it's not okay to compare names because SFields and
|
||||
// gRPC do not always agree on the names.
|
||||
if (entryDesc->field_count() == 0 || entryDesc->oneof_decl_count() != 1)
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") + entryDesc->name() +
|
||||
"' expected to have multiple OneOf fields and nothing else",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
pbuf::FieldDescriptor const* const fieldDesc = entryDesc->field(0);
|
||||
if (fieldDesc == nullptr)
|
||||
{
|
||||
fail(
|
||||
std::string("Internal test failure. Unhandled nullptr "
|
||||
"in FieldDescriptor for '") +
|
||||
entryDesc->name() + "'",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for CurrencyAmount
|
||||
if (sField->fieldType == STI_AMOUNT &&
|
||||
entryDesc->name() == "CurrencyAmount")
|
||||
{
|
||||
// SFields of type STI_AMOUNT are represented in gRPC by a
|
||||
// multi-field CurrencyAmount. We don't really learn anything
|
||||
// by diving into the interior of CurrencyAmount, so we stop here
|
||||
// and call it good.
|
||||
pass();
|
||||
return;
|
||||
}
|
||||
|
||||
fail(
|
||||
std::string("Unhandled OneOf Protobuf Descriptor '") +
|
||||
entryDesc->name() + "'",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
}
|
||||
|
||||
void
|
||||
validateMultiFieldDescriptor(
|
||||
google::protobuf::Descriptor const* const entryDesc,
|
||||
SField const* const sField)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
if (entryDesc->field_count() <= 1 || entryDesc->oneof_decl_count() != 0)
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") + entryDesc->name() +
|
||||
"' expected to have multiple fields and nothing else",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// There are composite fields that the SFields handle differently
|
||||
// from gRPC. Handle those here.
|
||||
{
|
||||
struct FieldContents
|
||||
{
|
||||
std::string_view fieldName;
|
||||
google::protobuf::FieldDescriptor::Type fieldType;
|
||||
|
||||
bool
|
||||
operator<(FieldContents const& other) const
|
||||
{
|
||||
return this->fieldName < other.fieldName;
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(FieldContents const& other) const
|
||||
{
|
||||
return this->fieldName == other.fieldName &&
|
||||
this->fieldType == other.fieldType;
|
||||
}
|
||||
};
|
||||
|
||||
struct SpecialEntry
|
||||
{
|
||||
std::string_view const descriptorName;
|
||||
SerializedTypeID const sFieldType;
|
||||
std::set<FieldContents> const fields;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
static const std::array specialEntries{
|
||||
SpecialEntry{
|
||||
"Currency", STI_UINT160,
|
||||
{
|
||||
{"name", fieldTYPE_STRING},
|
||||
{"code", fieldTYPE_BYTES}
|
||||
}
|
||||
},
|
||||
SpecialEntry{
|
||||
"Memo", STI_OBJECT,
|
||||
{
|
||||
{"memo_data", fieldTYPE_BYTES},
|
||||
{"memo_format", fieldTYPE_BYTES},
|
||||
{"memo_type", fieldTYPE_BYTES}
|
||||
}
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// If we're handling a SpecialEntry...
|
||||
if (auto const iter = std::find_if(
|
||||
specialEntries.begin(),
|
||||
specialEntries.end(),
|
||||
[entryDesc, sField](SpecialEntry const& entry) {
|
||||
return entryDesc->name() == entry.descriptorName &&
|
||||
sField->fieldType == entry.sFieldType;
|
||||
});
|
||||
iter != specialEntries.end())
|
||||
{
|
||||
// Verify the SField.
|
||||
if (!BEAST_EXPECT(sField->fieldType == iter->sFieldType))
|
||||
return;
|
||||
|
||||
// Verify all of the fields in the entryDesc.
|
||||
if (!BEAST_EXPECT(
|
||||
entryDesc->field_count() == iter->fields.size()))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < entryDesc->field_count(); ++i)
|
||||
{
|
||||
pbuf::FieldDescriptor const* const fieldDesc =
|
||||
entryDesc->field(i);
|
||||
|
||||
FieldContents const contents{
|
||||
fieldDesc->name(), fieldDesc->type()};
|
||||
|
||||
if (!BEAST_EXPECT(
|
||||
iter->fields.find(contents) != iter->fields.end()))
|
||||
return;
|
||||
}
|
||||
|
||||
// This field is good.
|
||||
pass();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not one of the SpecialEntries, we expect it to be
|
||||
// an InnerObjectFormat.
|
||||
SOTemplate const* const innerFormat =
|
||||
InnerObjectFormats::getInstance().findSOTemplateBySField(*sField);
|
||||
if (innerFormat == nullptr)
|
||||
{
|
||||
fail(
|
||||
"SOTemplate for field '" + sField->getName() + "' not found",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a map we can use use to correlate each field in the
|
||||
// gRPC Descriptor to its corresponding SField.
|
||||
std::map<std::string, SField const*> sFields =
|
||||
soTemplateToSFields(*innerFormat, 0);
|
||||
|
||||
// Compare the SFields to the FieldDescriptor->Descriptors.
|
||||
validateDescriptorAgainstSFields(
|
||||
entryDesc, nullptr, sField->getName(), std::move(sFields));
|
||||
}
|
||||
|
||||
// Compare a protobuf descriptor with only one field to an SField.
|
||||
void
|
||||
validateOneDescriptor(
|
||||
google::protobuf::Descriptor const* const entryDesc,
|
||||
SField const* const sField)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
// Note that it's not okay to compare names because SFields and
|
||||
// gRPC do not always agree on the names.
|
||||
if (entryDesc->field_count() != 1 || entryDesc->oneof_decl_count() != 0)
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") + entryDesc->name() +
|
||||
"' expected to be one field and nothing else",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
pbuf::FieldDescriptor const* const fieldDesc = entryDesc->field(0);
|
||||
if (fieldDesc == nullptr)
|
||||
{
|
||||
fail(
|
||||
std::string("Internal test failure. Unhandled nullptr "
|
||||
"in FieldDescriptor for '") +
|
||||
entryDesc->name() + "'",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a map from SerializedTypeID to pbuf::FieldDescriptor::Type.
|
||||
//
|
||||
// This works for most, but not all, types because of divergence
|
||||
// between the gRPC and LedgerFormat implementations. We deal
|
||||
// with the special cases later.
|
||||
// clang-format off
|
||||
static const std::map<SerializedTypeID, pbuf::FieldDescriptor::Type>
|
||||
sTypeToFieldDescType{
|
||||
{STI_UINT8, fieldTYPE_UINT32},
|
||||
{STI_UINT16, fieldTYPE_UINT32},
|
||||
{STI_UINT32, fieldTYPE_UINT32},
|
||||
|
||||
{STI_UINT64, fieldTYPE_UINT64},
|
||||
|
||||
{STI_ACCOUNT, fieldTYPE_STRING},
|
||||
|
||||
{STI_AMOUNT, fieldTYPE_BYTES},
|
||||
{STI_UINT128, fieldTYPE_BYTES},
|
||||
{STI_UINT160, fieldTYPE_BYTES},
|
||||
{STI_UINT256, fieldTYPE_BYTES},
|
||||
{STI_VL, fieldTYPE_BYTES},
|
||||
};
|
||||
//clang-format on
|
||||
|
||||
// If the SField and FieldDescriptor::Type correlate we're good.
|
||||
if (auto const iter = sTypeToFieldDescType.find(sField->fieldType);
|
||||
iter != sTypeToFieldDescType.end() &&
|
||||
iter->second == fieldDesc->type())
|
||||
{
|
||||
pass();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle special cases for specific SFields.
|
||||
static const std::map<int, pbuf::FieldDescriptor::Type>
|
||||
sFieldCodeToFieldDescType{
|
||||
{sfDomain.fieldCode, fieldTYPE_STRING},
|
||||
{sfFee.fieldCode, fieldTYPE_UINT64},
|
||||
{sfURI.fieldCode, fieldTYPE_STRING}};
|
||||
|
||||
if (auto const iter = sFieldCodeToFieldDescType.find(sField->fieldCode);
|
||||
iter != sFieldCodeToFieldDescType.end() &&
|
||||
iter->second == fieldDesc->type())
|
||||
{
|
||||
pass();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for all Message types.
|
||||
if (fieldDesc->type() == fieldTYPE_MESSAGE)
|
||||
{
|
||||
// We need to recurse to get to the bottom of the field(s)
|
||||
// in question.
|
||||
|
||||
// Start by identifying which fields we need to be handling.
|
||||
// clang-format off
|
||||
static const std::map<int, std::string> messageMap{
|
||||
{sfAccount.fieldCode, "AccountAddress"},
|
||||
{sfAmount.fieldCode, "CurrencyAmount"},
|
||||
{sfAuthorize.fieldCode, "AccountAddress"},
|
||||
{sfBalance.fieldCode, "CurrencyAmount"},
|
||||
{sfDestination.fieldCode, "AccountAddress"},
|
||||
{sfFee.fieldCode, "XRPDropsAmount"},
|
||||
{sfHighLimit.fieldCode, "CurrencyAmount"},
|
||||
{sfLowLimit.fieldCode, "CurrencyAmount"},
|
||||
{sfOwner.fieldCode, "AccountAddress"},
|
||||
{sfRegularKey.fieldCode, "AccountAddress"},
|
||||
{sfSendMax.fieldCode, "CurrencyAmount"},
|
||||
{sfTakerGets.fieldCode, "CurrencyAmount"},
|
||||
{sfTakerGetsCurrency.fieldCode, "Currency"},
|
||||
{sfTakerPays.fieldCode, "CurrencyAmount"},
|
||||
{sfTakerPaysCurrency.fieldCode, "Currency"},
|
||||
};
|
||||
// clang-format on
|
||||
if (messageMap.count(sField->fieldCode))
|
||||
{
|
||||
pbuf::Descriptor const* const entry2Desc =
|
||||
fieldDesc->message_type();
|
||||
|
||||
if (entry2Desc == nullptr)
|
||||
{
|
||||
fail(
|
||||
std::string("Unexpected gRPC. ") + fieldDesc->name() +
|
||||
" MESSAGE with null Descriptor",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// The Descriptor name should match the messageMap name.
|
||||
if (messageMap.at(sField->fieldCode) != entry2Desc->name())
|
||||
{
|
||||
fail(
|
||||
std::string(
|
||||
"Internal test error. Mismatch between SField '") +
|
||||
sField->getName() + "' and gRPC Descriptor name '" +
|
||||
entry2Desc->name() + "'",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
pass();
|
||||
|
||||
// Recurse to the next lower Descriptor.
|
||||
validateDescriptor(entry2Desc, sField);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fail(
|
||||
std::string("Internal test error. Unhandled FieldDescriptor '") +
|
||||
entryDesc->name() + "' has type `" + fieldDesc->type_name() +
|
||||
"` and label " + std::to_string(fieldDesc->label()),
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
}
|
||||
|
||||
// Compare a repeated protobuf FieldDescriptor to an SField.
|
||||
void
|
||||
validateRepeatedField(
|
||||
google::protobuf::FieldDescriptor const* const fieldDesc,
|
||||
SField const* const sField)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
pbuf::Descriptor const* const entryDesc = fieldDesc->message_type();
|
||||
if (entryDesc == nullptr)
|
||||
{
|
||||
fail(
|
||||
std::string("Expected Descriptor for repeated type ") +
|
||||
sField->getName(),
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// The following repeated types provide no further structure for their
|
||||
// in-ledger representation. We just have to trust that the gRPC
|
||||
// representation is reasonable for what the ledger implements.
|
||||
static const std::set<std::string> noFurtherDetail{
|
||||
{sfPaths.getName()},
|
||||
};
|
||||
|
||||
if (noFurtherDetail.count(sField->getName()))
|
||||
{
|
||||
// There is no Format representation for further details of this
|
||||
// repeated type. We've done the best we can.
|
||||
pass();
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the repeated types that the test currently supports.
|
||||
static const std::map<std::string, SField const*> repeatsWhat{
|
||||
{sfAmendments.getName(), &sfAmendment},
|
||||
{sfDisabledValidators.getName(), &sfDisabledValidator},
|
||||
{sfHashes.getName(), &sfLedgerHash},
|
||||
{sfIndexes.getName(), &sfLedgerIndex},
|
||||
{sfMajorities.getName(), &sfMajority},
|
||||
{sfMemos.getName(), &sfMemo},
|
||||
{sfNFTokens.getName(), &sfNFToken},
|
||||
{sfSignerEntries.getName(), &sfSignerEntry},
|
||||
{sfSigners.getName(), &sfSigner},
|
||||
{sfNFTokenOffers.getName(), &sfLedgerIndex}};
|
||||
|
||||
if (!repeatsWhat.count(sField->getName()))
|
||||
{
|
||||
fail(
|
||||
std::string("Unexpected repeated type ") + fieldDesc->name(),
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
pass();
|
||||
|
||||
// Process the type contained by the repeated type.
|
||||
validateDescriptor(entryDesc, repeatsWhat.at(sField->getName()));
|
||||
}
|
||||
|
||||
// Determine which of the Descriptor validators to dispatch to.
|
||||
void
|
||||
validateDescriptor(
|
||||
google::protobuf::Descriptor const* const entryDesc,
|
||||
SField const* const sField)
|
||||
{
|
||||
if (entryDesc->nested_type_count() != 0 ||
|
||||
entryDesc->enum_type_count() != 0 ||
|
||||
entryDesc->extension_range_count() != 0 ||
|
||||
entryDesc->reserved_range_count() != 0)
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") + entryDesc->name() +
|
||||
"' uses unsupported protobuf features",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch to the correct validator
|
||||
if (entryDesc->oneof_decl_count() > 0)
|
||||
return validateOneOfDescriptor(entryDesc, sField);
|
||||
|
||||
if (entryDesc->field_count() > 1)
|
||||
return validateMultiFieldDescriptor(entryDesc, sField);
|
||||
|
||||
return validateOneDescriptor(entryDesc, sField);
|
||||
}
|
||||
|
||||
// Compare a protobuf descriptor to a KnownFormat::Item
|
||||
template <typename FmtType, typename FmtName>
|
||||
void
|
||||
validateFields(
|
||||
google::protobuf::Descriptor const* const pbufDescriptor,
|
||||
google::protobuf::Descriptor const* const commonFields,
|
||||
typename KnownFormats<FmtType, FmtName>::Item const* const
|
||||
knownFormatItem)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
// The names should usually be the same, but the bpufDescriptor
|
||||
// name might have "Object" appended.
|
||||
if (knownFormatItem->getName() != pbufDescriptor->name() &&
|
||||
knownFormatItem->getName() + "Object" != pbufDescriptor->name())
|
||||
{
|
||||
fail(
|
||||
std::string("Protobuf Descriptor '") + pbufDescriptor->name() +
|
||||
"' and KnownFormat::Item '" + knownFormatItem->getName() +
|
||||
"' don't have the same name",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
return;
|
||||
}
|
||||
pass();
|
||||
|
||||
// Create a map we can use use to correlate each field in the
|
||||
// gRPC Descriptor to its corresponding SField.
|
||||
std::map<std::string, SField const*> sFields = soTemplateToSFields(
|
||||
knownFormatItem->getSOTemplate(), knownFormatItem->getType());
|
||||
|
||||
// Compare the SFields to the FieldDescriptor->Descriptors.
|
||||
validateDescriptorAgainstSFields(
|
||||
pbufDescriptor,
|
||||
commonFields,
|
||||
knownFormatItem->getName(),
|
||||
std::move(sFields));
|
||||
}
|
||||
|
||||
template <typename FmtType, typename FmtName>
|
||||
void
|
||||
testKnownFormats(
|
||||
KnownFormats<FmtType, FmtName> const& knownFormat,
|
||||
std::string const& knownFormatName,
|
||||
google::protobuf::Descriptor const* const commonFields,
|
||||
google::protobuf::OneofDescriptor const* const oneOfDesc)
|
||||
{
|
||||
// Create namespace aliases for shorter names.
|
||||
namespace grpc = org::xrpl::rpc::v1;
|
||||
namespace pbuf = google::protobuf;
|
||||
|
||||
if (!BEAST_EXPECT(oneOfDesc != nullptr))
|
||||
return;
|
||||
|
||||
// Get corresponding names for all KnownFormat Items.
|
||||
std::map<
|
||||
std::string,
|
||||
typename KnownFormats<FmtType, FmtName>::Item const*>
|
||||
formatTypes;
|
||||
|
||||
for (auto const& item : knownFormat)
|
||||
{
|
||||
if constexpr (std::is_same_v<FmtType, LedgerEntryType>)
|
||||
{
|
||||
// Skip LedgerEntryTypes that gRPC does not currently support.
|
||||
static constexpr std::array<LedgerEntryType, 0> notSupported{};
|
||||
|
||||
if (std::find(
|
||||
notSupported.begin(),
|
||||
notSupported.end(),
|
||||
item.getType()) != notSupported.end())
|
||||
continue;
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<FmtType, TxType>)
|
||||
{
|
||||
// Skip TxTypes that gRPC does not currently support.
|
||||
static constexpr std::array notSupported{
|
||||
ttAMENDMENT, ttFEE, ttUNL_MODIFY};
|
||||
|
||||
if (std::find(
|
||||
notSupported.begin(),
|
||||
notSupported.end(),
|
||||
item.getType()) != notSupported.end())
|
||||
continue;
|
||||
}
|
||||
|
||||
BEAST_EXPECT(
|
||||
formatTypes
|
||||
.insert({formatNameToEntryTypeName(item.getName()), &item})
|
||||
.second == true);
|
||||
}
|
||||
|
||||
// Verify that the OneOf objects match. Start by comparing
|
||||
// KnownFormat vs gRPC OneOf counts.
|
||||
{
|
||||
BEAST_EXPECT(formatTypes.size() == oneOfDesc->field_count());
|
||||
}
|
||||
|
||||
// This loop
|
||||
// 1. Iterates through the gRPC OneOfs,
|
||||
// 2. Finds each gRPC OneOf's matching KnownFormat::Item,
|
||||
// 3. Sanity checks that the fields of the objects align well.
|
||||
for (auto i = 0; i < oneOfDesc->field_count(); ++i)
|
||||
{
|
||||
pbuf::FieldDescriptor const* const fieldDesc = oneOfDesc->field(i);
|
||||
|
||||
// The Field should be a TYPE_MESSAGE, which means we can get its
|
||||
// descriptor.
|
||||
if (fieldDesc->type() != fieldTYPE_MESSAGE)
|
||||
{
|
||||
fail(
|
||||
std::string("gRPC OneOf '") + fieldDesc->name() +
|
||||
"' is not TYPE_MESSAGE",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const fmtIter = formatTypes.find(fieldDesc->name());
|
||||
|
||||
if (fmtIter == formatTypes.cend())
|
||||
{
|
||||
fail(
|
||||
std::string("gRPC OneOf '") + fieldDesc->name() +
|
||||
"' not found in " + knownFormatName,
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate that the gRPC and KnownFormat fields align.
|
||||
validateFields<FmtType, FmtName>(
|
||||
fieldDesc->message_type(), commonFields, fmtIter->second);
|
||||
|
||||
// Remove the checked KnownFormat from the map. This way we
|
||||
// can check for leftovers when we're done processing.
|
||||
formatTypes.erase(fieldDesc->name());
|
||||
}
|
||||
|
||||
// Report any KnownFormats that don't have gRPC OneOfs.
|
||||
for (auto const& spare : formatTypes)
|
||||
{
|
||||
fail(
|
||||
knownFormatName + " '" + spare.second->getName() +
|
||||
"' does not have a corresponding gRPC OneOf",
|
||||
__FILE__,
|
||||
__LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testLedgerObjectGRPCOneOfs()
|
||||
{
|
||||
testcase("Ledger object validation");
|
||||
|
||||
org::xrpl::rpc::v1::LedgerObject const ledgerObject;
|
||||
|
||||
testKnownFormats(
|
||||
LedgerFormats::getInstance(),
|
||||
"LedgerFormats",
|
||||
ledgerObject.GetDescriptor(),
|
||||
ledgerObject.GetDescriptor()->FindOneofByName("object"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
testTransactionGRPCOneOfs()
|
||||
{
|
||||
testcase("Transaction validation");
|
||||
|
||||
org::xrpl::rpc::v1::Transaction const txData;
|
||||
|
||||
testKnownFormats(
|
||||
TxFormats::getInstance(),
|
||||
"TxFormats",
|
||||
txData.GetDescriptor(),
|
||||
txData.GetDescriptor()->FindOneofByName("transaction_data"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testLedgerObjectGRPCOneOfs();
|
||||
testTransactionGRPCOneOfs();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(KnownFormatToGRPC, protocol, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
@@ -491,227 +491,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// gRPC stuff
|
||||
class GetAccountInfoClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
org::xrpl::rpc::v1::GetAccountInfoRequest request;
|
||||
org::xrpl::rpc::v1::GetAccountInfoResponse reply;
|
||||
|
||||
explicit GetAccountInfoClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
GetAccountInfo()
|
||||
{
|
||||
status = stub_->GetAccountInfo(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testSimpleGrpc()
|
||||
{
|
||||
testcase("gRPC simple");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
Account const alice{"alice"};
|
||||
env.fund(drops(1000 * 1000 * 1000), alice);
|
||||
|
||||
{
|
||||
// most simple case
|
||||
GetAccountInfoClient client(grpcPort);
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().account().value().address() ==
|
||||
alice.human());
|
||||
}
|
||||
{
|
||||
GetAccountInfoClient client(grpcPort);
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_queue(true);
|
||||
client.request.mutable_ledger()->set_sequence(3);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data()
|
||||
.balance()
|
||||
.value()
|
||||
.xrp_amount()
|
||||
.drops() == 1000 * 1000 * 1000);
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().account().value().address() ==
|
||||
alice.human());
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().sequence().value() ==
|
||||
env.seq(alice));
|
||||
BEAST_EXPECT(client.reply.queue_data().txn_count() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testErrorsGrpc()
|
||||
{
|
||||
testcase("gRPC errors");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
auto getClient = [&grpcPort]() {
|
||||
return GetAccountInfoClient(grpcPort);
|
||||
};
|
||||
Account const alice{"alice"};
|
||||
env.fund(drops(1000 * 1000 * 1000), alice);
|
||||
|
||||
{
|
||||
// bad address
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address("deadbeef");
|
||||
client.GetAccountInfo();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
{
|
||||
// no account
|
||||
Account const bogie{"bogie"};
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(bogie.human());
|
||||
client.GetAccountInfo();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
{
|
||||
// bad ledger_index
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.mutable_ledger()->set_sequence(0);
|
||||
client.GetAccountInfo();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSignerListsGrpc()
|
||||
{
|
||||
testcase("gRPC singer lists");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
auto getClient = [&grpcPort]() {
|
||||
return GetAccountInfoClient(grpcPort);
|
||||
};
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(drops(1000 * 1000 * 1000), alice);
|
||||
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(true);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
return;
|
||||
BEAST_EXPECT(client.reply.signer_list().signer_entries_size() == 0);
|
||||
}
|
||||
|
||||
// Give alice a SignerList.
|
||||
Account const bogie{"bogie"};
|
||||
Json::Value const smallSigners = signers(alice, 2, {{bogie, 3}});
|
||||
env(smallSigners);
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(false);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
return;
|
||||
BEAST_EXPECT(client.reply.signer_list().signer_entries_size() == 0);
|
||||
}
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(true);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().owner_count().value() == 1);
|
||||
BEAST_EXPECT(client.reply.signer_list().signer_entries_size() == 1);
|
||||
}
|
||||
|
||||
// Give alice a big signer list
|
||||
Account const demon{"demon"};
|
||||
Account const ghost{"ghost"};
|
||||
Account const haunt{"haunt"};
|
||||
Account const jinni{"jinni"};
|
||||
Account const phase{"phase"};
|
||||
Account const shade{"shade"};
|
||||
Account const spook{"spook"};
|
||||
Json::Value const bigSigners = signers(
|
||||
alice,
|
||||
4,
|
||||
{
|
||||
{bogie, 1},
|
||||
{demon, 1},
|
||||
{ghost, 1},
|
||||
{haunt, 1},
|
||||
{jinni, 1},
|
||||
{phase, 1},
|
||||
{shade, 1},
|
||||
{spook, 1},
|
||||
});
|
||||
env(bigSigners);
|
||||
|
||||
std::set<std::string> accounts;
|
||||
accounts.insert(bogie.human());
|
||||
accounts.insert(demon.human());
|
||||
accounts.insert(ghost.human());
|
||||
accounts.insert(haunt.human());
|
||||
accounts.insert(jinni.human());
|
||||
accounts.insert(phase.human());
|
||||
accounts.insert(shade.human());
|
||||
accounts.insert(spook.human());
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.mutable_account()->set_address(alice.human());
|
||||
client.request.set_signer_lists(true);
|
||||
client.GetAccountInfo();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.account_data().owner_count().value() == 1);
|
||||
auto& signerList = client.reply.signer_list();
|
||||
BEAST_EXPECT(signerList.signer_quorum().value() == 4);
|
||||
BEAST_EXPECT(signerList.signer_entries_size() == 8);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
signerList.signer_entries(i).signer_weight().value() == 1);
|
||||
BEAST_EXPECT(
|
||||
accounts.erase(signerList.signer_entries(i)
|
||||
.account()
|
||||
.value()
|
||||
.address()) == 1);
|
||||
}
|
||||
BEAST_EXPECT(accounts.size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -719,9 +498,6 @@ public:
|
||||
testSignerLists();
|
||||
testSignerListsApiVersion2();
|
||||
testSignerListsV2();
|
||||
testSimpleGrpc();
|
||||
testErrorsGrpc();
|
||||
testSignerListsGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/basics/mulDiv.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Fee_test : public beast::unit_test::suite
|
||||
{
|
||||
class GrpcFeeClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
org::xrpl::rpc::v1::GetFeeRequest request;
|
||||
org::xrpl::rpc::v1::GetFeeResponse reply;
|
||||
|
||||
explicit GrpcFeeClient(std::string const& grpcPort)
|
||||
: GRPCTestClientBase(grpcPort)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
GetFee()
|
||||
{
|
||||
status = stub_->GetFee(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
std::pair<bool, org::xrpl::rpc::v1::GetFeeResponse>
|
||||
grpcGetFee(std::string const& grpcPort)
|
||||
{
|
||||
GrpcFeeClient client(grpcPort);
|
||||
client.GetFee();
|
||||
return std::pair<bool, org::xrpl::rpc::v1::GetFeeResponse>(
|
||||
client.status.ok(), client.reply);
|
||||
}
|
||||
|
||||
void
|
||||
testFeeGrpc()
|
||||
{
|
||||
testcase("Test Fee Grpc");
|
||||
|
||||
using namespace test::jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
env.fund(XRP(10000), A1);
|
||||
env.fund(XRP(10000), A2);
|
||||
env.close();
|
||||
env.trust(A2["USD"](1000), A1);
|
||||
env.close();
|
||||
for (int i = 0; i < 7; ++i)
|
||||
{
|
||||
env(pay(A2, A1, A2["USD"](100)));
|
||||
if (i == 4)
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto view = env.current();
|
||||
|
||||
auto const metrics = env.app().getTxQ().getMetrics(*env.current());
|
||||
|
||||
auto const result = grpcGetFee(grpcPort);
|
||||
|
||||
BEAST_EXPECT(result.first == true);
|
||||
|
||||
auto reply = result.second;
|
||||
|
||||
// current ledger data
|
||||
BEAST_EXPECT(reply.current_ledger_size() == metrics.txInLedger);
|
||||
BEAST_EXPECT(reply.current_queue_size() == metrics.txCount);
|
||||
BEAST_EXPECT(reply.expected_ledger_size() == metrics.txPerLedger);
|
||||
BEAST_EXPECT(reply.ledger_current_index() == view->info().seq);
|
||||
BEAST_EXPECT(reply.max_queue_size() == *metrics.txQMaxSize);
|
||||
|
||||
// fee levels data
|
||||
org::xrpl::rpc::v1::FeeLevels& levels = *reply.mutable_levels();
|
||||
BEAST_EXPECT(levels.median_level() == metrics.medFeeLevel);
|
||||
BEAST_EXPECT(levels.minimum_level() == metrics.minProcessingFeeLevel);
|
||||
BEAST_EXPECT(levels.open_ledger_level() == metrics.openLedgerFeeLevel);
|
||||
BEAST_EXPECT(levels.reference_level() == metrics.referenceFeeLevel);
|
||||
|
||||
// fee data
|
||||
org::xrpl::rpc::v1::Fee& fee = *reply.mutable_fee();
|
||||
auto const baseFee = view->fees().base;
|
||||
BEAST_EXPECT(
|
||||
fee.base_fee().drops() ==
|
||||
toDrops(metrics.referenceFeeLevel, baseFee));
|
||||
BEAST_EXPECT(
|
||||
fee.minimum_fee().drops() ==
|
||||
toDrops(metrics.minProcessingFeeLevel, baseFee));
|
||||
BEAST_EXPECT(
|
||||
fee.median_fee().drops() == toDrops(metrics.medFeeLevel, baseFee));
|
||||
auto openLedgerFee =
|
||||
toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) + 1;
|
||||
BEAST_EXPECT(fee.open_ledger_fee().drops() == openLedgerFee.drops());
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testFeeGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Fee, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -773,25 +773,6 @@ class ReportingETL_test : public beast::unit_test::suite
|
||||
testNeedCurrentOrClosed()
|
||||
{
|
||||
testcase("NeedCurrentOrClosed");
|
||||
{
|
||||
org::xrpl::rpc::v1::GetAccountInfoRequest request;
|
||||
request.mutable_ledger()->set_sequence(1);
|
||||
BEAST_EXPECT(!needCurrentOrClosed(request));
|
||||
request.mutable_ledger()->set_hash("");
|
||||
BEAST_EXPECT(!needCurrentOrClosed(request));
|
||||
request.mutable_ledger()->set_shortcut(
|
||||
org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_VALIDATED);
|
||||
BEAST_EXPECT(!needCurrentOrClosed(request));
|
||||
request.mutable_ledger()->set_shortcut(
|
||||
org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_UNSPECIFIED);
|
||||
BEAST_EXPECT(!needCurrentOrClosed(request));
|
||||
request.mutable_ledger()->set_shortcut(
|
||||
org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_CURRENT);
|
||||
BEAST_EXPECT(needCurrentOrClosed(request));
|
||||
request.mutable_ledger()->set_shortcut(
|
||||
org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_CLOSED);
|
||||
BEAST_EXPECT(needCurrentOrClosed(request));
|
||||
}
|
||||
|
||||
{
|
||||
org::xrpl::rpc::v1::GetLedgerRequest request;
|
||||
@@ -904,18 +885,6 @@ class ReportingETL_test : public beast::unit_test::suite
|
||||
org::xrpl::rpc::v1::LedgerSpecifier::SHORTCUT_CURRENT);
|
||||
BEAST_EXPECT(needCurrentOrClosed(request));
|
||||
}
|
||||
|
||||
{
|
||||
org::xrpl::rpc::v1::GetFeeRequest feeRequest;
|
||||
BEAST_EXPECT(!needCurrentOrClosed(feeRequest));
|
||||
|
||||
org::xrpl::rpc::v1::GetAccountTransactionHistoryRequest
|
||||
accountTxRequest;
|
||||
BEAST_EXPECT(!needCurrentOrClosed(accountTxRequest));
|
||||
|
||||
org::xrpl::rpc::v1::GetTransactionRequest txRequest;
|
||||
BEAST_EXPECT(!needCurrentOrClosed(txRequest));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
|
||||
#include <ripple/resource/Charge.h>
|
||||
#include <ripple/resource/Fees.h>
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Submit_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
class SubmitClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
org::xrpl::rpc::v1::SubmitTransactionRequest request;
|
||||
org::xrpl::rpc::v1::SubmitTransactionResponse reply;
|
||||
|
||||
explicit SubmitClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
SubmitTransaction()
|
||||
{
|
||||
status = stub_->SubmitTransaction(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
struct TestData
|
||||
{
|
||||
std::string xrpTxBlob;
|
||||
std::string xrpTxHash;
|
||||
std::string usdTxBlob;
|
||||
std::string usdTxHash;
|
||||
const static int fund = 10000;
|
||||
} testData;
|
||||
|
||||
void
|
||||
fillTestData()
|
||||
{
|
||||
testcase("fill test data");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(addGrpcConfig));
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
env.fund(XRP(TestData::fund), "alice", "bob");
|
||||
env.trust(bob["USD"](TestData::fund), alice);
|
||||
env.close();
|
||||
|
||||
auto toBinary = [this](std::string const& text) {
|
||||
auto blob = strUnHex(text);
|
||||
BEAST_EXPECT(blob);
|
||||
return std::string{
|
||||
reinterpret_cast<char const*>(blob->data()), blob->size()};
|
||||
};
|
||||
|
||||
// use a websocket client to fill transaction blobs
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
{
|
||||
Json::Value jrequestXrp;
|
||||
jrequestXrp[jss::secret] = toBase58(generateSeed("alice"));
|
||||
jrequestXrp[jss::tx_json] =
|
||||
pay("alice", "bob", XRP(TestData::fund / 2));
|
||||
Json::Value jreply_xrp = wsc->invoke("sign", jrequestXrp);
|
||||
|
||||
if (!BEAST_EXPECT(jreply_xrp.isMember(jss::result)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(jreply_xrp[jss::result].isMember(jss::tx_blob)))
|
||||
return;
|
||||
testData.xrpTxBlob =
|
||||
toBinary(jreply_xrp[jss::result][jss::tx_blob].asString());
|
||||
if (!BEAST_EXPECT(jreply_xrp[jss::result].isMember(jss::tx_json)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(
|
||||
jreply_xrp[jss::result][jss::tx_json].isMember(jss::hash)))
|
||||
return;
|
||||
testData.xrpTxHash = toBinary(
|
||||
jreply_xrp[jss::result][jss::tx_json][jss::hash].asString());
|
||||
}
|
||||
{
|
||||
Json::Value jrequestUsd;
|
||||
jrequestUsd[jss::secret] = toBase58(generateSeed("bob"));
|
||||
jrequestUsd[jss::tx_json] =
|
||||
pay("bob", "alice", bob["USD"](TestData::fund / 2));
|
||||
Json::Value jreply_usd = wsc->invoke("sign", jrequestUsd);
|
||||
|
||||
if (!BEAST_EXPECT(jreply_usd.isMember(jss::result)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(jreply_usd[jss::result].isMember(jss::tx_blob)))
|
||||
return;
|
||||
testData.usdTxBlob =
|
||||
toBinary(jreply_usd[jss::result][jss::tx_blob].asString());
|
||||
if (!BEAST_EXPECT(jreply_usd[jss::result].isMember(jss::tx_json)))
|
||||
return;
|
||||
if (!BEAST_EXPECT(
|
||||
jreply_usd[jss::result][jss::tx_json].isMember(jss::hash)))
|
||||
return;
|
||||
testData.usdTxHash = toBinary(
|
||||
jreply_usd[jss::result][jss::tx_json][jss::hash].asString());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubmitGoodBlobGrpc()
|
||||
{
|
||||
testcase("Submit good blobs, XRP, USD, and same transaction twice");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
env.fund(XRP(TestData::fund), "alice", "bob");
|
||||
env.trust(bob["USD"](TestData::fund), alice);
|
||||
env.close();
|
||||
|
||||
auto getClient = [&grpcPort]() { return SubmitClient(grpcPort); };
|
||||
|
||||
// XRP
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.xrpTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.engine_result().result() == "tesSUCCESS");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == 0);
|
||||
BEAST_EXPECT(client.reply.hash() == testData.xrpTxHash);
|
||||
}
|
||||
// USD
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.usdTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(client.reply.engine_result().result() == "tesSUCCESS");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == 0);
|
||||
BEAST_EXPECT(client.reply.hash() == testData.usdTxHash);
|
||||
}
|
||||
// USD, error, same transaction again
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.usdTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.engine_result().result() == "tefPAST_SEQ");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == -190);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubmitErrorBlobGrpc()
|
||||
{
|
||||
testcase("Submit error, bad blob, no account");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
|
||||
auto getClient = [&grpcPort]() { return SubmitClient(grpcPort); };
|
||||
|
||||
// short transaction blob, cannot parse
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction("deadbeef");
|
||||
client.SubmitTransaction();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
// bad blob with correct length, cannot parse
|
||||
{
|
||||
auto client = getClient();
|
||||
auto xrpTxBlobCopy(testData.xrpTxBlob);
|
||||
std::reverse(xrpTxBlobCopy.begin(), xrpTxBlobCopy.end());
|
||||
client.request.set_signed_transaction(xrpTxBlobCopy);
|
||||
client.SubmitTransaction();
|
||||
BEAST_EXPECT(!client.status.ok());
|
||||
}
|
||||
// good blob, can parse but no account
|
||||
{
|
||||
auto client = getClient();
|
||||
client.request.set_signed_transaction(testData.xrpTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.engine_result().result() == "terNO_ACCOUNT");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == -96);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSubmitInsufficientFundsGrpc()
|
||||
{
|
||||
testcase("Submit good blobs but insufficient funds");
|
||||
|
||||
using namespace jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
// fund 1000 (TestData::fund/10) XRP, the transaction sends 5000
|
||||
// (TestData::fund/2) XRP, so insufficient funds
|
||||
env.fund(XRP(TestData::fund / 10), "alice", "bob");
|
||||
env.trust(bob["USD"](TestData::fund), alice);
|
||||
env.close();
|
||||
|
||||
{
|
||||
SubmitClient client(grpcPort);
|
||||
client.request.set_signed_transaction(testData.xrpTxBlob);
|
||||
client.SubmitTransaction();
|
||||
if (!BEAST_EXPECT(client.status.ok()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
client.reply.engine_result().result() == "tecUNFUNDED_PAYMENT");
|
||||
BEAST_EXPECT(client.reply.engine_result_code() == 104);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
fillTestData();
|
||||
testSubmitGoodBlobGrpc();
|
||||
testSubmitErrorBlobGrpc();
|
||||
testSubmitInsufficientFundsGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Submit, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1,829 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2020 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/app/rdb/backend/SQLiteDatabase.h>
|
||||
#include <ripple/basics/mulDiv.h>
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
#include <ripple/rpc/GRPCHandlers.h>
|
||||
#include <ripple/rpc/impl/GRPCHelpers.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Tx_test : public beast::unit_test::suite
|
||||
{
|
||||
template <class T>
|
||||
std::string
|
||||
toByteString(T const& data)
|
||||
{
|
||||
const char* bytes = reinterpret_cast<const char*>(data.data());
|
||||
return {bytes, data.size()};
|
||||
}
|
||||
|
||||
void
|
||||
cmpAmount(
|
||||
const org::xrpl::rpc::v1::CurrencyAmount& proto_amount,
|
||||
STAmount amount)
|
||||
{
|
||||
if (amount.native())
|
||||
{
|
||||
if (!BEAST_EXPECT(proto_amount.has_xrp_amount()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
proto_amount.xrp_amount().drops() == amount.xrp().drops());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!BEAST_EXPECT(proto_amount.has_issued_currency_amount()))
|
||||
return;
|
||||
|
||||
org::xrpl::rpc::v1::IssuedCurrencyAmount issuedCurrency =
|
||||
proto_amount.issued_currency_amount();
|
||||
Issue const& issue = amount.issue();
|
||||
Currency currency = issue.currency;
|
||||
BEAST_EXPECT(
|
||||
issuedCurrency.currency().name() == to_string(currency));
|
||||
BEAST_EXPECT(
|
||||
issuedCurrency.currency().code() == toByteString(currency));
|
||||
BEAST_EXPECT(issuedCurrency.value() == to_string(amount.iou()));
|
||||
BEAST_EXPECT(
|
||||
issuedCurrency.issuer().address() == toBase58(issue.account));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
cmpPaymentTx(
|
||||
const org::xrpl::rpc::v1::Transaction& proto,
|
||||
std::shared_ptr<STTx const> txnSt)
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.has_payment()))
|
||||
return;
|
||||
|
||||
if (!BEAST_EXPECT(
|
||||
safe_cast<TxType>(txnSt->getFieldU16(sfTransactionType)) ==
|
||||
TxType::ttPAYMENT))
|
||||
return;
|
||||
|
||||
AccountID account = txnSt->getAccountID(sfAccount);
|
||||
|
||||
if (!BEAST_EXPECT(proto.has_account()))
|
||||
return;
|
||||
BEAST_EXPECT(proto.account().value().address() == toBase58(account));
|
||||
|
||||
STAmount amount = txnSt->getFieldAmount(sfAmount);
|
||||
if (!BEAST_EXPECT(proto.payment().has_amount()))
|
||||
return;
|
||||
cmpAmount(proto.payment().amount().value(), amount);
|
||||
|
||||
AccountID accountDest = txnSt->getAccountID(sfDestination);
|
||||
if (!BEAST_EXPECT(proto.payment().has_destination()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
proto.payment().destination().value().address() ==
|
||||
toBase58(accountDest));
|
||||
|
||||
STAmount fee = txnSt->getFieldAmount(sfFee);
|
||||
if (!BEAST_EXPECT(proto.has_fee()))
|
||||
return;
|
||||
BEAST_EXPECT(proto.fee().drops() == fee.xrp().drops());
|
||||
|
||||
if (!BEAST_EXPECT(proto.has_sequence()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
proto.sequence().value() == txnSt->getFieldU32(sfSequence));
|
||||
|
||||
if (!BEAST_EXPECT(proto.has_signing_public_key()))
|
||||
return;
|
||||
|
||||
Blob signingPubKey = txnSt->getFieldVL(sfSigningPubKey);
|
||||
BEAST_EXPECT(
|
||||
proto.signing_public_key().value() == toByteString(signingPubKey));
|
||||
|
||||
if (txnSt->isFieldPresent(sfFlags))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.has_flags()))
|
||||
return;
|
||||
BEAST_EXPECT(proto.flags().value() == txnSt->getFieldU32(sfFlags));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.has_flags());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfLastLedgerSequence))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.has_last_ledger_sequence()))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
proto.last_ledger_sequence().value() ==
|
||||
txnSt->getFieldU32(sfLastLedgerSequence));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.has_last_ledger_sequence());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfTxnSignature))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.has_transaction_signature()))
|
||||
return;
|
||||
|
||||
Blob blob = txnSt->getFieldVL(sfTxnSignature);
|
||||
BEAST_EXPECT(
|
||||
proto.transaction_signature().value() == toByteString(blob));
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfSendMax))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.payment().has_send_max()))
|
||||
return;
|
||||
STAmount const& send_max = txnSt->getFieldAmount(sfSendMax);
|
||||
cmpAmount(proto.payment().send_max().value(), send_max);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.payment().has_send_max());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfAccountTxnID))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.has_account_transaction_id()))
|
||||
return;
|
||||
auto field = txnSt->getFieldH256(sfAccountTxnID);
|
||||
BEAST_EXPECT(
|
||||
proto.account_transaction_id().value() == toByteString(field));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.has_account_transaction_id());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfSourceTag))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.has_source_tag()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
proto.source_tag().value() == txnSt->getFieldU32(sfSourceTag));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.has_source_tag());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfDestinationTag))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.payment().has_destination_tag()))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
proto.payment().destination_tag().value() ==
|
||||
txnSt->getFieldU32(sfDestinationTag));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.payment().has_destination_tag());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfInvoiceID))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.payment().has_invoice_id()))
|
||||
return;
|
||||
|
||||
auto field = txnSt->getFieldH256(sfInvoiceID);
|
||||
BEAST_EXPECT(
|
||||
proto.payment().invoice_id().value() == toByteString(field));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.payment().has_invoice_id());
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfDeliverMin))
|
||||
{
|
||||
if (!BEAST_EXPECT(proto.payment().has_deliver_min()))
|
||||
return;
|
||||
STAmount const& deliverMin = txnSt->getFieldAmount(sfDeliverMin);
|
||||
cmpAmount(proto.payment().deliver_min().value(), deliverMin);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!proto.payment().has_deliver_min());
|
||||
}
|
||||
|
||||
STPathSet const& pathset = txnSt->getFieldPathSet(sfPaths);
|
||||
if (!BEAST_EXPECT(pathset.size() == proto.payment().paths_size()))
|
||||
return;
|
||||
|
||||
int ind = 0;
|
||||
for (auto it = pathset.begin(); it < pathset.end(); ++it)
|
||||
{
|
||||
STPath const& path = *it;
|
||||
|
||||
const org::xrpl::rpc::v1::Payment_Path& protoPath =
|
||||
proto.payment().paths(ind++);
|
||||
if (!BEAST_EXPECT(protoPath.elements_size() == path.size()))
|
||||
continue;
|
||||
|
||||
int ind2 = 0;
|
||||
for (auto it2 = path.begin(); it2 != path.end(); ++it2)
|
||||
{
|
||||
const org::xrpl::rpc::v1::Payment_PathElement& protoElement =
|
||||
protoPath.elements(ind2++);
|
||||
STPathElement const& elt = *it2;
|
||||
|
||||
if (elt.isOffer())
|
||||
{
|
||||
if (elt.hasCurrency())
|
||||
{
|
||||
Currency const& currency = elt.getCurrency();
|
||||
if (BEAST_EXPECT(protoElement.has_currency()))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
protoElement.currency().name() ==
|
||||
to_string(currency));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoElement.has_currency());
|
||||
}
|
||||
if (elt.hasIssuer())
|
||||
{
|
||||
AccountID const& issuer = elt.getIssuerID();
|
||||
if (BEAST_EXPECT(protoElement.has_issuer()))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
protoElement.issuer().address() ==
|
||||
toBase58(issuer));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoElement.has_issuer());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (BEAST_EXPECT(protoElement.has_account()))
|
||||
{
|
||||
AccountID const& path_account = elt.getAccountID();
|
||||
BEAST_EXPECT(
|
||||
protoElement.account().address() ==
|
||||
toBase58(path_account));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoElement.has_account());
|
||||
}
|
||||
|
||||
BEAST_EXPECT(!protoElement.has_issuer());
|
||||
BEAST_EXPECT(!protoElement.has_currency());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfMemos))
|
||||
{
|
||||
auto arr = txnSt->getFieldArray(sfMemos);
|
||||
if (BEAST_EXPECT(proto.memos_size() == arr.size()))
|
||||
{
|
||||
for (size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
auto protoMemo = proto.memos(i);
|
||||
auto stMemo = arr[i];
|
||||
|
||||
if (stMemo.isFieldPresent(sfMemoData))
|
||||
{
|
||||
if (BEAST_EXPECT(protoMemo.has_memo_data()))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
protoMemo.memo_data().value() ==
|
||||
toByteString(stMemo.getFieldVL(sfMemoData)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoMemo.has_memo_data());
|
||||
}
|
||||
|
||||
if (stMemo.isFieldPresent(sfMemoType))
|
||||
{
|
||||
if (BEAST_EXPECT(protoMemo.has_memo_type()))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
protoMemo.memo_type().value() ==
|
||||
toByteString(stMemo.getFieldVL(sfMemoType)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoMemo.has_memo_type());
|
||||
}
|
||||
|
||||
if (stMemo.isFieldPresent(sfMemoFormat))
|
||||
{
|
||||
if (BEAST_EXPECT(protoMemo.has_memo_format()))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
protoMemo.memo_format().value() ==
|
||||
toByteString(stMemo.getFieldVL(sfMemoFormat)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoMemo.has_memo_format());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(proto.memos_size() == 0);
|
||||
}
|
||||
|
||||
if (txnSt->isFieldPresent(sfSigners))
|
||||
{
|
||||
auto arr = txnSt->getFieldArray(sfSigners);
|
||||
if (BEAST_EXPECT(proto.signers_size() == arr.size()))
|
||||
{
|
||||
for (size_t i = 0; i < arr.size(); ++i)
|
||||
{
|
||||
auto protoSigner = proto.signers(i);
|
||||
auto stSigner = arr[i];
|
||||
|
||||
if (stSigner.isFieldPresent(sfAccount))
|
||||
{
|
||||
if (BEAST_EXPECT(protoSigner.has_account()))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
protoSigner.account().value().address() ==
|
||||
toBase58(stSigner.getAccountID(sfAccount)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoSigner.has_account());
|
||||
}
|
||||
|
||||
if (stSigner.isFieldPresent(sfTxnSignature))
|
||||
{
|
||||
if (BEAST_EXPECT(
|
||||
protoSigner.has_transaction_signature()))
|
||||
{
|
||||
Blob blob = stSigner.getFieldVL(sfTxnSignature);
|
||||
BEAST_EXPECT(
|
||||
protoSigner.transaction_signature().value() ==
|
||||
toByteString(blob));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoSigner.has_transaction_signature());
|
||||
}
|
||||
|
||||
if (stSigner.isFieldPresent(sfSigningPubKey))
|
||||
{
|
||||
if (BEAST_EXPECT(protoSigner.has_signing_public_key()))
|
||||
{
|
||||
Blob signingPubKey =
|
||||
stSigner.getFieldVL(sfSigningPubKey);
|
||||
BEAST_EXPECT(
|
||||
protoSigner.signing_public_key().value() ==
|
||||
toByteString(signingPubKey));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!protoSigner.has_signing_public_key());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(proto.signers_size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
cmpMeta(
|
||||
const org::xrpl::rpc::v1::Meta& proto,
|
||||
std::shared_ptr<TxMeta> txMeta)
|
||||
{
|
||||
BEAST_EXPECT(proto.transaction_index() == txMeta->getIndex());
|
||||
BEAST_EXPECT(
|
||||
proto.transaction_result().result() ==
|
||||
transToken(txMeta->getResultTER()));
|
||||
|
||||
org::xrpl::rpc::v1::TransactionResult r;
|
||||
|
||||
RPC::convert(r, txMeta->getResultTER());
|
||||
|
||||
BEAST_EXPECT(
|
||||
proto.transaction_result().result_type() == r.result_type());
|
||||
}
|
||||
|
||||
void
|
||||
cmpDeliveredAmount(
|
||||
const org::xrpl::rpc::v1::Meta& meta,
|
||||
const org::xrpl::rpc::v1::Transaction& txn,
|
||||
const std::shared_ptr<TxMeta> expMeta,
|
||||
const std::shared_ptr<STTx const> expTxn,
|
||||
bool checkAmount = true)
|
||||
{
|
||||
if (expMeta->hasDeliveredAmount())
|
||||
{
|
||||
if (!BEAST_EXPECT(meta.has_delivered_amount()))
|
||||
return;
|
||||
cmpAmount(
|
||||
meta.delivered_amount().value(), expMeta->getDeliveredAmount());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expTxn->isFieldPresent(sfAmount))
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
if (checkAmount)
|
||||
{
|
||||
cmpAmount(
|
||||
meta.delivered_amount().value(),
|
||||
expTxn->getFieldAmount(sfAmount));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!meta.has_delivered_amount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gRPC stuff
|
||||
class GrpcTxClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
org::xrpl::rpc::v1::GetTransactionRequest request;
|
||||
org::xrpl::rpc::v1::GetTransactionResponse reply;
|
||||
|
||||
explicit GrpcTxClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Tx()
|
||||
{
|
||||
status = stub_->GetTransaction(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
class GrpcAccountTxClient : public GRPCTestClientBase
|
||||
{
|
||||
public:
|
||||
org::xrpl::rpc::v1::GetAccountTransactionHistoryRequest request;
|
||||
org::xrpl::rpc::v1::GetAccountTransactionHistoryResponse reply;
|
||||
|
||||
explicit GrpcAccountTxClient(std::string const& port)
|
||||
: GRPCTestClientBase(port)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
AccountTx()
|
||||
{
|
||||
status =
|
||||
stub_->GetAccountTransactionHistory(&context, request, &reply);
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testTxGrpc()
|
||||
{
|
||||
testcase("Test Tx Grpc");
|
||||
|
||||
using namespace test::jtx;
|
||||
std::unique_ptr<Config> config = envconfig(addGrpcConfig);
|
||||
std::string grpcPort = *(*config)["port_grpc"].get<std::string>("port");
|
||||
Env env(*this, std::move(config));
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
// Set time to this value (or greater) to get delivered_amount in meta
|
||||
env.timeKeeper().set(NetClock::time_point{446000001s});
|
||||
|
||||
auto grpcTx = [&grpcPort](auto hash, auto binary) {
|
||||
GrpcTxClient client(grpcPort);
|
||||
client.request.set_hash(&hash, sizeof(hash));
|
||||
client.request.set_binary(binary);
|
||||
client.Tx();
|
||||
return std::pair<bool, org::xrpl::rpc::v1::GetTransactionResponse>(
|
||||
client.status.ok(), client.reply);
|
||||
};
|
||||
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
Account A3{"A3"};
|
||||
env.fund(XRP(10000), A1);
|
||||
env.fund(XRP(10000), A2);
|
||||
env.close();
|
||||
env.trust(A2["USD"](1000), A1);
|
||||
env.close();
|
||||
env(fset(A2, 5)); // set asfAccountTxnID flag
|
||||
|
||||
// SignerListSet
|
||||
env(signers(A2, 1, {{"bogie", 1}, {"demon", 1}, {A1, 1}, {A3, 1}}),
|
||||
sig(A2));
|
||||
env.close();
|
||||
std::vector<std::shared_ptr<STTx const>> txns;
|
||||
auto const startLegSeq = env.current()->info().seq;
|
||||
|
||||
uint256 prevHash;
|
||||
for (int i = 0; i < 14; ++i)
|
||||
{
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto txfee = fee(i + (2 * baseFee));
|
||||
auto lls = last_ledger_seq(i + startLegSeq + 20);
|
||||
auto dsttag = dtag(i * 456);
|
||||
auto srctag = stag(i * 321);
|
||||
auto sm = sendmax(A2["USD"](1000));
|
||||
auto dm = delivermin(A2["USD"](50));
|
||||
auto txf = txflags(131072); // partial payment flag
|
||||
auto txnid = account_txn_id(prevHash);
|
||||
auto inv = invoice_id(prevHash);
|
||||
auto mem1 = memo("foo", "bar", "baz");
|
||||
auto mem2 = memo("dragons", "elves", "goblins");
|
||||
|
||||
if (i & 1)
|
||||
{
|
||||
if (i & 2)
|
||||
{
|
||||
env(pay(A2, A1, A2["USD"](100)),
|
||||
txfee,
|
||||
srctag,
|
||||
dsttag,
|
||||
lls,
|
||||
sm,
|
||||
dm,
|
||||
txf,
|
||||
txnid,
|
||||
inv,
|
||||
mem1,
|
||||
mem2,
|
||||
sig(A2));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(pay(A2, A1, A2["USD"](100)),
|
||||
txfee,
|
||||
srctag,
|
||||
dsttag,
|
||||
lls,
|
||||
sm,
|
||||
dm,
|
||||
txf,
|
||||
txnid,
|
||||
inv,
|
||||
mem1,
|
||||
mem2,
|
||||
msig(A3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i & 2)
|
||||
{
|
||||
env(pay(A2, A1, A2["XRP"](200)),
|
||||
txfee,
|
||||
srctag,
|
||||
dsttag,
|
||||
lls,
|
||||
txnid,
|
||||
inv,
|
||||
mem1,
|
||||
mem2,
|
||||
sig(A2));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(pay(A2, A1, A2["XRP"](200)),
|
||||
txfee,
|
||||
srctag,
|
||||
dsttag,
|
||||
lls,
|
||||
txnid,
|
||||
inv,
|
||||
mem1,
|
||||
mem2,
|
||||
msig(A3));
|
||||
}
|
||||
}
|
||||
txns.emplace_back(env.tx());
|
||||
prevHash = txns.back()->getTransactionID();
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Payment with Paths
|
||||
auto const gw = Account("gateway");
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10000), "alice", "bob", gw);
|
||||
env.trust(USD(600), "alice");
|
||||
env.trust(USD(700), "bob");
|
||||
env(pay(gw, "alice", USD(70)));
|
||||
txns.emplace_back(env.tx());
|
||||
env.close();
|
||||
env(pay(gw, "bob", USD(50)));
|
||||
txns.emplace_back(env.tx());
|
||||
env.close();
|
||||
env(pay("alice", "bob", Account("bob")["USD"](5)), path(gw));
|
||||
txns.emplace_back(env.tx());
|
||||
env.close();
|
||||
|
||||
auto const endLegSeq = env.closed()->info().seq;
|
||||
|
||||
// Find the existing transactions
|
||||
auto& ledgerMaster = env.app().getLedgerMaster();
|
||||
int index = startLegSeq;
|
||||
for (auto&& tx : txns)
|
||||
{
|
||||
auto id = tx->getTransactionID();
|
||||
auto ledger = ledgerMaster.getLedgerBySeq(index);
|
||||
|
||||
for (bool b : {false, true})
|
||||
{
|
||||
auto const result = grpcTx(id, b);
|
||||
|
||||
BEAST_EXPECT(result.first == true);
|
||||
BEAST_EXPECT(result.second.ledger_index() == index);
|
||||
BEAST_EXPECT(result.second.validated() == true);
|
||||
if (b)
|
||||
{
|
||||
Serializer s = tx->getSerializer();
|
||||
BEAST_EXPECT(
|
||||
result.second.transaction_binary() == toByteString(s));
|
||||
}
|
||||
else
|
||||
{
|
||||
cmpPaymentTx(result.second.transaction(), tx);
|
||||
}
|
||||
|
||||
if (!ledger || b)
|
||||
continue;
|
||||
|
||||
auto rawMeta = ledger->txRead(id).second;
|
||||
if (!rawMeta)
|
||||
continue;
|
||||
|
||||
auto txMeta =
|
||||
std::make_shared<TxMeta>(id, ledger->seq(), *rawMeta);
|
||||
|
||||
cmpMeta(result.second.meta(), txMeta);
|
||||
cmpDeliveredAmount(
|
||||
result.second.meta(),
|
||||
result.second.transaction(),
|
||||
txMeta,
|
||||
tx);
|
||||
|
||||
auto grpcAccountTx = [&grpcPort](
|
||||
uint256 const& id,
|
||||
bool binary,
|
||||
AccountID const& account)
|
||||
-> std::
|
||||
pair<bool, org::xrpl::rpc::v1::GetTransactionResponse> {
|
||||
GrpcAccountTxClient client(grpcPort);
|
||||
client.request.set_binary(binary);
|
||||
client.request.mutable_account()->set_address(
|
||||
toBase58(account));
|
||||
client.AccountTx();
|
||||
org::xrpl::rpc::v1::GetTransactionResponse res;
|
||||
|
||||
for (auto const& tx : client.reply.transactions())
|
||||
{
|
||||
if (uint256::fromVoid(tx.hash().data()) == id)
|
||||
{
|
||||
return {client.status.ok(), tx};
|
||||
}
|
||||
}
|
||||
return {false, res};
|
||||
};
|
||||
|
||||
// Compare result to result from account_tx
|
||||
auto mentioned = tx->getMentionedAccounts();
|
||||
|
||||
if (!BEAST_EXPECT(mentioned.size()))
|
||||
continue;
|
||||
|
||||
auto account = *mentioned.begin();
|
||||
auto const accountTxResult = grpcAccountTx(id, b, account);
|
||||
|
||||
if (!BEAST_EXPECT(accountTxResult.first))
|
||||
continue;
|
||||
|
||||
cmpPaymentTx(accountTxResult.second.transaction(), tx);
|
||||
cmpMeta(accountTxResult.second.meta(), txMeta);
|
||||
cmpDeliveredAmount(
|
||||
accountTxResult.second.meta(),
|
||||
accountTxResult.second.transaction(),
|
||||
txMeta,
|
||||
tx);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// Find not existing transaction
|
||||
auto const tx = env.jt(noop(A1), seq(env.seq(A1))).stx;
|
||||
for (bool b : {false, true})
|
||||
{
|
||||
auto const result = grpcTx(tx->getTransactionID(), b);
|
||||
|
||||
BEAST_EXPECT(result.first == false);
|
||||
}
|
||||
|
||||
// Delete one transaction
|
||||
const auto deletedLedger = (startLegSeq + endLegSeq) / 2;
|
||||
{
|
||||
// Remove one of the ledgers from the database directly
|
||||
dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())
|
||||
->deleteTransactionByLedgerSeq(deletedLedger);
|
||||
}
|
||||
|
||||
for (bool b : {false, true})
|
||||
{
|
||||
auto const result = grpcTx(tx->getTransactionID(), b);
|
||||
|
||||
BEAST_EXPECT(result.first == false);
|
||||
}
|
||||
|
||||
// non final transaction
|
||||
env(pay(A2, A1, A2["XRP"](200)));
|
||||
auto res = grpcTx(env.tx()->getTransactionID(), false);
|
||||
BEAST_EXPECT(res.first);
|
||||
BEAST_EXPECT(res.second.has_transaction());
|
||||
if (!BEAST_EXPECT(res.second.has_meta()))
|
||||
return;
|
||||
if (!BEAST_EXPECT(res.second.meta().has_transaction_result()))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
res.second.meta().transaction_result().result() == "tesSUCCESS");
|
||||
BEAST_EXPECT(
|
||||
res.second.meta().transaction_result().result_type() ==
|
||||
org::xrpl::rpc::v1::TransactionResult::RESULT_TYPE_TES);
|
||||
BEAST_EXPECT(!res.second.validated());
|
||||
BEAST_EXPECT(!res.second.meta().has_delivered_amount());
|
||||
env.close();
|
||||
|
||||
res = grpcTx(env.tx()->getTransactionID(), false);
|
||||
BEAST_EXPECT(res.first);
|
||||
BEAST_EXPECT(res.second.has_transaction());
|
||||
if (!BEAST_EXPECT(res.second.has_meta()))
|
||||
return;
|
||||
if (!BEAST_EXPECT(res.second.meta().has_transaction_result()))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
res.second.meta().transaction_result().result() == "tesSUCCESS");
|
||||
BEAST_EXPECT(
|
||||
res.second.meta().transaction_result().result_type() ==
|
||||
org::xrpl::rpc::v1::TransactionResult::RESULT_TYPE_TES);
|
||||
BEAST_EXPECT(res.second.validated());
|
||||
BEAST_EXPECT(res.second.meta().has_delivered_amount());
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testTxGrpc();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Tx, app, ripple);
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
Reference in New Issue
Block a user