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.
This commit is contained in:
Scott Schurr
2017-10-13 13:52:41 -07:00
parent 76ad06ef47
commit 2d5ddbf1bf
31 changed files with 2981 additions and 107 deletions

View File

@@ -1309,6 +1309,12 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\BookTip.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CancelCheck.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CancelCheck.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CancelOffer.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -1321,12 +1327,24 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CancelTicket.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CashCheck.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CashCheck.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\Change.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\Change.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CreateCheck.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CreateCheck.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CreateOffer.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -4634,6 +4652,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\app\Check_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\test\app\CrossingLimits_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -1878,6 +1878,12 @@
<ClInclude Include="..\..\src\ripple\app\tx\impl\BookTip.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CancelCheck.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CancelCheck.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CancelOffer.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
@@ -1890,12 +1896,24 @@
<ClInclude Include="..\..\src\ripple\app\tx\impl\CancelTicket.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CashCheck.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CashCheck.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\Change.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\Change.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CreateCheck.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\CreateCheck.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\CreateOffer.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
@@ -5592,6 +5610,9 @@
<ClCompile Include="..\..\src\test\app\AmendmentTable_test.cpp">
<Filter>test\app</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\app\Check_test.cpp">
<Filter>test\app</Filter>
</ClCompile>
<ClCompile Include="..\..\src\test\app\CrossingLimits_test.cpp">
<Filter>test\app</Filter>
</ClCompile>

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/tx/impl/CancelCheck.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxFlags.h>
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

View File

@@ -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 <ripple/app/tx/impl/Transactor.h>
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

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/tx/impl/CashCheck.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/paths/Flow.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxFlags.h>
#include <algorithm>
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<bool>(optAmount) == static_cast<bool>(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<STAmount>(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<bool>(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

View File

@@ -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 <ripple/app/tx/impl/Transactor.h>
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

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/tx/impl/CreateCheck.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/TxFlags.h>
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<SLE>(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

View File

@@ -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 <ripple/app/tx/impl/Transactor.h>
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

View File

@@ -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();

View File

@@ -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;

View File

@@ -260,6 +260,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltFEE_SETTINGS:
case ltESCROW:
case ltPAYCHAN:
case ltCHECK:
break;
default:
invalidTypeAdded_ = true;

View File

@@ -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<TER, bool>

View File

@@ -20,9 +20,12 @@
#include <BeastConfig.h>
#include <ripple/app/tx/applySteps.h>
#include <ripple/app/tx/impl/ApplyContext.h>
#include <ripple/app/tx/impl/CancelCheck.h>
#include <ripple/app/tx/impl/CancelOffer.h>
#include <ripple/app/tx/impl/CancelTicket.h>
#include <ripple/app/tx/impl/CashCheck.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/app/tx/impl/CreateCheck.h>
#include <ripple/app/tx/impl/CreateOffer.h>
#include <ripple/app/tx/impl/CreateTicket.h>
#include <ripple/app/tx/impl/Escrow.h>
@@ -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<SetAccount>(ctx);
case ttCHECK_CANCEL: return invoke_preclaim<CancelCheck>(ctx);
case ttCHECK_CASH: return invoke_preclaim<CashCheck>(ctx);
case ttCHECK_CREATE: return invoke_preclaim<CreateCheck>(ctx);
case ttOFFER_CANCEL: return invoke_preclaim<CancelOffer>(ctx);
case ttOFFER_CREATE: return invoke_preclaim<CreateOffer>(ctx);
case ttPAYMENT: return invoke_preclaim<Payment>(ctx);
case ttESCROW_CREATE: return invoke_preclaim<EscrowCreate>(ctx);
case ttESCROW_FINISH: return invoke_preclaim<EscrowFinish>(ctx);
case ttESCROW_CANCEL: return invoke_preclaim<EscrowCancel>(ctx);
case ttPAYCHAN_CLAIM: return invoke_preclaim<PayChanClaim>(ctx);
case ttPAYCHAN_CREATE: return invoke_preclaim<PayChanCreate>(ctx);
case ttPAYCHAN_FUND: return invoke_preclaim<PayChanFund>(ctx);
case ttPAYMENT: return invoke_preclaim<Payment>(ctx);
case ttREGULAR_KEY_SET: return invoke_preclaim<SetRegularKey>(ctx);
case ttSIGNER_LIST_SET: return invoke_preclaim<SetSignerList>(ctx);
case ttTICKET_CANCEL: return invoke_preclaim<CancelTicket>(ctx);
@@ -120,9 +132,6 @@ invoke_preclaim (PreclaimContext const& ctx)
case ttTRUST_SET: return invoke_preclaim<SetTrust>(ctx);
case ttAMENDMENT:
case ttFEE: return invoke_preclaim<Change>(ctx);
case ttPAYCHAN_CREATE: return invoke_preclaim<PayChanCreate>(ctx);
case ttPAYCHAN_FUND: return invoke_preclaim<PayChanFund>(ctx);
case ttPAYCHAN_CLAIM: return invoke_preclaim<PayChanClaim>(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<SetAccount>(tx);
case ttCHECK_CANCEL: return invoke_calculateConsequences<CancelCheck>(tx);
case ttCHECK_CASH: return invoke_calculateConsequences<CashCheck>(tx);
case ttCHECK_CREATE: return invoke_calculateConsequences<CreateCheck>(tx);
case ttOFFER_CANCEL: return invoke_calculateConsequences<CancelOffer>(tx);
case ttOFFER_CREATE: return invoke_calculateConsequences<CreateOffer>(tx);
case ttPAYMENT: return invoke_calculateConsequences<Payment>(tx);
case ttESCROW_CREATE: return invoke_calculateConsequences<EscrowCreate>(tx);
case ttESCROW_FINISH: return invoke_calculateConsequences<EscrowFinish>(tx);
case ttESCROW_CANCEL: return invoke_calculateConsequences<EscrowCancel>(tx);
case ttPAYCHAN_CLAIM: return invoke_calculateConsequences<PayChanClaim>(tx);
case ttPAYCHAN_CREATE: return invoke_calculateConsequences<PayChanCreate>(tx);
case ttPAYCHAN_FUND: return invoke_calculateConsequences<PayChanFund>(tx);
case ttPAYMENT: return invoke_calculateConsequences<Payment>(tx);
case ttREGULAR_KEY_SET: return invoke_calculateConsequences<SetRegularKey>(tx);
case ttSIGNER_LIST_SET: return invoke_calculateConsequences<SetSignerList>(tx);
case ttTICKET_CANCEL: return invoke_calculateConsequences<CancelTicket>(tx);
case ttTICKET_CREATE: return invoke_calculateConsequences<CreateTicket>(tx);
case ttTRUST_SET: return invoke_calculateConsequences<SetTrust>(tx);
case ttPAYCHAN_CREATE: return invoke_calculateConsequences<PayChanCreate>(tx);
case ttPAYCHAN_FUND: return invoke_calculateConsequences<PayChanFund>(tx);
case ttPAYCHAN_CLAIM: return invoke_calculateConsequences<PayChanClaim>(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 };

View File

@@ -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.

View File

@@ -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)

View File

@@ -74,7 +74,8 @@ class FeatureCollections
"fix1513",
"fix1523",
"fix1528",
"DepositAuth"
"DepositAuth",
"Checks"
};
std::vector<uint256> 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

View File

@@ -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 */

View File

@@ -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.

View File

@@ -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;

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
;
}

View File

@@ -171,6 +171,7 @@ SF_U256 const sfTicketID = make::one<SF_U256::type>(&sfTicketID, STI_H
SF_U256 const sfDigest = make::one<SF_U256::type>(&sfDigest, STI_HASH256, 21, "Digest");
SF_U256 const sfPayChannel = make::one<SF_U256::type>(&sfPayChannel, STI_HASH256, 22, "Channel");
SF_U256 const sfConsensusHash = make::one<SF_U256::type>(&sfConsensusHash, STI_HASH256, 23, "ConsensusHash");
SF_U256 const sfCheckID = make::one<SF_U256::type>(&sfCheckID, STI_HASH256, 24, "CheckID");
// currency amount (common)
SF_Amount const sfAmount = make::one<SF_Amount::type>(&sfAmount, STI_AMOUNT, 1, "Amount");

View File

@@ -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." } },

View File

@@ -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
;
}

View File

@@ -22,9 +22,12 @@
#include <ripple/app/tx/impl/apply.cpp>
#include <ripple/app/tx/impl/applySteps.cpp>
#include <ripple/app/tx/impl/BookTip.cpp>
#include <ripple/app/tx/impl/CancelCheck.cpp>
#include <ripple/app/tx/impl/CancelOffer.cpp>
#include <ripple/app/tx/impl/CancelTicket.cpp>
#include <ripple/app/tx/impl/CashCheck.cpp>
#include <ripple/app/tx/impl/Change.cpp>
#include <ripple/app/tx/impl/CreateCheck.cpp>
#include <ripple/app/tx/impl/CreateOffer.cpp>
#include <ripple/app/tx/impl/CreateTicket.cpp>
#include <ripple/app/tx/impl/Escrow.cpp>

1768
src/test/app/Check_test.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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 (

View File

@@ -20,6 +20,7 @@
#include <test/app/AccountTxPaging_test.cpp>
#include <test/app/AmendmentTable_test.cpp>
#include <test/app/Check_test.cpp>
#include <test/app/CrossingLimits_test.cpp>
#include <test/app/DeliverMin_test.cpp>
#include <test/app/DepositAuth_test.cpp>