diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 8782cb82fa1..dcce0a563f6 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -148,6 +148,7 @@ void printHelp (const po::options_description& desc) " channel_verify \n" " connect []\n" " consensus_info\n" + " deposit_authorized []" " feature [ [accept|reject]]\n" " fetch_info [clear]\n" " gateway_balances [] [ [ ]]\n" diff --git a/src/ripple/app/tx/impl/CancelCheck.h b/src/ripple/app/tx/impl/CancelCheck.h index 7e544a299a8..ddea7fd6ba4 100644 --- a/src/ripple/app/tx/impl/CancelCheck.h +++ b/src/ripple/app/tx/impl/CancelCheck.h @@ -28,7 +28,7 @@ class CancelCheck : public Transactor { public: - CancelCheck (ApplyContext& ctx) + explicit CancelCheck (ApplyContext& ctx) : Transactor (ctx) { } diff --git a/src/ripple/app/tx/impl/CancelOffer.h b/src/ripple/app/tx/impl/CancelOffer.h index 42e9f891da4..ea37ff7f781 100644 --- a/src/ripple/app/tx/impl/CancelOffer.h +++ b/src/ripple/app/tx/impl/CancelOffer.h @@ -31,7 +31,7 @@ class CancelOffer : public Transactor { public: - CancelOffer (ApplyContext& ctx) + explicit CancelOffer (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CancelTicket.h b/src/ripple/app/tx/impl/CancelTicket.h index cdc1c4f4cfc..a82093dd589 100644 --- a/src/ripple/app/tx/impl/CancelTicket.h +++ b/src/ripple/app/tx/impl/CancelTicket.h @@ -30,7 +30,7 @@ class CancelTicket : public Transactor { public: - CancelTicket (ApplyContext& ctx) + explicit CancelTicket (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CashCheck.h b/src/ripple/app/tx/impl/CashCheck.h index e3b469e36ab..1b6816ca2d6 100644 --- a/src/ripple/app/tx/impl/CashCheck.h +++ b/src/ripple/app/tx/impl/CashCheck.h @@ -28,7 +28,7 @@ class CashCheck : public Transactor { public: - CashCheck (ApplyContext& ctx) + explicit CashCheck (ApplyContext& ctx) : Transactor (ctx) { } diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 47a493c71a0..72a475fd1f9 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -33,7 +33,7 @@ class Change : public Transactor { public: - Change (ApplyContext& ctx) + explicit Change (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CreateCheck.h b/src/ripple/app/tx/impl/CreateCheck.h index 805aefae011..5a180ed36b2 100644 --- a/src/ripple/app/tx/impl/CreateCheck.h +++ b/src/ripple/app/tx/impl/CreateCheck.h @@ -28,7 +28,7 @@ class CreateCheck : public Transactor { public: - CreateCheck (ApplyContext& ctx) + explicit CreateCheck (ApplyContext& ctx) : Transactor (ctx) { } diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index 1bc607e216a..c1fca1bfab3 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -198,7 +198,7 @@ CreateOffer::preclaim(PreclaimContext const& ctx) // // The return code change is attached to featureChecks as a convenience. // The change is not big enough to deserve its own amendment. - return ctx.view.rules().enabled(featureChecks) + return ctx.view.rules().enabled(featureDepositPreauth) ? TER {tecEXPIRED} : TER {tesSUCCESS}; } @@ -237,10 +237,10 @@ CreateOffer::checkAcceptAsset(ReadView const& view, : TER {tecNO_ISSUER}; } - // This code is attached to the FlowCross amendment as a matter of + // This code is attached to the DepositPreauth amendment as a matter of // convenience. The change is not significant enough to deserve its // own amendment. - if (view.rules().enabled(featureFlowCross) && (issue.account == id)) + if (view.rules().enabled(featureDepositPreauth) && (issue.account == id)) // An account can always accept its own issuance. return tesSUCCESS; @@ -1106,10 +1106,10 @@ CreateOffer::applyGuts (Sandbox& sb, Sandbox& sbCancel) // If the offer has expired, the transaction has successfully // done nothing, so short circuit from here. // - // The return code change is attached to featureChecks as a convenience. - // The change is not big enough to deserve its own amendment. + // The return code change is attached to featureDepositPreauth as a + // convenience. The change is not big enough to deserve a fix code. TER const ter {ctx_.view().rules().enabled( - featureChecks) ? TER {tecEXPIRED} : TER {tesSUCCESS}}; + featureDepositPreauth) ? TER {tecEXPIRED} : TER {tesSUCCESS}}; return{ ter, true }; } diff --git a/src/ripple/app/tx/impl/CreateOffer.h b/src/ripple/app/tx/impl/CreateOffer.h index 93026ecc5ca..675badb0f08 100644 --- a/src/ripple/app/tx/impl/CreateOffer.h +++ b/src/ripple/app/tx/impl/CreateOffer.h @@ -36,7 +36,7 @@ class CreateOffer { public: /** Construct a Transactor subclass that creates an offer in the ledger. */ - CreateOffer (ApplyContext& ctx) + explicit CreateOffer (ApplyContext& ctx) : Transactor(ctx) , stepCounter_ (1000, j_) { diff --git a/src/ripple/app/tx/impl/CreateTicket.h b/src/ripple/app/tx/impl/CreateTicket.h index 411307aaa79..745963cfb6f 100644 --- a/src/ripple/app/tx/impl/CreateTicket.h +++ b/src/ripple/app/tx/impl/CreateTicket.h @@ -31,7 +31,7 @@ class CreateTicket : public Transactor { public: - CreateTicket (ApplyContext& ctx) + explicit CreateTicket (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/DepositPreauth.cpp b/src/ripple/app/tx/impl/DepositPreauth.cpp new file mode 100644 index 00000000000..a604222fa1c --- /dev/null +++ b/src/ripple/app/tx/impl/DepositPreauth.cpp @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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 +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +DepositPreauth::preflight (PreflightContext const& ctx) +{ + if (! ctx.rules.enabled (featureDepositPreauth)) + return temDISABLED; + + auto const ret = preflight1 (ctx); + if (!isTesSuccess (ret)) + return ret; + + auto& tx = ctx.tx; + auto& j = ctx.j; + + if (tx.getFlags() & tfUniversalMask) + { + JLOG(j.trace()) << + "Malformed transaction: Invalid flags set."; + return temINVALID_FLAG; + } + + auto const optAuth = ctx.tx[~sfAuthorize]; + auto const optUnauth = ctx.tx[~sfUnauthorize]; + if (static_cast(optAuth) == static_cast(optUnauth)) + { + // Either both fields are present or neither field is present. In + // either case the transaction is malformed. + JLOG(j.trace()) << + "Malformed transaction: " + "Invalid Authorize and Unauthorize field combination."; + return temMALFORMED; + } + + // Make sure that the passed account is valid. + AccountID const target {optAuth ? *optAuth : *optUnauth}; + if (target == beast::zero) + { + JLOG(j.trace()) << + "Malformed transaction: Authorized or Unauthorized field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + // An account may not preauthorize itself. + if (optAuth && (target == ctx.tx[sfAccount])) + { + JLOG(j.trace()) << + "Malformed transaction: Attempting to DepositPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + + return preflight2 (ctx); +} + +TER +DepositPreauth::preclaim(PreclaimContext const& ctx) +{ + // Determine which operation we're performing: authorizing or unauthorizing. + if (ctx.tx.isFieldPresent (sfAuthorize)) + { + // Verify that the Authorize account is present in the ledger. + AccountID const auth {ctx.tx[sfAuthorize]}; + if (! ctx.view.exists (keylet::account (auth))) + return tecNO_TARGET; + + // Verify that the Preauth entry they asked to add is not already + // in the ledger. + if (ctx.view.exists ( + keylet::depositPreauth (ctx.tx[sfAccount], auth))) + return tecDUPLICATE; + } + else + { + // Verify that the Preauth entry they asked to remove is in the ledger. + AccountID const unauth {ctx.tx[sfUnauthorize]}; + if (! ctx.view.exists ( + keylet::depositPreauth (ctx.tx[sfAccount], unauth))) + return tecNO_ENTRY; + } + return tesSUCCESS; +} + +TER +DepositPreauth::doApply () +{ + auto const sleOwner = view().peek (keylet::account (account_)); + + if (ctx_.tx.isFieldPresent (sfAuthorize)) + { + // A preauth counts against the reserve of the issuing account, but we + // check the starting balance because we want to allow dipping into the + // reserve to pay fees. + { + STAmount const reserve {view().fees().accountReserve ( + sleOwner->getFieldU32 (sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + // Preclaim already verified that the Preauth entry does not yet exist. + // Create and populate the Preauth entry. + AccountID const auth {ctx_.tx[sfAuthorize]}; + auto slePreauth = + std::make_shared(keylet::depositPreauth (account_, auth)); + + slePreauth->setAccountID (sfAccount, account_); + slePreauth->setAccountID (sfAuthorize, auth); + view().insert (slePreauth); + + auto viewJ = ctx_.app.journal ("View"); + auto const page = view().dirInsert (keylet::ownerDir (account_), + slePreauth->key(), describeOwnerDir (account_)); + + JLOG(j_.trace()) + << "Adding DepositPreauth to owner directory " + << to_string (slePreauth->key()) + << ": " << (page ? "success" : "failure"); + + if (! page) + return tecDIR_FULL; + + slePreauth->setFieldU64 (sfOwnerNode, *page); + + // If we succeeded, the new entry counts against the creator's reserve. + adjustOwnerCount (view(), sleOwner, 1, viewJ); + } + else + { + // Verify that the Preauth entry they asked to remove is + // in the ledger. + AccountID const unauth {ctx_.tx[sfUnauthorize]}; + uint256 const preauthIndex {getDepositPreauthIndex (account_, unauth)}; + auto slePreauth = view().peek (keylet::depositPreauth (preauthIndex)); + + if (! slePreauth) + { + // Error should have been caught in preclaim. + JLOG(j_.warn()) << "Selected DepositPreauth does not exist."; + return tecNO_ENTRY; + } + + auto viewJ = ctx_.app.journal ("View"); + std::uint64_t const page {(*slePreauth)[sfOwnerNode]}; + if (! view().dirRemove ( + keylet::ownerDir (account_), page, preauthIndex, true)) + { + JLOG(j_.warn()) << "Unable to delete DepositPreauth from owner."; + return tefBAD_LEDGER; + } + + // If we succeeded, update the DepositPreauth owner's reserve. + auto const sleOwner = view().peek (keylet::account (account_)); + adjustOwnerCount (view(), sleOwner, -1, viewJ); + + // Remove DepositPreauth from ledger. + view().erase (slePreauth); + } + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/DepositPreauth.h b/src/ripple/app/tx/impl/DepositPreauth.h new file mode 100644 index 00000000000..d950d7eca87 --- /dev/null +++ b/src/ripple/app/tx/impl/DepositPreauth.h @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_DEPOSIT_PREAUTH_H_INCLUDED +#define RIPPLE_TX_DEPOSIT_PREAUTH_H_INCLUDED + +#include + +namespace ripple { + +class DepositPreauth + : public Transactor +{ +public: + explicit DepositPreauth (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + NotTEC + preflight (PreflightContext const& ctx); + + static + TER + preclaim(PreclaimContext const& ctx); + + TER doApply () override; +}; + +} // ripple + +#endif + diff --git a/src/ripple/app/tx/impl/Escrow.cpp b/src/ripple/app/tx/impl/Escrow.cpp index f14333dd96a..4b9de741d0f 100644 --- a/src/ripple/app/tx/impl/Escrow.cpp +++ b/src/ripple/app/tx/impl/Escrow.cpp @@ -446,8 +446,9 @@ EscrowFinish::doApply() return tecCRYPTOCONDITION_ERROR; } - // NOTE: Escrow payments cannot be used to fund accounts - auto const sled = ctx_.view().peek(keylet::account((*slep)[sfDestination])); + // NOTE: Escrow payments cannot be used to fund accounts. + AccountID const destID = (*slep)[sfDestination]; + auto const sled = ctx_.view().peek(keylet::account(destID)); if (! sled) return tecNO_DST; @@ -456,10 +457,15 @@ EscrowFinish::doApply() // Is EscrowFinished authorized? if (sled->getFlags() & lsfDepositAuth) { - // Authorized if Destination == Account, otherwise no permission. - AccountID const destID = (*slep)[sfDestination]; - if (ctx_.tx[sfAccount] != destID) - return tecNO_PERMISSION; + // A destination account that requires authorization has two + // ways to get an EscrowFinished into the account: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + if (account_ != destID) + { + if (! view().exists (keylet::depositPreauth (destID, account_))) + return tecNO_PERMISSION; + } } } @@ -479,7 +485,7 @@ EscrowFinish::doApply() if (ctx_.view ().rules().enabled(fix1523) && (*slep)[~sfDestinationNode]) { TER const ter = dirDelete(ctx_.view(), true, - (*slep)[sfDestinationNode], keylet::ownerDir((*slep)[sfDestination]), + (*slep)[sfDestinationNode], keylet::ownerDir(destID), k.key, false, false, ctx_.app.journal ("View")); if (! isTesSuccess(ter)) return ter; diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index fd97b43f2aa..135b6e2e32e 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -330,6 +330,7 @@ LedgerEntryTypesMatch::visitEntry( case ltESCROW: case ltPAYCHAN: case ltCHECK: + case ltDEPOSIT_PREAUTH: break; default: invalidTypeAdded_ = true; diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp index 923ae0cf3f5..a91bf5ad746 100644 --- a/src/ripple/app/tx/impl/PayChan.cpp +++ b/src/ripple/app/tx/impl/PayChan.cpp @@ -472,12 +472,18 @@ PayChanClaim::doApply() (txAccount == src && (sled->getFlags() & lsfDisallowXRP))) return tecNO_TARGET; - // Check whether the destination account requires deposit authorization - if (txAccount != dst) + // Check whether the destination account requires deposit authorization. + if (depositAuth && (sled->getFlags() & lsfDepositAuth)) { - if (depositAuth && - ((sled->getFlags() & lsfDepositAuth) == lsfDepositAuth)) - return tecNO_PERMISSION; + // A destination account that requires authorization has two + // ways to get a Payment Channel Claim into the account: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + if (txAccount != dst) + { + if (! view().exists (keylet::depositPreauth (dst, txAccount))) + return tecNO_PERMISSION; + } } (*slep)[sfBalance] = ctx_.tx[sfBalance]; diff --git a/src/ripple/app/tx/impl/Payment.cpp b/src/ripple/app/tx/impl/Payment.cpp index eb7e7a1a576..a45f29a8e68 100644 --- a/src/ripple/app/tx/impl/Payment.cpp +++ b/src/ripple/app/tx/impl/Payment.cpp @@ -350,12 +350,13 @@ Payment::doApply () bool const reqDepositAuth = sleDst->getFlags() & lsfDepositAuth && view().rules().enabled(featureDepositAuth); + bool const depositPreauth = view().rules().enabled(featureDepositPreauth); + bool const bRipple = paths || sendMax || !saDstAmount.native (); - // XXX Should sendMax be sufficient to imply ripple? // If the destination has lsfDepositAuth set, then only direct XRP // payments (no intermediate steps) are allowed to the destination. - if (bRipple && reqDepositAuth) + if (!depositPreauth && bRipple && reqDepositAuth) return tecNO_PERMISSION; if (bRipple) @@ -363,6 +364,20 @@ Payment::doApply () // Ripple payment with at least one intermediate step and uses // transitive balances. + if (depositPreauth && reqDepositAuth) + { + // If depositPreauth is enabled, then an account that requires + // authorization has two ways to get an IOU Payment in: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + if (uDstAccountID != account_) + { + if (! view().exists ( + keylet::depositPreauth (uDstAccountID, account_))) + return tecNO_PERMISSION; + } + } + // Copy paths into an editable class. STPathSet spsPaths = ctx_.tx.getFieldPathSet (sfPaths); @@ -450,15 +465,16 @@ Payment::doApply () // source account has authority to deposit to the destination. if (reqDepositAuth) { - // Get the base reserve. - XRPAmount const dstReserve {view().fees().accountReserve (0)}; - - // If the destination's XRP balance is - // 1. below the base reserve and - // 2. the deposit amount is also below the base reserve, + // If depositPreauth is enabled, then an account that requires + // authorization has three ways to get an XRP Payment in: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination, or + // 3. If the destination's XRP balance is + // a. less than or equal to the base reserve and + // b. the deposit amount is less than or equal to the base reserve, // then we allow the deposit. // - // This rule is designed to keep an account from getting wedged + // Rule 3 is designed to keep an account from getting wedged // in an unusable state if it sets the lsfDepositAuth flag and // then consumes all of its XRP. Without the rule if an // account with lsfDepositAuth set spent all of its XRP, it @@ -467,9 +483,19 @@ Payment::doApply () // We choose the base reserve as our bound because it is // a small number that seldom changes but is always sufficient // to get the account un-wedged. - if (saDstAmount > dstReserve || - sleDst->getFieldAmount (sfBalance) > dstReserve) - return tecNO_PERMISSION; + if (uDstAccountID != account_) + { + if (! view().exists ( + keylet::depositPreauth (uDstAccountID, account_))) + { + // Get the base reserve. + XRPAmount const dstReserve {view().fees().accountReserve (0)}; + + if (saDstAmount > dstReserve || + sleDst->getFieldAmount (sfBalance) > dstReserve) + return tecNO_PERMISSION; + } + } } // Do the arithmetic for the transfer and make the ledger change. diff --git a/src/ripple/app/tx/impl/Payment.h b/src/ripple/app/tx/impl/Payment.h index 66a9d688ad1..b4f455fcc60 100644 --- a/src/ripple/app/tx/impl/Payment.h +++ b/src/ripple/app/tx/impl/Payment.h @@ -39,7 +39,7 @@ class Payment static std::size_t const MaxPathLength = 8; public: - Payment (ApplyContext& ctx) + explicit Payment (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/SetAccount.h b/src/ripple/app/tx/impl/SetAccount.h index 83eaacbf285..c67fbb65489 100644 --- a/src/ripple/app/tx/impl/SetAccount.h +++ b/src/ripple/app/tx/impl/SetAccount.h @@ -35,7 +35,7 @@ class SetAccount static std::size_t const DOMAIN_BYTES_MAX = 256; public: - SetAccount (ApplyContext& ctx) + explicit SetAccount (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/SetRegularKey.h b/src/ripple/app/tx/impl/SetRegularKey.h index 668be7488b4..9fd39c9b4a3 100644 --- a/src/ripple/app/tx/impl/SetRegularKey.h +++ b/src/ripple/app/tx/impl/SetRegularKey.h @@ -31,7 +31,7 @@ class SetRegularKey : public Transactor { public: - SetRegularKey (ApplyContext& ctx) + explicit SetRegularKey (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/SetSignerList.h b/src/ripple/app/tx/impl/SetSignerList.h index 4c2d35f2641..f07ed2e6d38 100644 --- a/src/ripple/app/tx/impl/SetSignerList.h +++ b/src/ripple/app/tx/impl/SetSignerList.h @@ -48,7 +48,7 @@ class SetSignerList : public Transactor std::vector signers_; public: - SetSignerList (ApplyContext& ctx) + explicit SetSignerList (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/SetTrust.h b/src/ripple/app/tx/impl/SetTrust.h index c804bfdca8b..5ef0a0a4bc9 100644 --- a/src/ripple/app/tx/impl/SetTrust.h +++ b/src/ripple/app/tx/impl/SetTrust.h @@ -32,7 +32,7 @@ class SetTrust : public Transactor { public: - SetTrust (ApplyContext& ctx) + explicit SetTrust (ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 5a28a3011cd..0f8051ecf9c 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ invoke_preflight (PreflightContext const& ctx) case ttCHECK_CANCEL: return CancelCheck ::preflight(ctx); case ttCHECK_CASH: return CashCheck ::preflight(ctx); case ttCHECK_CREATE: return CreateCheck ::preflight(ctx); + case ttDEPOSIT_PREAUTH: return DepositPreauth ::preflight(ctx); case ttOFFER_CANCEL: return CancelOffer ::preflight(ctx); case ttOFFER_CREATE: return CreateOffer ::preflight(ctx); case ttESCROW_CREATE: return EscrowCreate ::preflight(ctx); @@ -115,6 +117,7 @@ invoke_preclaim (PreclaimContext const& ctx) case ttCHECK_CANCEL: return invoke_preclaim(ctx); case ttCHECK_CASH: return invoke_preclaim(ctx); case ttCHECK_CREATE: return invoke_preclaim(ctx); + case ttDEPOSIT_PREAUTH: return invoke_preclaim(ctx); case ttOFFER_CANCEL: return invoke_preclaim(ctx); case ttOFFER_CREATE: return invoke_preclaim(ctx); case ttESCROW_CREATE: return invoke_preclaim(ctx); @@ -147,6 +150,7 @@ invoke_calculateBaseFee(PreclaimContext const& ctx) case ttCHECK_CANCEL: return CancelCheck::calculateBaseFee(ctx); case ttCHECK_CASH: return CashCheck::calculateBaseFee(ctx); case ttCHECK_CREATE: return CreateCheck::calculateBaseFee(ctx); + case ttDEPOSIT_PREAUTH: return DepositPreauth::calculateBaseFee(ctx); case ttOFFER_CANCEL: return CancelOffer::calculateBaseFee(ctx); case ttOFFER_CREATE: return CreateOffer::calculateBaseFee(ctx); case ttESCROW_CREATE: return EscrowCreate::calculateBaseFee(ctx); @@ -192,6 +196,7 @@ invoke_calculateConsequences(STTx const& tx) case ttCHECK_CANCEL: return invoke_calculateConsequences(tx); case ttCHECK_CASH: return invoke_calculateConsequences(tx); case ttCHECK_CREATE: return invoke_calculateConsequences(tx); + case ttDEPOSIT_PREAUTH: return invoke_calculateConsequences(tx); case ttOFFER_CANCEL: return invoke_calculateConsequences(tx); case ttOFFER_CREATE: return invoke_calculateConsequences(tx); case ttESCROW_CREATE: return invoke_calculateConsequences(tx); @@ -222,26 +227,27 @@ invoke_apply (ApplyContext& ctx) { switch(ctx.tx.getTxnType()) { - case ttACCOUNT_SET: { SetAccount p(ctx); return p(); } - case ttCHECK_CANCEL: { CancelCheck p(ctx); return p(); } - case ttCHECK_CASH: { CashCheck p(ctx); return p(); } - case ttCHECK_CREATE: { CreateCheck p(ctx); return p(); } - case ttOFFER_CANCEL: { CancelOffer p(ctx); return p(); } - case ttOFFER_CREATE: { CreateOffer p(ctx); return p(); } - case ttESCROW_CREATE: { EscrowCreate p(ctx); return p(); } - case ttESCROW_FINISH: { EscrowFinish p(ctx); return p(); } - case ttESCROW_CANCEL: { EscrowCancel p(ctx); return p(); } - case ttPAYCHAN_CLAIM: { PayChanClaim p(ctx); return p(); } - case ttPAYCHAN_CREATE: { PayChanCreate p(ctx); return p(); } - case ttPAYCHAN_FUND: { PayChanFund p(ctx); return p(); } - case ttPAYMENT: { Payment p(ctx); return p(); } - case ttREGULAR_KEY_SET: { SetRegularKey p(ctx); return p(); } - case ttSIGNER_LIST_SET: { SetSignerList p(ctx); return p(); } - case ttTICKET_CANCEL: { CancelTicket p(ctx); return p(); } - case ttTICKET_CREATE: { CreateTicket p(ctx); return p(); } - case ttTRUST_SET: { SetTrust p(ctx); return p(); } + case ttACCOUNT_SET: { SetAccount p(ctx); return p(); } + case ttCHECK_CANCEL: { CancelCheck p(ctx); return p(); } + case ttCHECK_CASH: { CashCheck p(ctx); return p(); } + case ttCHECK_CREATE: { CreateCheck p(ctx); return p(); } + case ttDEPOSIT_PREAUTH: { DepositPreauth p(ctx); return p(); } + case ttOFFER_CANCEL: { CancelOffer p(ctx); return p(); } + case ttOFFER_CREATE: { CreateOffer p(ctx); return p(); } + case ttESCROW_CREATE: { EscrowCreate p(ctx); return p(); } + case ttESCROW_FINISH: { EscrowFinish p(ctx); return p(); } + case ttESCROW_CANCEL: { EscrowCancel p(ctx); return p(); } + case ttPAYCHAN_CLAIM: { PayChanClaim p(ctx); return p(); } + case ttPAYCHAN_CREATE: { PayChanCreate p(ctx); return p(); } + case ttPAYCHAN_FUND: { PayChanFund p(ctx); return p(); } + case ttPAYMENT: { Payment p(ctx); return p(); } + case ttREGULAR_KEY_SET: { SetRegularKey p(ctx); return p(); } + case ttSIGNER_LIST_SET: { SetSignerList p(ctx); return p(); } + case ttTICKET_CANCEL: { CancelTicket p(ctx); return p(); } + case ttTICKET_CREATE: { CreateTicket p(ctx); return p(); } + case ttTRUST_SET: { SetTrust p(ctx); return p(); } case ttAMENDMENT: - case ttFEE: { Change p(ctx); return p(); } + case ttFEE: { Change p(ctx); return p(); } default: assert(false); return { temUNKNOWN, false }; diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index a796abe0778..d12ebad6e01 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -403,6 +403,19 @@ class RPCParser return jvRequest; } + // deposit_authorized [] + Json::Value parseDepositAuthorized (Json::Value const& jvParams) + { + Json::Value jvRequest (Json::objectValue); + jvRequest[jss::source_account] = jvParams[0u].asString (); + jvRequest[jss::destination_account] = jvParams[1u].asString (); + + if (jvParams.size () == 3) + jvParseLedger (jvRequest, jvParams[2u].asString ()); + + return jvRequest; + } + // Return an error for attemping to subscribe/unsubscribe via RPC. Json::Value parseEvented (Json::Value const& jvParams) { @@ -1071,9 +1084,10 @@ class RPCParser { "channel_verify", &RPCParser::parseChannelVerify, 4, 4 }, { "connect", &RPCParser::parseConnect, 1, 2 }, { "consensus_info", &RPCParser::parseAsIs, 0, 0 }, + { "deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3 }, { "feature", &RPCParser::parseFeature, 0, 2 }, { "fetch_info", &RPCParser::parseFetchInfo, 0, 1 }, - { "gateway_balances", &RPCParser::parseGatewayBalances , 1, -1 }, + { "gateway_balances", &RPCParser::parseGatewayBalances, 1, -1 }, { "get_counts", &RPCParser::parseGetCounts, 0, 1 }, { "json", &RPCParser::parseJson, 2, 2 }, { "json2", &RPCParser::parseJson2, 1, 1 }, @@ -1109,12 +1123,12 @@ class RPCParser { "validation_seed", &RPCParser::parseValidationSeed, 0, 1 }, { "version", &RPCParser::parseAsIs, 0, 0 }, { "wallet_propose", &RPCParser::parseWalletPropose, 0, 1 }, - { "internal", &RPCParser::parseInternal, 1, -1 }, + { "internal", &RPCParser::parseInternal, 1, -1 }, // Evented methods - { "path_find", &RPCParser::parseEvented, -1, -1 }, - { "subscribe", &RPCParser::parseEvented, -1, -1 }, - { "unsubscribe", &RPCParser::parseEvented, -1, -1 }, + { "path_find", &RPCParser::parseEvented, -1, -1 }, + { "subscribe", &RPCParser::parseEvented, -1, -1 }, + { "unsubscribe", &RPCParser::parseEvented, -1, -1 }, }; auto const count = jvParams.size (); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index b380f3eaadf..43e3f941fcc 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -78,7 +78,8 @@ class FeatureCollections "Checks", "fix1571", "fix1543", - "fix1623" + "fix1623", + "DepositPreauth" }; std::vector features; @@ -363,6 +364,7 @@ extern uint256 const featureChecks; extern uint256 const fix1571; extern uint256 const fix1543; extern uint256 const fix1623; +extern uint256 const featureDepositPreauth; } // ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 95ced1dc56a..c79db43e0e7 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -93,6 +93,9 @@ getSignerListIndex (AccountID const& account); uint256 getCheckIndex (AccountID const& account, std::uint32_t uSequence); +uint256 +getDepositPreauthIndex (AccountID const& owner, AccountID const& preauthorized); + //------------------------------------------------------------------------------ /* VFALCO TODO @@ -253,6 +256,21 @@ struct check_t }; static check_t const check {}; +/** A DepositPreauth */ +struct depositPreauth_t +{ + explicit depositPreauth_t() = default; + + Keylet operator()(AccountID const& owner, + AccountID const& preauthorized) const; + + Keylet operator()(uint256 const& key) const + { + return { ltDEPOSIT_PREAUTH, key }; + } +}; +static depositPreauth_t const depositPreauth {}; + //------------------------------------------------------------------------------ /** Any ledger entry */ diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h index d535c578cc9..c4bf5e8ace8 100644 --- a/src/ripple/protocol/JsonFields.h +++ b/src/ripple/protocol/JsonFields.h @@ -141,6 +141,8 @@ JSS ( dbKBTotal ); // out: getCounts JSS ( dbKBTransaction ); // out: getCounts JSS ( debug_signing ); // in: TransactionSign JSS ( delivered_amount ); // out: addPaymentDeliveredAmount +JSS ( deposit_authorized ); // out: deposit_authorized +JSS ( deposit_preauth ); // in: AccountObjects, LedgerData JSS ( deprecated ); // out JSS ( descending ); // in: AccountTx* JSS ( destination_account ); // in: PathRequest, RipplePathFind, account_lines diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index b024210b6d6..4e1bab0ef17 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -85,6 +85,8 @@ enum LedgerEntryType ltCHECK = 'C', + ltDEPOSIT_PREAUTH = 'p', + // No longer used or supported. Left here to prevent accidental // reassignment of the ledger type. ltNICKNAME = 'n', @@ -114,6 +116,7 @@ enum LedgerNameSpace spaceSignerList = 'S', spaceXRPUChannel = 'x', spaceCheck = 'C', + spaceDepositPreauth = 'p', // No longer used or supported. Left here to reserve the space and // avoid accidental reuse of the space. diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 1c11f4852b6..85e6e4940c0 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -464,6 +464,8 @@ extern SF_Account const sfAccount; extern SF_Account const sfOwner; extern SF_Account const sfDestination; extern SF_Account const sfIssuer; +extern SF_Account const sfAuthorize; +extern SF_Account const sfUnauthorize; extern SF_Account const sfTarget; extern SF_Account const sfRegularKey; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 5af438c428a..1b4a9c8e23d 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -106,6 +106,8 @@ enum TEMcodes : TERUnderlyingType temBAD_QUORUM, temBAD_WEIGHT, temBAD_TICK_SIZE, + temINVALID_ACCOUNT_ID, + temCANNOT_PREAUTH_SELF, // An intermediate result used internally, should never be returned. temUNCERTAIN, @@ -259,7 +261,8 @@ enum TECcodes : TERUnderlyingType tecOVERSIZE = 145, tecCRYPTOCONDITION_ERROR = 146, tecINVARIANT_FAILED = 147, - tecEXPIRED = 148 + tecEXPIRED = 148, + tecDUPLICATE = 149, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index bef20bb6ca4..73c2721cd19 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -53,7 +53,7 @@ enum TxType ttCHECK_CREATE = 16, ttCHECK_CASH = 17, ttCHECK_CANCEL = 18, - + ttDEPOSIT_PREAUTH = 19, ttTRUST_SET = 20, ttAMENDMENT = 100, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 4479b8098e8..b212ec3c5ce 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -110,7 +110,8 @@ detail::supportedAmendments () { "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks" }, { "7117E2EC2DBF119CA55181D69819F1999ECEE1A0225A7FD2B9ED47940968479C fix1571" }, { "CA7C02118BA27599528543DFE77BA6838D1B0F43B447D4D7F53523CE6A0E9AC2 fix1543" }, - { "58BE9B5968C4DA7C59BA900961828B113E5490699B21877DEF9A31E9D0FE5D5F fix1623" } + { "58BE9B5968C4DA7C59BA900961828B113E5490699B21877DEF9A31E9D0FE5D5F fix1623" }, + { "3CBC5C4E630A1B82380295CDA84B32B49DD066602E74E39B85EF64137FA65194 DepositPreauth"} }; return supported; } @@ -163,5 +164,6 @@ uint256 const featureChecks = *getRegisteredFeature("Checks"); uint256 const fix1571 = *getRegisteredFeature("fix1571"); uint256 const fix1543 = *getRegisteredFeature("fix1543"); uint256 const fix1623 = *getRegisteredFeature("fix1623"); +uint256 const featureDepositPreauth = *getRegisteredFeature("DepositPreauth"); } // ripple diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 7d5f65d5d24..31773d65b03 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -198,6 +198,15 @@ getCheckIndex (AccountID const& account, std::uint32_t uSequence) std::uint32_t(uSequence)); } +uint256 +getDepositPreauthIndex (AccountID const& owner, AccountID const& preauthorized) +{ + return sha512Half( + std::uint16_t(spaceDepositPreauth), + owner, + preauthorized); +} + //------------------------------------------------------------------------------ namespace keylet { @@ -300,6 +309,13 @@ Keylet check_t::operator()(AccountID const& id, getCheckIndex(id, seq) }; } +Keylet depositPreauth_t::operator()(AccountID const& owner, + AccountID const& preauthorized) const +{ + return { ltDEPOSIT_PREAUTH, + getDepositPreauthIndex(owner, preauthorized) }; +} + //------------------------------------------------------------------------------ Keylet unchecked (uint256 const& key) diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index f931b0514ee..e3410c5ddb9 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -168,6 +168,14 @@ LedgerFormats::LedgerFormats () << SOElement (sfPreviousTxnID, SOE_REQUIRED) << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) ; + + add ("DepositPreauth", ltDEPOSIT_PREAUTH) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfAuthorize, SOE_REQUIRED) + << SOElement (sfOwnerNode, SOE_REQUIRED) + << SOElement (sfPreviousTxnID, SOE_REQUIRED) + << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) + ; } void LedgerFormats::addCommonFields (Item& item) diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 6b2b5c07753..5aeaddd28ee 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -220,6 +220,8 @@ SF_Account const sfAccount = make::one(&sfAccount, STI SF_Account const sfOwner = make::one(&sfOwner, STI_ACCOUNT, 2, "Owner"); SF_Account const sfDestination = make::one(&sfDestination, STI_ACCOUNT, 3, "Destination"); SF_Account const sfIssuer = make::one(&sfIssuer, STI_ACCOUNT, 4, "Issuer"); +SF_Account const sfAuthorize = make::one(&sfAuthorize, STI_ACCOUNT, 5, "Authorize"); +SF_Account const sfUnauthorize = make::one(&sfUnauthorize, STI_ACCOUNT, 6, "Unauthorize"); SF_Account const sfTarget = make::one(&sfTarget, STI_ACCOUNT, 7, "Target"); SF_Account const sfRegularKey = make::one(&sfRegularKey, STI_ACCOUNT, 8, "RegularKey"); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 1cc3166c8f4..aea0214e868 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -72,6 +72,7 @@ transResults() { tecCRYPTOCONDITION_ERROR, { "tecCRYPTOCONDITION_ERROR", "Malformed, invalid, or mismatched conditional or fulfillment." } }, { tecINVARIANT_FAILED, { "tecINVARIANT_FAILED", "One or more invariants for the transaction were not satisfied." } }, { tecEXPIRED, { "tecEXPIRED", "Expiration time is passed." } }, + { tecDUPLICATE, { "tecDUPLICATE", "Ledger object already exists." } }, { tefALREADY, { "tefALREADY", "The exact transaction was already in this ledger." } }, { tefBAD_ADD_AUTH, { "tefBAD_ADD_AUTH", "Not authorized to add account." } }, @@ -138,6 +139,8 @@ transResults() { temUNKNOWN, { "temUNKNOWN", "The transaction requires logic that is not implemented yet." } }, { temDISABLED, { "temDISABLED", "The transaction requires logic that is currently disabled." } }, { temBAD_TICK_SIZE, { "temBAD_TICK_SIZE", "Malformed: Tick size out of range." } }, + { temINVALID_ACCOUNT_ID, { "temINVALID_ACCOUNT_ID", "Malformed: A field contains an invalid account ID." } }, + { temCANNOT_PREAUTH_SELF, { "temCANNOT_PREAUTH_SELF", "Malformed: An account may not preauthorize itself." } }, { terRETRY, { "terRETRY", "Retry transaction." } }, { terFUNDS_SPENT, { "terFUNDS_SPENT", "Can't set password, password set funds already spent." } }, diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 8e7fc424825..db7b712e2b2 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -156,6 +156,11 @@ TxFormats::TxFormats () add ("CheckCancel", ttCHECK_CANCEL) << SOElement (sfCheckID, SOE_REQUIRED) ; + + add ("DepositPreauth", ttDEPOSIT_PREAUTH) + << SOElement (sfAuthorize, SOE_OPTIONAL) + << SOElement (sfUnauthorize, SOE_OPTIONAL) + ; } void TxFormats::addCommonFields (Item& item) diff --git a/src/ripple/rpc/handlers/DepositAuthorized.cpp b/src/ripple/rpc/handlers/DepositAuthorized.cpp new file mode 100644 index 00000000000..f0ff731804d --- /dev/null +++ b/src/ripple/rpc/handlers/DepositAuthorized.cpp @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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 +#include +#include +#include +#include + +namespace ripple { + +// { +// source_account : +// destination_account : +// ledger_hash : +// ledger_index : +// } + +Json::Value doDepositAuthorized (RPC::Context& context) +{ + Json::Value const& params = context.params; + + // Validate source_account. + if (! params.isMember (jss::source_account)) + return RPC::missing_field_error (jss::source_account); + if (! params[jss::source_account].isString()) + return RPC::make_error (rpcINVALID_PARAMS, + RPC::expected_field_message (jss::source_account, + "a string")); + + AccountID srcAcct; + { + Json::Value const jvAccepted = RPC::accountFromString ( + srcAcct, params[jss::source_account].asString(), true); + if (jvAccepted) + return jvAccepted; + } + + // Validate destination_account. + if (! params.isMember (jss::destination_account)) + return RPC::missing_field_error (jss::destination_account); + if (! params[jss::destination_account].isString()) + return RPC::make_error (rpcINVALID_PARAMS, + RPC::expected_field_message (jss::destination_account, + "a string")); + + AccountID dstAcct; + { + Json::Value const jvAccepted = RPC::accountFromString ( + dstAcct, params[jss::destination_account].asString(), true); + if (jvAccepted) + return jvAccepted; + } + + // Validate ledger. + std::shared_ptr ledger; + Json::Value result = RPC::lookupLedger (ledger, context); + + if (!ledger) + return result; + + // If destination account is not in the ledger you can't deposit to it, eh? + auto const sleDest = ledger->read (keylet::account(dstAcct)); + if (! sleDest) + { + RPC::inject_error (rpcDST_ACT_MISSING, result); + return result; + } + + // If the two accounts are the same, then the deposit should be fine. + bool depositAuthorized {true}; + if (srcAcct != dstAcct) + { + // Check destination for the DepositAuth flag. If that flag is + // not set then a deposit should be just fine. + if (sleDest->getFlags() & lsfDepositAuth) + { + // See if a preauthorization entry is in the ledger. + auto const sleDepositAuth = + ledger->read(keylet::depositPreauth (dstAcct, srcAcct)); + depositAuthorized = static_cast(sleDepositAuth); + } + } + result[jss::source_account] = params[jss::source_account].asString(); + result[jss::destination_account] = + params[jss::destination_account].asString(); + + result[jss::deposit_authorized] = depositAuthorized; + return result; +} + +} // ripple diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index bdb5bf54014..06e2cbed590 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -40,6 +40,7 @@ Json::Value doChannelAuthorize (RPC::Context&); Json::Value doChannelVerify (RPC::Context&); Json::Value doConnect (RPC::Context&); Json::Value doConsensusInfo (RPC::Context&); +Json::Value doDepositAuthorized (RPC::Context&); Json::Value doFeature (RPC::Context&); Json::Value doFee (RPC::Context&); Json::Value doFetchInfo (RPC::Context&); diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index 11e590f9930..60498e6f93e 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -49,7 +49,7 @@ Json::Value doLedgerEntry (RPC::Context& context) if (context.params.isMember (jss::index)) { - uNodeIndex.SetHex (context.params[jss::index].asString ()); + uNodeIndex.SetHex (context.params[jss::index].asString()); bNodeBinary = true; } else if (context.params.isMember (jss::account_root)) @@ -65,7 +65,44 @@ Json::Value doLedgerEntry (RPC::Context& context) else if (context.params.isMember (jss::check)) { expectedType = ltCHECK; - uNodeIndex.SetHex (context.params[jss::check].asString ()); + uNodeIndex.SetHex (context.params[jss::check].asString()); + } + else if (context.params.isMember (jss::deposit_preauth)) + { + expectedType = ltDEPOSIT_PREAUTH; + + if (!context.params[jss::deposit_preauth].isObject()) + { + if (! context.params[jss::deposit_preauth].isString() || + ! uNodeIndex.SetHex ( + context.params[jss::deposit_preauth].asString())) + { + uNodeIndex = zero; + jvResult[jss::error] = "malformedRequest"; + } + } + else if (!context.params[jss::deposit_preauth].isMember (jss::owner) + || !context.params[jss::deposit_preauth][jss::owner].isString() + || !context.params[jss::deposit_preauth].isMember (jss::authorized) + || !context.params[jss::deposit_preauth][jss::authorized].isString()) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + auto const owner = parseBase58(context.params[ + jss::deposit_preauth][jss::owner].asString()); + + auto const authorized = parseBase58(context.params[ + jss::deposit_preauth][jss::authorized].asString()); + + if (! owner) + jvResult[jss::error] = "malformedOwner"; + else if (! authorized) + jvResult[jss::error] = "malformedAuthorized"; + else + uNodeIndex = keylet::depositPreauth (*owner, *authorized).key; + } } else if (context.params.isMember (jss::directory)) { diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index 71d73a737f7..1727359e7f6 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -72,6 +72,7 @@ Handler const handlerArray[] { { "channel_verify", byRef (&doChannelVerify), Role::USER, NO_CONDITION }, { "connect", byRef (&doConnect), Role::ADMIN, NO_CONDITION }, { "consensus_info", byRef (&doConsensusInfo), Role::ADMIN, NO_CONDITION }, + { "deposit_authorized", byRef (&doDepositAuthorized), Role::USER, NO_CONDITION }, { "gateway_balances", byRef (&doGatewayBalances), Role::USER, NO_CONDITION }, { "get_counts", byRef (&doGetCounts), Role::ADMIN, NO_CONDITION }, { "feature", byRef (&doFeature), Role::ADMIN, NO_CONDITION }, diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 43eecab6597..2e33457a4a9 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -688,11 +688,12 @@ chooseLedgerEntryType(Json::Value const& params) if (params.isMember(jss::type)) { static - std::array, 12> const types + std::array, 13> const types { { { jss::account, ltACCOUNT_ROOT }, { jss::amendments, ltAMENDMENTS }, { jss::check, ltCHECK }, + { jss::deposit_preauth, ltDEPOSIT_PREAUTH }, { jss::directory, ltDIR_NODE }, { jss::escrow, ltESCROW }, { jss::fee, ltFEE_SETTINGS }, diff --git a/src/ripple/unity/app_tx.cpp b/src/ripple/unity/app_tx.cpp index 01521e7ab73..74cbd4ed4c5 100644 --- a/src/ripple/unity/app_tx.cpp +++ b/src/ripple/unity/app_tx.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/unity/rpcx1.cpp b/src/ripple/unity/rpcx1.cpp index d3e062f60f1..4fb80d0afdd 100644 --- a/src/ripple/unity/rpcx1.cpp +++ b/src/ripple/unity/rpcx1.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 68336a69d9f..49af0dbb939 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -17,37 +17,28 @@ */ //============================================================================== -#include -#include -#include -#include -#include -#include -#include #include -#include #include -#include namespace ripple { namespace test { -struct DepositAuth_test : public beast::unit_test::suite +// Helper function that returns the reserve on an account based on +// the passed in number of owners. +static XRPAmount reserve (jtx::Env& env, std::uint32_t count) { - // Helper function that returns the reserve on an account based on - // the passed in number of owners. - static XRPAmount reserve(jtx::Env& env, std::uint32_t count) - { - return env.current()->fees().accountReserve (count); - } + return env.current()->fees().accountReserve (count); +} - // Helper function that returns true if acct has the lsfDepostAuth flag set. - static bool hasDepositAuth (jtx::Env const& env, jtx::Account const& acct) - { - return ((*env.le(acct))[sfFlags] & lsfDepositAuth) == lsfDepositAuth; - } +// Helper function that returns true if acct has the lsfDepostAuth flag set. +static bool hasDepositAuth (jtx::Env const& env, jtx::Account const& acct) +{ + return ((*env.le(acct))[sfFlags] & lsfDepositAuth) == lsfDepositAuth; +} +struct DepositAuth_test : public beast::unit_test::suite +{ void testEnable() { testcase ("Enable"); @@ -389,7 +380,325 @@ struct DepositAuth_test : public beast::unit_test::suite } }; +struct DepositPreauth_test : public beast::unit_test::suite +{ + void testEnable() + { + testcase ("Enable"); + + using namespace jtx; + Account const alice {"alice"}; + Account const becky {"becky"}; + { + // featureDepositPreauth is disabled. + Env env (*this, supported_amendments() - featureDepositPreauth); + env.fund (XRP (10000), alice, becky); + env.close(); + + // Should not be able to add a DepositPreauth to alice. + env (deposit::auth (alice, becky), ter (temDISABLED)); + env.close(); + env.require (owners (alice, 0)); + env.require (owners (becky, 0)); + + // Should not be able to remove a DepositPreauth from alice. + env (deposit::unauth (alice, becky), ter (temDISABLED)); + env.close(); + env.require (owners (alice, 0)); + env.require (owners (becky, 0)); + } + { + // featureDepositPreauth is enabled. The valid case is really + // simple: + // o We should be able to add and remove an entry, and + // o That entry should cost one reserve. + // o The reserve should be returned when the entry is removed. + Env env (*this); + env.fund (XRP (10000), alice, becky); + env.close(); + + // Add a DepositPreauth to alice. + env (deposit::auth (alice, becky)); + env.close(); + env.require (owners (alice, 1)); + env.require (owners (becky, 0)); + + // Remove a DepositPreauth from alice. + env (deposit::unauth (alice, becky)); + env.close(); + env.require (owners (alice, 0)); + env.require (owners (becky, 0)); + } + } + + void testInvalid() + { + testcase ("Invalid"); + + using namespace jtx; + Account const alice {"alice"}; + Account const becky {"becky"}; + Account const carol {"carol"}; + + Env env (*this); + + // Tell env about alice, becky and carol since they are not yet funded. + env.memoize (alice); + env.memoize (becky); + env.memoize (carol); + + // Add DepositPreauth to an unfunded account. + env (deposit::auth (alice, becky), seq (1), ter (terNO_ACCOUNT)); + + env.fund (XRP (10000), alice, becky); + env.close(); + + // Bad fee. + env (deposit::auth (alice, becky), fee (drops(-10)), ter (temBAD_FEE)); + env.close(); + + // Bad flags. + env (deposit::auth (alice, becky), + txflags (tfSell), ter (temINVALID_FLAG)); + env.close(); + + { + // Neither auth not unauth. + Json::Value tx {deposit::auth (alice, becky)}; + tx.removeMember (sfAuthorize.jsonName); + env (tx, ter (temMALFORMED)); + env.close(); + } + { + // Both auth and unauth. + Json::Value tx {deposit::auth (alice, becky)}; + tx[sfUnauthorize.jsonName] = becky.human(); + env (tx, ter (temMALFORMED)); + env.close(); + } + { + // Alice authorizes a zero account. + Json::Value tx {deposit::auth (alice, becky)}; + tx[sfAuthorize.jsonName] = to_string (xrpAccount()); + env (tx, ter (temINVALID_ACCOUNT_ID)); + env.close(); + } + + // alice authorizes herself. + env (deposit::auth (alice, alice), ter (temCANNOT_PREAUTH_SELF)); + env.close(); + + // alice authorizes an unfunded account. + env (deposit::auth (alice, carol), ter (tecNO_TARGET)); + env.close(); + + // alice successfully authorizes becky. + env.require (owners (alice, 0)); + env.require (owners (becky, 0)); + env (deposit::auth (alice, becky)); + env.close(); + env.require (owners (alice, 1)); + env.require (owners (becky, 0)); + + // alice attempts to create a duplicate authorization. + env (deposit::auth (alice, becky), ter (tecDUPLICATE)); + env.close(); + env.require (owners (alice, 1)); + env.require (owners (becky, 0)); + + // carol attempts to preauthorize but doesn't have enough reserve. + env.fund (drops (249'999'999), carol); + env.close(); + + env (deposit::auth (carol, becky), ter (tecINSUFFICIENT_RESERVE)); + env.close(); + env.require (owners (carol, 0)); + env.require (owners (becky, 0)); + + // carol gets enough XRP to (barely) meet the reserve. + env (pay (alice, carol, drops (11))); + env.close(); + env (deposit::auth (carol, becky)); + env.close(); + env.require (owners (carol, 1)); + env.require (owners (becky, 0)); + + // But carol can't meet the reserve for another preauthorization. + env (deposit::auth (carol, alice), ter (tecINSUFFICIENT_RESERVE)); + env.close(); + env.require (owners (carol, 1)); + env.require (owners (becky, 0)); + env.require (owners (alice, 1)); + + // alice attempts to remove an authorization she doesn't have. + env (deposit::unauth (alice, carol), ter (tecNO_ENTRY)); + env.close(); + env.require (owners (alice, 1)); + env.require (owners (becky, 0)); + + // alice successfully removes her authorization of becky. + env (deposit::unauth (alice, becky)); + env.close(); + env.require (owners (alice, 0)); + env.require (owners (becky, 0)); + + // alice removes becky again and gets an error. + env (deposit::unauth (alice, becky), ter (tecNO_ENTRY)); + env.close(); + env.require (owners (alice, 0)); + env.require (owners (becky, 0)); + } + + void testPayment (FeatureBitset features) + { + testcase ("Payment"); + + using namespace jtx; + Account const alice {"alice"}; + Account const becky {"becky"}; + Account const gw {"gw"}; + IOU const USD (gw["USD"]); + + bool const supportsPreauth = {features[featureDepositPreauth]}; + + { + // The initial implementation of DepositAuth had a bug where an + // account with the DepositAuth flag set could not make a payment + // to itself. That bug was fixed in the DepositPreauth amendment. + Env env (*this, features); + env.fund (XRP (5000), alice, becky, gw); + env.close(); + + env.trust (USD (1000), alice); + env.trust (USD (1000), becky); + env.close(); + + env (pay (gw, alice, USD (500))); + env.close(); + + env (offer (alice, XRP (100), USD (100), tfPassive), + require (offers (alice, 1))); + env.close(); + + // becky pays herself USD (10) by consuming part of alice's offer. + // Make sure the payment works if PaymentAuth is not involved. + env (pay (becky, becky, USD (10)), + path (~USD), sendmax (XRP (10))); + env.close(); + + // becky decides to require authorization for deposits. + env(fset (becky, asfDepositAuth)); + env.close(); + + // becky pays herself again. Whether it succeeds depends on + // whether featureDepositPreauth is enabled. + TER const expect { + supportsPreauth ? TER {tesSUCCESS} : TER {tecNO_PERMISSION}}; + + env (pay (becky, becky, USD (10)), + path (~USD), sendmax (XRP (10)), ter (expect)); + env.close(); + } + + if (supportsPreauth) + { + // Make sure DepositPreauthorization works for payments. + + Account const carol {"carol"}; + + Env env (*this, features); + env.fund (XRP (5000), alice, becky, carol, gw); + env.close(); + + env.trust (USD (1000), alice); + env.trust (USD (1000), becky); + env.trust (USD (1000), carol); + env.close(); + + env (pay (gw, alice, USD (1000))); + env.close(); + + // Make XRP and IOU payments from alice to becky. Should be fine. + env (pay (alice, becky, XRP (100))); + env (pay (alice, becky, USD (100))); + env.close(); + + // becky decides to require authorization for deposits. + env(fset (becky, asfDepositAuth)); + env.close(); + + // alice can no longer pay becky. + env (pay (alice, becky, XRP (100)), ter (tecNO_PERMISSION)); + env (pay (alice, becky, USD (100)), ter (tecNO_PERMISSION)); + env.close(); + + // becky preauthorizes carol for deposit, which doesn't provide + // authorization for alice. + env (deposit::auth (becky, carol)); + env.close(); + + // alice still can't pay becky. + env (pay (alice, becky, XRP (100)), ter (tecNO_PERMISSION)); + env (pay (alice, becky, USD (100)), ter (tecNO_PERMISSION)); + env.close(); + + // becky preauthorizes alice for deposit. + env (deposit::auth (becky, alice)); + env.close(); + + // alice can now pay becky. + env (pay (alice, becky, XRP (100))); + env (pay (alice, becky, USD (100))); + env.close(); + + // alice decides to require authorization for deposits. + env(fset (alice, asfDepositAuth)); + env.close(); + + // Even though alice is authorized to pay becky, becky is not + // authorized to pay alice. + env (pay (becky, alice, XRP (100)), ter (tecNO_PERMISSION)); + env (pay (becky, alice, USD (100)), ter (tecNO_PERMISSION)); + env.close(); + + // becky unauthorizes carol. Should have no impact on alice. + env (deposit::unauth (becky, carol)); + env.close(); + + env (pay (alice, becky, XRP (100))); + env (pay (alice, becky, USD (100))); + env.close(); + + // becky unauthorizes alice. alice now can't pay becky. + env (deposit::unauth (becky, alice)); + env.close(); + + env (pay (alice, becky, XRP (100)), ter (tecNO_PERMISSION)); + env (pay (alice, becky, USD (100)), ter (tecNO_PERMISSION)); + env.close(); + + // becky decides to remove authorization for deposits. Now + // alice can pay becky again. + env(fclear (becky, asfDepositAuth)); + env.close(); + + env (pay (alice, becky, XRP (100))); + env (pay (alice, becky, USD (100))); + env.close(); + } + } + + void run() override + { + testEnable(); + testInvalid(); + testPayment (jtx::supported_amendments() - featureDepositPreauth); + testPayment (jtx::supported_amendments()); + } +}; + BEAST_DEFINE_TESTSUITE(DepositAuth,app,ripple); +BEAST_DEFINE_TESTSUITE(DepositPreauth,app,ripple); } // test } // ripple diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 9b973188d90..1836ad84db4 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -90,7 +90,7 @@ struct Escrow_test : public beast::unit_test::suite void operator()(jtx::Env&, jtx::JTx& jt) const { - jt.jv["FinishAfter"] = value_.time_since_epoch().count(); + jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); } }; @@ -110,7 +110,7 @@ struct Escrow_test : public beast::unit_test::suite void operator()(jtx::Env&, jtx::JTx& jt) const { - jt.jv["CancelAfter"] = value_.time_since_epoch().count(); + jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); } }; @@ -135,7 +135,7 @@ struct Escrow_test : public beast::unit_test::suite void operator()(jtx::Env&, jtx::JTx& jt) const { - jt.jv["Condition"] = value_; + jt.jv[sfCondition.jsonName] = value_; } }; @@ -160,7 +160,7 @@ struct Escrow_test : public beast::unit_test::suite void operator()(jtx::Env&, jtx::JTx& jt) const { - jt.jv["Fulfillment"] = value_; + jt.jv[sfFulfillment.jsonName] = value_; } }; @@ -188,8 +188,8 @@ struct Escrow_test : public beast::unit_test::suite jv[jss::TransactionType] = "EscrowFinish"; jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); - jv["Owner"] = from.human(); - jv["OfferSequence"] = seq; + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; return jv; } @@ -202,8 +202,8 @@ struct Escrow_test : public beast::unit_test::suite jv[jss::TransactionType] = "EscrowCancel"; jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); - jv["Owner"] = from.human(); - jv["OfferSequence"] = seq; + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; return jv; } @@ -587,15 +587,15 @@ struct Escrow_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono; - { // Unconditional - + { + // Unconditional Env env(*this); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); env(escrow("alice", "alice", XRP(1000)), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); - // Not enough time has elapsed for a finish and cancelling isn't + // Not enough time has elapsed for a finish and canceling isn't // possible. env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); @@ -609,37 +609,37 @@ struct Escrow_test : public beast::unit_test::suite env.require(balance("alice", XRP(5000) - drops(10))); } { - // Unconditionally pay from alice to bob. jack (neither source nor + // Unconditionally pay from Alice to Bob. Zelda (neither source nor // destination) signs all cancels and finishes. This shows that - // Escrow will make a payment to bob with no intervention from bob. + // Escrow will make a payment to Bob with no intervention from Bob. Env env(*this); - env.fund(XRP(5000), "alice", "bob", "jack"); + env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); - // Not enough time has elapsed for a finish and cancelling isn't + // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("jack", "alice", seq)); + env(finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - drops(10))); env.require(balance("bob", XRP(6000))); - env.require(balance("jack", XRP(5000) - drops(40))); + env.require(balance("zelda", XRP(5000) - drops(40))); } { - // bob sets PaymentAuth so only bob can finish the escrow. + // Bob sets DepositAuth so only Bob can finish the escrow. Env env(*this); - env.fund(XRP(5000), "alice", "bob", "jack"); + env.fund(XRP(5000), "alice", "bob", "zelda"); env(fset ("bob", asfDepositAuth)); env.close(); @@ -647,22 +647,22 @@ struct Escrow_test : public beast::unit_test::suite env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); - // Not enough time has elapsed for a finish and cancelling isn't + // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible. Finish will only succeed for - // Bob, because of PaymentAuth. - env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); + // Bob, because of DepositAuth. + env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); env(finish("bob", "alice", seq)); env.close(); @@ -670,7 +670,35 @@ struct Escrow_test : public beast::unit_test::suite auto const baseFee = env.current()->fees().base; env.require(balance("alice", XRP(4000) - (baseFee * 5))); env.require(balance("bob", XRP(6000) - (baseFee * 5))); - env.require(balance("jack", XRP(5000) - (baseFee * 4))); + env.require(balance("zelda", XRP(5000) - (baseFee * 4))); + } + { + // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can + // finish the escrow. + Env env(*this); + + env.fund(XRP(5000), "alice", "bob", "zelda"); + env(fset ("bob", asfDepositAuth)); + env.close(); + env(deposit::auth ("bob", "zelda")); + env.close(); + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env.require(balance("alice", XRP(4000) - drops(10))); + env.close(); + + // DepositPreauth allows Finish to succeed for either Zelda or + // Bob. But Finish won't succeed for Alice since she is not + // preauthorized. + env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("zelda", "alice", seq)); + env.close(); + + auto const baseFee = env.current()->fees().base; + env.require(balance("alice", XRP(4000) - (baseFee * 2))); + env.require(balance("bob", XRP(6000) - (baseFee * 2))); + env.require(balance("zelda", XRP(5000) - (baseFee * 1))); } { // Conditional @@ -680,7 +708,7 @@ struct Escrow_test : public beast::unit_test::suite env(escrow("alice", "alice", XRP(1000)), condition(cb2), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); - // Not enough time has elapsed for a finish and cancelling isn't + // Not enough time has elapsed for a finish and canceling isn't // possible. env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); @@ -704,45 +732,63 @@ struct Escrow_test : public beast::unit_test::suite condition(cb2), fulfillment(fb2), fee(1500)); } { - // Self-escrowed conditional with PaymentAuth + // Self-escrowed conditional with DepositAuth. Env env(*this); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); env(escrow("alice", "alice", XRP(1000)), condition(cb3), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); - - // Not enough time has elapsed for a finish and cancelling isn't - // possible. - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), - condition(cb3), fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), - condition(cb3), fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); env.close(); - // Cancel continues to not be possible. Finish is now possible but - // requires the associated cryptocondition. - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + // Finish is now possible but requires the cryptocondition. env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - // Enable deposit authorization. After this, only Alice can finish + // Enable deposit authorization. After this only Alice can finish // the escrow. env(fset ("alice", asfDepositAuth)); env.close(); - env(finish("bob", "alice", seq), condition(cb2), - fulfillment(fb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("alice", "alice", seq), condition(cb2), + env(finish("alice", "alice", seq), condition(cb2), fulfillment(fb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb3), + fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); env(finish("alice", "alice", seq), condition(cb3), fulfillment(fb3), fee(1500)); } + { + // Self-escrowed conditional with DepositAuth and DepositPreauth. + Env env(*this); + + env.fund(XRP(5000), "alice", "bob", "zelda"); + auto const seq = env.seq("alice"); + env(escrow("alice", "alice", XRP(1000)), condition(cb3), finish_time(env.now() + 5s)); + env.require(balance("alice", XRP(4000) - drops(10))); + env.close(); + + // Alice preauthorizes Zelda for deposit, even though Alice has not + // set the lsfDepositAuth flag (yet). + env(deposit::auth("alice", "zelda")); + env.close(); + + // Finish is now possible but requires the cryptocondition. + env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("zelda", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + + // Alice enables deposit authorization. After this only Alice or + // Zelda (because Zelda is preauthorized) can finish the escrow. + env(fset ("alice", asfDepositAuth)); + env.close(); + + env(finish("alice", "alice", seq), condition(cb2), + fulfillment(fb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb3), + fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); + env(finish("zelda", "alice", seq), condition(cb3), + fulfillment(fb3), fee(1500)); + } } void diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 0de95f29f27..44f38d94f20 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -233,13 +233,13 @@ class Offer_test : public beast::unit_test::suite // an offer. Show that the attempt to remove the offer fails. env.require (offers (alice, 2)); - // featureChecks changes the return code on an expired Offer. Adapt - // to that. - bool const featChecks {features[featureChecks]}; + // featureDepositPreauths changes the return code on an expired Offer. + // Adapt to that. + bool const featPreauth {features[featureDepositPreauth]}; env (offer (alice, XRP (5), USD (2)), json (sfExpiration.fieldName, lastClose(env)), json (jss::OfferSequence, offer2Seq), - ter (featChecks ? TER {tecEXPIRED} : TER {tesSUCCESS})); + ter (featPreauth ? TER {tecEXPIRED} : TER {tesSUCCESS})); env.close(); env.require (offers (alice, 2)); @@ -954,12 +954,12 @@ class Offer_test : public beast::unit_test::suite owners (alice, 1)); // Place an offer that should have already expired. - // The Checks amendment changes the return code; adapt to that. - bool const featChecks {features[featureChecks]}; + // The DepositPreauth amendment changes the return code; adapt to that. + bool const featPreauth {features[featureDepositPreauth]}; env (offer (alice, xrpOffer, usdOffer), json (sfExpiration.fieldName, lastClose(env)), - ter (featChecks ? TER {tecEXPIRED} : TER {tesSUCCESS})); + ter (featPreauth ? TER {tecEXPIRED} : TER {tesSUCCESS})); env.require ( balance (alice, startBalance - f - f), @@ -4390,20 +4390,20 @@ class Offer_test : public beast::unit_test::suite env(fset (gw, asfRequireAuth)); env.close(); - // The test behaves differently with or without FlowCross. - bool const flowCross = features[featureFlowCross]; + // The test behaves differently with or without DepositPreauth. + bool const preauth = features[featureDepositPreauth]; - // Before FlowCross an account with lsfRequireAuth set could not - // create an offer to buy their own currency. After FlowCross + // Before DepositPreauth an account with lsfRequireAuth set could not + // create an offer to buy their own currency. After DepositPreauth // they can. env (offer (gw, gwUSD(40), XRP(4000)), - ter (flowCross ? TER {tesSUCCESS} : TER {tecNO_LINE})); + ter (preauth ? TER {tesSUCCESS} : TER {tecNO_LINE})); env.close(); - env.require (offers (gw, flowCross ? 1 : 0)); + env.require (offers (gw, preauth ? 1 : 0)); - if (!flowCross) - // The rest of the test verifies FlowCross behavior. + if (!preauth) + // The rest of the test verifies DepositPreauth behavior. return; // Set up an authorized trust line and pay alice gwUSD 50. diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 855ec24d666..db08d0465e1 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -644,7 +644,7 @@ struct PayChan_test : public beast::unit_test::suite { // Create a channel where dst disallows XRP. Ignore that flag, // since it's just advisory. - Env env (*this); + Env env (*this, supported_amendments()); env.fund (XRP (10000), alice, bob); env (fset (bob, asfDisallowXRP)); env (create (alice, bob, XRP (1000), 3600s, alice.pk())); @@ -669,7 +669,7 @@ struct PayChan_test : public beast::unit_test::suite // Claim to a channel where dst disallows XRP (channel is // created before disallow xrp is set). Ignore that flag // since it is just advisory. - Env env (*this); + Env env (*this, supported_amendments()); env.fund (XRP (10000), alice, bob); env (create (alice, bob, XRP (1000), 3600s, alice.pk())); auto const chan = channel (*env.current (), alice, bob); @@ -716,10 +716,11 @@ struct PayChan_test : public beast::unit_test::suite auto const alice = Account ("alice"); auto const bob = Account ("bob"); + auto const carol = Account ("carol"); auto USDA = alice["USD"]; { Env env (*this); - env.fund (XRP (10000), alice, bob); + env.fund (XRP (10000), alice, bob, carol); env (fset (bob, asfDepositAuth)); env.close(); @@ -757,22 +758,76 @@ struct PayChan_test : public beast::unit_test::suite env.close(); BEAST_EXPECT (env.balance (bob) == preBob); + // bob claims but omits the signature. Fails because only + // alice can claim without a signature. + env (claim (bob, chan, delta, delta), ter (temBAD_SIGNATURE)); + env.close(); + // bob claims with signature. Succeeds even though bob's - // lsfDepositAuth flag is set since bob signed the transaction. + // lsfDepositAuth flag is set since bob submitted the + // transaction. env (claim (bob, chan, delta, delta, Slice (sig), pk)); env.close(); BEAST_EXPECT (env.balance (bob) == preBob + delta - baseFee); } + { + // Explore the limits of deposit preauthorization. + auto const delta = XRP (600).value(); + auto const sig = signClaimAuth (pk, alice.sk (), chan, delta); - // bob clears lsfDepositAuth. Now alice can use an unsigned claim. - env (fclear (bob, asfDepositAuth)); - env.close(); + // carol claims and fails. Only channel participants (bob or + // alice) may claim. + env (claim (carol, chan, + delta, delta, Slice (sig), pk), ter (tecNO_PERMISSION)); + env.close(); - // alice claims successfully. - env (claim (alice, chan, XRP (800).value(), XRP (800).value())); - env.close(); - BEAST_EXPECT ( - env.balance (bob) == preBob + XRP (800) - (2 * baseFee)); + // bob preauthorizes carol for deposit. But after that carol + // still can't claim since only channel participants may claim. + env(deposit::auth (bob, carol)); + env.close(); + + env (claim (carol, chan, + delta, delta, Slice (sig), pk), ter (tecNO_PERMISSION)); + + // Since alice is not preauthorized she also may not claim + // for bob. + env (claim (alice, chan, delta, delta, + Slice (sig), pk), ter (tecNO_PERMISSION)); + env.close(); + + // However if bob preauthorizes alice for deposit then she can + // successfully submit a claim. + env(deposit::auth (bob, alice)); + env.close(); + + env (claim (alice, chan, delta, delta, Slice (sig), pk)); + env.close(); + + BEAST_EXPECT ( + env.balance (bob) == preBob + delta - (3 * baseFee)); + } + { + // bob removes preauthorization of alice. Once again she + // cannot submit a claim. + auto const delta = XRP (800).value(); + + env(deposit::unauth (bob, alice)); + env.close(); + + // alice claims and fails since she is no longer preauthorized. + env (claim (alice, chan, delta, delta), ter (tecNO_PERMISSION)); + env.close(); + + // bob clears lsfDepositAuth. Now alice can claim. + env (fclear (bob, asfDepositAuth)); + env.close(); + + // alice claims successfully. + env (claim (alice, chan, delta, delta)); + env.close(); + BEAST_EXPECT ( + env.balance (bob) == preBob + XRP (800) - (5 * baseFee)); + } } } diff --git a/src/test/jtx.h b/src/test/jtx.h index 3a2b72c7f26..58d3a0d140a 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h new file mode 100644 index 00000000000..0e99762c207 --- /dev/null +++ b/src/test/jtx/deposit.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_DEPOSIT_H_INCLUDED +#define RIPPLE_TEST_JTX_DEPOSIT_H_INCLUDED + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Deposit preauthorize operations */ +namespace deposit { + +/** Preauthorize for deposit. Invoke as deposit::auth. */ +Json::Value +auth (Account const& account, Account const& auth); + +/** Remove preauthorization for deposit. Invoke as deposit::unauth. */ +Json::Value +unauth (Account const& account, Account const& unauth); + +} // deposit + +} // jtx + +} // test +} // ripple + +#endif diff --git a/src/test/jtx/impl/deposit.cpp b/src/test/jtx/impl/deposit.cpp new file mode 100644 index 00000000000..f9b00289b2f --- /dev/null +++ b/src/test/jtx/impl/deposit.cpp @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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 { +namespace test { +namespace jtx { + +namespace deposit { + +// Add DepositPreauth. +Json::Value +auth (jtx::Account const& account, jtx::Account const& auth) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfAuthorize.jsonName] = auth.human(); + jv[sfTransactionType.jsonName] = "DepositPreauth"; + return jv; +} + +// Remove DepositPreauth. +Json::Value +unauth (jtx::Account const& account, jtx::Account const& unauth) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfUnauthorize.jsonName] = unauth.human(); + jv[sfTransactionType.jsonName] = "DepositPreauth"; + return jv; +} + +} // deposit + +} // jtx +} // test +} // ripple diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 7f75e89cafc..807677ccb0b 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -355,6 +355,7 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::account), 0)); BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::amendments), 0)); BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::check), 0)); + BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::deposit_preauth), 0)); BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::directory), 0)); BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::escrow), 0)); BEAST_EXPECT (acct_objs_is_size (acct_objs (gw, jss::fee), 0)); @@ -400,6 +401,18 @@ class AccountObjects_test : public beast::unit_test::suite BEAST_EXPECT (check[sfDestination.jsonName] == alice.human()); BEAST_EXPECT (check[sfSendMax.jsonName][jss::value].asUInt() == 10); } + // gw preauthorizes payments from alice. + env (deposit::auth (gw, alice)); + env.close(); + { + // Find the preauthorization. + Json::Value const resp = acct_objs (gw, jss::deposit_preauth); + BEAST_EXPECT (acct_objs_is_size (resp, 1)); + + auto const& preauth = resp[jss::result][jss::account_objects][0u]; + BEAST_EXPECT (preauth[sfAccount.jsonName] == gw.human()); + BEAST_EXPECT (preauth[sfAuthorize.jsonName] == alice.human()); + } { // gw creates an escrow that we can look for in the ledger. Json::Value jvEscrow; @@ -485,7 +498,7 @@ class AccountObjects_test : public beast::unit_test::suite auto const& ticket = resp[jss::result][jss::account_objects][0u]; BEAST_EXPECT (ticket[sfAccount.jsonName] == gw.human()); BEAST_EXPECT (ticket[sfLedgerEntryType.jsonName] == "Ticket"); - BEAST_EXPECT (ticket[sfSequence.jsonName].asUInt() == 8); + BEAST_EXPECT (ticket[sfSequence.jsonName].asUInt() == 9); } // Run up the number of directory entries so gw has two // directory nodes. diff --git a/src/test/rpc/DepositAuthorized_test.cpp b/src/test/rpc/DepositAuthorized_test.cpp new file mode 100644 index 00000000000..9117d94c608 --- /dev/null +++ b/src/test/rpc/DepositAuthorized_test.cpp @@ -0,0 +1,230 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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 // jss:: definitions +#include + +namespace ripple { +namespace test { + +class DepositAuthorized_test : public beast::unit_test::suite +{ +public: + // Helper function that builds arguments for a deposit_authorized command. + static Json::Value depositAuthArgs ( + jtx::Account const& source, + jtx::Account const& dest, std::string const& ledger = "") + { + Json::Value args {Json::objectValue}; + args[jss::source_account] = source.human(); + args[jss::destination_account] = dest.human(); + if (! ledger.empty()) + args[jss::ledger_index] = ledger; + return args; + } + + // Helper function that verifies a deposit_authorized request was + // successful and returned the expected value. + void validateDepositAuthResult (Json::Value const& result, bool authorized) + { + Json::Value const& results {result[jss::result]}; + BEAST_EXPECT (results[jss::deposit_authorized] == authorized); + BEAST_EXPECT (results[jss::status] == jss::success); + } + + // Test a variety of non-malformed cases. + void testValid() + { + using namespace jtx; + Account const alice {"alice"}; + Account const becky {"becky"}; + Account const carol {"carol"}; + + Env env(*this); + env.fund(XRP(1000), alice, becky, carol); + env.close(); + + // becky is authorized to deposit to herself. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs ( + becky, becky, "validated").toStyledString()), true); + + // alice should currently be authorized to deposit to becky. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs ( + alice, becky, "validated").toStyledString()), true); + + // becky sets the DepositAuth flag in the current ledger. + env (fset (becky, asfDepositAuth)); + + // alice is no longer authorized to deposit to becky in current ledger. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs (alice, becky).toStyledString()), false); + env.close(); + + // becky is still authorized to deposit to herself. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs ( + becky, becky, "validated").toStyledString()), true); + + // It's not a reciprocal arrangement. becky can deposit to alice. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs ( + becky, alice, "current").toStyledString()), true); + + // becky creates a deposit authorization for alice. + env (deposit::auth (becky, alice)); + env.close(); + + // alice is now authorized to deposit to becky. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs (alice, becky, "closed").toStyledString()), true); + + // carol is still not authorized to deposit to becky. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs (carol, becky).toStyledString()), false); + + // becky clears the the DepositAuth flag so carol becomes authorized. + env (fclear (becky, asfDepositAuth)); + env.close(); + + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs (carol, becky).toStyledString()), true); + + // alice is still authorized to deposit to becky. + validateDepositAuthResult (env.rpc ("json", "deposit_authorized", + depositAuthArgs (alice, becky).toStyledString()), true); + } + + // Test malformed cases. + void testErrors() + { + using namespace jtx; + Account const alice {"alice"}; + Account const becky {"becky"}; + + // Lambda that checks the (error) result of deposit_authorized. + auto verifyErr = [this] ( + Json::Value const& result, char const* error, char const* errorMsg) + { + BEAST_EXPECT (result[jss::result][jss::status] == jss::error); + BEAST_EXPECT (result[jss::result][jss::error] == error); + BEAST_EXPECT (result[jss::result][jss::error_message] == errorMsg); + }; + + Env env(*this); + { + // Missing source_account field. + Json::Value args {depositAuthArgs (alice, becky)}; + args.removeMember (jss::source_account); + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "invalidParams", + "Missing field 'source_account'."); + } + { + // Non-string source_account field. + Json::Value args {depositAuthArgs (alice, becky)}; + args[jss::source_account] = 7.3; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "invalidParams", + "Invalid field 'source_account', not a string."); + } + { + // Corrupt source_account field. + Json::Value args {depositAuthArgs (alice, becky)}; + args[jss::source_account] = "rG1QQv2nh2gr7RCZ!P8YYcBUKCCN633jCn"; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "actMalformed", "Account malformed."); + } + { + // Missing destination_account field. + Json::Value args {depositAuthArgs (alice, becky)}; + args.removeMember (jss::destination_account); + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "invalidParams", + "Missing field 'destination_account'."); + } + { + // Non-string destination_account field. + Json::Value args {depositAuthArgs (alice, becky)}; + args[jss::destination_account] = 7.3; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "invalidParams", + "Invalid field 'destination_account', not a string."); + } + { + // Corrupt destination_account field. + Json::Value args {depositAuthArgs (alice, becky)}; + args[jss::destination_account] = + "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "actMalformed", "Account malformed."); + } + { + // Request an invalid ledger. + Json::Value args {depositAuthArgs (alice, becky, "17")}; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "invalidParams", "ledgerIndexMalformed"); + } + { + // Request a ledger that doesn't exist yet. + Json::Value args {depositAuthArgs (alice, becky)}; + args[jss::ledger_index] = 17; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "lgrNotFound", "ledgerNotFound"); + } + { + // becky is not yet funded. + Json::Value args {depositAuthArgs (alice, becky)}; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + verifyErr (result, "dstActMissing", + "Destination account does not exist."); + } + env.fund(XRP(1000), becky); + env.close(); + { + // Once becky is funded try it again and see it succeed. + Json::Value args {depositAuthArgs (alice, becky)}; + Json::Value const result {env.rpc ( + "json", "deposit_authorized", args.toStyledString())}; + validateDepositAuthResult (result, true); + } + } + + void run() + { + testValid(); + testErrors(); + } +}; + +BEAST_DEFINE_TESTSUITE(DepositAuthorized,app,ripple); + +} +} + diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index 78ed175e470..91b8ac4c54b 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -325,9 +325,11 @@ class LedgerData_test : public beast::unit_test::suite env(jv); } + // bob9 DepositPreauths bob4 and bob8. + env (deposit::auth (Account {"bob9"}, Account {"bob4"})); + env (deposit::auth (Account {"bob9"}, Account {"bob8"})); env.close(); - // Now fetch each type auto makeRequest = [&env](Json::StaticString t) { @@ -354,7 +356,7 @@ class LedgerData_test : public beast::unit_test::suite { // jvParams[jss::type] = "directory"; auto const jrr = makeRequest(jss::directory); - BEAST_EXPECT( checkArraySize(jrr[jss::state], 7) ); + BEAST_EXPECT( checkArraySize(jrr[jss::state], 8) ); for (auto const& j : jrr[jss::state]) BEAST_EXPECT( j["LedgerEntryType"] == "DirectoryNode" ); } @@ -415,6 +417,13 @@ class LedgerData_test : public beast::unit_test::suite BEAST_EXPECT( j["LedgerEntryType"] == "PayChannel" ); } + { // jvParams[jss::type] = "deposit_preauth"; + auto const jrr = makeRequest(jss::deposit_preauth); + BEAST_EXPECT( checkArraySize(jrr[jss::state], 2) ); + for (auto const& j : jrr[jss::state]) + BEAST_EXPECT( j["LedgerEntryType"] == "DepositPreauth" ); + } + { // jvParams[jss::type] = "misspelling"; Json::Value jvParams; jvParams[jss::ledger_index] = "current"; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 472d896b118..091a84e6653 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -377,6 +377,132 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void testLedgerEntryDepositPreauth() + { + testcase ("ledger_entry Request Directory"); + using namespace test::jtx; + Env env {*this}; + Account const alice {"alice"}; + Account const becky {"becky"}; + + env.fund (XRP(10000), alice, becky); + env.close(); + + env (deposit::auth (alice, becky)); + env.close(); + + std::string const ledgerHash {to_string (env.closed()->info().hash)}; + std::string depositPreauthIndex; + { + // Request a depositPreauth by owner and authorized. + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::owner] = alice.human(); + jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == "DepositPreauth"); + BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == becky.human()); + depositPreauthIndex = jrr[jss::node][jss::index].asString(); + } + { + // Request a depositPreauth by index. + Json::Value jvParams; + jvParams[jss::deposit_preauth] = depositPreauthIndex; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == "DepositPreauth"); + BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == becky.human()); + } + { + // Malformed request: deposit_preauth neither object nor string. + Json::Value jvParams; + jvParams[jss::deposit_preauth] = -5; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedRequest", ""); + } + { + // Malformed request: deposit_preauth not hex string. + Json::Value jvParams; + jvParams[jss::deposit_preauth] = "0123456789ABCDEFG"; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedRequest", ""); + } + { + // Malformed request: missing [jss::deposit_preauth][jss::owner] + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedRequest", ""); + } + { + // Malformed request: [jss::deposit_preauth][jss::owner] not string. + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::owner] = 7; + jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedRequest", ""); + } + { + // Malformed: missing [jss::deposit_preauth][jss::authorized] + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::owner] = alice.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedRequest", ""); + } + { + // Malformed: [jss::deposit_preauth][jss::authorized] not string. + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::owner] = alice.human(); + jvParams[jss::deposit_preauth][jss::authorized] = 47; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedRequest", ""); + } + { + // Malformed: [jss::deposit_preauth][jss::owner] is malformed. + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::owner] = + "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; + + jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedOwner", ""); + } + { + // Malformed: [jss::deposit_preauth][jss::authorized] is malformed. + Json::Value jvParams; + jvParams[jss::deposit_preauth][jss::owner] = alice.human(); + jvParams[jss::deposit_preauth][jss::authorized] = + "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; + + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc ( + "json", "ledger_entry", to_string (jvParams))[jss::result]; + checkErrorValue (jrr, "malformedAuthorized", ""); + } + } + void testLedgerEntryDirectory() { testcase ("ledger_entry Request Directory"); @@ -1385,6 +1511,7 @@ class LedgerRPC_test : public beast::unit_test::suite testLedgerAccounts(); testLedgerEntryAccountRoot(); testLedgerEntryCheck(); + testLedgerEntryDepositPreauth(); testLedgerEntryDirectory(); testLedgerEntryEscrow(); testLedgerEntryGenerator(); diff --git a/src/test/unity/jtx_unity1.cpp b/src/test/unity/jtx_unity1.cpp index 178ab991333..8c4ff6838e2 100644 --- a/src/test/unity/jtx_unity1.cpp +++ b/src/test/unity/jtx_unity1.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/unity/rpc_test_unity.cpp b/src/test/unity/rpc_test_unity.cpp index 237f0b77aa9..08ea12f38aa 100644 --- a/src/test/unity/rpc_test_unity.cpp +++ b/src/test/unity/rpc_test_unity.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include