mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-28 23:15:52 +00:00
Add more unit tests to rpc/impl/TransactionSign (RIPD-480):
By adding a mock it is possible to test the transactionSign function without interacting with the ledger. This is the smallest change I could come up with that allows transactionSign to be unit tested. The unit tests are white boxed. Each test case is a result of examining the code and identifying behavior associated with different JSON fields. That means the tests are not based on requirements, they are based on observed behavior.
This commit is contained in:
committed by
Nik Bougalis
parent
685fe5b0fb
commit
f9aa3e0da5
@@ -997,7 +997,7 @@ amountFromJsonNoThrow (STAmount& result, Json::Value const& jvSource)
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
WriteLog (lsINFO, STAmount) <<
|
||||
WriteLog (lsDEBUG, STAmount) <<
|
||||
"amountFromJsonNoThrow: caught: " << e.what ();
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -28,6 +28,140 @@ namespace ripple {
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace RPC {
|
||||
namespace RPCDetail {
|
||||
|
||||
// LedgerFacade methods
|
||||
|
||||
void LedgerFacade::snapshotAccountState (RippleAddress const& accountID)
|
||||
{
|
||||
if (!netOPs_) // Unit testing.
|
||||
return;
|
||||
|
||||
ledger_ = netOPs_->getCurrentLedger ();
|
||||
accountID_ = accountID;
|
||||
accountState_ = netOPs_->getAccountState (ledger_, accountID_);
|
||||
}
|
||||
|
||||
bool LedgerFacade::isValidAccount () const
|
||||
{
|
||||
if (!ledger_) // Unit testing.
|
||||
return true;
|
||||
|
||||
return static_cast <bool> (accountState_);
|
||||
}
|
||||
|
||||
std::uint32_t LedgerFacade::getSeq () const
|
||||
{
|
||||
if (!ledger_) // Unit testing.
|
||||
return 0;
|
||||
|
||||
return accountState_->getSeq ();
|
||||
}
|
||||
|
||||
Transaction::pointer LedgerFacade::submitTransactionSync (
|
||||
Transaction::ref tpTrans,
|
||||
bool bAdmin,
|
||||
bool bLocal,
|
||||
bool bFailHard,
|
||||
bool bSubmit)
|
||||
{
|
||||
if (!netOPs_) // Unit testing.
|
||||
return tpTrans;
|
||||
|
||||
return netOPs_->submitTransactionSync (
|
||||
tpTrans, bAdmin, bLocal, bFailHard, bSubmit);
|
||||
}
|
||||
|
||||
bool LedgerFacade::findPathsForOneIssuer (
|
||||
RippleAddress const& dstAccountID,
|
||||
Issue const& srcIssue,
|
||||
STAmount const& dstAmount,
|
||||
int searchLevel,
|
||||
unsigned int const maxPaths,
|
||||
STPathSet& pathsOut,
|
||||
STPath& fullLiquidityPath) const
|
||||
{
|
||||
if (!ledger_) // Unit testing.
|
||||
// Note that unit tests don't (yet) need pathsOut or fullLiquidityPath.
|
||||
return true;
|
||||
|
||||
auto cache = std::make_shared<RippleLineCache> (ledger_);
|
||||
return ripple::findPathsForOneIssuer (
|
||||
cache,
|
||||
accountID_.getAccountID (),
|
||||
dstAccountID.getAccountID (),
|
||||
srcIssue,
|
||||
dstAmount,
|
||||
searchLevel,
|
||||
maxPaths,
|
||||
pathsOut,
|
||||
fullLiquidityPath);
|
||||
}
|
||||
|
||||
std::uint64_t LedgerFacade::scaleFeeBase (std::uint64_t fee) const
|
||||
{
|
||||
if (!ledger_) // Unit testing.
|
||||
return fee;
|
||||
|
||||
return ledger_->scaleFeeBase (fee);
|
||||
}
|
||||
|
||||
std::uint64_t LedgerFacade::scaleFeeLoad (std::uint64_t fee, bool bAdmin) const
|
||||
{
|
||||
if (!ledger_) // Unit testing.
|
||||
return fee;
|
||||
|
||||
return ledger_->scaleFeeLoad (fee, bAdmin);
|
||||
}
|
||||
|
||||
bool LedgerFacade::hasAccountRoot () const
|
||||
{
|
||||
if (!netOPs_) // Unit testing.
|
||||
return true;
|
||||
|
||||
SLE::pointer const sleAccountRoot =
|
||||
netOPs_->getSLEi (ledger_, getAccountRootIndex (accountID_));
|
||||
|
||||
return static_cast <bool> (sleAccountRoot);
|
||||
}
|
||||
|
||||
bool LedgerFacade::accountMasterDisabled () const
|
||||
{
|
||||
if (!accountState_) // Unit testing.
|
||||
return false;
|
||||
|
||||
STLedgerEntry const& sle = accountState_->peekSLE ();
|
||||
return sle.isFlag(lsfDisableMaster);
|
||||
}
|
||||
|
||||
bool LedgerFacade::accountMatchesRegularKey (Account account) const
|
||||
{
|
||||
if (!accountState_) // Unit testing.
|
||||
return true;
|
||||
|
||||
STLedgerEntry const& sle = accountState_->peekSLE ();
|
||||
return ((sle.isFieldPresent (sfRegularKey)) &&
|
||||
(account == sle.getFieldAccount160 (sfRegularKey)));
|
||||
}
|
||||
|
||||
int LedgerFacade::getValidatedLedgerAge () const
|
||||
{
|
||||
if (!netOPs_) // Unit testing.
|
||||
return 0;
|
||||
|
||||
return getApp( ).getLedgerMaster ().getValidatedLedgerAge ();
|
||||
}
|
||||
|
||||
bool LedgerFacade::isLoadedCluster () const
|
||||
{
|
||||
if (!netOPs_) // Unit testing.
|
||||
return false;
|
||||
|
||||
return getApp().getFeeTrack().isLoadedCluster();
|
||||
}
|
||||
} // namespace RPCDetail
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Fill in the fee on behalf of the client.
|
||||
This is called when the client does not explicitly specify the fee.
|
||||
@@ -51,7 +185,7 @@ namespace RPC {
|
||||
*/
|
||||
static void autofill_fee (
|
||||
Json::Value& request,
|
||||
Ledger::pointer ledger,
|
||||
RPCDetail::LedgerFacade& ledgerFacade,
|
||||
Json::Value& result,
|
||||
bool admin)
|
||||
{
|
||||
@@ -74,12 +208,12 @@ static void autofill_fee (
|
||||
}
|
||||
}
|
||||
|
||||
// Default fee in fee units
|
||||
// Default fee in fee units.
|
||||
std::uint64_t const feeDefault = getConfig().TRANSACTION_FEE_BASE;
|
||||
|
||||
// Administrative endpoints are exempt from local fees
|
||||
std::uint64_t const fee = ledger->scaleFeeLoad (feeDefault, admin);
|
||||
std::uint64_t const limit = mult * ledger->scaleFeeBase (feeDefault);
|
||||
// Administrative endpoints are exempt from local fees.
|
||||
std::uint64_t const fee = ledgerFacade.scaleFeeLoad (feeDefault, admin);
|
||||
std::uint64_t const limit = mult * ledgerFacade.scaleFeeBase (feeDefault);
|
||||
|
||||
if (fee > limit)
|
||||
{
|
||||
@@ -98,7 +232,7 @@ static Json::Value signPayment(
|
||||
Json::Value const& params,
|
||||
Json::Value& tx_json,
|
||||
RippleAddress const& raSrcAddressID,
|
||||
Ledger::pointer lSnapshot,
|
||||
RPCDetail::LedgerFacade& ledgerFacade,
|
||||
Role role)
|
||||
{
|
||||
RippleAddress dstAccountID;
|
||||
@@ -119,7 +253,7 @@ static Json::Value signPayment(
|
||||
|
||||
if (tx_json.isMember ("Paths") && params.isMember ("build_path"))
|
||||
return RPC::make_error (rpcINVALID_PARAMS,
|
||||
"Cannot specify both 'tx_json.Paths' and 'tx_json.build_path'");
|
||||
"Cannot specify both 'tx_json.Paths' and 'build_path'");
|
||||
|
||||
if (!tx_json.isMember ("Paths")
|
||||
&& tx_json.isMember ("Amount")
|
||||
@@ -152,13 +286,10 @@ static Json::Value signPayment(
|
||||
if (!lpf.isOk ())
|
||||
return rpcError (rpcTOO_BUSY);
|
||||
|
||||
auto cache = std::make_shared<RippleLineCache> (lSnapshot);
|
||||
STPathSet spsPaths;
|
||||
STPath fullLiquidityPath;
|
||||
auto valid = findPathsForOneIssuer (
|
||||
cache,
|
||||
raSrcAddressID.getAccountID(),
|
||||
dstAccountID.getAccountID(),
|
||||
bool valid = ledgerFacade.findPathsForOneIssuer (
|
||||
dstAccountID,
|
||||
saSendMax.issue (),
|
||||
amount,
|
||||
getConfig ().PATH_SEARCH_OLD,
|
||||
@@ -166,6 +297,7 @@ static Json::Value signPayment(
|
||||
spsPaths,
|
||||
fullLiquidityPath);
|
||||
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
WriteLog (lsDEBUG, RPCHandler)
|
||||
@@ -187,14 +319,14 @@ static Json::Value signPayment(
|
||||
|
||||
// VFALCO TODO This function should take a reference to the params, modify it
|
||||
// as needed, and then there should be a separate function to
|
||||
// submit the tranaction
|
||||
// submit the transaction.
|
||||
//
|
||||
Json::Value
|
||||
transactionSign (
|
||||
Json::Value params,
|
||||
bool bSubmit,
|
||||
bool bFailHard,
|
||||
NetworkOPs& netOps,
|
||||
RPCDetail::LedgerFacade& ledgerFacade,
|
||||
Role role)
|
||||
{
|
||||
Json::Value jvResult;
|
||||
@@ -239,21 +371,19 @@ transactionSign (
|
||||
if (!tx_json.isMember ("Sequence") && !verify)
|
||||
return RPC::missing_field_error ("tx_json.Sequence");
|
||||
|
||||
// Check for current ledger
|
||||
// Check for current ledger.
|
||||
if (verify && !getConfig ().RUN_STANDALONE &&
|
||||
(getApp().getLedgerMaster().getValidatedLedgerAge() > 120))
|
||||
(ledgerFacade.getValidatedLedgerAge () > 120))
|
||||
return rpcError (rpcNO_CURRENT);
|
||||
|
||||
// Check for load
|
||||
if (getApp().getFeeTrack().isLoadedCluster() && (role != Role::ADMIN))
|
||||
return rpcError(rpcTOO_BUSY);
|
||||
// Check for load.
|
||||
if (ledgerFacade.isLoadedCluster () && (role != Role::ADMIN))
|
||||
return rpcError (rpcTOO_BUSY);
|
||||
|
||||
ledgerFacade.snapshotAccountState (raSrcAddressID);
|
||||
|
||||
Ledger::pointer lSnapshot = netOps.getCurrentLedger ();
|
||||
AccountState::pointer asSrc;
|
||||
if (verify) {
|
||||
asSrc = netOps.getAccountState (lSnapshot, raSrcAddressID);
|
||||
|
||||
if (!asSrc)
|
||||
if (!ledgerFacade.isValidAccount ())
|
||||
{
|
||||
// If not offline and did not find account, error.
|
||||
WriteLog (lsDEBUG, RPCHandler)
|
||||
@@ -265,7 +395,7 @@ transactionSign (
|
||||
}
|
||||
}
|
||||
|
||||
autofill_fee (params, lSnapshot, jvResult, role == Role::ADMIN);
|
||||
autofill_fee (params, ledgerFacade, jvResult, role == Role::ADMIN);
|
||||
if (RPC::contains_error (jvResult))
|
||||
return jvResult;
|
||||
|
||||
@@ -275,24 +405,21 @@ transactionSign (
|
||||
params,
|
||||
tx_json,
|
||||
raSrcAddressID,
|
||||
lSnapshot,
|
||||
ledgerFacade,
|
||||
role);
|
||||
if (contains_error(e))
|
||||
return e;
|
||||
}
|
||||
|
||||
if (!tx_json.isMember ("Sequence"))
|
||||
tx_json["Sequence"] = asSrc->getSeq ();
|
||||
tx_json["Sequence"] = ledgerFacade.getSeq ();
|
||||
|
||||
if (!tx_json.isMember ("Flags"))
|
||||
tx_json["Flags"] = tfFullyCanonicalSig;
|
||||
|
||||
if (verify)
|
||||
{
|
||||
SLE::pointer sleAccountRoot = netOps.getSLEi (lSnapshot,
|
||||
getAccountRootIndex (raSrcAddressID.getAccountID ()));
|
||||
|
||||
if (!sleAccountRoot)
|
||||
if (!ledgerFacade.hasAccountRoot ())
|
||||
// XXX Ignore transactions for accounts not created.
|
||||
return rpcError (rpcSRC_ACT_NOT_FOUND);
|
||||
}
|
||||
@@ -306,19 +433,17 @@ transactionSign (
|
||||
|
||||
if (verify)
|
||||
{
|
||||
auto account = masterAccountPublic.getAccountID();
|
||||
auto const& sle = asSrc->peekSLE();
|
||||
|
||||
WriteLog (lsWARNING, RPCHandler) <<
|
||||
WriteLog (lsTRACE, RPCHandler) <<
|
||||
"verify: " << masterAccountPublic.humanAccountID () <<
|
||||
" : " << raSrcAddressID.humanAccountID ();
|
||||
if (raSrcAddressID.getAccountID () == account)
|
||||
|
||||
auto const secretAccountID = masterAccountPublic.getAccountID();
|
||||
if (raSrcAddressID.getAccountID () == secretAccountID)
|
||||
{
|
||||
if (sle.isFlag(lsfDisableMaster))
|
||||
if (ledgerFacade.accountMasterDisabled ())
|
||||
return rpcError (rpcMASTER_DISABLED);
|
||||
}
|
||||
else if (!sle.isFieldPresent(sfRegularKey) ||
|
||||
account != sle.getFieldAccount160 (sfRegularKey))
|
||||
else if (!ledgerFacade.accountMatchesRegularKey (secretAccountID))
|
||||
{
|
||||
return rpcError (rpcBAD_SECRET);
|
||||
}
|
||||
@@ -381,8 +506,8 @@ transactionSign (
|
||||
|
||||
try
|
||||
{
|
||||
// FIXME: For performance, should use asynch interface
|
||||
tpTrans = netOps.submitTransactionSync (tpTrans,
|
||||
// FIXME: For performance, should use asynch interface.
|
||||
tpTrans = ledgerFacade.submitTransactionSync (tpTrans,
|
||||
role == Role::ADMIN, true, bFailHard, bSubmit);
|
||||
|
||||
if (!tpTrans)
|
||||
@@ -424,6 +549,455 @@ transactionSign (
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Struct used to test calls to transactionSign and transactionSubmit.
|
||||
struct TxnTestData
|
||||
{
|
||||
// Gah, without constexpr I can't make this an enum and initialize
|
||||
// OR operators at compile time. Punting with integer constants.
|
||||
static unsigned int const allGood = 0x0;
|
||||
static unsigned int const signFail = 0x1;
|
||||
static unsigned int const submitFail = 0x2;
|
||||
|
||||
char const* const json;
|
||||
unsigned int result;
|
||||
|
||||
TxnTestData () = delete;
|
||||
TxnTestData (TxnTestData const&) = delete;
|
||||
TxnTestData& operator= (TxnTestData const&) = delete;
|
||||
TxnTestData (char const* jsonIn, unsigned int resultIn)
|
||||
: json (jsonIn)
|
||||
, result (resultIn)
|
||||
{ }
|
||||
};
|
||||
|
||||
// Declare storage for statics to avoid link errors.
|
||||
unsigned int const TxnTestData::allGood;
|
||||
unsigned int const TxnTestData::signFail;
|
||||
unsigned int const TxnTestData::submitFail;
|
||||
|
||||
|
||||
static TxnTestData const txnTestArray [] =
|
||||
{
|
||||
|
||||
// Minimal payment.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Pass in Fee with minimal payment.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Fee": 10,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Pass in Sequence.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Pass in Sequence and Fee with minimal payment.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Fee": 10,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Add "fee_mult_max" field.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"fee_mult_max": 7,
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// "fee_mult_max is ignored if "Fee" is present.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"fee_mult_max": 0,
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Fee": 10,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Invalid "fee_mult_max" field.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"fee_mult_max": "NotAFeeMultiplier",
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Invalid value for "fee_mult_max" field.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"fee_mult_max": 0,
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Missing "Amount".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Invalid "Amount".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "NotAnAmount",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Missing "Destination".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Invalid "Destination".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "NotADestination",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Cannot create XRP to XRP paths.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"build_path": 1,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Successful "build_path".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"build_path": 1,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": {
|
||||
"value": "10",
|
||||
"currency": "USD",
|
||||
"issuer": "0123456789012345678901234567890123456789"
|
||||
},
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Not valid to include both "Paths" and "build_path".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"build_path": 1,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": {
|
||||
"value": "10",
|
||||
"currency": "USD",
|
||||
"issuer": "0123456789012345678901234567890123456789"
|
||||
},
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"Paths": "",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Successful "SendMax".
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"build_path": 1,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": {
|
||||
"value": "10",
|
||||
"currency": "USD",
|
||||
"issuer": "0123456789012345678901234567890123456789"
|
||||
},
|
||||
"SendMax": {
|
||||
"value": "5",
|
||||
"currency": "USD",
|
||||
"issuer": "0123456789012345678901234567890123456789"
|
||||
},
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// Even though "Amount" may not be XRP for pathfinding, "SendMax" may be XRP.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"build_path": 1,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": {
|
||||
"value": "10",
|
||||
"currency": "USD",
|
||||
"issuer": "0123456789012345678901234567890123456789"
|
||||
},
|
||||
"SendMax": 10000,
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// "secret" must be present.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// "secret" must be non-empty.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// "tx_json" must be present.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"rx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// "TransactionType" must be present.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// The "TransactionType" must be one of the pre-established transaction types.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "tt"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// The "TransactionType", however, may be represented with an integer.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": 0
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// "Account" must be present.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// "Account" must be well formed.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Account": "NotAnAccount",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// The "offline" tag may be added to the transaction.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"offline": 0,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// If "offline" is true then a "Sequence" field must be supplied.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"offline": 1,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// Valid transaction if "offline" is true.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"offline": 1,
|
||||
"tx_json": {
|
||||
"Sequence": 0,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// A "Flags' field may be specified.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Flags": 0,
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
// The "Flags" field must be numeric.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"tx_json": {
|
||||
"Flags": "NotGoodFlags",
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::signFail | TxnTestData::submitFail},
|
||||
|
||||
// It's okay to add a "debug_signing" field.
|
||||
{R"({
|
||||
"command": "submit",
|
||||
"secret": "masterpassphrase",
|
||||
"debug_signing": 0,
|
||||
"tx_json": {
|
||||
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"Amount": "1000000000",
|
||||
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
|
||||
"TransactionType": "Payment"
|
||||
}
|
||||
})", TxnTestData::allGood},
|
||||
|
||||
};
|
||||
|
||||
class JSONRPC_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
@@ -439,13 +1013,16 @@ public:
|
||||
Ledger::pointer ledger (std::make_shared <Ledger> (
|
||||
rootAddress, startAmount));
|
||||
|
||||
using namespace RPCDetail;
|
||||
LedgerFacade facade (LedgerFacade::noNetOPs, ledger);
|
||||
|
||||
{
|
||||
Json::Value req;
|
||||
Json::Value result;
|
||||
Json::Reader ().parse (
|
||||
"{ \"fee_mult_max\" : 1, \"tx_json\" : { } } "
|
||||
R"({ "fee_mult_max" : 1, "tx_json" : { } } )"
|
||||
, req);
|
||||
autofill_fee (req, ledger, result, true);
|
||||
autofill_fee (req, facade, result, true);
|
||||
|
||||
expect (! contains_error (result));
|
||||
}
|
||||
@@ -454,17 +1031,65 @@ public:
|
||||
Json::Value req;
|
||||
Json::Value result;
|
||||
Json::Reader ().parse (
|
||||
"{ \"fee_mult_max\" : 0, \"tx_json\" : { } } "
|
||||
R"({ "fee_mult_max" : 0, "tx_json" : { } } )"
|
||||
, req);
|
||||
autofill_fee (req, ledger, result, true);
|
||||
autofill_fee (req, facade, result, true);
|
||||
|
||||
expect (contains_error (result));
|
||||
}
|
||||
}
|
||||
|
||||
void testTransactionRPC ()
|
||||
{
|
||||
// This loop is forward-looking for when there are separate
|
||||
// transactionSign () and transcationSubmit () functions. For now
|
||||
// they just have a bool (false = sign, true = submit) and a flag
|
||||
// to help classify failure types.
|
||||
using TestStuff = std::pair <bool, unsigned int>;
|
||||
static TestStuff const testFuncs [] =
|
||||
{
|
||||
TestStuff {false, TxnTestData::signFail},
|
||||
TestStuff {true, TxnTestData::submitFail},
|
||||
};
|
||||
|
||||
for (auto testFunc : testFuncs)
|
||||
{
|
||||
// For each JSON test.
|
||||
for (auto const& txnTest : txnTestArray)
|
||||
{
|
||||
Json::Value req;
|
||||
Json::Reader ().parse (txnTest.json, req);
|
||||
if (contains_error (req))
|
||||
throw std::runtime_error (
|
||||
"Internal JSONRPC_test error. Bad test JSON.");
|
||||
|
||||
static Role const testedRoles[] =
|
||||
{Role::GUEST, Role::USER, Role::ADMIN, Role::FORBID};
|
||||
|
||||
for (Role testRole : testedRoles)
|
||||
{
|
||||
// Mock so we can run without a ledger.
|
||||
RPCDetail::LedgerFacade fakeNetOPs (
|
||||
RPCDetail::LedgerFacade::noNetOPs);
|
||||
|
||||
Json::Value result = transactionSign (
|
||||
req,
|
||||
testFunc.first,
|
||||
true,
|
||||
fakeNetOPs,
|
||||
testRole);
|
||||
|
||||
expect (contains_error (result) ==
|
||||
static_cast <bool> (txnTest.result & testFunc.second));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void run ()
|
||||
{
|
||||
testAutoFillFees ();
|
||||
testTransactionRPC ();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,13 +25,99 @@
|
||||
namespace ripple {
|
||||
namespace RPC {
|
||||
|
||||
namespace RPCDetail {
|
||||
// A class that allows these methods to be called with or without a
|
||||
// real NetworkOPs instance. This allows for unit testing.
|
||||
class LedgerFacade
|
||||
{
|
||||
private:
|
||||
NetworkOPs* const netOPs_;
|
||||
Ledger::pointer ledger_;
|
||||
RippleAddress accountID_;
|
||||
AccountState::pointer accountState_;
|
||||
|
||||
public:
|
||||
// Enum used to construct a Facade for unit tests.
|
||||
enum NoNetworkOPs{
|
||||
noNetOPs
|
||||
};
|
||||
|
||||
LedgerFacade () = delete;
|
||||
LedgerFacade (LedgerFacade const&) = delete;
|
||||
LedgerFacade& operator= (LedgerFacade const&) = delete;
|
||||
|
||||
// For use in non unit testing circumstances.
|
||||
explicit LedgerFacade (NetworkOPs& netOPs)
|
||||
: netOPs_ (&netOPs)
|
||||
{ }
|
||||
|
||||
// For testTransactionRPC unit tests.
|
||||
explicit LedgerFacade (NoNetworkOPs noOPs)
|
||||
: netOPs_ (nullptr) { }
|
||||
|
||||
// For testAutoFillFees unit tests.
|
||||
LedgerFacade (NoNetworkOPs noOPs, Ledger::pointer ledger)
|
||||
: netOPs_ (nullptr)
|
||||
, ledger_ (ledger)
|
||||
{ }
|
||||
|
||||
void snapshotAccountState (RippleAddress const& accountID);
|
||||
|
||||
bool isValidAccount () const;
|
||||
|
||||
std::uint32_t getSeq () const;
|
||||
|
||||
bool findPathsForOneIssuer (
|
||||
RippleAddress const& dstAccountID,
|
||||
Issue const& srcIssue,
|
||||
STAmount const& dstAmount,
|
||||
int searchLevel,
|
||||
unsigned int const maxPaths,
|
||||
STPathSet& pathsOut,
|
||||
STPath& fullLiquidityPath) const;
|
||||
|
||||
Transaction::pointer submitTransactionSync (
|
||||
Transaction::ref tpTrans,
|
||||
bool bAdmin,
|
||||
bool bLocal,
|
||||
bool bFailHard,
|
||||
bool bSubmit);
|
||||
|
||||
std::uint64_t scaleFeeBase (std::uint64_t fee) const;
|
||||
|
||||
std::uint64_t scaleFeeLoad (std::uint64_t fee, bool bAdmin) const;
|
||||
|
||||
bool hasAccountRoot () const;
|
||||
|
||||
bool accountMasterDisabled () const;
|
||||
|
||||
bool accountMatchesRegularKey (Account account) const;
|
||||
|
||||
int getValidatedLedgerAge () const;
|
||||
|
||||
bool isLoadedCluster () const;
|
||||
};
|
||||
|
||||
} // namespace RPCDetail
|
||||
|
||||
Json::Value transactionSign (
|
||||
Json::Value jvRequest,
|
||||
Json::Value params,
|
||||
bool bSubmit,
|
||||
bool bFailHard,
|
||||
NetworkOPs& netOps,
|
||||
RPCDetail::LedgerFacade& ledgerFacade,
|
||||
Role role);
|
||||
|
||||
inline Json::Value transactionSign (
|
||||
Json::Value params,
|
||||
bool bSubmit,
|
||||
bool bFailHard,
|
||||
NetworkOPs& netOPs,
|
||||
Role role)
|
||||
{
|
||||
RPCDetail::LedgerFacade ledgerFacade (netOPs);
|
||||
return transactionSign (params, bSubmit, bFailHard, ledgerFacade, role);
|
||||
}
|
||||
|
||||
} // RPC
|
||||
} // ripple
|
||||
|
||||
|
||||
Reference in New Issue
Block a user