diff --git a/Builds/VisualStudio2013/RippleD.vcxproj b/Builds/VisualStudio2013/RippleD.vcxproj index 56b366881..493e02346 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj +++ b/Builds/VisualStudio2013/RippleD.vcxproj @@ -3208,6 +3208,9 @@ True + + True + True diff --git a/Builds/VisualStudio2013/RippleD.vcxproj.filters b/Builds/VisualStudio2013/RippleD.vcxproj.filters index 74f5d0db8..780471755 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2013/RippleD.vcxproj.filters @@ -3738,6 +3738,9 @@ ripple\rpc\handlers + + ripple\rpc\handlers + ripple\rpc\handlers diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index f7b4b10db..e5028af5a 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -138,6 +138,9 @@ void printHelp (const po::options_description& desc) " version\n" " server_info\n" " sign\n" +#if RIPPLE_ENABLE_MULTI_SIGN + " sign_for\n" +#endif // RIPPLE_ENABLE_MULTI_SIGN " stop\n" " submit\n" " tx \n" diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 6ad3119d6..ef84a5e99 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -211,19 +211,16 @@ public: Job&, STTx::pointer, stCallback callback = stCallback ()) override; - Transaction::pointer submitTransactionSync ( - Transaction::ref tpTrans, - bool bAdmin, bool bLocal, bool bFailHard, bool bSubmit) override; - Transaction::pointer processTransactionCb ( Transaction::pointer, - bool bAdmin, bool bLocal, bool bFailHard, stCallback) override; + bool bAdmin, bool bLocal, FailHard failType, stCallback) override; + Transaction::pointer processTransaction ( Transaction::pointer transaction, - bool bAdmin, bool bLocal, bool bFailHard) override + bool bAdmin, bool bLocal, FailHard failType) override { return processTransactionCb ( - transaction, bAdmin, bLocal, bFailHard, stCallback ()); + transaction, bAdmin, bLocal, failType, stCallback ()); } // VFALCO Workaround for MSVC std::function which doesn't swallow return @@ -232,9 +229,9 @@ public: private: void processTransactionCbVoid ( Transaction::pointer p, - bool bAdmin, bool bLocal, bool bFailHard, stCallback cb) + bool bAdmin, bool bLocal, FailHard failType, stCallback cb) { - processTransactionCb (p, bAdmin, bLocal, bFailHard, cb); + processTransactionCb (p, bAdmin, bLocal, failType, cb); } public: @@ -943,51 +940,13 @@ void NetworkOPsImp::submitTransaction ( std::make_shared (trans, Validate::NO, reason), false, false, - false, + FailHard::no, callback)); } -// Sterilize transaction through serialization. -// This is fully synchronous and deprecated -Transaction::pointer NetworkOPsImp::submitTransactionSync ( - Transaction::ref tpTrans, - bool bAdmin, bool bLocal, bool bFailHard, bool bSubmit) -{ - Serializer s; - tpTrans->getSTransaction ()->add (s); - - auto tpTransNew = Transaction::sharedTransaction ( - s.getData (), Validate::YES); - - if (!tpTransNew) - { - // Could not construct transaction. - return tpTransNew; - } - - if (tpTransNew->getSTransaction ()->isEquivalent ( - *tpTrans->getSTransaction ())) - { - if (bSubmit) - processTransaction (tpTransNew, bAdmin, bLocal, bFailHard); - } - else - { - m_journal.fatal << "Transaction reconstruction failure"; - m_journal.fatal << tpTransNew->getSTransaction ()->getJson (0); - m_journal.fatal << tpTrans->getSTransaction ()->getJson (0); - - // assert (false); "1e-95" as amount can trigger this - - tpTransNew.reset (); - } - - return tpTransNew; -} - Transaction::pointer NetworkOPsImp::processTransactionCb ( Transaction::pointer trans, - bool bAdmin, bool bLocal, bool bFailHard, stCallback callback) + bool bAdmin, bool bLocal, FailHard failType, stCallback callback) { auto ev = m_job_queue.getLoadEventAP (jtTXN_PROC, "ProcessTXN"); int newFlags = getApp().getHashRouter ().getFlags (trans->getID ()); @@ -1065,7 +1024,7 @@ Transaction::pointer NetworkOPsImp::processTransactionCb ( } else if (isTerRetry (r)) { - if (bFailHard) + if (failType == FailHard::yes) addLocal = false; else { @@ -1088,7 +1047,8 @@ Transaction::pointer NetworkOPsImp::processTransactionCb ( trans->getSTransaction ()); } - if (didApply || ((mMode != omFULL) && !bFailHard && bLocal)) + if (didApply || + ((mMode != omFULL) && (failType != FailHard::yes) && bLocal)) { std::set peers; diff --git a/src/ripple/app/misc/NetworkOPs.h b/src/ripple/app/misc/NetworkOPs.h index 316f6074b..8627f4ae0 100644 --- a/src/ripple/app/misc/NetworkOPs.h +++ b/src/ripple/app/misc/NetworkOPs.h @@ -91,6 +91,16 @@ public: omFULL = 4 // we have the ledger and can even validate }; + enum class FailHard : unsigned char + { + no, + yes + }; + static inline FailHard doFailHard (bool noMeansDont) + { + return noMeansDont ? FailHard::yes : FailHard::no; + } + // VFALCO TODO Fix OrderBookDB to not need this unrelated type. // typedef hash_map SubMapType; @@ -149,12 +159,10 @@ public: typedef std::function stCallback; virtual void submitTransaction (Job&, STTx::pointer, stCallback callback = stCallback ()) = 0; - virtual Transaction::pointer submitTransactionSync (Transaction::ref tpTrans, - bool bAdmin, bool bLocal, bool bFailHard, bool bSubmit) = 0; virtual Transaction::pointer processTransactionCb (Transaction::pointer, - bool bAdmin, bool bLocal, bool bFailHard, stCallback) = 0; + bool bAdmin, bool bLocal, FailHard failType, stCallback) = 0; virtual Transaction::pointer processTransaction (Transaction::pointer transaction, - bool bAdmin, bool bLocal, bool bFailHard) = 0; + bool bAdmin, bool bLocal, FailHard failType) = 0; virtual Transaction::pointer findTransactionByID (uint256 const& transactionID) = 0; virtual int findTransactionsByDestination (std::list&, RippleAddress const& destinationAccount, std::uint32_t startLedgerSeq, diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 23b2103c6..42710d163 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -420,6 +420,30 @@ private: return jvRequest; } + // sign_for + Json::Value parseSignFor (Json::Value const& jvParams) + { + Json::Value txJSON; + Json::Reader reader; + + if ((4 == jvParams.size ()) + && reader.parse (jvParams[3u].asString (), txJSON)) + { + if (txJSON.type () == Json::objectValue) + { + // Return SigningFor object for the submitted transaction. + Json::Value jvRequest; + jvRequest["signing_for"] = jvParams[0u].asString (); + jvRequest["account"] = jvParams[1u].asString (); + jvRequest["secret"] = jvParams[2u].asString (); + jvRequest["tx_json"] = txJSON; + + return jvRequest; + } + } + return rpcError (rpcINVALID_PARAMS); + } + // json Json::Value parseJson (Json::Value const& jvParams) { @@ -605,7 +629,7 @@ private: { Json::Value txJSON; Json::Reader reader; - bool bOffline = 3 == jvParams.size () && jvParams[2u].asString () == "offline"; + bool const bOffline = 3 == jvParams.size () && jvParams[2u].asString () == "offline"; if (1 == jvParams.size ()) { @@ -831,6 +855,9 @@ public: { "random", &RPCParser::parseAsIs, 0, 0 }, { "ripple_path_find", &RPCParser::parseRipplePathFind, 1, 2 }, { "sign", &RPCParser::parseSignSubmit, 2, 3 }, +#if RIPPLE_ENABLE_MULTI_SIGN + { "sign_for", &RPCParser::parseSignFor, 4, 4 }, +#endif // RIPPLE_ENABLE_MULTI_SIGN { "submit", &RPCParser::parseSignSubmit, 1, 3 }, { "server_info", &RPCParser::parseAsIs, 0, 0 }, { "server_state", &RPCParser::parseAsIs, 0, 0 }, diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index d39239513..dcb0a6cec 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -1652,7 +1652,8 @@ PeerImp::checkTransaction (Job&, int flags, } bool const trusted (flags & SF_TRUSTED); - getApp().getOPs ().processTransaction (tx, trusted, false, false); + getApp().getOPs ().processTransaction ( + tx, trusted, false, NetworkOPs::FailHard::no); } catch (...) { diff --git a/src/ripple/protocol/ErrorCodes.h b/src/ripple/protocol/ErrorCodes.h index bd90fa8b3..d60f07060 100644 --- a/src/ripple/protocol/ErrorCodes.h +++ b/src/ripple/protocol/ErrorCodes.h @@ -100,6 +100,7 @@ enum error_code_i rpcPAYS_AMT_MALFORMED, rpcPORT_MALFORMED, rpcPUBLIC_MALFORMED, + rpcSIGN_FOR_MALFORMED, rpcSRC_ACT_MALFORMED, rpcSRC_ACT_MISSING, rpcSRC_ACT_NOT_FOUND, diff --git a/src/ripple/protocol/HashPrefix.h b/src/ripple/protocol/HashPrefix.h index e03e265eb..dc0cd9495 100644 --- a/src/ripple/protocol/HashPrefix.h +++ b/src/ripple/protocol/HashPrefix.h @@ -84,6 +84,9 @@ public: /** inner transaction to sign */ static HashPrefix const txSign; + /** inner transaction to multi-sign */ + static HashPrefix const txMultiSign; + /** validation for signing */ static HashPrefix const validation; diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 41f73b471..7fab19cef 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -387,7 +387,7 @@ extern SField const sfMinimumOffer; extern SField const sfRippleEscrow; extern SField const sfDeliveredAmount; -// variable length +// variable length (common) extern TypedField const sfPublicKey; extern SField const sfMessageKey; extern TypedField const sfSigningPubKey; @@ -402,6 +402,9 @@ extern SField const sfMemoType; extern SField const sfMemoData; extern SField const sfMemoFormat; +// variable length (uncommon) +extern SField const sfMultiSignature; + // account extern SField const sfAccount; extern SField const sfOwner; @@ -430,11 +433,13 @@ extern SField const sfNewFields; extern SField const sfTemplateEntry; extern SField const sfMemo; extern SField const sfSignerEntry; +extern SField const sfSigningAccount; +extern SField const sfSigningFor; // array of objects // ARRAY/1 is reserved for end of array extern SField const sfSigningAccounts; -extern SField const sfTxnSignatures; +extern SField const sfMultiSigners; extern SField const sfSignerEntries; extern SField const sfTemplate; extern SField const sfNecessary; diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index 0086228b2..503de5ad3 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -212,6 +212,17 @@ public: uint256 getHash (std::uint32_t prefix) const; uint256 getSigningHash (std::uint32_t prefix) const; + // Break the multi-signing hash computation into 2 parts for optimization. + Serializer startMultiSigningData () const; + void finishMultiSigningData ( + RippleAddress const& signingForID, + RippleAddress const& signingID, Serializer& s) const; + + // Get data to compute a complete multi-signature. + Serializer getMultiSigningData ( + RippleAddress const& signingForID, + RippleAddress const& signingID) const; + const STBase& peekAtIndex (int offset) const { return v_[offset].get(); diff --git a/src/ripple/protocol/impl/ErrorCodes.cpp b/src/ripple/protocol/impl/ErrorCodes.cpp index 8649a1ff5..ae6e25fc9 100644 --- a/src/ripple/protocol/impl/ErrorCodes.cpp +++ b/src/ripple/protocol/impl/ErrorCodes.cpp @@ -98,6 +98,7 @@ public: add (rpcPORT_MALFORMED, "portMalformed", "Port is malformed."); add (rpcPUBLIC_MALFORMED, "publicMalformed", "Public key is malformed."); add (rpcQUALITY_MALFORMED, "qualityMalformed", "Quality malformed."); + add (rpcSIGN_FOR_MALFORMED, "signForMalformed", "Signing for account is malformed."); add (rpcSLOW_DOWN, "slowDown", "You are placing too much load on the server."); add (rpcSRC_ACT_MALFORMED, "srcActMalformed", "Source account is malformed."); add (rpcSRC_ACT_MISSING, "srcActMissing", "Source account not provided."); diff --git a/src/ripple/protocol/impl/HashPrefix.cpp b/src/ripple/protocol/impl/HashPrefix.cpp index a0c3d0935..b107420b7 100644 --- a/src/ripple/protocol/impl/HashPrefix.cpp +++ b/src/ripple/protocol/impl/HashPrefix.cpp @@ -31,6 +31,7 @@ HashPrefix const HashPrefix::leafNode ('M', 'L', 'N'); HashPrefix const HashPrefix::innerNode ('M', 'I', 'N'); HashPrefix const HashPrefix::ledgerMaster ('L', 'W', 'R'); HashPrefix const HashPrefix::txSign ('S', 'T', 'X'); +HashPrefix const HashPrefix::txMultiSign ('S', 'M', 'T'); HashPrefix const HashPrefix::validation ('V', 'A', 'L'); HashPrefix const HashPrefix::proposal ('P', 'R', 'P'); diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 2a2a8f07a..c87b9e765 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -175,7 +175,7 @@ SField const sfMinimumOffer = make::one(&sfMinimumOffer, STI_AMOUNT, 16, " SField const sfRippleEscrow = make::one(&sfRippleEscrow, STI_AMOUNT, 17, "RippleEscrow"); SField const sfDeliveredAmount = make::one(&sfDeliveredAmount, STI_AMOUNT, 18, "DeliveredAmount"); -// variable length +// variable length (common) TypedField const sfPublicKey = make::one(&sfPublicKey, STI_VL, 1, "PublicKey"); TypedField const sfSigningPubKey = make::one(&sfSigningPubKey, STI_VL, 3, "SigningPubKey"); TypedField const sfSignature = make::one(&sfSignature, STI_VL, 6, "Signature", SField::sMD_Default, SField::notSigning); @@ -190,6 +190,9 @@ SField const sfMemoType = make::one(&sfMemoType, STI_VL SField const sfMemoData = make::one(&sfMemoData, STI_VL, 13, "MemoData"); SField const sfMemoFormat = make::one(&sfMemoFormat, STI_VL, 14, "MemoFormat"); +// variable length (uncommon) +SField const sfMultiSignature = make::one(&sfMultiSignature, STI_VL, 16, "MultiSignature"); + // account SField const sfAccount = make::one(&sfAccount, STI_ACCOUNT, 1, "Account"); SField const sfOwner = make::one(&sfOwner, STI_ACCOUNT, 2, "Owner"); @@ -219,10 +222,14 @@ SField const sfTemplateEntry = make::one(&sfTemplateEntry, STI_OBJEC SField const sfMemo = make::one(&sfMemo, STI_OBJECT, 10, "Memo"); SField const sfSignerEntry = make::one(&sfSignerEntry, STI_OBJECT, 11, "SignerEntry"); +// inner object (uncommon) +SField const sfSigningAccount = make::one(&sfSigningAccount, STI_OBJECT, 16, "SigningAccount"); +SField const sfSigningFor = make::one(&sfSigningFor, STI_OBJECT, 17, "SigningFor"); + // array of objects // ARRAY/1 is reserved for end of array SField const sfSigningAccounts = make::one(&sfSigningAccounts, STI_ARRAY, 2, "SigningAccounts"); -SField const sfTxnSignatures = make::one(&sfTxnSignatures, STI_ARRAY, 3, "TxnSignatures", SField::sMD_Default, SField::notSigning); +SField const sfMultiSigners = make::one(&sfMultiSigners, STI_ARRAY, 3, "MultiSigners", SField::sMD_Default, SField::notSigning); SField const sfSignerEntries = make::one(&sfSignerEntries, STI_ARRAY, 4, "SignerEntries"); SField const sfTemplate = make::one(&sfTemplate, STI_ARRAY, 5, "Template"); SField const sfNecessary = make::one(&sfNecessary, STI_ARRAY, 6, "Necessary"); diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index 0e1e0a2da..635c070a2 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -337,6 +338,34 @@ uint256 STObject::getSigningHash (std::uint32_t prefix) const return s.getSHA512Half (); } +Serializer +STObject::startMultiSigningData () const +{ + Serializer s; + s.add32 (HashPrefix::txMultiSign); + add (s, false); + return s; +} + +void +STObject::finishMultiSigningData ( + RippleAddress const& signingForID, + RippleAddress const& signingID, + Serializer& s) const +{ + s.add160 (signingForID.getAccountID ()); + s.add160 (signingID.getAccountID ()); +} + +Serializer +STObject::getMultiSigningData ( + RippleAddress const& signingForID, RippleAddress const& signingID) const +{ + Serializer s (startMultiSigningData ()); + finishMultiSigningData (signingForID, signingID, s); + return s; +} + int STObject::getFieldIndex (SField const& field) const { if (mType != nullptr) diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index f2f652d15..0ccebad63 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -62,6 +62,7 @@ Json::Value doServerState (RPC::Context&); // for machines Json::Value doSessionClose (RPC::Context&); Json::Value doSessionOpen (RPC::Context&); Json::Value doSign (RPC::Context&); +Json::Value doSignFor (RPC::Context&); Json::Value doStop (RPC::Context&); Json::Value doSubmit (RPC::Context&); Json::Value doSubscribe (RPC::Context&); diff --git a/src/ripple/rpc/handlers/Sign.cpp b/src/ripple/rpc/handlers/Sign.cpp index b26a6a95e..9355da0b3 100644 --- a/src/ripple/rpc/handlers/Sign.cpp +++ b/src/ripple/rpc/handlers/Sign.cpp @@ -29,10 +29,13 @@ namespace ripple { Json::Value doSign (RPC::Context& context) { context.loadType = Resource::feeHighBurdenRPC; - bool bFailHard = context.params.isMember (jss::fail_hard) - && context.params[jss::fail_hard].asBool (); + NetworkOPs::FailHard const failType = + NetworkOPs::doFailHard ( + context.params.isMember (jss::fail_hard) + && context.params[jss::fail_hard].asBool ()); + return RPC::transactionSign ( - context.params, false, bFailHard, context.netOps, context.role); + context.params, failType, context.netOps, context.role); } } // ripple diff --git a/src/ripple/rpc/handlers/SignFor.cpp b/src/ripple/rpc/handlers/SignFor.cpp new file mode 100755 index 000000000..f09aded63 --- /dev/null +++ b/src/ripple/rpc/handlers/SignFor.cpp @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2014 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 +#include + +namespace ripple { + +// { +// tx_json: , +// account: +// secret: +// } +Json::Value doSignFor (RPC::Context& context) +{ + context.loadType = Resource::feeHighBurdenRPC; + NetworkOPs::FailHard const failType = + NetworkOPs::doFailHard ( + context.params.isMember ("fail_hard") + && context.params["fail_hard"].asBool ()); + + return RPC::transactionSignFor ( + context.params, failType, context.netOps, context.role); +} + +} // ripple diff --git a/src/ripple/rpc/handlers/Submit.cpp b/src/ripple/rpc/handlers/Submit.cpp index 74117b557..494ae7daa 100644 --- a/src/ripple/rpc/handlers/Submit.cpp +++ b/src/ripple/rpc/handlers/Submit.cpp @@ -23,6 +23,13 @@ namespace ripple { +static NetworkOPs::FailHard getFailHard (RPC::Context const& context) +{ + return NetworkOPs::doFailHard ( + context.params.isMember ("fail_hard") + && context.params["fail_hard"].asBool ()); +} + // { // tx_json: , // secret: @@ -33,10 +40,10 @@ Json::Value doSubmit (RPC::Context& context) if (!context.params.isMember (jss::tx_blob)) { - bool bFailHard = context.params.isMember (jss::fail_hard) - && context.params[jss::fail_hard].asBool (); - return RPC::transactionSign ( - context.params, true, bFailHard, context.netOps, context.role); + auto const failType = getFailHard (context); + + return RPC::transactionSubmit ( + context.params, failType, context.netOps, context.role); } Json::Value jvResult; @@ -76,10 +83,10 @@ Json::Value doSubmit (RPC::Context& context) try { - (void) context.netOps.processTransaction ( - tpTrans, context.role == Role::ADMIN, true, - context.params.isMember (jss::fail_hard) - && context.params[jss::fail_hard].asBool ()); + auto const failType = getFailHard (context); + + context.netOps.processTransaction ( + tpTrans, context.role == Role::ADMIN, true, failType); } catch (std::exception& e) { diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index 9a24cbe5c..63018b087 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -133,6 +133,9 @@ HandlerTable HANDLERS({ { "random", byRef (&doRandom), Role::USER, NO_CONDITION }, { "ripple_path_find", byRef (&doRipplePathFind), Role::USER, NO_CONDITION }, { "sign", byRef (&doSign), Role::USER, NO_CONDITION }, +#if RIPPLE_ENABLE_MULTI_SIGN + { "sign_for", byRef (&doSignFor), Role::USER, NO_CONDITION }, +#endif // RIPPLE_ENABLE_MULTI_SIGN { "submit", byRef (&doSubmit), Role::USER, NEEDS_CURRENT_LEDGER }, { "server_info", byRef (&doServerInfo), Role::USER, NO_CONDITION }, { "server_state", byRef (&doServerState), Role::USER, NO_CONDITION }, diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index d720548ad..d998bedb2 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -18,24 +18,92 @@ //============================================================================== #include +#include +#include #include -#include #include #include -#include -#include -#include +#include namespace ripple { //------------------------------------------------------------------------------ + namespace RPC { -namespace RPCDetail { +namespace detail { -// LedgerFacade methods +// A local class used to pass extra parameters used when returning a +// a SigningFor object. +class SigningForParams +{ +private: + RippleAddress const* const signingForAcctID_; + RippleAddress const* const multiSigningAcctID_; + RippleAddress* const multiSignPublicKey_; + Blob* const multiSignature_; +public: + explicit SigningForParams () + : signingForAcctID_ (nullptr) + , multiSigningAcctID_ (nullptr) + , multiSignPublicKey_ (nullptr) + , multiSignature_ (nullptr) + { } -void LedgerFacade::snapshotAccountState (RippleAddress const& accountID) + SigningForParams (SigningForParams const& rhs) = delete; + + SigningForParams ( + RippleAddress const& signingForAcctID, + RippleAddress const& multiSigningAcctID, + RippleAddress& multiSignPublicKey, + Blob& multiSignature) + : signingForAcctID_ (&signingForAcctID) + , multiSigningAcctID_ (&multiSigningAcctID) + , multiSignPublicKey_ (&multiSignPublicKey) + , multiSignature_ (&multiSignature) + { } + + bool isMultiSigning () const + { + return ((multiSigningAcctID_ != nullptr) && + (signingForAcctID_ != nullptr) && + (multiSignPublicKey_ != nullptr) && + (multiSignature_ != nullptr)); + } + + // When multi-signing we should not edit the tx_json fields. + bool editFields () const + { + return !isMultiSigning(); + } + + // Don't call this method unless isMultiSigning() returns true. + RippleAddress const& getSigningFor () + { + return *signingForAcctID_; + } + + RippleAddress const& getSigner () + { + return *multiSigningAcctID_; + } + + void setPublicKey (RippleAddress const& multiSignPublicKey) + { + *multiSignPublicKey_ = multiSignPublicKey; + } + + void moveMultiSignature (Blob&& multiSignature) + { + *multiSignature_ = std::move (multiSignature); + } +}; + +//------------------------------------------------------------------------------ + +// TxnSignApiFacade methods + +void TxnSignApiFacade::snapshotAccountState (RippleAddress const& accountID) { if (!netOPs_) // Unit testing. return; @@ -45,7 +113,7 @@ void LedgerFacade::snapshotAccountState (RippleAddress const& accountID) accountState_ = netOPs_->getAccountState (ledger_, accountID_); } -bool LedgerFacade::isValidAccount () const +bool TxnSignApiFacade::isValidAccount () const { if (!ledger_) // Unit testing. return true; @@ -53,7 +121,7 @@ bool LedgerFacade::isValidAccount () const return static_cast (accountState_); } -std::uint32_t LedgerFacade::getSeq () const +std::uint32_t TxnSignApiFacade::getSeq () const { if (!ledger_) // Unit testing. return 0; @@ -61,21 +129,19 @@ std::uint32_t LedgerFacade::getSeq () const return accountState_->getSeq (); } -Transaction::pointer LedgerFacade::submitTransactionSync ( +Transaction::pointer TxnSignApiFacade::processTransaction ( Transaction::ref tpTrans, bool bAdmin, bool bLocal, - bool bFailHard, - bool bSubmit) + NetworkOPs::FailHard failType) { if (!netOPs_) // Unit testing. return tpTrans; - return netOPs_->submitTransactionSync ( - tpTrans, bAdmin, bLocal, bFailHard, bSubmit); + return netOPs_->processTransaction (tpTrans, bAdmin, bLocal, failType); } -bool LedgerFacade::findPathsForOneIssuer ( +bool TxnSignApiFacade::findPathsForOneIssuer ( RippleAddress const& dstAccountID, Issue const& srcIssue, STAmount const& dstAmount, @@ -101,7 +167,7 @@ bool LedgerFacade::findPathsForOneIssuer ( fullLiquidityPath); } -std::uint64_t LedgerFacade::scaleFeeBase (std::uint64_t fee) const +std::uint64_t TxnSignApiFacade::scaleFeeBase (std::uint64_t fee) const { if (!ledger_) // Unit testing. return fee; @@ -109,7 +175,8 @@ std::uint64_t LedgerFacade::scaleFeeBase (std::uint64_t fee) const return ledger_->scaleFeeBase (fee); } -std::uint64_t LedgerFacade::scaleFeeLoad (std::uint64_t fee, bool bAdmin) const +std::uint64_t +TxnSignApiFacade::scaleFeeLoad (std::uint64_t fee, bool bAdmin) const { if (!ledger_) // Unit testing. return fee; @@ -117,7 +184,7 @@ std::uint64_t LedgerFacade::scaleFeeLoad (std::uint64_t fee, bool bAdmin) const return ledger_->scaleFeeLoad (fee, bAdmin); } -bool LedgerFacade::hasAccountRoot () const +bool TxnSignApiFacade::hasAccountRoot () const { if (!netOPs_) // Unit testing. return true; @@ -128,26 +195,65 @@ bool LedgerFacade::hasAccountRoot () const return static_cast (sleAccountRoot); } -bool LedgerFacade::accountMasterDisabled () const +error_code_i acctMatchesPubKey ( + AccountState::pointer accountState, + RippleAddress const& accountID, + RippleAddress const& publicKey) { - if (!accountState_) // Unit testing. - return false; + Account const publicKeyAcctID = publicKey.getAccountID (); + bool const isMasterKey = publicKeyAcctID == accountID.getAccountID (); - STLedgerEntry const& sle = accountState_->peekSLE (); - return sle.isFlag(lsfDisableMaster); + // If we can't get the accountRoot, but the accountIDs match, that's + // good enough. + if (!accountState) + { + if (isMasterKey) + return rpcSUCCESS; + return rpcBAD_SECRET; + } + + // If we *can* get to the accountRoot, check for MASTER_DISABLED + STLedgerEntry const& sle = accountState->peekSLE (); + if (isMasterKey) + { + if (sle.isFlag(lsfDisableMaster)) + return rpcMASTER_DISABLED; + return rpcSUCCESS; + } + + // The last gasp is that we have public Regular key. + if ((sle.isFieldPresent (sfRegularKey)) && + (publicKeyAcctID == sle.getFieldAccount160 (sfRegularKey))) + { + return rpcSUCCESS; + } + return rpcBAD_SECRET; } -bool LedgerFacade::accountMatchesRegularKey (Account account) const +error_code_i TxnSignApiFacade::singleAcctMatchesPubKey ( + RippleAddress const& publicKey) const { - if (!accountState_) // Unit testing. - return true; + if (!netOPs_) // Unit testing. + return rpcSUCCESS; - STLedgerEntry const& sle = accountState_->peekSLE (); - return ((sle.isFieldPresent (sfRegularKey)) && - (account == sle.getFieldAccount160 (sfRegularKey))); + return acctMatchesPubKey (accountState_, accountID_, publicKey); } -int LedgerFacade::getValidatedLedgerAge () const +error_code_i TxnSignApiFacade::multiAcctMatchesPubKey ( + RippleAddress const& accountID, + RippleAddress const& publicKey) const +{ + AccountState::pointer accountState; + if (netOPs_ && ledger_) + // If it's available, get the AccountState for the multi-signer's + // accountID. It's okay if the AccountState is not available, + // since they might be signing with a phantom (unfunded) account. + accountState = netOPs_->getAccountState (ledger_, accountID); + + return acctMatchesPubKey (accountState, accountID, publicKey); +} + +int TxnSignApiFacade::getValidatedLedgerAge () const { if (!netOPs_) // Unit testing. return 0; @@ -155,14 +261,13 @@ int LedgerFacade::getValidatedLedgerAge () const return getApp( ).getLedgerMaster ().getValidatedLedgerAge (); } -bool LedgerFacade::isLoadedCluster () const +bool TxnSignApiFacade::isLoadedCluster () const { if (!netOPs_) // Unit testing. return false; return getApp().getFeeTrack().isLoadedCluster(); } -} // namespace RPCDetail //------------------------------------------------------------------------------ @@ -181,20 +286,25 @@ bool LedgerFacade::isLoadedCluster () const If this optional field is not specified, then a default multiplier is used. - @param tx The JSON corresponding to the transaction to fill in - @param ledger A ledger for retrieving the current fee schedule - @param result A JSON object for injecting error results, if any - @param admin `true` if this is called by an administrative endpoint. + @param tx The JSON corresponding to the transaction to fill in. + @param ledger A ledger for retrieving the current fee schedule. + @param roll Identifies if this is called by an administrative endpoint. + + @return A JSON object containing the error results, if any */ -static void autofill_fee ( + +static Json::Value checkFee ( Json::Value& request, - RPCDetail::LedgerFacade& ledgerFacade, - Json::Value& result, - bool admin) + TxnSignApiFacade& apiFacade, + Role const role, + AutoFill const doAutoFill) { Json::Value& tx (request[jss::tx_json]); if (tx.isMember (jss::Fee)) - return; + return Json::Value(); + + if (doAutoFill == AutoFill::dont) + return RPC::missing_field_error ("tx_json.Fee"); int mult = Tuning::defaultAutoFillFeeMultiplier; if (request.isMember (jss::fee_mult_max)) @@ -205,9 +315,8 @@ static void autofill_fee ( } else { - RPC::inject_error (rpcHIGH_FEE, RPC::expected_field_message ( - jss::fee_mult_max, "a number"), result); - return; + return RPC::make_error (rpcHIGH_FEE, + RPC::expected_field_message (jss::fee_mult_max, "a number")); } } @@ -215,8 +324,10 @@ static void autofill_fee ( std::uint64_t const feeDefault = getConfig().TRANSACTION_FEE_BASE; // 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); + std::uint64_t const fee = + apiFacade.scaleFeeLoad (feeDefault, role == Role::ADMIN); + + std::uint64_t const limit = mult * apiFacade.scaleFeeBase (feeDefault); if (fee > limit) { @@ -224,20 +335,31 @@ static void autofill_fee ( ss << "Fee of " << fee << " exceeds the requested tx limit of " << limit; - RPC::inject_error (rpcHIGH_FEE, ss.str(), result); - return; + return RPC::make_error (rpcHIGH_FEE, ss.str()); } tx [jss::Fee] = static_cast(fee); + return Json::Value(); } -static Json::Value signPayment( +enum class PathFind : unsigned char +{ + dont, + might +}; + +static Json::Value checkPayment( Json::Value const& params, Json::Value& tx_json, RippleAddress const& raSrcAddressID, - RPCDetail::LedgerFacade& ledgerFacade, - Role role) + TxnSignApiFacade const& apiFacade, + Role const role, + PathFind const doPath) { + // Only path find for Payments. + if (tx_json[jss::TransactionType].asString () != "Payment") + return Json::Value(); + RippleAddress dstAccountID; if (!tx_json.isMember (jss::Amount)) @@ -254,18 +376,16 @@ static Json::Value signPayment( if (!dstAccountID.setAccountID (tx_json[jss::Destination].asString ())) return RPC::invalid_field_error ("tx_json.Destination"); + if ((doPath == PathFind::dont) && params.isMember (jss::build_path)) + return RPC::make_error (rpcINVALID_PARAMS, + "Field 'build_path' not allowed in this context."); + if (tx_json.isMember (jss::Paths) && params.isMember (jss::build_path)) return RPC::make_error (rpcINVALID_PARAMS, "Cannot specify both 'tx_json.Paths' and 'build_path'"); - if (!tx_json.isMember (jss::Paths) - && tx_json.isMember (jss::Amount) - && params.isMember (jss::build_path)) + if (!tx_json.isMember (jss::Paths) && params.isMember (jss::build_path)) { - // Need a ripple path. - Currency uSrcCurrencyID; - Account uSrcIssuerID; - STAmount saSendMax; if (tx_json.isMember (jss::SendMax)) @@ -291,7 +411,7 @@ static Json::Value signPayment( STPathSet spsPaths; STPath fullLiquidityPath; - bool valid = ledgerFacade.findPathsForOneIssuer ( + bool valid = apiFacade.findPathsForOneIssuer ( dstAccountID, saSendMax.issue (), amount, @@ -300,7 +420,6 @@ static Json::Value signPayment( spsPaths, fullLiquidityPath); - if (!valid) { WriteLog (lsDEBUG, RPCHandler) @@ -320,191 +439,321 @@ 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 transaction. +// Validate (but don't modify) the contents of the tx_json. // -Json::Value -transactionSign ( - Json::Value params, - bool bSubmit, - bool bFailHard, - RPCDetail::LedgerFacade& ledgerFacade, - Role role) +// Returns a pair. The Json::Value is non-empty +// and contains as error if there was an error. The returned RippleAddress +// is the "Account" addressID if there was no error. +// +// This code does not check the "Sequence" field, since the expectations +// for that field are particularly context sensitive. +static std::pair +checkTxJsonFields ( + Json::Value const& tx_json, + TxnSignApiFacade const& apiFacade, + Role const role, + bool const verify) { - Json::Value jvResult; + std::pair ret; - WriteLog (lsDEBUG, RPCHandler) << "transactionSign: " << params; - - KeyPair const keypair = keypairForSignature (params, jvResult); - - if (contains_error (jvResult)) + if (! tx_json.isObject ()) { - return jvResult; + ret.first = RPC::object_field_error (jss::tx_json); + return ret; } + if (! tx_json.isMember (jss::TransactionType)) + { + ret.first = RPC::missing_field_error ("tx_json.TransactionType"); + return ret; + } + + if (! tx_json.isMember (jss::Account)) + { + ret.first = RPC::make_error (rpcSRC_ACT_MISSING, + RPC::missing_field_message ("tx_json.Account")); + return ret; + } + + RippleAddress srcAddressID; + + if (! srcAddressID.setAccountID (tx_json[jss::Account].asString ())) + { + ret.first = RPC::make_error (rpcSRC_ACT_MALFORMED, + RPC::invalid_field_message ("tx_json.Account")); + return ret; + } + + // Check for current ledger. + if (verify && !getConfig ().RUN_STANDALONE && + (apiFacade.getValidatedLedgerAge() > 120)) + { + ret.first = rpcError (rpcNO_CURRENT); + return ret; + } + + // Check for load. + if (apiFacade.isLoadedCluster() && (role != Role::ADMIN)) + { + ret.first = rpcError (rpcTOO_BUSY); + return ret; + } + + // It's all good. Return the AddressID. + ret.second = std::move (srcAddressID); + return ret; +} + +//------------------------------------------------------------------------------ + +// A move-only struct that makes it easy to return either a Json::Value or a +// STTx::pointer from transactionPreProcessImpl (). +struct transactionPreProcessResult +{ + Json::Value const first; + STTx::pointer const second; + + transactionPreProcessResult () = delete; + transactionPreProcessResult (transactionPreProcessResult const&) = delete; + transactionPreProcessResult (transactionPreProcessResult&& rhs) + : first (std::move (rhs.first)) // VS2013 won't default this + , second (std::move (rhs.second)) + { } + + transactionPreProcessResult& operator= + (transactionPreProcessResult const&) = delete; + transactionPreProcessResult& operator= + (transactionPreProcessResult&&) = delete; + + + transactionPreProcessResult (Json::Value&& json) + : first (std::move (json)) + , second () + { } + + transactionPreProcessResult (STTx::pointer&& st) + : first () + , second (std::move (st)) + { } +}; + +static transactionPreProcessResult transactionPreProcessImpl ( + Json::Value& params, + TxnSignApiFacade& apiFacade, + Role role, + SigningForParams& signingArgs) +{ + KeyPair keypair; + { + Json::Value jvResult; + keypair = keypairForSignature (params, jvResult); + if (contains_error (jvResult)) + return std::move (jvResult); + } + + bool const verify = !(params.isMember (jss::offline) + && params[jss::offline].asBool()); + if (! params.isMember (jss::tx_json)) return RPC::missing_field_error (jss::tx_json); - Json::Value& tx_json (params [jss::tx_json]); - if (! tx_json.isObject ()) - return RPC::object_field_error (jss::tx_json); + // Check tx_json fields, but don't add any. + auto txJsonResult = checkTxJsonFields (tx_json, apiFacade, role, verify); + if (RPC::contains_error (txJsonResult.first)) + return std::move (txJsonResult.first); - if (! tx_json.isMember (jss::TransactionType)) - return RPC::missing_field_error ("tx_json.TransactionType"); + RippleAddress const raSrcAddressID = std::move (txJsonResult.second); - std::string const sType = tx_json [jss::TransactionType].asString (); - - if (! tx_json.isMember (jss::Account)) - return RPC::make_error (rpcSRC_ACT_MISSING, - RPC::missing_field_message ("tx_json.Account")); - - RippleAddress raSrcAddressID; - - if (! raSrcAddressID.setAccountID (tx_json[jss::Account].asString ())) - return RPC::make_error (rpcSRC_ACT_MALFORMED, - RPC::invalid_field_message ("tx_json.Account")); - - bool const verify = !(params.isMember (jss::offline) - && params[jss::offline].asBool ()); - - if (!tx_json.isMember (jss::Sequence) && !verify) + // This test covers the case where we're offline so the sequence number + // cannot be determined locally. If we're offline then the caller must + // provide the sequence number. + if (!verify && !tx_json.isMember (jss::Sequence)) return RPC::missing_field_error ("tx_json.Sequence"); - // Check for current ledger. - if (verify && !getConfig ().RUN_STANDALONE && - (ledgerFacade.getValidatedLedgerAge () > 120)) - return rpcError (rpcNO_CURRENT); - - // Check for load. - if (ledgerFacade.isLoadedCluster () && (role != Role::ADMIN)) - return rpcError (rpcTOO_BUSY); - - ledgerFacade.snapshotAccountState (raSrcAddressID); + apiFacade.snapshotAccountState (raSrcAddressID); if (verify) { - if (!ledgerFacade.isValidAccount ()) + if (!apiFacade.isValidAccount()) { // If not offline and did not find account, error. WriteLog (lsDEBUG, RPCHandler) << "transactionSign: Failed to find source account " << "in current ledger: " - << raSrcAddressID.humanAccountID (); + << raSrcAddressID.humanAccountID(); return rpcError (rpcSRC_ACT_NOT_FOUND); } } - autofill_fee (params, ledgerFacade, jvResult, role == Role::ADMIN); - if (RPC::contains_error (jvResult)) - return jvResult; - - if ("Payment" == sType) { - auto e = signPayment( + Json::Value err = checkFee ( + params, + apiFacade, + role, + signingArgs.editFields() ? AutoFill::might : AutoFill::dont); + + if (RPC::contains_error (err)) + return std::move (err); + + err = checkPayment ( params, tx_json, raSrcAddressID, - ledgerFacade, - role); - if (contains_error(e)) - return e; + apiFacade, + role, + signingArgs.editFields() ? PathFind::might : PathFind::dont); + + if (RPC::contains_error(err)) + return std::move (err); } - if (!tx_json.isMember (jss::Sequence)) - tx_json[jss::Sequence] = ledgerFacade.getSeq (); + if (signingArgs.editFields()) + { + if (!tx_json.isMember (jss::Sequence)) + tx_json[jss::Sequence] = apiFacade.getSeq(); - if (!tx_json.isMember (jss::Flags)) - tx_json[jss::Flags] = tfFullyCanonicalSig; + if (!tx_json.isMember (jss::Flags)) + tx_json[jss::Flags] = tfFullyCanonicalSig; + } if (verify) { - if (!ledgerFacade.hasAccountRoot ()) + if (!apiFacade.hasAccountRoot()) // XXX Ignore transactions for accounts not created. return rpcError (rpcSRC_ACT_NOT_FOUND); - } - if (verify) - { WriteLog (lsTRACE, RPCHandler) << - "verify: " << keypair.publicKey.humanAccountID() << - " : " << raSrcAddressID.humanAccountID (); + "verify: " << keypair.publicKey.humanAccountID() << + " : " << raSrcAddressID.humanAccountID(); - auto const secretAccountID = keypair.publicKey.getAccountID(); - if (raSrcAddressID.getAccountID () == secretAccountID) + // If multisigning then we need to return the public key. + if (signingArgs.isMultiSigning()) { - if (ledgerFacade.accountMasterDisabled ()) - return rpcError (rpcMASTER_DISABLED); + signingArgs.setPublicKey (keypair.publicKey); } - else if (!ledgerFacade.accountMatchesRegularKey (secretAccountID)) + else { - return rpcError (rpcBAD_SECRET); + // Make sure the account and secret belong together. + error_code_i const err = + apiFacade.singleAcctMatchesPubKey (keypair.publicKey); + + if (err != rpcSUCCESS) + return rpcError (err); } } STParsedJSONObject parsed (std::string (jss::tx_json), tx_json); - if (! parsed.object) + if (parsed.object == boost::none) { - jvResult [jss::error] = parsed.error [jss::error]; - jvResult [jss::error_code] = parsed.error [jss::error_code]; - jvResult [jss::error_message] = parsed.error [jss::error_message]; - return jvResult; + Json::Value err; + err [jss::error] = parsed.error [jss::error]; + err [jss::error_code] = parsed.error [jss::error_code]; + err [jss::error_message] = parsed.error [jss::error_message]; + return std::move (err); } - parsed.object->setFieldVL ( - sfSigningPubKey, - keypair.publicKey.getAccountPublic()); STTx::pointer stpTrans; - try { - stpTrans = std::make_shared (std::move (*parsed.object)); + // If we're generating a multi-signature the SigningPubKey must be + // empty: + // o If we're multi-signing, use an empty blob for the signingPubKey + // o Otherwise use the master account's public key. + Blob emptyBlob; + Blob const& signingPubKey = signingArgs.isMultiSigning() ? + emptyBlob : keypair.publicKey.getAccountPublic(); + + parsed.object->setFieldVL (sfSigningPubKey, signingPubKey); + + stpTrans = std::make_shared (std::move (parsed.object.get())); } catch (std::exception&) { return RPC::make_error (rpcINTERNAL, - "Exception occurred during transaction"); + "Exception occurred constructing serialized transaction"); } std::string reason; if (!passesLocalChecks (*stpTrans, reason)) return RPC::make_error (rpcINVALID_PARAMS, reason); - if (params.isMember (jss::debug_signing)) + // If multisign then return multiSignature, else set TxnSignature field. + if (signingArgs.isMultiSigning ()) { - jvResult[jss::tx_unsigned] = strHex ( - stpTrans->getSerializer ().peekData ()); - jvResult[jss::tx_signing_hash] = to_string (stpTrans->getSigningHash ()); + Serializer s = stpTrans->getMultiSigningData ( + signingArgs.getSigningFor (), signingArgs.getSigner ()); + Blob multiSignature = keypair.secretKey.accountPrivateSign(s.getData()); + signingArgs.moveMultiSignature (std::move (multiSignature)); + } + else + { + stpTrans->sign (keypair.secretKey); } - // FIXME: For performance, transactions should not be signed in this code - // path. + return std::move (stpTrans); +} - stpTrans->sign (keypair.secretKey); - - Transaction::pointer tpTrans = std::make_shared (stpTrans, - Validate::NO, reason); +static +std::pair +transactionConstructImpl (STTx::pointer stpTrans) +{ + std::pair ret; + // Turn the passed in STTx into a Transaction + Transaction::pointer tpTrans; + { + std::string reason; + tpTrans = std::make_shared(stpTrans, Validate::NO, reason); + if (tpTrans->getStatus () != NEW) + { + ret.first = RPC::make_error (rpcINTERNAL, + "Unable to construct transaction: " + reason); + return ret; + } + } try { - // FIXME: For performance, should use asynch interface. - tpTrans = ledgerFacade.submitTransactionSync (tpTrans, - role == Role::ADMIN, true, bFailHard, bSubmit); - - if (!tpTrans) + // Make sure the Transaction we just built is legit by serializing it + // and then de-serializing it. If the result isn't equivalent + // to the initial transaction then there's something wrong with the + // passed-in STTx. { - return RPC::make_error (rpcINTERNAL, - "Unable to sterilize transaction."); + Serializer s; + tpTrans->getSTransaction ()->add (s); + + Transaction::pointer tpTransNew = + Transaction::sharedTransaction (s.getData (), Validate::YES); + + if (tpTransNew && ( + !tpTransNew->getSTransaction ()->isEquivalent ( + *tpTrans->getSTransaction ()))) + { + tpTransNew.reset (); + } + tpTrans = std::move (tpTransNew); } } catch (std::exception&) { - return RPC::make_error (rpcINTERNAL, - "Exception occurred during transaction submission."); + // Assume that any exceptions are related to transaction sterilization. + tpTrans.reset (); } + if (!tpTrans) + { + ret.first = RPC::make_error (rpcINTERNAL, + "Unable to sterilize transaction."); + return ret; + } + ret.second = std::move (tpTrans); + return ret; +} + +Json::Value transactionFormatResultImpl (Transaction::pointer tpTrans) +{ + Json::Value jvResult; try { jvResult[jss::tx_json] = tpTrans->getJson (0); @@ -522,14 +771,252 @@ transactionSign ( jvResult[jss::engine_result_code] = tpTrans->getResult (); jvResult[jss::engine_result_message] = sHuman; } + } + catch (std::exception&) + { + jvResult = RPC::make_error (rpcINTERNAL, + "Exception occurred during JSON handling."); + } + return jvResult; +} - return jvResult; +void insertMultiSigners ( + Json::Value& jvResult, + RippleAddress const& signingForAcctID, + RippleAddress const& signingAcctID, + RippleAddress const& accountPublic, + Blob const& signature) +{ + // Build a SigningAccount object to inject into the SigningAccounts. + Json::Value signingAccount (Json::objectValue); + + signingAccount[sfAccount.getJsonName ()] = signingAcctID.humanAccountID (); + + signingAccount[sfSigningPubKey.getJsonName ()] = + strHex (accountPublic.getAccountPublic ()); + + signingAccount[sfMultiSignature.getJsonName ()] = strHex (signature); + + // Give the SigningAccount an object name and put it in the + // SigningAccounts array. + Json::Value nameSigningAccount (Json::objectValue); + nameSigningAccount[sfSigningAccount.getJsonName ()] = signingAccount; + + Json::Value signingAccounts (Json::arrayValue); + signingAccounts.append (nameSigningAccount); + + // Put the signingForAcctID and the SigningAccounts in the SigningFor. + Json::Value signingFor (Json::objectValue); + + signingFor[sfAccount.getJsonName ()] = signingForAcctID.humanAccountID (); + + signingFor[sfSigningAccounts.getJsonName ()] = signingAccounts; + + // Give the SigningFor an object name and put it in the MultiSigners array. + Json::Value nameSigningFor (Json::objectValue); + nameSigningFor[sfSigningFor.getJsonName ()] = signingFor; + + Json::Value multiSigners (Json::arrayValue); + multiSigners.append (nameSigningFor); + + // Inject the MultiSigners into the jvResult. + jvResult[sfMultiSigners.getName ()] = multiSigners; +} + +} // detail + +//------------------------------------------------------------------------------ + +/** Returns a Json::objectValue. */ +Json::Value transactionSign ( + Json::Value jvRequest, + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role) +{ + WriteLog (lsDEBUG, RPCHandler) << "transactionSign: " << jvRequest; + + using namespace detail; + + // Add and amend fields based on the transaction type. + SigningForParams signForParams; + transactionPreProcessResult preprocResult = + transactionPreProcessImpl (jvRequest, apiFacade, role, signForParams); + + if (!preprocResult.second) + return preprocResult.first; + + // Make sure the STTx makes a legitimate Transaction. + std::pair txn = + transactionConstructImpl (preprocResult.second); + + if (!txn.second) + return txn.first; + + return transactionFormatResultImpl (txn.second); +} + +/** Returns a Json::objectValue. */ +Json::Value transactionSubmit ( + Json::Value jvRequest, + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role) +{ + WriteLog (lsDEBUG, RPCHandler) << "transactionSubmit: " << jvRequest; + + using namespace detail; + + // Add and amend fields based on the transaction type. + SigningForParams signForParams; + transactionPreProcessResult preprocResult = + transactionPreProcessImpl (jvRequest, apiFacade, role, signForParams); + + if (!preprocResult.second) + return preprocResult.first; + + // Make sure the STTx makes a legitimate Transaction. + std::pair txn = + transactionConstructImpl (preprocResult.second); + + if (!txn.second) + return txn.first; + + // Finally, submit the transaction. + try + { + // FIXME: For performance, should use asynch interface + apiFacade.processTransaction ( + txn.second, role == Role::ADMIN, true, failType); } catch (std::exception&) { return RPC::make_error (rpcINTERNAL, - "Exception occurred during JSON handling."); + "Exception occurred during transaction submission."); } + + return transactionFormatResultImpl (txn.second); +} + +namespace detail +{ +// There are a some field checks shared by transactionSignFor +// and transactionSubmitMultiSigned. Gather them together here. +Json::Value checkMultiSignFields (Json::Value const& jvRequest) +{ + if (! jvRequest.isMember (jss::tx_json)) + return RPC::missing_field_error (jss::tx_json); + + Json::Value const& tx_json (jvRequest [jss::tx_json]); + + // There are a couple of additional fields we need to check before + // we serialize. If we serialize first then we generate less useful + //error messages. + if (!tx_json.isMember (jss::Sequence)) + return RPC::missing_field_error ("tx_json.Sequence"); + + if (!tx_json.isMember ("SigningPubKey")) + return RPC::missing_field_error ("tx_json.SigningPubKey"); + + if (!tx_json["SigningPubKey"].asString().empty()) + return RPC::make_error (rpcINVALID_PARAMS, + "When multi-signing 'tx_json.SigningPubKey' must be empty."); + + return Json::Value (); +} + +} // detail + +/** Returns a Json::objectValue. */ +Json::Value transactionSignFor ( + Json::Value jvRequest, + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role) +{ + WriteLog (lsDEBUG, RPCHandler) << + "transactionSignFor: " << jvRequest; + + // Verify presence of the signer's account field. + const char accountField[] = "account"; + + if (! jvRequest.isMember (accountField)) + return RPC::missing_field_error (accountField); + + // Turn the signer's account into a RippleAddress for multi-sign. + RippleAddress multiSignAddrID; + if (! multiSignAddrID.setAccountID ( + jvRequest[accountField].asString ())) + { + return RPC::make_error (rpcSRC_ACT_MALFORMED, + RPC::invalid_field_message (accountField)); + } + + // Verify the presence of the "signing_for" field. + const char signing_forField[] = "signing_for"; + + if (! jvRequest.isMember (signing_forField)) + return RPC::missing_field_error (signing_forField); + + // Turn the signing_for account into a RippleAddress for multi-sign. + RippleAddress multiSignForAddrID; + if (! multiSignForAddrID.setAccountID ( + jvRequest[signing_forField].asString ())) + { + return RPC::make_error (rpcSIGN_FOR_MALFORMED, + RPC::invalid_field_message (signing_forField)); + } + + // When multi-signing, the "Sequence" and "SigningPubKey" fields must + // be passed in by the caller. + using namespace detail; + { + Json::Value err = checkMultiSignFields (jvRequest); + if (RPC::contains_error (err)) + return std::move (err); + } + + // Add and amend fields based on the transaction type. + Blob multiSignature; + RippleAddress multiSignPubKey; + SigningForParams signForParams ( + multiSignForAddrID, multiSignAddrID, multiSignPubKey, multiSignature); + + transactionPreProcessResult preprocResult = + transactionPreProcessImpl ( + jvRequest, + apiFacade, + role, + signForParams); + + if (!preprocResult.second) + return preprocResult.first; + + // Make sure the multiSignAddrID can legitimately multi-sign. + { + error_code_i const err = + apiFacade.multiAcctMatchesPubKey (multiSignAddrID, multiSignPubKey); + + if (err != rpcSUCCESS) + return rpcError (err); + } + + // Make sure the STTx makes a legitimate Transaction. + std::pair txn = + transactionConstructImpl (preprocResult.second); + + if (!txn.second) + return txn.first; + + Json::Value json = transactionFormatResultImpl (txn.second); + if (RPC::contains_error (json)) + return json; + + // Finally, do what we were called for: return a SigningFor object. + insertMultiSigners (json, + multiSignForAddrID, multiSignAddrID, multiSignPubKey, multiSignature); + + return json; } } // RPC diff --git a/src/ripple/rpc/impl/TransactionSign.h b/src/ripple/rpc/impl/TransactionSign.h index 847be3b9d..729949bf5 100644 --- a/src/ripple/rpc/impl/TransactionSign.h +++ b/src/ripple/rpc/impl/TransactionSign.h @@ -23,10 +23,10 @@ namespace ripple { namespace RPC { -namespace RPCDetail { +namespace detail { // A class that allows these methods to be called with or without a // real NetworkOPs instance. This allows for unit testing. -class LedgerFacade +class TxnSignApiFacade { private: NetworkOPs* const netOPs_; @@ -40,21 +40,21 @@ public: noNetOPs }; - LedgerFacade () = delete; - LedgerFacade (LedgerFacade const&) = delete; - LedgerFacade& operator= (LedgerFacade const&) = delete; + TxnSignApiFacade () = delete; + TxnSignApiFacade (TxnSignApiFacade const&) = delete; + TxnSignApiFacade& operator= (TxnSignApiFacade const&) = delete; // For use in non unit testing circumstances. - explicit LedgerFacade (NetworkOPs& netOPs) + explicit TxnSignApiFacade (NetworkOPs& netOPs) : netOPs_ (&netOPs) { } // For testTransactionRPC unit tests. - explicit LedgerFacade (NoNetworkOPs noOPs) + explicit TxnSignApiFacade (NoNetworkOPs noOPs) : netOPs_ (nullptr) { } // For testAutoFillFees unit tests. - LedgerFacade (NoNetworkOPs noOPs, Ledger::pointer ledger) + TxnSignApiFacade (NoNetworkOPs noOPs, Ledger::pointer ledger) : netOPs_ (nullptr) , ledger_ (ledger) { } @@ -74,12 +74,11 @@ public: STPathSet& pathsOut, STPath& fullLiquidityPath) const; - Transaction::pointer submitTransactionSync ( + Transaction::pointer processTransaction ( Transaction::ref tpTrans, bool bAdmin, bool bLocal, - bool bFailHard, - bool bSubmit); + NetworkOPs::FailHard failType); std::uint64_t scaleFeeBase (std::uint64_t fee) const; @@ -87,35 +86,86 @@ public: bool hasAccountRoot () const; - bool accountMasterDisabled () const; + error_code_i singleAcctMatchesPubKey ( + RippleAddress const& publicKey) const; - bool accountMatchesRegularKey (Account account) const; + error_code_i multiAcctMatchesPubKey ( + RippleAddress const& acctID, + RippleAddress const& publicKey) const; int getValidatedLedgerAge () const; bool isLoadedCluster () const; }; -} // namespace RPCDetail +// A function to auto-fill fees. +enum class AutoFill : unsigned char +{ + dont, + might +}; + +static Json::Value checkFee ( + Json::Value& request, + TxnSignApiFacade& apiFacade, + Role const role, + AutoFill const doAutoFill); + +} // namespace detail /** Returns a Json::objectValue. */ Json::Value transactionSign ( - Json::Value params, - bool bSubmit, - bool bFailHard, - RPCDetail::LedgerFacade& ledgerFacade, + Json::Value params, // Passed by value so it can be modified locally. + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, Role role); +/** Returns a Json::objectValue. */ inline Json::Value transactionSign ( - Json::Value params, - bool bSubmit, - bool bFailHard, + Json::Value const& params, + NetworkOPs::FailHard failType, NetworkOPs& netOPs, Role role) { - RPCDetail::LedgerFacade ledgerFacade (netOPs); - return transactionSign (params, bSubmit, bFailHard, ledgerFacade, role); + detail::TxnSignApiFacade apiFacade (netOPs); + return transactionSign (params, failType, apiFacade, role); +} + +/** Returns a Json::objectValue. */ +Json::Value transactionSubmit ( + Json::Value params, // Passed by value so it can be modified locally. + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role); + +/** Returns a Json::objectValue. */ +Json::Value transactionSubmit ( + Json::Value const& params, + NetworkOPs::FailHard failType, + NetworkOPs& netOPs, + Role role) +{ + detail::TxnSignApiFacade apiFacade (netOPs); + return transactionSubmit (params, failType, apiFacade, role); +} + +/** Returns a Json::objectValue. */ +Json::Value transactionSignFor ( + Json::Value params, // Passed by value so it can be modified locally. + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role); + +/** Returns a Json::objectValue. */ +Json::Value transactionSignFor ( + Json::Value const& params, + NetworkOPs::FailHard failType, + NetworkOPs& netOPs, + Role role) +{ + detail::TxnSignApiFacade apiFacade (netOPs); + return transactionSignFor (params, failType, apiFacade, role); } } // RPC diff --git a/src/ripple/rpc/tests/JSONRPC.test.cpp b/src/ripple/rpc/tests/JSONRPC.test.cpp index 4e92be24c..11d7ce54b 100644 --- a/src/ripple/rpc/tests/JSONRPC.test.cpp +++ b/src/ripple/rpc/tests/JSONRPC.test.cpp @@ -29,39 +29,26 @@ namespace ripple { namespace RPC { -// 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 description; char const* const json; - unsigned int result; + char const* const expMsg[4]; - TxnTestData () = delete; - TxnTestData (TxnTestData const&) = delete; + // Default and copy ctors should be deleted, but that displeases gcc 4.6.3. +// TxnTestData () = delete; +// TxnTestData (TxnTestData const&) = delete; +// TxnTestData (TxnTestData&&) = delete; TxnTestData& operator= (TxnTestData const&) = delete; - TxnTestData (char const* jsonIn, unsigned int resultIn) - : json (jsonIn) - , result (resultIn) - { } + TxnTestData& operator= (TxnTestData&&) = delete; }; -// 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", +{ "Minimal payment.", +R"({ + "command": "doesnt_matter", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -69,11 +56,17 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'account'.", +"Missing field 'tx_json.Sequence'."}}, -// Pass in Fee with minimal payment. -{R"({ - "command": "submit", +{ "Pass in Fee with minimal payment.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "secret": "masterpassphrase", "tx_json": { "Fee": 10, @@ -82,11 +75,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'signing_for'.", +"Missing field 'tx_json.Sequence'."}}, -// Pass in Sequence. -{R"({ - "command": "submit", +{ "Pass in Sequence.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Sequence": 0, @@ -95,11 +95,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// Pass in Sequence and Fee with minimal payment. -{R"({ - "command": "submit", +{ "Pass in Sequence and Fee with minimal payment.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Sequence": 0, @@ -109,11 +116,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// Add "fee_mult_max" field. -{R"({ - "command": "submit", +{ "Add 'fee_mult_max' field.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "fee_mult_max": 7, "tx_json": { @@ -123,11 +137,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// "fee_mult_max is ignored if "Fee" is present. -{R"({ - "command": "submit", +{ "fee_mult_max is ignored if 'Fee' is present.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "fee_mult_max": 0, "tx_json": { @@ -138,11 +159,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// Invalid "fee_mult_max" field. -{R"({ - "command": "submit", +{ "Invalid 'fee_mult_max' field.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "fee_mult_max": "NotAFeeMultiplier", "tx_json": { @@ -152,11 +180,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Invalid field 'fee_mult_max', not a number.", +"Invalid field 'fee_mult_max', not a number.", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// Invalid value for "fee_mult_max" field. -{R"({ - "command": "submit", +{ "Invalid value for 'fee_mult_max' field.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "fee_mult_max": 0, "tx_json": { @@ -166,22 +201,36 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Fee of 10 exceeds the requested tx limit of 0", +"Fee of 10 exceeds the requested tx limit of 0", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// Missing "Amount". -{R"({ - "command": "submit", +{ "Missing 'Amount'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'tx_json.Amount'.", +"Missing field 'tx_json.Amount'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Invalid "Amount". -{R"({ - "command": "submit", +{ "Invalid 'Amount'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -189,22 +238,36 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Invalid field 'tx_json.Amount'.", +"Invalid field 'tx_json.Amount'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Missing "Destination". -{R"({ - "command": "submit", +{ "Missing 'Destination'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "Amount": "1000000000", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'tx_json.Destination'.", +"Missing field 'tx_json.Destination'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Invalid "Destination". -{R"({ - "command": "submit", +{ "Invalid 'Destination'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -212,11 +275,18 @@ static TxnTestData const txnTestArray [] = "Destination": "NotADestination", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Invalid field 'tx_json.Destination'.", +"Invalid field 'tx_json.Destination'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Cannot create XRP to XRP paths. -{R"({ - "command": "submit", +{ "Cannot create XRP to XRP paths.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "build_path": 1, "tx_json": { @@ -225,11 +295,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Cannot build XRP to XRP paths.", +"Cannot build XRP to XRP paths.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Successful "build_path". -{R"({ - "command": "submit", +{ "Successful 'build_path'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "build_path": 1, "tx_json": { @@ -242,11 +319,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Not valid to include both "Paths" and "build_path". -{R"({ - "command": "submit", +{ "Not valid to include both 'Paths' and 'build_path'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "build_path": 1, "tx_json": { @@ -260,11 +344,18 @@ static TxnTestData const txnTestArray [] = "Paths": "", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Cannot specify both 'tx_json.Paths' and 'build_path'", +"Cannot specify both 'tx_json.Paths' and 'build_path'", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Successful "SendMax". -{R"({ - "command": "submit", +{ "Successful 'SendMax'.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "build_path": 1, "tx_json": { @@ -282,11 +373,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Even though "Amount" may not be XRP for pathfinding, "SendMax" may be XRP. -{R"({ - "command": "submit", +{ "Even though 'Amount' may not be XRP for pathfinding, 'SendMax' may be XRP.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "build_path": 1, "tx_json": { @@ -300,22 +398,36 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// "secret" must be present. -{R"({ - "command": "submit", +{ "'secret' must be present.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "Amount": "1000000000", "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'secret'.", +"Missing field 'secret'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// "secret" must be non-empty. -{R"({ - "command": "submit", +{ "'secret' must be non-empty.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -323,11 +435,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Invalid field 'secret'.", +"Invalid field 'secret'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// "tx_json" must be present. -{R"({ - "command": "submit", +{ "'tx_json' must be present.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "rx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -335,22 +454,36 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'tx_json'.", +"Missing field 'tx_json'.", +"Missing field 'tx_json'.", +"Missing field 'tx_json'."}}, -// "TransactionType" must be present. -{R"({ - "command": "submit", +{ "'TransactionType' must be present.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "Amount": "1000000000", "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'tx_json.TransactionType'.", +"Missing field 'tx_json.TransactionType'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// The "TransactionType" must be one of the pre-established transaction types. -{R"({ - "command": "submit", +{ "The 'TransactionType' must be one of the pre-established transaction types.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -358,11 +491,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "tt" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Field 'tx_json.TransactionType' has invalid data.", +"Field 'tx_json.TransactionType' has invalid data.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// The "TransactionType", however, may be represented with an integer. -{R"({ - "command": "submit", +{ "The 'TransactionType', however, may be represented with an integer.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", @@ -370,22 +510,36 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": 0 } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// "Account" must be present. -{R"({ - "command": "submit", +{ "'Account' must be present.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Amount": "1000000000", "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'tx_json.Account'.", +"Missing field 'tx_json.Account'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// "Account" must be well formed. -{R"({ - "command": "submit", +{ "'Account' must be well formed.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Account": "NotAnAccount", @@ -393,11 +547,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Invalid field 'tx_json.Account'.", +"Invalid field 'tx_json.Account'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// The "offline" tag may be added to the transaction. -{R"({ - "command": "submit", +{ "The 'offline' tag may be added to the transaction.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "offline": 0, "tx_json": { @@ -406,11 +567,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// If "offline" is true then a "Sequence" field must be supplied. -{R"({ - "command": "submit", +{ "If 'offline' is true then a 'Sequence' field must be supplied.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "offline": 1, "tx_json": { @@ -419,11 +587,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// Valid transaction if "offline" is true. -{R"({ - "command": "submit", +{ "Valid transaction if 'offline' is true.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "offline": 1, "tx_json": { @@ -433,11 +608,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, -// A "Flags' field may be specified. -{R"({ - "command": "submit", +{ "A 'Flags' field may be specified.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Flags": 0, @@ -446,11 +628,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// The "Flags" field must be numeric. -{R"({ - "command": "submit", +{ "The 'Flags' field must be numeric.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "tx_json": { "Flags": "NotGoodFlags", @@ -459,11 +648,18 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::signFail | TxnTestData::submitFail}, +})", +{ +"Field 'tx_json.Flags' has invalid data.", +"Field 'tx_json.Flags' has invalid data.", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, -// It's okay to add a "debug_signing" field. -{R"({ - "command": "submit", +{ "It's okay to add a 'debug_signing' field.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "secret": "masterpassphrase", "debug_signing": 0, "tx_json": { @@ -472,62 +668,302 @@ static TxnTestData const txnTestArray [] = "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", "TransactionType": "Payment" } -})", TxnTestData::allGood}, +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, + +{ "Minimal sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"", +"", +"", +"Missing field 'MultiSigners'."}}, + +{ "Missing 'Account' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"Missing field 'tx_json.Account'.", +"Missing field 'tx_json.Account'.", +"Missing field 'tx_json.Account'.", +"Missing field 'tx_json.Account'."}}, + +{ "Missing 'Amount' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"Missing field 'tx_json.Amount'.", +"Missing field 'tx_json.Amount'.", +"Missing field 'tx_json.Amount'.", +"Missing field 'tx_json.Amount'."}}, + +{ "Missing 'Destination' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"Missing field 'tx_json.Destination'.", +"Missing field 'tx_json.Destination'.", +"Missing field 'tx_json.Destination'.", +"Missing field 'tx_json.Destination'."}}, + +{ "Missing 'Fee' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"", +"", +"Missing field 'tx_json.Fee'.", +"Missing field 'tx_json.Fee'."}}, + +{ "Missing 'Sequence' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"", +"", +"Missing field 'tx_json.Sequence'.", +"Missing field 'tx_json.Sequence'."}}, + +{ "Missing 'SigningPubKey' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "Sequence": 0, + "TransactionType": "Payment" + } +})", +{ +"", +"", +"Missing field 'tx_json.SigningPubKey'.", +"Missing field 'tx_json.SigningPubKey'."}}, + +{ "Non-empty 'SigningPubKey' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "1", + "TransactionType": "Payment" + } +})", +{ +"", +"", +"When multi-signing 'tx_json.SigningPubKey' must be empty.", +"When multi-signing 'tx_json.SigningPubKey' must be empty."}}, + +{ "Missing 'TransactionType' in sign_for.", +R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "signing_for": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "secret": "masterpassphrase", + "tx_json": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Amount": "1000000000", + "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + } +})", +{ +"Missing field 'tx_json.TransactionType'.", +"Missing field 'tx_json.TransactionType'.", +"Missing field 'tx_json.TransactionType'.", +"Missing field 'tx_json.TransactionType'."}}, + +{ "Minimal submit_multisigned.", +R"({ + "command": "submit_multisigned", + "MultiSigners": [ + { + "SigningFor": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "SigningAccounts": [ + { + "SigningAccount": { + "Account": "rPcNzota6B8YBokhYtcTNqQVCngtbnWfux", + "MultiSignature": "3045022100F9ED357606932697A4FAB2BE7F222C21DD93CA4CFDD90357AADD07465E8457D6022038173193E3DFFFB5D78DD738CC0905395F885DA65B98FDB9793901FE3FD26ECE", + "SigningPubKey": "02FE36A690D6973D55F88553F5D2C4202DE75F2CF8A6D0E17C70AC223F044501F8" + } + } + ] + } + } + ], + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment" + } +})", +{ +"Missing field 'secret'.", +"Missing field 'secret'.", +"Missing field 'account'.", +""}}, }; + class JSONRPC_test : public beast::unit_test::suite { public: void testAutoFillFees () { + std::string const secret = "masterpassphrase"; RippleAddress rootSeedMaster - = RippleAddress::createSeedGeneric ("masterpassphrase"); + = RippleAddress::createSeedGeneric (secret); + RippleAddress rootGeneratorMaster = RippleAddress::createGeneratorPublic (rootSeedMaster); + RippleAddress rootAddress = RippleAddress::createAccountPublic (rootGeneratorMaster, 0); + std::uint64_t startAmount (100000); Ledger::pointer ledger (std::make_shared ( rootAddress, startAmount)); - using namespace RPCDetail; - LedgerFacade facade (LedgerFacade::noNetOPs, ledger); + using namespace detail; + TxnSignApiFacade apiFacade (TxnSignApiFacade::noNetOPs, ledger); - { + { Json::Value req; - Json::Value result; Json::Reader ().parse ( - R"({ "fee_mult_max" : 1, "tx_json" : { } } )" - , req); - autofill_fee (req, facade, result, true); + "{ \"fee_mult_max\" : 1, \"tx_json\" : { } } ", req); + Json::Value result = + checkFee (req, apiFacade, Role::ADMIN, AutoFill::might); - expect (! contains_error (result)); + expect (! RPC::contains_error (result), "Legal checkFee"); } { Json::Value req; - Json::Value result; Json::Reader ().parse ( - R"({ "fee_mult_max" : 0, "tx_json" : { } } )" - , req); - autofill_fee (req, facade, result, true); + "{ \"fee_mult_max\" : 0, \"tx_json\" : { } } ", req); + Json::Value result = + checkFee (req, apiFacade, Role::ADMIN, AutoFill::might); - expect (contains_error (result)); + expect (RPC::contains_error (result), "Invalid checkFee"); } } 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 ; + // A list of all the functions we want to test and their fail bits. + using transactionFunc = Json::Value (*) ( + Json::Value params, + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role); + + using TestStuff = + std::tuple ; + static TestStuff const testFuncs [] = { - TestStuff {false, TxnTestData::signFail}, - TestStuff {true, TxnTestData::submitFail}, + TestStuff {transactionSign, "sign", 0}, + TestStuff {transactionSubmit, "submit", 1}, + TestStuff {transactionSignFor, "sign_for", 2} }; for (auto testFunc : testFuncs) @@ -537,7 +973,7 @@ public: { Json::Value req; Json::Reader ().parse (txnTest.json, req); - if (contains_error (req)) + if (RPC::contains_error (req)) throw std::runtime_error ( "Internal JSONRPC_test error. Bad test JSON."); @@ -547,18 +983,24 @@ public: for (Role testRole : testedRoles) { // Mock so we can run without a ledger. - RPCDetail::LedgerFacade fakeNetOPs ( - RPCDetail::LedgerFacade::noNetOPs); + detail::TxnSignApiFacade apiFacade ( + detail::TxnSignApiFacade::noNetOPs); - Json::Value result = transactionSign ( + Json::Value result = get<0>(testFunc) ( req, - testFunc.first, - true, - fakeNetOPs, + NetworkOPs::FailHard::yes, + apiFacade, testRole); - expect (contains_error (result) == - static_cast (txnTest.result & testFunc.second)); + std::string errStr; + if (RPC::contains_error (result)) + errStr = result["error_message"].asString (); + + std::string const expStr (txnTest.expMsg[get<2>(testFunc)]); + expect (errStr == expStr, + "Expected: \"" + expStr + "\"\n Got: \"" + errStr + + "\"\nIn " + std::string (get<1>(testFunc)) + + ": " + txnTest.description); } } } @@ -575,3 +1017,4 @@ BEAST_DEFINE_TESTSUITE(JSONRPC,ripple_app,ripple); } // RPC } // ripple + diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp index 6543a7b8c..6b9091b93 100644 --- a/src/ripple/unity/rpcx.cpp +++ b/src/ripple/unity/rpcx.cpp @@ -73,6 +73,7 @@ #include #include #include +#include #include #include #include