From 2d5ddbf1bf5bbf65ddf92a2175af83129b74f745 Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Fri, 13 Oct 2017 13:52:41 -0700 Subject: [PATCH] Checks (RIPD-1487): Introduce a new ledger type: ltCHECK Introduce three new transactions that operate on checks: - "CheckCreate" which adds the check entry to the ledger. The check is a promise from the source of the check that the destination of the check may cash the check and receive up to the SendMax specified on the check. The check may have an expiration, after which the check may no longer be cashed. - "CheckCash" is a request by the destination of the check to transfer a requested amount of funds, up to the check's SendMax, from the source to the destination. The destination may receive less than the SendMax due to transfer fees. When cashing a check, the destination specifies the smallest amount of funds that will be acceptable. If the transfer completes and delivers the requested amount, then the check is considered cashed and removed from the ledger. If enough funds cannot be delivered, then the transaction fails and the check remains in the ledger. Attempting to cash the check after its expiration will fail. - "CheckCancel" removes the check from the ledger without transferring funds. Either the check's source or destination can cancel the check at any time. After a check has expired, any account can cancel the check. Facilities related to checks are on the "Checks" amendment. --- Builds/VisualStudio2015/RippleD.vcxproj | 22 + .../VisualStudio2015/RippleD.vcxproj.filters | 21 + src/ripple/app/tx/impl/CancelCheck.cpp | 136 ++ src/ripple/app/tx/impl/CancelCheck.h | 49 + src/ripple/app/tx/impl/CashCheck.cpp | 410 ++++ src/ripple/app/tx/impl/CashCheck.h | 49 + src/ripple/app/tx/impl/CreateCheck.cpp | 243 +++ src/ripple/app/tx/impl/CreateCheck.h | 49 + src/ripple/app/tx/impl/CreateOffer.cpp | 17 +- src/ripple/app/tx/impl/CreateTicket.cpp | 2 +- src/ripple/app/tx/impl/InvariantCheck.cpp | 1 + src/ripple/app/tx/impl/Transactor.h | 4 + src/ripple/app/tx/impl/applySteps.cpp | 58 +- src/ripple/ledger/View.h | 4 + src/ripple/ledger/impl/View.cpp | 1 - src/ripple/protocol/Feature.h | 4 +- src/ripple/protocol/Indexes.h | 18 +- src/ripple/protocol/LedgerFormats.h | 3 + src/ripple/protocol/SField.h | 1 + src/ripple/protocol/TER.h | 3 +- src/ripple/protocol/TxFormats.h | 3 + src/ripple/protocol/impl/Feature.cpp | 4 +- src/ripple/protocol/impl/Indexes.cpp | 16 + src/ripple/protocol/impl/LedgerFormats.cpp | 68 +- src/ripple/protocol/impl/SField.cpp | 1 + src/ripple/protocol/impl/TER.cpp | 1 + src/ripple/protocol/impl/TxFormats.cpp | 116 +- src/ripple/unity/app_tx.cpp | 3 + src/test/app/Check_test.cpp | 1768 +++++++++++++++++ src/test/app/Offer_test.cpp | 12 +- src/test/unity/app_test_unity1.cpp | 1 + 31 files changed, 2981 insertions(+), 107 deletions(-) create mode 100644 src/ripple/app/tx/impl/CancelCheck.cpp create mode 100644 src/ripple/app/tx/impl/CancelCheck.h create mode 100644 src/ripple/app/tx/impl/CashCheck.cpp create mode 100644 src/ripple/app/tx/impl/CashCheck.h create mode 100644 src/ripple/app/tx/impl/CreateCheck.cpp create mode 100644 src/ripple/app/tx/impl/CreateCheck.h create mode 100644 src/test/app/Check_test.cpp diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 657b35ba6..2e8134720 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1309,6 +1309,12 @@ + + True + True + + + True True @@ -1321,12 +1327,24 @@ + + True + True + + + True True + + True + True + + + True True @@ -4634,6 +4652,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 5f96dbdf2..89437dd78 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -1878,6 +1878,12 @@ ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl @@ -1890,12 +1896,24 @@ ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl @@ -5592,6 +5610,9 @@ test\app + + test\app + test\app diff --git a/src/ripple/app/tx/impl/CancelCheck.cpp b/src/ripple/app/tx/impl/CancelCheck.cpp new file mode 100644 index 000000000..9c0796056 --- /dev/null +++ b/src/ripple/app/tx/impl/CancelCheck.cpp @@ -0,0 +1,136 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 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 +#include +#include + +namespace ripple { + +TER +CancelCheck::preflight (PreflightContext const& ctx) +{ + if (! ctx.rules.enabled (featureChecks)) + return temDISABLED; + + TER const ret {preflight1 (ctx)}; + if (! isTesSuccess (ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + // There are no flags (other than universal) for CreateCheck yet. + JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; + return temINVALID_FLAG; + } + + return preflight2 (ctx); +} + +TER +CancelCheck::preclaim (PreclaimContext const& ctx) +{ + auto const sleCheck = ctx.view.read (keylet::check (ctx.tx[sfCheckID])); + if (! sleCheck) + { + JLOG(ctx.j.warn()) << "Check does not exist."; + return tecNO_ENTRY; + } + + using duration = NetClock::duration; + using timepoint = NetClock::time_point; + auto const optExpiry = (*sleCheck)[~sfExpiration]; + + // Expiration is defined in terms of the close time of the parent + // ledger, because we definitively know the time that it closed but + // we do not know the closing time of the ledger that is under + // construction. + if (! optExpiry || + (ctx.view.parentCloseTime() < timepoint {duration {*optExpiry}})) + { + // If the check is not yet expired, then only the creator or the + // destination may cancel the check. + AccountID const acctId {ctx.tx[sfAccount]}; + if (acctId != (*sleCheck)[sfAccount] && + acctId != (*sleCheck)[sfDestination]) + { + JLOG(ctx.j.warn()) << "Check is not expired and canceler is " + "neither check source nor destination."; + return tecNO_PERMISSION; + } + } + return tesSUCCESS; +} + +TER +CancelCheck::doApply () +{ + uint256 const checkId {ctx_.tx[sfCheckID]}; + auto const sleCheck = view().peek (keylet::check (checkId)); + if (! sleCheck) + { + // Error should have been caught in preclaim. + JLOG(j_.warn()) << "Check does not exist."; + return tecNO_ENTRY; + } + + AccountID const srcId {sleCheck->getAccountID (sfAccount)}; + AccountID const dstId {sleCheck->getAccountID (sfDestination)}; + auto viewJ = ctx_.app.journal ("View"); + + // If the check is not written to self (and it shouldn't be), remove the + // check from the destination account root. + if (srcId != dstId) + { + std::uint64_t const page {(*sleCheck)[sfDestinationNode]}; + TER const ter {dirDelete (view(), true, page, + keylet::ownerDir (dstId), checkId, false, false, viewJ)}; + if (! isTesSuccess (ter)) + { + JLOG(j_.warn()) << "Unable to delete check from destination."; + return ter; + } + } + { + std::uint64_t const page {(*sleCheck)[sfOwnerNode]}; + TER const ter {dirDelete (view(), true, page, + keylet::ownerDir (srcId), checkId, false, false, viewJ)}; + if (! isTesSuccess (ter)) + { + JLOG(j_.warn()) << "Unable to delete check from owner."; + return ter; + } + } + + // If we succeeded, update the check owner's reserve. + auto const sleSrc = view().peek (keylet::account (srcId)); + adjustOwnerCount (view(), sleSrc, -1, viewJ); + + // Remove check from ledger. + view().erase (sleCheck); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/CancelCheck.h b/src/ripple/app/tx/impl/CancelCheck.h new file mode 100644 index 000000000..1c1ab3627 --- /dev/null +++ b/src/ripple/app/tx/impl/CancelCheck.h @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 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_CANCELCHECK_H_INCLUDED +#define RIPPLE_TX_CANCELCHECK_H_INCLUDED + +#include + +namespace ripple { + +class CancelCheck + : public Transactor +{ +public: + CancelCheck (ApplyContext& ctx) + : Transactor (ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + static + TER + preclaim (PreclaimContext const& ctx); + + TER doApply () override; +}; + +} + +#endif diff --git a/src/ripple/app/tx/impl/CashCheck.cpp b/src/ripple/app/tx/impl/CashCheck.cpp new file mode 100644 index 000000000..56bde04a8 --- /dev/null +++ b/src/ripple/app/tx/impl/CashCheck.cpp @@ -0,0 +1,410 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 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 +#include +#include +#include + +#include + +namespace ripple { + +TER +CashCheck::preflight (PreflightContext const& ctx) +{ + if (! ctx.rules.enabled (featureChecks)) + return temDISABLED; + + TER const ret {preflight1 (ctx)}; + if (! isTesSuccess (ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + // There are no flags (other than universal) for CashCheck yet. + JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; + return temINVALID_FLAG; + } + + // Exactly one of Amount or DeliverMin must be present. + auto const optAmount = ctx.tx[~sfAmount]; + auto const optDeliverMin = ctx.tx[~sfDeliverMin]; + + if (static_cast(optAmount) == static_cast(optDeliverMin)) + { + JLOG(ctx.j.warn()) << "Malformed transaction: " + "does not specify exactly one of Amount and DeliverMin."; + return temMALFORMED; + } + + // Make sure the amount is valid. + STAmount const value {optAmount ? *optAmount : *optDeliverMin}; + if (!isLegalNet (value) || value.signum() <= 0) + { + JLOG(ctx.j.warn()) << "Malformed transaction: bad amount: " + << value.getFullText(); + return temBAD_AMOUNT; + } + + if (badCurrency() == value.getCurrency()) + { + JLOG(ctx.j.warn()) <<"Malformed transaction: Bad currency."; + return temBAD_CURRENCY; + } + + return preflight2 (ctx); +} + +TER +CashCheck::preclaim (PreclaimContext const& ctx) +{ + auto const sleCheck = ctx.view.read (keylet::check (ctx.tx[sfCheckID])); + if (! sleCheck) + { + JLOG(ctx.j.warn()) << "Check does not exist."; + return tecNO_ENTRY; + } + + // Only cash a check with this account as the destination. + AccountID const dstId {(*sleCheck)[sfDestination]}; + if (ctx.tx[sfAccount] != dstId) + { + JLOG(ctx.j.warn()) << "Cashing a check with wrong Destination."; + return tecNO_PERMISSION; + } + AccountID const srcId {(*sleCheck)[sfAccount]}; + if (srcId == dstId) + { + // They wrote a check to themselves. This should be caught when + // the check is created, but better late than never. + JLOG(ctx.j.error()) << "Malformed transaction: Cashing check to self."; + return tecINTERNAL; + } + { + auto const sleSrc = ctx.view.read (keylet::account (srcId)); + auto const sleDst = ctx.view.read (keylet::account (dstId)); + if (!sleSrc || !sleDst) + { + // If the check exists this should never occur. + JLOG(ctx.j.warn()) + << "Malformed transaction: source or destination not in ledger"; + return tecNO_ENTRY; + } + + if ((sleDst->getFlags() & lsfRequireDestTag) && + !sleCheck->isFieldPresent (sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. + JLOG(ctx.j.warn()) + << "Malformed transaction: DestinationTag required in check."; + return tecDST_TAG_NEEDED; + } + } + { + using duration = NetClock::duration; + using timepoint = NetClock::time_point; + auto const optExpiry = (*sleCheck)[~sfExpiration]; + + // Expiration is defined in terms of the close time of the parent + // ledger, because we definitively know the time that it closed but + // we do not know the closing time of the ledger that is under + // construction. + if (optExpiry && + (ctx.view.parentCloseTime() >= timepoint {duration {*optExpiry}})) + { + JLOG(ctx.j.warn()) << "Cashing a check that has already expired."; + return tecEXPIRED; + } + } + { + // Preflight verified exactly one of Amount or DeliverMin is present. + // Make sure the requested amount is reasonable. + STAmount const value {[] (STTx const& tx) + { + auto const optAmount = tx[~sfAmount]; + return optAmount ? *optAmount : tx[sfDeliverMin]; + } (ctx.tx)}; + + STAmount const sendMax {(*sleCheck)[sfSendMax]}; + Currency const currency {value.getCurrency()}; + if (currency != sendMax.getCurrency()) + { + JLOG(ctx.j.warn()) << "Check cash does not match check currency."; + return temMALFORMED; + } + AccountID const issuerId {value.getIssuer()}; + if (issuerId != sendMax.getIssuer()) + { + JLOG(ctx.j.warn()) << "Check cash does not match check issuer."; + return temMALFORMED; + } + if (value > sendMax) + { + JLOG(ctx.j.warn()) << "Check cashed for more than check sendMax."; + return tecPATH_PARTIAL; + } + + // Make sure the check owner holds at least value. If they have + // less than value the check cannot be cashed. + { + STAmount availableFunds {accountFunds (ctx.view, + (*sleCheck)[sfAccount], value, fhZERO_IF_FROZEN, ctx.j)}; + + // Note that src will have one reserve's worth of additional XRP + // once the check is cashed, since the check's reserve will no + // longer be required. So, if we're dealing in XRP, we add one + // reserve's worth to the available funds. + if (value.native()) + availableFunds += XRPAmount (ctx.view.fees().increment); + + if (value > availableFunds) + { + JLOG(ctx.j.warn()) + << "Check cashed for more than owner's balance."; + return tecPATH_PARTIAL; + } + } + + // An issuer can always accept their own currency. + if (! value.native() && (value.getIssuer() != dstId)) + { + auto const sleTrustLine = ctx.view.read ( + keylet::line (dstId, issuerId, currency)); + if (! sleTrustLine) + { + JLOG(ctx.j.warn()) + << "Cannot cash check for IOU without trustline."; + return tecNO_LINE; + } + + auto const sleIssuer = ctx.view.read (keylet::account (issuerId)); + if (! sleIssuer) + { + JLOG(ctx.j.warn()) + << "Can't receive IOUs from non-existent issuer: " + << to_string (issuerId); + return tecNO_ISSUER; + } + + if ((*sleIssuer)[sfFlags] & lsfRequireAuth) + { + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing strict + // weak ordering. Determine which entry we need to access. + bool const canonical_gt (dstId > issuerId); + + bool const is_authorized ((*sleTrustLine)[sfFlags] & + (canonical_gt ? lsfLowAuth : lsfHighAuth)); + + if (! is_authorized) + { + JLOG(ctx.j.warn()) + << "Can't receive IOUs from issuer without auth."; + return tecNO_AUTH; + } + } + + // The trustline from source to issuer does not need to + // be checked for freezing, since we already verified that the + // source has sufficient non-frozen funds available. + + // However, the trustline from destination to issuer may not + // be frozen. + if (isFrozen (ctx.view, dstId, currency, issuerId)) + { + JLOG(ctx.j.warn()) + << "Cashing a check to a frozen trustline."; + return tecFROZEN; + } + } + } + return tesSUCCESS; +} + +TER +CashCheck::doApply () +{ + // Flow requires that we operate on a PaymentSandbox, rather than + // directly on a View. + PaymentSandbox psb (&ctx_.view()); + + uint256 const checkKey {ctx_.tx[sfCheckID]}; + auto const sleCheck = psb.peek (keylet::check (checkKey)); + if (! sleCheck) + { + JLOG(j_.fatal()) + << "Precheck did not verify check's existence."; + return tecFAILED_PROCESSING; + } + + AccountID const srcId {sleCheck->getAccountID (sfAccount)}; + auto const sleSrc = psb.peek (keylet::account (srcId)); + auto const sleDst = psb.peek (keylet::account (account_)); + + if (!sleSrc || !sleDst) + { + JLOG(ctx_.journal.fatal()) + << "Precheck did not verify source or destination's existence."; + return tecFAILED_PROCESSING; + } + + // Preclaim already checked that source has at least the requested + // funds. + // + // Therefore, if this is a check written to self, (and it shouldn't be) + // we know they have sufficient funds to pay the check. Since they are + // taking the funds from their own pocket and putting it back in their + // pocket no balance will change. + // + // If it is not a check to self (as should be the case), then there's + // work to do... + auto viewJ = ctx_.app.journal ("View"); + if (srcId != account_) + { + STAmount const sendMax {sleCheck->getFieldAmount (sfSendMax)}; + + // Flow() doesn't do XRP to XRP transfers. + if (sendMax.native()) + { + // Here we need to calculate the amount of XRP sleSrc can send. + // The amount they have available is their balance minus their + // reserve. + // + // Since (if we're successful) we're about to remove an entry + // from src's directory, we allow them to send that additional + // incremental reserve amount in the transfer. Hence the -1 + // argument. + STAmount const srcLiquid {xrpLiquid (psb, srcId, -1, viewJ)}; + + // Now, how much do they need in order to be successful? + STAmount const xrpDeliver { + [&sendMax, &srcLiquid] (STTx const& tx) + { + // If the check cash specified Amount deliver exactly that. + auto const optDeliverMin = tx[~sfDeliverMin]; + if (! optDeliverMin) + return tx.getFieldAmount (sfAmount); + + return (std::max ( + *optDeliverMin, std::min (sendMax, srcLiquid))); + }(ctx_.tx)}; + + if (srcLiquid < xrpDeliver) + { + // Vote no. However the transaction might succeed if applied + // in a different order. + JLOG(j_.trace()) << "Cash Check: Insufficient XRP: " + << srcLiquid.getFullText() + << " < " << xrpDeliver.getFullText(); + return tecUNFUNDED_PAYMENT; + } + else + { + // The source account has enough XRP so make the ledger change. + transferXRP (psb, srcId, account_, xrpDeliver, viewJ); + } + } + else + { + // Let flow() do the heavy lifting on a check for an IOU. + auto const optDeliverMin = ctx_.tx[~sfDeliverMin]; + STAmount const flowDeliver {[&optDeliverMin] (STTx const& tx) + { + // If the check cash specified Amount deliver exactly that. + if (! optDeliverMin) + return static_cast(tx[sfAmount]); + + // We can't use the maximum possible currency here because + // there might be a gateway transfer rate to account for. + // Since the transfer rate cannot exceed 200%, we use 1/2 + // maxValue for our limit. + return STAmount { optDeliverMin->issue(), + STAmount::cMaxValue / 2, STAmount::cMaxOffset }; + }(ctx_.tx)}; + + // Call the payment engine's flow() to do the actual work. + auto const result = flow (psb, flowDeliver, srcId, account_, + STPathSet{}, + true, // default path + static_cast(optDeliverMin), // partial payment + true, // owner pays transfer fee + false, // offer crossing + boost::none, + sleCheck->getFieldAmount (sfSendMax), + viewJ); + + if (result.result() != tesSUCCESS) + { + JLOG(ctx_.journal.warn()) + << "flow failed when cashing check."; + return result.result(); + } + + // Make sure that deliverMin was satisfied. + if (optDeliverMin && result.actualAmountOut < *optDeliverMin) + { + JLOG(ctx_.journal.warn()) << "flow did not produce DeliverMin."; + return tecPATH_PARTIAL; + } + } + } + + // Check was cashed. If not a self send (and it shouldn't be), remove + // check link from destination directory. + if (srcId != account_) + { + std::uint64_t const page {(*sleCheck)[sfDestinationNode]}; + TER const ter {dirDelete (psb, true, page, + keylet::ownerDir (account_), checkKey, false, false, viewJ)}; + if (! isTesSuccess (ter)) + { + JLOG(j_.warn()) << "Unable to delete check from destination."; + return ter; + } + } + // Remove check from check owner's directory. + { + std::uint64_t const page {(*sleCheck)[sfOwnerNode]}; + TER const ter {dirDelete (psb, true, page, + keylet::ownerDir (srcId), checkKey, false, false, viewJ)}; + if (! isTesSuccess (ter)) + { + JLOG(j_.warn()) << "Unable to delete check from owner."; + return ter; + } + } + // If we succeeded, update the check owner's reserve. + adjustOwnerCount (psb, sleSrc, -1, viewJ); + + // Remove check from ledger. + psb.erase (sleCheck); + + psb.apply (ctx_.rawView()); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/CashCheck.h b/src/ripple/app/tx/impl/CashCheck.h new file mode 100644 index 000000000..4114be5d0 --- /dev/null +++ b/src/ripple/app/tx/impl/CashCheck.h @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 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_CASHCHECK_H_INCLUDED +#define RIPPLE_TX_CASHCHECK_H_INCLUDED + +#include + +namespace ripple { + +class CashCheck + : public Transactor +{ +public: + CashCheck (ApplyContext& ctx) + : Transactor (ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + static + TER + preclaim (PreclaimContext const& ctx); + + TER doApply () override; +}; + +} + +#endif diff --git a/src/ripple/app/tx/impl/CreateCheck.cpp b/src/ripple/app/tx/impl/CreateCheck.cpp new file mode 100644 index 000000000..646c312e0 --- /dev/null +++ b/src/ripple/app/tx/impl/CreateCheck.cpp @@ -0,0 +1,243 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 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 +#include +#include + +namespace ripple { + +TER +CreateCheck::preflight (PreflightContext const& ctx) +{ + if (! ctx.rules.enabled (featureChecks)) + return temDISABLED; + + TER const ret {preflight1 (ctx)}; + if (! isTesSuccess (ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + // There are no flags (other than universal) for CreateCheck yet. + JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; + return temINVALID_FLAG; + } + if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) + { + // They wrote a check to themselves. + JLOG(ctx.j.warn()) << "Malformed transaction: Check to self."; + return temREDUNDANT; + } + + { + STAmount const sendMax {ctx.tx.getFieldAmount (sfSendMax)}; + if (!isLegalNet (sendMax) || sendMax.signum() <= 0) + { + JLOG(ctx.j.warn()) << "Malformed transaction: bad sendMax amount: " + << sendMax.getFullText(); + return temBAD_AMOUNT; + } + + if (badCurrency() == sendMax.getCurrency()) + { + JLOG(ctx.j.warn()) <<"Malformed transaction: Bad currency."; + return temBAD_CURRENCY; + } + } + + if (auto const optExpiry = ctx.tx[~sfExpiration]) + { + if (*optExpiry == 0) + { + JLOG(ctx.j.warn()) << "Malformed transaction: bad expiration"; + return temBAD_EXPIRATION; + } + } + + return preflight2 (ctx); +} + +TER +CreateCheck::preclaim (PreclaimContext const& ctx) +{ + AccountID const dstId {ctx.tx[sfDestination]}; + auto const sleDst = ctx.view.read (keylet::account (dstId)); + if (! sleDst) + { + JLOG(ctx.j.warn()) << "Destination account does not exist."; + return tecNO_DST; + } + + if ((sleDst->getFlags() & lsfRequireDestTag) && + !ctx.tx.isFieldPresent (sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. + JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required."; + return tecDST_TAG_NEEDED; + } + + { + STAmount const sendMax {ctx.tx[sfSendMax]}; + if (! sendMax.native()) + { + // The currency may not be globally frozen + AccountID const& issuerId {sendMax.getIssuer()}; + if (isGlobalFrozen (ctx.view, issuerId)) + { + JLOG(ctx.j.warn()) << "Creating a check for frozen asset"; + return tecFROZEN; + } + // If this account has a trustline for the currency, that + // trustline may not be frozen. + // + // Note that we DO allow create check for a currency that the + // account does not yet have a trustline to. + AccountID const srcId {ctx.tx.getAccountID (sfAccount)}; + if (issuerId != srcId) + { + // Check if the issuer froze the line + auto const sleTrust = ctx.view.read ( + keylet::line (srcId, issuerId, sendMax.getCurrency())); + if (sleTrust && + sleTrust->isFlag ( + (issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze)) + { + JLOG(ctx.j.warn()) + << "Creating a check for frozen trustline."; + return tecFROZEN; + } + } + if (issuerId != dstId) + { + // Check if dst froze the line. + auto const sleTrust = ctx.view.read ( + keylet::line (issuerId, dstId, sendMax.getCurrency())); + if (sleTrust && + sleTrust->isFlag ( + (dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze)) + { + JLOG(ctx.j.warn()) + << "Creating a check for destination frozen trustline."; + return tecFROZEN; + } + } + } + } + { + using duration = NetClock::duration; + using timepoint = NetClock::time_point; + auto const optExpiry = ctx.tx[~sfExpiration]; + + // Expiration is defined in terms of the close time of the parent + // ledger, because we definitively know the time that it closed but + // we do not know the closing time of the ledger that is under + // construction. + if (optExpiry && + (ctx.view.parentCloseTime() >= timepoint {duration {*optExpiry}})) + { + JLOG(ctx.j.warn()) << "Creating a check that has already expired."; + return tecEXPIRED; + } + } + return tesSUCCESS; +} + +TER +CreateCheck::doApply () +{ + auto const sle = view().peek (keylet::account (account_)); + + // A check 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 ( + sle->getFieldU32 (sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + AccountID const dstAccountId {ctx_.tx[sfDestination]}; + std::uint32_t const seq {ctx_.tx.getSequence()}; + auto sleCheck = + std::make_shared(ltCHECK, getCheckIndex (account_, seq)); + + sleCheck->setAccountID (sfAccount, account_); + sleCheck->setAccountID (sfDestination, dstAccountId); + sleCheck->setFieldU32 (sfSequence, seq); + sleCheck->setFieldAmount (sfSendMax, ctx_.tx[sfSendMax]); + if (auto const srcTag = ctx_.tx[~sfSourceTag]) + sleCheck->setFieldU32 (sfSourceTag, *srcTag); + if (auto const dstTag = ctx_.tx[~sfDestinationTag]) + sleCheck->setFieldU32 (sfDestinationTag, *dstTag); + if (auto const invoiceId = ctx_.tx[~sfInvoiceID]) + sleCheck->setFieldH256 (sfInvoiceID, *invoiceId); + if (auto const expiry = ctx_.tx[~sfExpiration]) + sleCheck->setFieldU32 (sfExpiration, *expiry); + + view().insert (sleCheck); + + auto viewJ = ctx_.app.journal ("View"); + // If it's not a self-send (and it shouldn't be), add Check to the + // destination's owner directory. + if (dstAccountId != account_) + { + auto const page = dirAdd (view(), keylet::ownerDir (dstAccountId), + sleCheck->key(), false, describeOwnerDir (dstAccountId), viewJ); + + JLOG(j_.trace()) + << "Adding Check to destination directory " + << to_string (sleCheck->key()) + << ": " << (page ? "success" : "failure"); + + if (! page) + return tecDIR_FULL; + + sleCheck->setFieldU64 (sfDestinationNode, *page); + } + + { + auto const page = dirAdd (view(), keylet::ownerDir (account_), + sleCheck->key(), false, describeOwnerDir (account_), viewJ); + + JLOG(j_.trace()) + << "Adding Check to owner directory " + << to_string (sleCheck->key()) + << ": " << (page ? "success" : "failure"); + + if (! page) + return tecDIR_FULL; + + sleCheck->setFieldU64 (sfOwnerNode, *page); + } + // If we succeeded, the new entry counts against the creator's reserve. + adjustOwnerCount (view(), sle, 1, viewJ); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/CreateCheck.h b/src/ripple/app/tx/impl/CreateCheck.h new file mode 100644 index 000000000..49cdfce16 --- /dev/null +++ b/src/ripple/app/tx/impl/CreateCheck.h @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2017 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_CREATECHECK_H_INCLUDED +#define RIPPLE_TX_CREATECHECK_H_INCLUDED + +#include + +namespace ripple { + +class CreateCheck + : public Transactor +{ +public: + CreateCheck (ApplyContext& ctx) + : Transactor (ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + static + TER + preclaim (PreclaimContext const& ctx); + + TER doApply () override; +}; + +} + +#endif diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index 320e03f0d..6a02c603c 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -194,10 +194,12 @@ CreateOffer::preclaim(PreclaimContext const& ctx) if (expiration && (ctx.view.parentCloseTime() >= tp{d{*expiration}})) { - // Note that this will get checked again in applyGuts, - // but it saves us a call to checkAcceptAsset and - // possible false negative. - return tesSUCCESS; + // Note that this will get checked again in applyGuts, but it saves + // us a call to checkAcceptAsset and possible false negative. + // + // 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) ? tecEXPIRED : tesSUCCESS; } // Make sure that we are authorized to hold what the taker will pay us. @@ -1102,7 +1104,12 @@ CreateOffer::applyGuts (Sandbox& sb, Sandbox& sbCancel) { // If the offer has expired, the transaction has successfully // done nothing, so short circuit from here. - return{ tesSUCCESS, true }; + // + // The return code change is attached to featureChecks as a convenience. + // The change is not big enough to deserve its own amendment. + TER const ter {ctx_.view().rules().enabled( + featureChecks) ? tecEXPIRED : tesSUCCESS}; + return{ ter, true }; } bool const bOpenLedger = ctx_.view().open(); diff --git a/src/ripple/app/tx/impl/CreateTicket.cpp b/src/ripple/app/tx/impl/CreateTicket.cpp index 2220899a7..0e642e45e 100644 --- a/src/ripple/app/tx/impl/CreateTicket.cpp +++ b/src/ripple/app/tx/impl/CreateTicket.cpp @@ -113,7 +113,7 @@ CreateTicket::doApply () sleTicket->setFieldU64(sfOwnerNode, *page); - // If we succeeded, the new entry counts agains the + // If we succeeded, the new entry counts against the // creator's reserve. adjustOwnerCount(view(), sle, 1, viewJ); return tesSUCCESS; diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 840abea56..dfe22bcba 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -260,6 +260,7 @@ LedgerEntryTypesMatch::visitEntry( case ltFEE_SETTINGS: case ltESCROW: case ltPAYCHAN: + case ltCHECK: break; default: invalidTypeAdded_ = true; diff --git a/src/ripple/app/tx/impl/Transactor.h b/src/ripple/app/tx/impl/Transactor.h index cf74ee0a9..105688945 100644 --- a/src/ripple/app/tx/impl/Transactor.h +++ b/src/ripple/app/tx/impl/Transactor.h @@ -84,6 +84,10 @@ protected: XRPAmount mPriorBalance; // Balance before fees. XRPAmount mSourceBalance; // Balance after fees. + virtual ~Transactor() = default; + Transactor (Transactor const&) = delete; + Transactor& operator= (Transactor const&) = delete; + public: /** Process the transaction. */ std::pair diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 0809f6fa8..c457387ab 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -20,9 +20,12 @@ #include #include #include +#include #include #include +#include #include +#include #include #include #include @@ -42,12 +45,18 @@ invoke_preflight (PreflightContext const& ctx) switch(ctx.tx.getTxnType()) { case ttACCOUNT_SET: return SetAccount ::preflight(ctx); + case ttCHECK_CANCEL: return CancelCheck ::preflight(ctx); + case ttCHECK_CASH: return CashCheck ::preflight(ctx); + case ttCHECK_CREATE: return CreateCheck ::preflight(ctx); case ttOFFER_CANCEL: return CancelOffer ::preflight(ctx); case ttOFFER_CREATE: return CreateOffer ::preflight(ctx); - case ttPAYMENT: return Payment ::preflight(ctx); case ttESCROW_CREATE: return EscrowCreate ::preflight(ctx); case ttESCROW_FINISH: return EscrowFinish ::preflight(ctx); case ttESCROW_CANCEL: return EscrowCancel ::preflight(ctx); + case ttPAYCHAN_CLAIM: return PayChanClaim ::preflight(ctx); + case ttPAYCHAN_CREATE: return PayChanCreate ::preflight(ctx); + case ttPAYCHAN_FUND: return PayChanFund ::preflight(ctx); + case ttPAYMENT: return Payment ::preflight(ctx); case ttREGULAR_KEY_SET: return SetRegularKey ::preflight(ctx); case ttSIGNER_LIST_SET: return SetSignerList ::preflight(ctx); case ttTICKET_CANCEL: return CancelTicket ::preflight(ctx); @@ -55,9 +64,6 @@ invoke_preflight (PreflightContext const& ctx) case ttTRUST_SET: return SetTrust ::preflight(ctx); case ttAMENDMENT: case ttFEE: return Change ::preflight(ctx); - case ttPAYCHAN_CREATE: return PayChanCreate ::preflight(ctx); - case ttPAYCHAN_FUND: return PayChanFund ::preflight(ctx); - case ttPAYCHAN_CLAIM: return PayChanClaim ::preflight(ctx); default: assert(false); return temUNKNOWN; @@ -107,12 +113,18 @@ invoke_preclaim (PreclaimContext const& ctx) switch(ctx.tx.getTxnType()) { case ttACCOUNT_SET: return invoke_preclaim(ctx); + case ttCHECK_CANCEL: return invoke_preclaim(ctx); + case ttCHECK_CASH: return invoke_preclaim(ctx); + case ttCHECK_CREATE: return invoke_preclaim(ctx); case ttOFFER_CANCEL: return invoke_preclaim(ctx); case ttOFFER_CREATE: return invoke_preclaim(ctx); - case ttPAYMENT: return invoke_preclaim(ctx); case ttESCROW_CREATE: return invoke_preclaim(ctx); case ttESCROW_FINISH: return invoke_preclaim(ctx); case ttESCROW_CANCEL: return invoke_preclaim(ctx); + case ttPAYCHAN_CLAIM: return invoke_preclaim(ctx); + case ttPAYCHAN_CREATE: return invoke_preclaim(ctx); + case ttPAYCHAN_FUND: return invoke_preclaim(ctx); + case ttPAYMENT: return invoke_preclaim(ctx); case ttREGULAR_KEY_SET: return invoke_preclaim(ctx); case ttSIGNER_LIST_SET: return invoke_preclaim(ctx); case ttTICKET_CANCEL: return invoke_preclaim(ctx); @@ -120,9 +132,6 @@ invoke_preclaim (PreclaimContext const& ctx) case ttTRUST_SET: return invoke_preclaim(ctx); case ttAMENDMENT: case ttFEE: return invoke_preclaim(ctx); - case ttPAYCHAN_CREATE: return invoke_preclaim(ctx); - case ttPAYCHAN_FUND: return invoke_preclaim(ctx); - case ttPAYCHAN_CLAIM: return invoke_preclaim(ctx); default: assert(false); return { temUNKNOWN, 0 }; @@ -136,12 +145,18 @@ invoke_calculateBaseFee(PreclaimContext const& ctx) switch (ctx.tx.getTxnType()) { case ttACCOUNT_SET: return SetAccount::calculateBaseFee(ctx); + case ttCHECK_CANCEL: return CancelCheck::calculateBaseFee(ctx); + case ttCHECK_CASH: return CashCheck::calculateBaseFee(ctx); + case ttCHECK_CREATE: return CreateCheck::calculateBaseFee(ctx); case ttOFFER_CANCEL: return CancelOffer::calculateBaseFee(ctx); case ttOFFER_CREATE: return CreateOffer::calculateBaseFee(ctx); - case ttPAYMENT: return Payment::calculateBaseFee(ctx); case ttESCROW_CREATE: return EscrowCreate::calculateBaseFee(ctx); case ttESCROW_FINISH: return EscrowFinish::calculateBaseFee(ctx); case ttESCROW_CANCEL: return EscrowCancel::calculateBaseFee(ctx); + case ttPAYCHAN_CLAIM: return PayChanClaim::calculateBaseFee(ctx); + case ttPAYCHAN_CREATE: return PayChanCreate::calculateBaseFee(ctx); + case ttPAYCHAN_FUND: return PayChanFund::calculateBaseFee(ctx); + case ttPAYMENT: return Payment::calculateBaseFee(ctx); case ttREGULAR_KEY_SET: return SetRegularKey::calculateBaseFee(ctx); case ttSIGNER_LIST_SET: return SetSignerList::calculateBaseFee(ctx); case ttTICKET_CANCEL: return CancelTicket::calculateBaseFee(ctx); @@ -149,9 +164,6 @@ invoke_calculateBaseFee(PreclaimContext const& ctx) case ttTRUST_SET: return SetTrust::calculateBaseFee(ctx); case ttAMENDMENT: case ttFEE: return Change::calculateBaseFee(ctx); - case ttPAYCHAN_CREATE: return PayChanCreate::calculateBaseFee(ctx); - case ttPAYCHAN_FUND: return PayChanFund::calculateBaseFee(ctx); - case ttPAYCHAN_CLAIM: return PayChanClaim::calculateBaseFee(ctx); default: assert(false); return 0; @@ -178,20 +190,23 @@ invoke_calculateConsequences(STTx const& tx) switch (tx.getTxnType()) { case ttACCOUNT_SET: return invoke_calculateConsequences(tx); + case ttCHECK_CANCEL: return invoke_calculateConsequences(tx); + case ttCHECK_CASH: return invoke_calculateConsequences(tx); + case ttCHECK_CREATE: return invoke_calculateConsequences(tx); case ttOFFER_CANCEL: return invoke_calculateConsequences(tx); case ttOFFER_CREATE: return invoke_calculateConsequences(tx); - case ttPAYMENT: return invoke_calculateConsequences(tx); case ttESCROW_CREATE: return invoke_calculateConsequences(tx); case ttESCROW_FINISH: return invoke_calculateConsequences(tx); case ttESCROW_CANCEL: return invoke_calculateConsequences(tx); + case ttPAYCHAN_CLAIM: return invoke_calculateConsequences(tx); + case ttPAYCHAN_CREATE: return invoke_calculateConsequences(tx); + case ttPAYCHAN_FUND: return invoke_calculateConsequences(tx); + case ttPAYMENT: return invoke_calculateConsequences(tx); case ttREGULAR_KEY_SET: return invoke_calculateConsequences(tx); case ttSIGNER_LIST_SET: return invoke_calculateConsequences(tx); case ttTICKET_CANCEL: return invoke_calculateConsequences(tx); case ttTICKET_CREATE: return invoke_calculateConsequences(tx); case ttTRUST_SET: return invoke_calculateConsequences(tx); - case ttPAYCHAN_CREATE: return invoke_calculateConsequences(tx); - case ttPAYCHAN_FUND: return invoke_calculateConsequences(tx); - case ttPAYCHAN_CLAIM: return invoke_calculateConsequences(tx); case ttAMENDMENT: case ttFEE: // fall through to default @@ -209,12 +224,18 @@ 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 ttPAYMENT: { Payment 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(); } @@ -222,9 +243,6 @@ invoke_apply (ApplyContext& ctx) case ttTRUST_SET: { SetTrust p(ctx); return p(); } case ttAMENDMENT: case ttFEE: { Change p(ctx); return p(); } - case ttPAYCHAN_CREATE: { PayChanCreate p(ctx); return p(); } - case ttPAYCHAN_FUND: { PayChanFund p(ctx); return p(); } - case ttPAYCHAN_CLAIM: { PayChanClaim p(ctx); return p(); } default: assert(false); return { temUNKNOWN, false }; diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index c249c34ad..965bee0e4 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -60,6 +60,10 @@ bool isGlobalFrozen (ReadView const& view, AccountID const& issuer); +bool +isFrozen (ReadView const& view, AccountID const& account, + Currency const& currency, AccountID const& issuer); + // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of currency held by account. May be negative. diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 219402c35..7bd627efd 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -138,7 +138,6 @@ isGlobalFrozen (ReadView const& view, // Can the specified account spend the specified currency issued by // the specified issuer or does the freeze flag prohibit it? -static bool isFrozen (ReadView const& view, AccountID const& account, Currency const& currency, AccountID const& issuer) diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 7f0042603..a6c9c050b 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,8 @@ class FeatureCollections "fix1513", "fix1523", "fix1528", - "DepositAuth" + "DepositAuth", + "Checks" }; std::vector features; @@ -355,6 +356,7 @@ extern uint256 const fix1513; extern uint256 const fix1523; extern uint256 const fix1528; extern uint256 const featureDepositAuth; +extern uint256 const featureChecks; } // ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 86504537d..23252445b 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -90,13 +90,16 @@ getRippleStateIndex (AccountID const& a, Issue const& issue); uint256 getSignerListIndex (AccountID const& account); +uint256 +getCheckIndex (AccountID const& account, std::uint32_t uSequence); + //------------------------------------------------------------------------------ /* VFALCO TODO For each of these operators that take just the uin256 and only attach the LedgerEntryType, we can comment out that operator to see what breaks, and those call sites are - candidates for having the Keylet either passed in a a + candidates for having the Keylet either passed in as a parameter, or having a data member that stores the keylet. */ @@ -213,6 +216,19 @@ struct signers_t }; static signers_t const signers {}; +/** A Check */ +struct check_t +{ + Keylet operator()(AccountID const& id, + std::uint32_t seq) const; + + Keylet operator()(uint256 const& key) const + { + return { ltCHECK, key }; + } +}; +static check_t const check {}; + //------------------------------------------------------------------------------ /** Any ledger entry */ diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 14d43eb06..b024210b6 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -83,6 +83,8 @@ enum LedgerEntryType // Simple unidirection xrp channel ltPAYCHAN = 'x', + ltCHECK = 'C', + // No longer used or supported. Left here to prevent accidental // reassignment of the ledger type. ltNICKNAME = 'n', @@ -111,6 +113,7 @@ enum LedgerNameSpace spaceTicket = 'T', spaceSignerList = 'S', spaceXRPUChannel = 'x', + spaceCheck = 'C', // 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 232cb4a85..cdbd3cc00 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -418,6 +418,7 @@ extern SF_U256 const sfTicketID; extern SF_U256 const sfDigest; extern SF_U256 const sfPayChannel; extern SF_U256 const sfConsensusHash; +extern SF_U256 const sfCheckID; // currency amount (common) extern SF_Amount const sfAmount; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 55e690003..791221bd8 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -214,7 +214,8 @@ enum TER tecINTERNAL = 144, tecOVERSIZE = 145, tecCRYPTOCONDITION_ERROR = 146, - tecINVARIANT_FAILED = 147 + tecINVARIANT_FAILED = 147, + tecEXPIRED = 148 }; inline bool isTelLocal(TER x) diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 4a3bd1473..bef20bb6c 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -50,6 +50,9 @@ enum TxType ttPAYCHAN_CREATE = 13, ttPAYCHAN_FUND = 14, ttPAYCHAN_CLAIM = 15, + ttCHECK_CREATE = 16, + ttCHECK_CASH = 17, + ttCHECK_CANCEL = 18, ttTRUST_SET = 20, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index add750387..e7b8b3860 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -107,7 +107,8 @@ detail::supportedAmendments () { "67A34F2CF55BFC0F93AACD5B281413176FEE195269FA6D95219A2DF738671172 fix1513" }, { "B9E739B8296B4A1BB29BE990B17D66E21B62A300A909F25AC55C22D6C72E1F9D fix1523" }, { "1D3463A5891F9E589C5AE839FFAC4A917CE96197098A1EF22304E1BC5B98A454 fix1528" }, - { "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"} + { "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"}, + { "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"} }; return supported; } @@ -156,5 +157,6 @@ uint256 const fix1513 = *getRegisteredFeature("fix1513"); uint256 const fix1523 = *getRegisteredFeature("fix1523"); uint256 const fix1528 = *getRegisteredFeature("fix1528"); uint256 const featureDepositAuth = *getRegisteredFeature("DepositAuth"); +uint256 const featureChecks = *getRegisteredFeature("Checks"); } // ripple diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 2c4251e19..beb230b67 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -190,6 +190,15 @@ getSignerListIndex (AccountID const& account) std::uint32_t (0)); // 0 == default SignerList ID. } +uint256 +getCheckIndex (AccountID const& account, std::uint32_t uSequence) +{ + return sha512Half( + std::uint16_t(spaceCheck), + account, + std::uint32_t(uSequence)); +} + //------------------------------------------------------------------------------ namespace keylet { @@ -285,6 +294,13 @@ Keylet signers_t::operator()(AccountID const& id) const getSignerListIndex(id) }; } +Keylet check_t::operator()(AccountID const& id, + std::uint32_t seq) const +{ + return { ltCHECK, + getCheckIndex(id, seq) }; +} + //------------------------------------------------------------------------------ Keylet unchecked (uint256 const& key) diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 9dac61ba2..b3be8078f 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -87,19 +87,20 @@ LedgerFormats::LedgerFormats () << SOElement (sfHighQualityOut, SOE_OPTIONAL) ; - add ("Escrow", ltESCROW) << - SOElement (sfAccount, SOE_REQUIRED) << - SOElement (sfDestination, SOE_REQUIRED) << - SOElement (sfAmount, SOE_REQUIRED) << - SOElement (sfCondition, SOE_OPTIONAL) << - SOElement (sfCancelAfter, SOE_OPTIONAL) << - SOElement (sfFinishAfter, SOE_OPTIONAL) << - SOElement (sfSourceTag, SOE_OPTIONAL) << - SOElement (sfDestinationTag, SOE_OPTIONAL) << - SOElement (sfOwnerNode, SOE_REQUIRED) << - SOElement (sfPreviousTxnID, SOE_REQUIRED) << - SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) << - SOElement (sfDestinationNode, SOE_OPTIONAL); + add ("Escrow", ltESCROW) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfAmount, SOE_REQUIRED) + << SOElement (sfCondition, SOE_OPTIONAL) + << SOElement (sfCancelAfter, SOE_OPTIONAL) + << SOElement (sfFinishAfter, SOE_OPTIONAL) + << SOElement (sfSourceTag, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + << SOElement (sfOwnerNode, SOE_REQUIRED) + << SOElement (sfPreviousTxnID, SOE_REQUIRED) + << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) + << SOElement (sfDestinationNode, SOE_OPTIONAL) + ; add ("LedgerHashes", ltLEDGER_HASHES) << SOElement (sfFirstLedgerSequence, SOE_OPTIONAL) // Remove if we do a ledger restart @@ -139,19 +140,34 @@ LedgerFormats::LedgerFormats () ; add ("PayChannel", ltPAYCHAN) - << SOElement (sfAccount, SOE_REQUIRED) - << SOElement (sfDestination, SOE_REQUIRED) - << SOElement (sfAmount, SOE_REQUIRED) - << SOElement (sfBalance, SOE_REQUIRED) - << SOElement (sfPublicKey, SOE_REQUIRED) - << SOElement (sfSettleDelay, SOE_REQUIRED) - << SOElement (sfExpiration, SOE_OPTIONAL) - << SOElement (sfCancelAfter, SOE_OPTIONAL) - << SOElement (sfSourceTag, SOE_OPTIONAL) - << SOElement (sfDestinationTag, SOE_OPTIONAL) - << SOElement (sfOwnerNode, SOE_REQUIRED) - << SOElement (sfPreviousTxnID, SOE_REQUIRED) - << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfAmount, SOE_REQUIRED) + << SOElement (sfBalance, SOE_REQUIRED) + << SOElement (sfPublicKey, SOE_REQUIRED) + << SOElement (sfSettleDelay, SOE_REQUIRED) + << SOElement (sfExpiration, SOE_OPTIONAL) + << SOElement (sfCancelAfter, SOE_OPTIONAL) + << SOElement (sfSourceTag, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + << SOElement (sfOwnerNode, SOE_REQUIRED) + << SOElement (sfPreviousTxnID, SOE_REQUIRED) + << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) + ; + + add ("Check", ltCHECK) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfSendMax, SOE_REQUIRED) + << SOElement (sfSequence, SOE_REQUIRED) + << SOElement (sfOwnerNode, SOE_REQUIRED) + << SOElement (sfDestinationNode, SOE_REQUIRED) + << SOElement (sfExpiration, SOE_OPTIONAL) + << SOElement (sfInvoiceID, SOE_OPTIONAL) + << SOElement (sfSourceTag, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + << SOElement (sfPreviousTxnID, SOE_REQUIRED) + << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) ; } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index a73dcd950..d9515b990 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -171,6 +171,7 @@ SF_U256 const sfTicketID = make::one(&sfTicketID, STI_H SF_U256 const sfDigest = make::one(&sfDigest, STI_HASH256, 21, "Digest"); SF_U256 const sfPayChannel = make::one(&sfPayChannel, STI_HASH256, 22, "Channel"); SF_U256 const sfConsensusHash = make::one(&sfConsensusHash, STI_HASH256, 23, "ConsensusHash"); +SF_U256 const sfCheckID = make::one(&sfCheckID, STI_HASH256, 24, "CheckID"); // currency amount (common) SF_Amount const sfAmount = make::one(&sfAmount, STI_AMOUNT, 1, "Amount"); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 1ce16fb5b..472ea565d 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -72,6 +72,7 @@ transResults() { tecINTERNAL, { "tecINTERNAL", "An internal error has occurred during processing." } }, { 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." } }, { tefALREADY, { "tefALREADY", "The exact transaction was already in this ledger." } }, { tefBAD_ADD_AUTH, { "tefBAD_ADD_AUTH", "Not authorized to add account." } }, diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 10bbfdf54..1d09cd65a 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -67,23 +67,26 @@ TxFormats::TxFormats () << SOElement (sfDeliverMin, SOE_OPTIONAL) ; - add ("EscrowCreate", ttESCROW_CREATE) << - SOElement (sfDestination, SOE_REQUIRED) << - SOElement (sfAmount, SOE_REQUIRED) << - SOElement (sfCondition, SOE_OPTIONAL) << - SOElement (sfCancelAfter, SOE_OPTIONAL) << - SOElement (sfFinishAfter, SOE_OPTIONAL) << - SOElement (sfDestinationTag, SOE_OPTIONAL); + add ("EscrowCreate", ttESCROW_CREATE) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfAmount, SOE_REQUIRED) + << SOElement (sfCondition, SOE_OPTIONAL) + << SOElement (sfCancelAfter, SOE_OPTIONAL) + << SOElement (sfFinishAfter, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + ; - add ("EscrowFinish", ttESCROW_FINISH) << - SOElement (sfOwner, SOE_REQUIRED) << - SOElement (sfOfferSequence, SOE_REQUIRED) << - SOElement (sfFulfillment, SOE_OPTIONAL) << - SOElement (sfCondition, SOE_OPTIONAL); + add ("EscrowFinish", ttESCROW_FINISH) + << SOElement (sfOwner, SOE_REQUIRED) + << SOElement (sfOfferSequence, SOE_REQUIRED) + << SOElement (sfFulfillment, SOE_OPTIONAL) + << SOElement (sfCondition, SOE_OPTIONAL) + ; - add ("EscrowCancel", ttESCROW_CANCEL) << - SOElement (sfOwner, SOE_REQUIRED) << - SOElement (sfOfferSequence, SOE_REQUIRED); + add ("EscrowCancel", ttESCROW_CANCEL) + << SOElement (sfOwner, SOE_REQUIRED) + << SOElement (sfOfferSequence, SOE_REQUIRED) + ; add ("EnableAmendment", ttAMENDMENT) << SOElement (sfLedgerSequence, SOE_REQUIRED) @@ -114,44 +117,65 @@ TxFormats::TxFormats () << SOElement (sfSignerEntries, SOE_OPTIONAL) ; - add ("PaymentChannelCreate", ttPAYCHAN_CREATE) << - SOElement (sfDestination, SOE_REQUIRED) << - SOElement (sfAmount, SOE_REQUIRED) << - SOElement (sfSettleDelay, SOE_REQUIRED) << - SOElement (sfPublicKey, SOE_REQUIRED) << - SOElement (sfCancelAfter, SOE_OPTIONAL) << - SOElement (sfDestinationTag, SOE_OPTIONAL); + add ("PaymentChannelCreate", ttPAYCHAN_CREATE) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfAmount, SOE_REQUIRED) + << SOElement (sfSettleDelay, SOE_REQUIRED) + << SOElement (sfPublicKey, SOE_REQUIRED) + << SOElement (sfCancelAfter, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + ; - add ("PaymentChannelFund", ttPAYCHAN_FUND) << - SOElement (sfPayChannel, SOE_REQUIRED) << - SOElement (sfAmount, SOE_REQUIRED) << - SOElement (sfExpiration, SOE_OPTIONAL); + add ("PaymentChannelFund", ttPAYCHAN_FUND) + << SOElement (sfPayChannel, SOE_REQUIRED) + << SOElement (sfAmount, SOE_REQUIRED) + << SOElement (sfExpiration, SOE_OPTIONAL) + ; - add ("PaymentChannelClaim", ttPAYCHAN_CLAIM) << - SOElement (sfPayChannel, SOE_REQUIRED) << - SOElement (sfAmount, SOE_OPTIONAL) << - SOElement (sfBalance, SOE_OPTIONAL) << - SOElement (sfSignature, SOE_OPTIONAL) << - SOElement (sfPublicKey, SOE_OPTIONAL); + add ("PaymentChannelClaim", ttPAYCHAN_CLAIM) + << SOElement (sfPayChannel, SOE_REQUIRED) + << SOElement (sfAmount, SOE_OPTIONAL) + << SOElement (sfBalance, SOE_OPTIONAL) + << SOElement (sfSignature, SOE_OPTIONAL) + << SOElement (sfPublicKey, SOE_OPTIONAL) + ; + + add ("CheckCreate", ttCHECK_CREATE) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfSendMax, SOE_REQUIRED) + << SOElement (sfExpiration, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + << SOElement (sfInvoiceID, SOE_OPTIONAL) + ; + + add ("CheckCash", ttCHECK_CASH) + << SOElement (sfCheckID, SOE_REQUIRED) + << SOElement (sfAmount, SOE_OPTIONAL) + << SOElement (sfDeliverMin, SOE_OPTIONAL) + ; + + add ("CheckCancel", ttCHECK_CANCEL) + << SOElement (sfCheckID, SOE_REQUIRED) + ; } void TxFormats::addCommonFields (Item& item) { item - << SOElement(sfTransactionType, SOE_REQUIRED) - << SOElement(sfFlags, SOE_OPTIONAL) - << SOElement(sfSourceTag, SOE_OPTIONAL) - << SOElement(sfAccount, SOE_REQUIRED) - << SOElement(sfSequence, SOE_REQUIRED) - << SOElement(sfPreviousTxnID, SOE_OPTIONAL) // emulate027 - << SOElement(sfLastLedgerSequence, SOE_OPTIONAL) - << SOElement(sfAccountTxnID, SOE_OPTIONAL) - << SOElement(sfFee, SOE_REQUIRED) - << SOElement(sfOperationLimit, SOE_OPTIONAL) - << SOElement(sfMemos, SOE_OPTIONAL) - << SOElement(sfSigningPubKey, SOE_REQUIRED) - << SOElement(sfTxnSignature, SOE_OPTIONAL) - << SOElement(sfSigners, SOE_OPTIONAL) // submit_multisigned + << SOElement(sfTransactionType, SOE_REQUIRED) + << SOElement(sfFlags, SOE_OPTIONAL) + << SOElement(sfSourceTag, SOE_OPTIONAL) + << SOElement(sfAccount, SOE_REQUIRED) + << SOElement(sfSequence, SOE_REQUIRED) + << SOElement(sfPreviousTxnID, SOE_OPTIONAL) // emulate027 + << SOElement(sfLastLedgerSequence, SOE_OPTIONAL) + << SOElement(sfAccountTxnID, SOE_OPTIONAL) + << SOElement(sfFee, SOE_REQUIRED) + << SOElement(sfOperationLimit, SOE_OPTIONAL) + << SOElement(sfMemos, SOE_OPTIONAL) + << SOElement(sfSigningPubKey, SOE_REQUIRED) + << SOElement(sfTxnSignature, SOE_OPTIONAL) + << SOElement(sfSigners, SOE_OPTIONAL) // submit_multisigned ; } diff --git a/src/ripple/unity/app_tx.cpp b/src/ripple/unity/app_tx.cpp index a676fb5f3..f4d9e960f 100644 --- a/src/ripple/unity/app_tx.cpp +++ b/src/ripple/unity/app_tx.cpp @@ -22,9 +22,12 @@ #include #include #include +#include #include #include +#include #include +#include #include #include #include diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp new file mode 100644 index 000000000..777070b77 --- /dev/null +++ b/src/test/app/Check_test.cpp @@ -0,0 +1,1768 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2017 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 + +namespace ripple { + +// For the time being Checks seem pretty much self contained. So the +// functions that operate on jtx are defined here, locally. If they are +// needed by other unit tests they could put in another file. +namespace test { +namespace jtx { +namespace check { + +// Create a check. +Json::Value +create (jtx::Account const& account, + jtx::Account const& dest, STAmount const& sendMax) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfSendMax.jsonName] = sendMax.getJson(0); + jv[sfDestination.jsonName] = dest.human(); + jv[sfTransactionType.jsonName] = "CheckCreate"; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +// Type used to specify DeliverMin for cashing a check. +struct DeliverMin +{ + STAmount value; + DeliverMin (STAmount const& deliverMin) + : value (deliverMin) { } +}; + +// Cash a check. +Json::Value +cash (jtx::Account const& dest, + uint256 const& checkId, STAmount const& amount) +{ + Json::Value jv; + jv[sfAccount.jsonName] = dest.human(); + jv[sfAmount.jsonName] = amount.getJson(0); + jv[sfCheckID.jsonName] = to_string (checkId); + jv[sfTransactionType.jsonName] = "CheckCash"; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +Json::Value +cash (jtx::Account const& dest, + uint256 const& checkId, DeliverMin const& atLeast) +{ + Json::Value jv; + jv[sfAccount.jsonName] = dest.human(); + jv[sfDeliverMin.jsonName] = atLeast.value.getJson(0); + jv[sfCheckID.jsonName] = to_string (checkId); + jv[sfTransactionType.jsonName] = "CheckCash"; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +// Cancel a check. +Json::Value +cancel (jtx::Account const& dest, uint256 const& checkId) +{ + Json::Value jv; + jv[sfAccount.jsonName] = dest.human(); + jv[sfCheckID.jsonName] = to_string (checkId); + jv[sfTransactionType.jsonName] = "CheckCancel"; + jv[sfFlags.jsonName] = tfUniversal; + return jv; +} + +} // namespace check + +/** Set Expiration on a JTx. */ +class expiration +{ +private: + std::uint32_t const expry_; + +public: + expiration (NetClock::time_point const& expiry) + : expry_{expiry.time_since_epoch().count()} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfExpiration.jsonName] = expry_; + } +}; + +/** Set SourceTag on a JTx. */ +class source_tag +{ +private: + std::uint32_t const tag_; + +public: + source_tag (std::uint32_t tag) + : tag_{tag} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfSourceTag.jsonName] = tag_; + } +}; + +/** Set DestinationTag on a JTx. */ +class dest_tag +{ +private: + std::uint32_t const tag_; + +public: + dest_tag (std::uint32_t tag) + : tag_{tag} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfDestinationTag.jsonName] = tag_; + } +}; + +/** Set InvoiceID on a JTx. */ +class invoice_id +{ +private: + uint256 const id_; + +public: + invoice_id (uint256 const& id) + : id_{id} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfInvoiceID.jsonName] = to_string (id_); + } +}; + +} // namespace jtx +} // namespace test + +class Check_test : public beast::unit_test::suite +{ + // Helper function that returns the Checks on an account. + static std::vector> + checksOnAccount (test::jtx::Env& env, test::jtx::Account account) + { + std::vector> result; + forEachItem (*env.current (), account, + [&result](std::shared_ptr const& sle) + { + if (sle->getType() == ltCHECK) + result.push_back (sle); + }); + return result; + } + + // Helper function that returns the owner count on an account. + static std::uint32_t + ownerCount (test::jtx::Env const& env, test::jtx::Account const& account) + { + std::uint32_t ret {0}; + if (auto const sleAccount = env.le(account)) + ret = sleAccount->getFieldU32(sfOwnerCount); + return ret; + } + + void testEnabled() + { + testcase ("Enabled"); + + using namespace test::jtx; + Account const alice {"alice"}; + { + // If the Checks amendment is not enabled, you should not be able + // to create, cash, or cancel checks. + Env env {*this, supported_amendments() - featureChecks}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), alice); + + uint256 const checkId { + getCheckIndex (env.master, env.seq (env.master))}; + env (check::create (env.master, alice, XRP(100)), + ter(temDISABLED)); + env.close(); + + env (check::cash (alice, checkId, XRP(100)), + ter (temDISABLED)); + env.close(); + + env (check::cancel (alice, checkId), ter (temDISABLED)); + env.close(); + } + { + // If the Checks amendment is enabled all check-related + // facilities should be available. + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), alice); + + uint256 const checkId1 { + getCheckIndex (env.master, env.seq (env.master))}; + env (check::create (env.master, alice, XRP(100))); + env.close(); + + env (check::cash (alice, checkId1, XRP(100))); + env.close(); + + uint256 const checkId2 { + getCheckIndex (env.master, env.seq (env.master))}; + env (check::create (env.master, alice, XRP(100))); + env.close(); + + env (check::cancel (alice, checkId2)); + env.close(); + } + } + + void testCreateValid() + { + // Explore many of the valid ways to create a check. + testcase ("Create valid"); + + using namespace test::jtx; + + Account const gw {"gateway"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + IOU const USD {gw["USD"]}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + STAmount const startBalance {XRP(1000).value()}; + env.fund (startBalance, gw, alice, bob); + + // Note that no trust line has been set up for alice, but alice can + // still write a check for USD. You don't have to have the funds + // necessary to cover a check in order to write a check. + auto writeTwoChecks = + [&env, &USD, this] (Account const& from, Account const& to) + { + std::uint32_t const fromOwnerCount {ownerCount (env, from)}; + std::uint32_t const toOwnerCount {ownerCount (env, to )}; + + std::size_t const fromCkCount {checksOnAccount (env, from).size()}; + std::size_t const toCkCount {checksOnAccount (env, to ).size()}; + + env (check::create (from, to, XRP(2000))); + env.close(); + + env (check::create (from, to, USD(50))); + env.close(); + + BEAST_EXPECT ( + checksOnAccount (env, from).size() == fromCkCount + 2); + BEAST_EXPECT ( + checksOnAccount (env, to ).size() == toCkCount + 2); + + env.require (owners (from, fromOwnerCount + 2)); + env.require (owners (to, + to == from ? fromOwnerCount + 2 : toOwnerCount)); + }; + // from to + writeTwoChecks (alice, bob); + writeTwoChecks (gw, alice); + writeTwoChecks (alice, gw); + + // Now try adding the various optional fields. There's no + // expected interaction between these optional fields; other than + // the expiration, they are just plopped into the ledger. So I'm + // not looking at interactions. + std::size_t const aliceCount {checksOnAccount (env, alice).size()}; + std::size_t const bobCount {checksOnAccount (env, bob).size()}; + env (check::create (alice, bob, USD(50)), expiration (env.now() + 1s)); + env.close(); + + env (check::create (alice, bob, USD(50)), source_tag (2)); + env.close(); + env (check::create (alice, bob, USD(50)), dest_tag (3)); + env.close(); + env (check::create (alice, bob, USD(50)), invoice_id (uint256{4})); + env.close(); + env (check::create (alice, bob, USD(50)), expiration (env.now() + 1s), + source_tag (12), dest_tag (13), invoice_id (uint256{4})); + env.close(); + + BEAST_EXPECT (checksOnAccount (env, alice).size() == aliceCount + 5); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == bobCount + 5); + + // Use a regular key and also multisign to create a check. + Account const alie {"alie", KeyType::ed25519}; + env (regkey (alice, alie)); + env.close(); + + Account const bogie {"bogie", KeyType::secp256k1}; + Account const demon {"demon", KeyType::ed25519}; + env (signers (alice, 2, {{bogie, 1}, {demon, 1}}), sig (alie)); + env.close(); + + // alice uses her regular key to create a check. + env (check::create (alice, bob, USD(50)), sig (alie)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == aliceCount + 6); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == bobCount + 6); + + // alice uses multisigning to create a check. + std::uint64_t const baseFeeDrops {env.current()->fees().base}; + env (check::create (alice, bob, USD(50)), + msig (bogie, demon), fee (3 * baseFeeDrops)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == aliceCount + 7); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == bobCount + 7); + } + + void testCreateInvalid() + { + // Explore many of the invalid ways to create a check. + testcase ("Create invalid"); + + using namespace test::jtx; + + Account const gw1 {"gateway1"}; + Account const gwF {"gatewayFrozen"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + IOU const USD {gw1["USD"]}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + STAmount const startBalance {XRP(1000).value()}; + env.fund (startBalance, gw1, gwF, alice, bob); + + // Bad fee. + env (check::create (alice, bob, USD(50)), fee (drops(-10)), + ter (temBAD_FEE)); + env.close(); + + // Bad flags. + env (check::create (alice, bob, USD(50)), + txflags (tfImmediateOrCancel), ter (temINVALID_FLAG)); + env.close(); + + // Check to self. + env (check::create (alice, alice, XRP(10)), ter (temREDUNDANT)); + env.close(); + + // Bad amount. + env (check::create (alice, bob, drops(-1)), ter (temBAD_AMOUNT)); + env.close(); + + env (check::create (alice, bob, drops(0)), ter (temBAD_AMOUNT)); + env.close(); + + env (check::create (alice, bob, drops(1))); + env.close(); + + env (check::create (alice, bob, USD(-1)), ter (temBAD_AMOUNT)); + env.close(); + + env (check::create (alice, bob, USD(0)), ter (temBAD_AMOUNT)); + env.close(); + + env (check::create (alice, bob, USD(1))); + env.close(); + { + IOU const BAD {gw1, badCurrency()}; + env (check::create (alice, bob, BAD(2)), ter (temBAD_CURRENCY)); + env.close(); + } + + // Bad expiration. + env (check::create (alice, bob, USD(50)), + expiration (NetClock::time_point{}), ter (temBAD_EXPIRATION)); + env.close(); + + // Destination does not exist. + Account const bogie {"bogie"}; + env (check::create (alice, bogie, USD(50)), ter (tecNO_DST)); + env.close(); + + // Require destination tag. + env (fset (bob, asfRequireDest)); + env.close(); + + env (check::create (alice, bob, USD(50)), ter (tecDST_TAG_NEEDED)); + env.close(); + + env (check::create (alice, bob, USD(50)), dest_tag(11)); + env.close(); + + env (fclear (bob, asfRequireDest)); + env.close(); + { + // Globally frozen asset. + IOU const USF {gwF["USF"]}; + env (fset(gwF, asfGlobalFreeze)); + env.close(); + + env (check::create (alice, bob, USF(50)), ter (tecFROZEN)); + env.close(); + + env (fclear(gwF, asfGlobalFreeze)); + env.close(); + + env (check::create (alice, bob, USF(50))); + env.close(); + } + { + // Frozen trust line. Check creation should be similar to payment + // behavior in the face of frozen trust lines. + env.trust (USD(1000), alice); + env.trust (USD(1000), bob); + env.close(); + env (pay (gw1, alice, USD(25))); + env (pay (gw1, bob, USD(25))); + env.close(); + + // Setting trustline freeze in one direction prevents alice from + // creating a check for USD. But bob and gw1 should still be able + // to create a check for USD to alice. + env (trust(gw1, alice["USD"](0), tfSetFreeze)); + env.close(); + env (check::create (alice, bob, USD(50)), ter (tecFROZEN)); + env.close(); + env (pay (alice, bob, USD(1)), ter (tecPATH_DRY)); + env.close(); + env (check::create (bob, alice, USD(50))); + env.close(); + env (pay (bob, alice, USD(1))); + env.close(); + env (check::create (gw1, alice, USD(50))); + env.close(); + env (pay (gw1, alice, USD(1))); + env.close(); + + // Clear that freeze. Now check creation works. + env (trust(gw1, alice["USD"](0), tfClearFreeze)); + env.close(); + env (check::create (alice, bob, USD(50))); + env.close(); + env (check::create (bob, alice, USD(50))); + env.close(); + env (check::create (gw1, alice, USD(50))); + env.close(); + + // Freezing in the other direction does not effect alice's USD + // check creation, but prevents bob and gw1 from writing a check + // for USD to alice. + env (trust(alice, USD(0), tfSetFreeze)); + env.close(); + env (check::create (alice, bob, USD(50))); + env.close(); + env (pay (alice, bob, USD(1))); + env.close(); + env (check::create (bob, alice, USD(50)), ter (tecFROZEN)); + env.close(); + env (pay (bob, alice, USD(1)), ter (tecPATH_DRY)); + env.close(); + env (check::create (gw1, alice, USD(50)), ter (tecFROZEN)); + env.close(); + env (pay (gw1, alice, USD(1)), ter (tecPATH_DRY)); + env.close(); + + // Clear that freeze. + env(trust(alice, USD(0), tfClearFreeze)); + env.close(); + } + + // Expired expiration. + env (check::create (alice, bob, USD(50)), + expiration (env.now()), ter (tecEXPIRED)); + env.close(); + + env (check::create (alice, bob, USD(50)), expiration (env.now() + 1s)); + env.close(); + + // Insufficient reserve. + Account const cheri {"cheri"}; + env.fund ( + env.current()->fees().accountReserve(1) - drops(1), cheri); + + env (check::create (cheri, bob, USD(50)), + fee (drops (env.current()->fees().base)), + ter (tecINSUFFICIENT_RESERVE)); + env.close(); + + env (pay (bob, cheri, drops (env.current()->fees().base + 1))); + env.close(); + + env (check::create (cheri, bob, USD(50))); + env.close(); + } + + void testCashXRP() + { + // Explore many of the valid ways to cash a check for XRP. + testcase ("Cash XRP"); + + using namespace test::jtx; + + Account const alice {"alice"}; + Account const bob {"bob"}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + std::uint64_t const baseFeeDrops {env.current()->fees().base}; + STAmount const startBalance {XRP(300).value()}; + env.fund (startBalance, alice, bob); + { + // Basic XRP check. + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10))); + env.close(); + env.require (balance (alice, startBalance - drops (baseFeeDrops))); + env.require (balance (bob, startBalance)); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 0); + + env (check::cash (bob, chkId, XRP(10))); + env.close(); + env.require (balance (alice, + startBalance - XRP(10) - drops (baseFeeDrops))); + env.require (balance (bob, + startBalance + XRP(10) - drops (baseFeeDrops))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 0); + BEAST_EXPECT (ownerCount (env, bob ) == 0); + + // Make alice's and bob's balances easy to think about. + env (pay (env.master, alice, XRP(10) + drops (baseFeeDrops))); + env (pay (bob, env.master, XRP(10) - drops (baseFeeDrops * 2))); + env.close(); + env.require (balance (alice, startBalance)); + env.require (balance (bob, startBalance)); + } + { + // Write a check that chews into alice's reserve. + STAmount const reserve {env.current()->fees().accountReserve (0)}; + STAmount const checkAmount { + startBalance - reserve - drops (baseFeeDrops)}; + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, checkAmount)); + env.close(); + + // bob tries to cash for more than the check amount. + env (check::cash (bob, chkId, checkAmount + drops(1)), + ter (tecPATH_PARTIAL)); + env.close(); + env (check::cash ( + bob, chkId, check::DeliverMin (checkAmount + drops(1))), + ter (tecPATH_PARTIAL)); + env.close(); + + // bob cashes exactly the check amount. This is successful + // because one unit of alice's reserve is released when the + // check is consumed. + env (check::cash (bob, chkId, check::DeliverMin (checkAmount))); + env.close(); + env.require (balance (alice, reserve)); + env.require (balance (bob, + startBalance + checkAmount - drops (baseFeeDrops * 3))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 0); + BEAST_EXPECT (ownerCount (env, bob ) == 0); + + // Make alice's and bob's balances easy to think about. + env (pay (env.master, alice, checkAmount + drops (baseFeeDrops))); + env (pay (bob, env.master, checkAmount - drops (baseFeeDrops * 4))); + env.close(); + env.require (balance (alice, startBalance)); + env.require (balance (bob, startBalance)); + } + { + // Write a check that goes one drop past what alice can pay. + STAmount const reserve {env.current()->fees().accountReserve (0)}; + STAmount const checkAmount { + startBalance - reserve - drops (baseFeeDrops - 1)}; + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, checkAmount)); + env.close(); + + // bob tries to cash for exactly the check amount. Fails because + // alice is one drop shy of funding the check. + env (check::cash (bob, chkId, checkAmount), ter (tecPATH_PARTIAL)); + env.close(); + + // bob decides to get what he can from the bounced check. + env (check::cash (bob, chkId, check::DeliverMin (drops(1)))); + env.close(); + env.require (balance (alice, reserve)); + env.require (balance (bob, + startBalance + checkAmount - drops ((baseFeeDrops * 2) + 1))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 0); + BEAST_EXPECT (ownerCount (env, bob ) == 0); + + // Make alice's and bob's balances easy to think about. + env (pay (env.master, alice, + checkAmount + drops (baseFeeDrops - 1))); + env (pay (bob, env.master, + checkAmount - drops ((baseFeeDrops * 3) + 1))); + env.close(); + env.require (balance (alice, startBalance)); + env.require (balance (bob, startBalance)); + } + } + + void testCashIOU () + { + // Explore many of the valid ways to cash a check for an IOU. + testcase ("Cash IOU"); + + using namespace test::jtx; + + Account const gw {"gateway"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + IOU const USD {gw["USD"]}; + { + // Simple IOU check cashed with Amount (with failures). + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob); + + // alice writes the check before she gets the funds. + uint256 const chkId1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10))); + env.close(); + + // bob attempts to cash the check. Should fail. + env (check::cash (bob, chkId1, USD(10)), ter (tecPATH_PARTIAL)); + env.close(); + + // alice gets almost enough funds. bob tries and fails again. + env (trust (alice, USD(20))); + env.close(); + env (pay (gw, alice, USD(9.5))); + env.close(); + env (check::cash (bob, chkId1, USD(10)), ter (tecPATH_PARTIAL)); + env.close(); + + // alice gets the last of the necessary funds. bob tries again + // and fails because he hasn't got a trust line for USD. + env (pay (gw, alice, USD(0.5))); + env.close(); + env (check::cash (bob, chkId1, USD(10)), ter (tecNO_LINE)); + env.close(); + + // bob sets up the trust line, but not at a high enough limit. + env (trust (bob, USD(9.5))); + env.close(); + env (check::cash (bob, chkId1, USD(10)), ter (tecPATH_PARTIAL)); + env.close(); + + // bob sets the trust line limit high enough but asks for more + // than the check's SendMax. + env (trust (bob, USD(10.5))); + env.close(); + env (check::cash (bob, chkId1, USD(10.5)), ter (tecPATH_PARTIAL)); + env.close(); + + // bob asks for exactly the check amount and the check clears. + env (check::cash (bob, chkId1, USD(10))); + env.close(); + env.require (balance (alice, USD( 0))); + env.require (balance (bob, USD(10))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // bob tries to cash the same check again, which fails. + env (check::cash (bob, chkId1, USD(10)), ter (tecNO_ENTRY)); + env.close(); + + // bob pays alice USD(7) so he can try another case. + env (pay (bob, alice, USD(7))); + env.close(); + + uint256 const chkId2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(7))); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + + // bob cashes the check for less than the face amount. That works, + // consumes the check, and bob receives as much as he asked for. + env (check::cash (bob, chkId2, USD(5))); + env.close(); + env.require (balance (alice, USD(2))); + env.require (balance (bob, USD(8))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // alice writes two checks for USD(2), although she only has USD(2). + uint256 const chkId3 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(2))); + env.close(); + uint256 const chkId4 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(2))); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 2); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 2); + + // bob cashes the second check for the face amount. + env (check::cash (bob, chkId4, USD(2))); + env.close(); + env.require (balance (alice, USD( 0))); + env.require (balance (bob, USD(10))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + BEAST_EXPECT (ownerCount (env, alice) == 2); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // bob is not allowed to cash the last check for USD(0), he must + // use check::cancel instead. + env (check::cash (bob, chkId3, USD(0)), ter (temBAD_AMOUNT)); + env.close(); + env.require (balance (alice, USD( 0))); + env.require (balance (bob, USD(10))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + BEAST_EXPECT (ownerCount (env, alice) == 2); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // ... so bob cancels alice's remaining check. + env (check::cancel (bob, chkId3)); + env.close(); + env.require (balance (alice, USD( 0))); + env.require (balance (bob, USD(10))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + } + { + // Simple IOU check cashed with DeliverMin (with failures). + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob); + + env (trust (alice, USD(20))); + env (trust (bob, USD(20))); + env.close(); + env (pay (gw, alice, USD(8))); + env.close(); + + // alice creates several checks ahead of time. + uint256 const chkId9 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(9))); + env.close(); + uint256 const chkId8 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(8))); + env.close(); + uint256 const chkId7 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(7))); + env.close(); + uint256 const chkId6 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(6))); + env.close(); + + // bob attempts to cash a check for the amount on the check. + // Should fail, since alice doesn't have the funds. + env (check::cash (bob, chkId9, check::DeliverMin (USD(9))), + ter (tecPATH_PARTIAL)); + env.close(); + + // bob sets a DeliverMin of 7 and gets all that alice has. + env (check::cash (bob, chkId9, check::DeliverMin (USD(7)))); + env.close(); + env.require (balance (alice, USD(0))); + env.require (balance (bob, USD(8))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 3); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 3); + BEAST_EXPECT (ownerCount (env, alice) == 4); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // bob pays alice USD(7) so he can use another check. + env (pay (bob, alice, USD(7))); + env.close(); + + // Using DeliverMin for the SendMax value of the check (and no + // transfer fees) should work just like setting Amount. + env (check::cash (bob, chkId7, check::DeliverMin (USD(7)))); + env.close(); + env.require (balance (alice, USD(0))); + env.require (balance (bob, USD(8))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 2); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 2); + BEAST_EXPECT (ownerCount (env, alice) == 3); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // bob pays alice USD(8) so he can use the last two checks. + env (pay (bob, alice, USD(8))); + env.close(); + + // alice has USD(8). If bob uses the check for USD(6) and uses a + // DeliverMin of 4, he should get the SendMax value of the check. + env (check::cash (bob, chkId6, check::DeliverMin (USD(4)))); + env.close(); + env.require (balance (alice, USD(2))); + env.require (balance (bob, USD(6))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + BEAST_EXPECT (ownerCount (env, alice) == 2); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + + // bob cashes the last remaining check setting a DeliverMin. + // of exactly alice's remaining USD. + env (check::cash (bob, chkId8, check::DeliverMin (USD(2)))); + env.close(); + env.require (balance (alice, USD(0))); + env.require (balance (bob, USD(8))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + } + { + // Examine the effects of the asfRequireAuth flag. + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob); + env (fset (gw, asfRequireAuth)); + env.close(); + env (trust (gw, alice["USD"](100)), txflags (tfSetfAuth)); + env (trust (alice, USD(20))); + env.close(); + env (pay (gw, alice, USD(8))); + env.close(); + + // alice writes a check to bob for USD. bob can't cash it + // because he is not authorized to hold gw["USD"]. + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(7))); + env.close(); + + env (check::cash (bob, chkId, USD(7)), ter (tecNO_LINE)); + env.close(); + + // Now give bob a trustline for USD. bob still can't cash the + // check because he is not authorized. + env (trust (bob, USD(5))); + env.close(); + + env (check::cash (bob, chkId, USD(7)), ter (tecNO_AUTH)); + env.close(); + + // bob gets authorization to hold gw["USD"]. + env (trust (gw, bob["USD"](1)), txflags (tfSetfAuth)); + env.close(); + + // bob tries to cash the check again but fails because his trust + // limit is too low. + env (check::cash (bob, chkId, USD(7)), ter (tecPATH_PARTIAL)); + env.close(); + + // Since bob set his limit low, he cashes the check with a + // DeliverMin and hits his trust limit. + env (check::cash (bob, chkId, check::DeliverMin (USD(4)))); + env.close(); + env.require (balance (alice, USD(3))); + env.require (balance (bob, USD(5))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 1); + } + { + // Use a regular key and also multisign to cash a check. + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob); + + // alice creates her checks ahead of time. + uint256 const chkId1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(1))); + env.close(); + + uint256 const chkId2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(2))); + env.close(); + + env (trust (alice, USD(20))); + env (trust (bob, USD(20))); + env.close(); + env (pay (gw, alice, USD(8))); + env.close(); + + // Give bob a regular key and signers + Account const bobby {"bobby", KeyType::secp256k1}; + env (regkey (bob, bobby)); + env.close(); + + Account const bogie {"bogie", KeyType::secp256k1}; + Account const demon {"demon", KeyType::ed25519}; + env (signers (bob, 2, {{bogie, 1}, {demon, 1}}), sig (bobby)); + env.close(); + BEAST_EXPECT (ownerCount (env, bob) == 5); // signerList -> 4 owners + + // bob uses his regular key to cash a check. + env (check::cash (bob, chkId1, (USD(1))), sig (bobby)); + env.close(); + env.require (balance (alice, USD(7))); + env.require (balance (bob, USD(1))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + BEAST_EXPECT (ownerCount (env, alice) == 2); + BEAST_EXPECT (ownerCount (env, bob ) == 5); + + // bob uses multisigning to cash a check. + std::uint64_t const baseFeeDrops {env.current()->fees().base}; + env (check::cash (bob, chkId2, (USD(2))), + msig (bogie, demon), fee (3 * baseFeeDrops)); + env.close(); + env.require (balance (alice, USD(5))); + env.require (balance (bob, USD(3))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 1); + BEAST_EXPECT (ownerCount (env, bob ) == 5); + } + } + + void testCashXferFee() + { + // Look at behavior when the issuer charges a transfer fee. + testcase ("Cash with transfer fee"); + + using namespace test::jtx; + + Account const gw {"gateway"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + IOU const USD {gw["USD"]}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob); + + env (trust (alice, USD(1000))); + env (trust (bob, USD(1000))); + env.close(); + env (pay (gw, alice, USD(1000))); + env.close(); + + // Set gw's transfer rate and see the consequences when cashing a check. + env (rate (gw, 1.25)); + env.close(); + + // alice writes a check with a SendMax of USD(125). The most bob + // can get is USD(100) because of the transfer rate. + uint256 const chkId125 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(125))); + env.close(); + + // alice writes another check that won't get cashed until the transfer + // rate changes so we can see the rate applies when the check is + // cashed, not when it is created. + uint256 const chkId120 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(120))); + env.close(); + + // bob attempts to cash the check for face value. Should fail. + env (check::cash (bob, chkId125, USD(125)), ter (tecPATH_PARTIAL)); + env.close(); + env (check::cash (bob, chkId125, check::DeliverMin (USD(101))), + ter (tecPATH_PARTIAL)); + env.close(); + + // bob decides that he'll accept anything USD(75) or up. + // He gets USD(100). + env (check::cash (bob, chkId125, check::DeliverMin (USD(75)))); + env.close(); + env.require (balance (alice, USD(1000 - 125))); + env.require (balance (bob, USD( 0 + 100))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 1); + + // Adjust gw's rate... + env (rate (gw, 1.2)); + env.close(); + + // bob cashes the second check for less than the face value. The new + // rate applies to the actual value transferred. + env (check::cash (bob, chkId120, USD(50))); + env.close(); + env.require (balance (alice, USD(1000 - 125 - 60))); + env.require (balance (bob, USD( 0 + 100 + 50))); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (checksOnAccount (env, bob ).size() == 0); + } + + void testCashQuality () + { + // Look at the eight possible cases for Quality In/Out. + testcase ("Cash quality"); + + using namespace test::jtx; + + Account const gw {"gateway"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + IOU const USD {gw["USD"]}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob); + + env (trust (alice, USD(1000))); + env (trust (bob, USD(1000))); + env.close(); + env (pay (gw, alice, USD(1000))); + env.close(); + + // + // Quality effects on transfers between two non-issuers. + // + + // Provide lambdas that return a qualityInPercent and qualityOutPercent. + auto qIn = + [] (double percent) { return qualityInPercent (percent); }; + auto qOut = + [] (double percent) { return qualityOutPercent (percent); }; + + // There are two test lambdas: one for a Payment and one for a Check. + // This shows whether a Payment and a Check behave the same. + auto testNonIssuerQPay = [&env, &alice, &bob, &USD] + (Account const& truster, + IOU const& iou, auto const& inOrOut, double pct, double amount) + { + // Capture bob's and alice's balances so we can test at the end. + STAmount const aliceStart {env.balance (alice, USD.issue()).value()}; + STAmount const bobStart {env.balance (bob, USD.issue()).value()}; + + // Set the modified quality. + env (trust (truster, iou(1000)), inOrOut (pct)); + env.close(); + + env (pay (alice, bob, USD(amount)), sendmax (USD(10))); + env.close(); + env.require (balance (alice, aliceStart - USD(10))); + env.require (balance (bob, bobStart + USD(10))); + + // Return the quality to the unmodified state so it doesn't + // interfere with upcoming tests. + env (trust (truster, iou(1000)), inOrOut (0)); + env.close(); + }; + + auto testNonIssuerQCheck = [&env, &alice, &bob, &USD] + (Account const& truster, + IOU const& iou, auto const& inOrOut, double pct, double amount) + { + // Capture bob's and alice's balances so we can test at the end. + STAmount const aliceStart {env.balance (alice, USD.issue()).value()}; + STAmount const bobStart {env.balance (bob, USD.issue()).value()}; + + // Set the modified quality. + env (trust (truster, iou(1000)), inOrOut (pct)); + env.close(); + + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10))); + env.close(); + + env (check::cash (bob, chkId, USD(amount))); + env.close(); + env.require (balance (alice, aliceStart - USD(10))); + env.require (balance (bob, bobStart + USD(10))); + + // Return the quality to the unmodified state so it doesn't + // interfere with upcoming tests. + env (trust (truster, iou(1000)), inOrOut (0)); + env.close(); + }; + + // pct amount + testNonIssuerQPay (alice, gw["USD"], qIn, 50, 10); + testNonIssuerQCheck (alice, gw["USD"], qIn, 50, 10); + + // This is the only case where the Quality affects the outcome. + testNonIssuerQPay (bob, gw["USD"], qIn, 50, 5); + testNonIssuerQCheck (bob, gw["USD"], qIn, 50, 5); + + testNonIssuerQPay (gw, alice["USD"], qIn, 50, 10); + testNonIssuerQCheck (gw, alice["USD"], qIn, 50, 10); + + testNonIssuerQPay (gw, bob["USD"], qIn, 50, 10); + testNonIssuerQCheck (gw, bob["USD"], qIn, 50, 10); + + testNonIssuerQPay (alice, gw["USD"], qOut, 200, 10); + testNonIssuerQCheck (alice, gw["USD"], qOut, 200, 10); + + testNonIssuerQPay (bob, gw["USD"], qOut, 200, 10); + testNonIssuerQCheck (bob, gw["USD"], qOut, 200, 10); + + testNonIssuerQPay (gw, alice["USD"], qOut, 200, 10); + testNonIssuerQCheck (gw, alice["USD"], qOut, 200, 10); + + testNonIssuerQPay (gw, bob["USD"], qOut, 200, 10); + testNonIssuerQCheck (gw, bob["USD"], qOut, 200, 10); + + // + // Quality effects on transfers between an issuer and a non-issuer. + // + + // There are two test lambdas for the same reason as before. + auto testIssuerQPay = [&env, &gw, &alice, &USD] + (Account const& truster, IOU const& iou, + auto const& inOrOut, double pct, + double amt1, double max1, double amt2, double max2) + { + // Capture alice's balance so we can test at the end. It doesn't + // make any sense to look at the balance of a gateway. + STAmount const aliceStart {env.balance (alice, USD.issue()).value()}; + + // Set the modified quality. + env (trust (truster, iou(1000)), inOrOut (pct)); + env.close(); + + // alice pays gw. + env (pay (alice, gw, USD(amt1)), sendmax (USD(max1))); + env.close(); + env.require (balance (alice, aliceStart - USD(10))); + + // gw pays alice. + env (pay (gw, alice, USD(amt2)), sendmax (USD(max2))); + env.close(); + env.require (balance (alice, aliceStart)); + + // Return the quality to the unmodified state so it doesn't + // interfere with upcoming tests. + env (trust (truster, iou(1000)), inOrOut (0)); + env.close(); + }; + + auto testIssuerQCheck = [&env, &gw, &alice, &USD] + (Account const& truster, IOU const& iou, + auto const& inOrOut, double pct, + double amt1, double max1, double amt2, double max2) + { + // Capture alice's balance so we can test at the end. It doesn't + // make any sense to look at the balance of the issuer. + STAmount const aliceStart {env.balance (alice, USD.issue()).value()}; + + // Set the modified quality. + env (trust (truster, iou(1000)), inOrOut (pct)); + env.close(); + + // alice writes check to gw. gw cashes. + uint256 const chkAliceId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, gw, USD(max1))); + env.close(); + + env (check::cash (gw, chkAliceId, USD(amt1))); + env.close(); + env.require (balance (alice, aliceStart - USD(10))); + + // gw writes check to alice. alice cashes. + uint256 const chkGwId {getCheckIndex (gw, env.seq (gw))}; + env (check::create (gw, alice, USD(max2))); + env.close(); + + env (check::cash (alice, chkGwId, USD(amt2))); + env.close(); + env.require (balance (alice, aliceStart)); + + // Return the quality to the unmodified state so it doesn't + // interfere with upcoming tests. + env (trust (truster, iou(1000)), inOrOut (0)); + env.close(); + }; + + // The first case is the only one where the quality affects the outcome. + // pct amt1 max1 amt2 max2 + testIssuerQPay (alice, gw["USD"], qIn, 50, 10, 10, 5, 10); + testIssuerQCheck (alice, gw["USD"], qIn, 50, 10, 10, 5, 10); + + testIssuerQPay (gw, alice["USD"], qIn, 50, 10, 10, 10, 10); + testIssuerQCheck (gw, alice["USD"], qIn, 50, 10, 10, 10, 10); + + testIssuerQPay (alice, gw["USD"], qOut, 200, 10, 10, 10, 10); + testIssuerQCheck (alice, gw["USD"], qOut, 200, 10, 10, 10, 10); + + testIssuerQPay (gw, alice["USD"], qOut, 200, 10, 10, 10, 10); + testIssuerQCheck (gw, alice["USD"], qOut, 200, 10, 10, 10, 10); + } + + void testCashInvalid() + { + // Explore many of the ways to fail at cashing a check. + testcase ("Cash invalid"); + + using namespace test::jtx; + + Account const gw {"gateway"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + Account const zoe {"zoe"}; + IOU const USD {gw["USD"]}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob, zoe); + + // Now set up alice's trustline. + env (trust (alice, USD(20))); + env.close(); + env (pay (gw, alice, USD(20))); + env.close(); + + // Before bob gets a trustline, have him try to cash a check. + // Should fail. + { + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(20))); + env.close(); + + env (check::cash (bob, chkId, USD(20)), ter (tecNO_LINE)); + env.close(); + } + + // Now set up bob's trustline. + env (trust (bob, USD(20))); + env.close(); + + // bob tries to cash a non-existent check from alice. + { + uint256 const chkId {getCheckIndex (alice, env.seq (alice))}; + env (check::cash (bob, chkId, USD(20)), ter (tecNO_ENTRY)); + env.close(); + } + + // alice creates her checks ahead of time. + uint256 const chkIdU {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(20))); + env.close(); + + uint256 const chkIdX {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10))); + env.close(); + + uint256 const chkIdExp {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10)), expiration (env.now() + 1s)); + env.close(); + + uint256 const chkIdFroz1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(1))); + env.close(); + + uint256 const chkIdFroz2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(2))); + env.close(); + + uint256 const chkIdFroz3 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(3))); + env.close(); + + uint256 const chkIdFroz4 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(4))); + env.close(); + + uint256 const chkIdNoDest1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(1))); + env.close(); + + uint256 const chkIdHasDest2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(2)), dest_tag (7)); + env.close(); + + // Same set of failing cases for both IOU and XRP check cashing. + auto failingCases = [&env, &gw, &alice, &bob] ( + uint256 const& chkId, STAmount const& amount) + { + // Bad fee. + env (check::cash (bob, chkId, amount), fee (drops(-10)), + ter (temBAD_FEE)); + env.close(); + + // Bad flags. + env (check::cash (bob, chkId, amount), + txflags (tfImmediateOrCancel), ter (temINVALID_FLAG)); + env.close(); + + // Missing both Amount and DeliverMin. + { + Json::Value tx {check::cash (bob, chkId, amount)}; + tx.removeMember (sfAmount.jsonName); + env (tx, ter (temMALFORMED)); + env.close(); + } + // Both Amount and DeliverMin present. + { + Json::Value tx {check::cash (bob, chkId, amount)}; + tx[sfDeliverMin.jsonName] = amount.getJson(0); + env (tx, ter (temMALFORMED)); + env.close(); + } + + // Negative or zero amount. + { + STAmount neg {amount}; + neg.negate(); + env (check::cash (bob, chkId, neg), ter (temBAD_AMOUNT)); + env.close(); + env (check::cash (bob, chkId, amount.zeroed()), + ter (temBAD_AMOUNT)); + env.close(); + } + + // Bad currency. + if (! amount.native()) { + Issue const badIssue {badCurrency(), amount.getIssuer()}; + STAmount badAmount {amount}; + badAmount.setIssue (Issue {badCurrency(), amount.getIssuer()}); + env (check::cash (bob, chkId, badAmount), + ter (temBAD_CURRENCY)); + env.close(); + } + + // Not destination cashing check. + env (check::cash (alice, chkId, amount), ter (tecNO_PERMISSION)); + env.close(); + env (check::cash (gw, chkId, amount), ter (tecNO_PERMISSION)); + env.close(); + + // Currency mismatch. + { + IOU const wrongCurrency {gw["EUR"]}; + STAmount badAmount {amount}; + badAmount.setIssue (wrongCurrency.issue()); + env (check::cash (bob, chkId, badAmount), + ter (temMALFORMED)); + env.close(); + } + + // Issuer mismatch. + { + IOU const wrongIssuer {alice["USD"]}; + STAmount badAmount {amount}; + badAmount.setIssue (wrongIssuer.issue()); + env (check::cash (bob, chkId, badAmount), + ter (temMALFORMED)); + env.close(); + } + + // Amount bigger than SendMax. + env (check::cash (bob, chkId, amount + amount), + ter (tecPATH_PARTIAL)); + env.close(); + + // DeliverMin bigger than SendMax. + env (check::cash (bob, chkId, check::DeliverMin (amount + amount)), + ter (tecPATH_PARTIAL)); + env.close(); + }; + + failingCases (chkIdX, XRP(10)); + failingCases (chkIdU, USD(20)); + + // Verify that those two checks really were cashable. + env (check::cash (bob, chkIdU, USD(20))); + env.close(); + env (check::cash (bob, chkIdX, check::DeliverMin (XRP(10)))); + env.close(); + + // Try to cash an expired check. + env (check::cash (bob, chkIdExp, XRP(10)), ter (tecEXPIRED)); + env.close(); + + // Cancel the expired check. Anyone can cancel an expired check. + env (check::cancel (zoe, chkIdExp)); + env.close(); + + // Can we cash a check with frozen currency? + { + env (pay (bob, alice, USD(20))); + env.close(); + env.require (balance (alice, USD(20))); + env.require (balance (bob, USD( 0))); + + // Global freeze + env (fset (gw, asfGlobalFreeze)); + env.close(); + + env (check::cash (bob, chkIdFroz1, USD(1)), ter (tecPATH_PARTIAL)); + env.close(); + env (check::cash (bob, chkIdFroz1, check::DeliverMin (USD(0.5))), + ter (tecPATH_PARTIAL)); + env.close(); + + env (fclear (gw, asfGlobalFreeze)); + env.close(); + + // No longer frozen. Success. + env (check::cash (bob, chkIdFroz1, USD(1))); + env.close(); + env.require (balance (alice, USD(19))); + env.require (balance (bob, USD( 1))); + + // Freeze individual trustlines. + env (trust(gw, alice["USD"](0), tfSetFreeze)); + env.close(); + env (check::cash (bob, chkIdFroz2, USD(2)), ter (tecPATH_PARTIAL)); + env.close(); + env (check::cash (bob, chkIdFroz2, check::DeliverMin (USD(1))), + ter (tecPATH_PARTIAL)); + env.close(); + + // Clear that freeze. Now check cashing works. + env (trust(gw, alice["USD"](0), tfClearFreeze)); + env.close(); + env (check::cash (bob, chkIdFroz2, USD(2))); + env.close(); + env.require (balance (alice, USD(17))); + env.require (balance (bob, USD( 3))); + + // Freeze bob's trustline. bob can't cash the check. + env (trust(gw, bob["USD"](0), tfSetFreeze)); + env.close(); + env (check::cash (bob, chkIdFroz3, USD(3)), ter (tecFROZEN)); + env.close(); + env (check::cash (bob, chkIdFroz3, check::DeliverMin (USD(1))), + ter (tecFROZEN)); + env.close(); + + // Clear that freeze. Now check cashing works again. + env (trust(gw, bob["USD"](0), tfClearFreeze)); + env.close(); + env (check::cash (bob, chkIdFroz3, check::DeliverMin (USD(1)))); + env.close(); + env.require (balance (alice, USD(14))); + env.require (balance (bob, USD( 6))); + + // Set bob's freeze bit in the other direction. Check + // cashing fails. + env (trust (bob, USD(20), tfSetFreeze)); + env.close(); + env (check::cash (bob, chkIdFroz4, USD(4)), ter (terNO_LINE)); + env.close(); + env (check::cash (bob, chkIdFroz4, check::DeliverMin (USD(1))), + ter (terNO_LINE)); + env.close(); + + // Clear bob's freeze bit and the check should be cashable. + env (trust (bob, USD(20), tfClearFreeze)); + env.close(); + env (check::cash (bob, chkIdFroz4, USD(4))); + env.close(); + env.require (balance (alice, USD(10))); + env.require (balance (bob, USD(10))); + } + { + // Set the RequireDest flag on bob's account (after the check + // was created) then cash a check without a destination tag. + env (fset (bob, asfRequireDest)); + env.close(); + env (check::cash (bob, chkIdNoDest1, USD(1)), + ter (tecDST_TAG_NEEDED)); + env.close(); + env (check::cash (bob, chkIdNoDest1, check::DeliverMin (USD(0.5))), + ter (tecDST_TAG_NEEDED)); + env.close(); + + // bob can cash a check with a destination tag. + env (check::cash (bob, chkIdHasDest2, USD(2))); + env.close(); + env.require (balance (alice, USD( 8))); + env.require (balance (bob, USD(12))); + + // Clear the RequireDest flag on bob's account so he can + // cash the check with no DestinationTag. + env (fclear (bob, asfRequireDest)); + env.close(); + env (check::cash (bob, chkIdNoDest1, USD(1))); + env.close(); + env.require (balance (alice, USD( 7))); + env.require (balance (bob, USD(13))); + } + } + + void testCancelValid() + { + // Explore many of the ways to cancel a check. + testcase ("Cancel valid"); + + using namespace test::jtx; + + Account const gw {"gateway"}; + Account const alice {"alice"}; + Account const bob {"bob"}; + Account const zoe {"zoe"}; + IOU const USD {gw["USD"]}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), gw, alice, bob, zoe); + + // alice creates her checks ahead of time. + // Three ordinary checks with no expiration. + uint256 const chkId1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10))); + env.close(); + + uint256 const chkId2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10))); + env.close(); + + uint256 const chkId3 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10))); + env.close(); + + // Three checks that expire in 10 minutes. + uint256 const chkIdNotExp1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10)), expiration (env.now()+600s)); + env.close(); + + uint256 const chkIdNotExp2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10)), expiration (env.now()+600s)); + env.close(); + + uint256 const chkIdNotExp3 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10)), expiration (env.now()+600s)); + env.close(); + + // Three checks that expire in one second. + uint256 const chkIdExp1 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10)), expiration (env.now()+1s)); + env.close(); + + uint256 const chkIdExp2 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10)), expiration (env.now()+1s)); + env.close(); + + uint256 const chkIdExp3 {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10)), expiration (env.now()+1s)); + env.close(); + + // Two checks to cancel using a regular key and using multisigning. + uint256 const chkIdReg {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, USD(10))); + env.close(); + + uint256 const chkIdMSig {getCheckIndex (alice, env.seq (alice))}; + env (check::create (alice, bob, XRP(10))); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 11); + BEAST_EXPECT (ownerCount (env, alice) == 11); + + // Creator, destination, and an outsider cancel the checks. + env (check::cancel (alice, chkId1)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 10); + BEAST_EXPECT (ownerCount (env, alice) == 10); + + env (check::cancel (bob, chkId2)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 9); + BEAST_EXPECT (ownerCount (env, alice) == 9); + + env (check::cancel (zoe, chkId3), ter (tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 9); + BEAST_EXPECT (ownerCount (env, alice) == 9); + + // Creator, destination, and an outsider cancel unexpired checks. + env (check::cancel (alice, chkIdNotExp1)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 8); + BEAST_EXPECT (ownerCount (env, alice) == 8); + + env (check::cancel (bob, chkIdNotExp2)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 7); + BEAST_EXPECT (ownerCount (env, alice) == 7); + + env (check::cancel (zoe, chkIdNotExp3), ter (tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 7); + BEAST_EXPECT (ownerCount (env, alice) == 7); + + // Creator, destination, and an outsider cancel expired checks. + env (check::cancel (alice, chkIdExp1)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 6); + BEAST_EXPECT (ownerCount (env, alice) == 6); + + env (check::cancel (bob, chkIdExp2)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 5); + BEAST_EXPECT (ownerCount (env, alice) == 5); + + env (check::cancel (zoe, chkIdExp3)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 4); + BEAST_EXPECT (ownerCount (env, alice) == 4); + + // Use a regular key and also multisign to cancel checks. + Account const alie {"alie", KeyType::ed25519}; + env (regkey (alice, alie)); + env.close(); + + Account const bogie {"bogie", KeyType::secp256k1}; + Account const demon {"demon", KeyType::ed25519}; + env (signers (alice, 2, {{bogie, 1}, {demon, 1}}), sig (alie)); + env.close(); + + // alice uses her regular key to cancel a check. + env (check::cancel (alice, chkIdReg), sig (alie)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 3); + BEAST_EXPECT (ownerCount (env, alice) == 7); + + // alice uses multisigning to cancel a check. + std::uint64_t const baseFeeDrops {env.current()->fees().base}; + env (check::cancel (alice, chkIdMSig), + msig (bogie, demon), fee (3 * baseFeeDrops)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 2); + BEAST_EXPECT (ownerCount (env, alice) == 6); + + // Creator and destination cancel the remaining unexpired checks. + env (check::cancel (alice, chkId3), sig (alice)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 1); + BEAST_EXPECT (ownerCount (env, alice) == 5); + + env (check::cancel (bob, chkIdNotExp3)); + env.close(); + BEAST_EXPECT (checksOnAccount (env, alice).size() == 0); + BEAST_EXPECT (ownerCount (env, alice) == 4); + } + + void testCancelInvalid() + { + // Explore many of the ways to fail at canceling a check. + testcase ("Cancel invalid"); + + using namespace test::jtx; + + Account const alice {"alice"}; + Account const bob {"bob"}; + + Env env {*this}; + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP(1000), alice, bob); + + // Bad fee. + env (check::cancel (bob, getCheckIndex (alice, env.seq (alice))), + fee (drops(-10)), ter (temBAD_FEE)); + env.close(); + + // Bad flags. + env (check::cancel (bob, getCheckIndex (alice, env.seq (alice))), + txflags (tfImmediateOrCancel), ter (temINVALID_FLAG)); + env.close(); + + // Non-existent check. + env (check::cancel (bob, getCheckIndex (alice, env.seq (alice))), + ter (tecNO_ENTRY)); + env.close(); + } + +public: + void run () + { + testEnabled(); + testCreateValid(); + testCreateInvalid(); + testCashXRP(); + testCashIOU(); + testCashXferFee(); + testCashQuality(); + testCashInvalid(); + testCancelValid(); + testCancelInvalid(); + } +}; + +BEAST_DEFINE_TESTSUITE (Check, tx, ripple); + +} // ripple + diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 729113b94..f0e4accca 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -918,7 +918,6 @@ public: auto const f = env.current ()->fees ().base; - // Place an offer that should have already expired env (trust (alice, usdOffer), ter(tesSUCCESS)); env (pay (gw, alice, usdOffer), ter(tesSUCCESS)); env.close(); @@ -928,8 +927,13 @@ public: offers (alice, 0), owners (alice, 1)); - env (offer (alice, xrpOffer, usdOffer), - json (key, lastClose(env)), ter(tesSUCCESS)); + // Place an offer that should have already expired. + // The Checks amendment changes the return code; adapt to that. + bool const featChecks {features[featureChecks]}; + + env (offer (alice, xrpOffer, usdOffer), json (key, lastClose(env)), + ter (featChecks ? tecEXPIRED : tesSUCCESS)); + env.require ( balance (alice, startBalance - f - f), balance (alice, usdOffer), @@ -937,7 +941,7 @@ public: owners (alice, 1)); env.close(); - // Add an offer that's expires before the next ledger close + // Add an offer that expires before the next ledger close env (offer (alice, xrpOffer, usdOffer), json (key, lastClose(env) + 1), ter(tesSUCCESS)); env.require ( diff --git a/src/test/unity/app_test_unity1.cpp b/src/test/unity/app_test_unity1.cpp index 613a81202..374f88dcb 100644 --- a/src/test/unity/app_test_unity1.cpp +++ b/src/test/unity/app_test_unity1.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include