Add support for XLS-85 Token Escrow (#5185)

- Specification: https://github.com/XRPLF/XRPL-Standards/pull/272
- Amendment: `TokenEscrow`
- Enables escrowing of IOU and MPT tokens in addition to native XRP.
- Allows accounts to lock issued tokens (IOU/MPT) in escrow objects, with support for freeze, authorization, and transfer rates.
- Adds new ledger fields (`sfLockedAmount`, `sfIssuerNode`, etc.) to track locked balances for IOU and MPT escrows.
- Updates EscrowCreate, EscrowFinish, and EscrowCancel transaction logic to support IOU and MPT assets, including proper handling of trustlines and MPT authorization, transfer rates, and locked balances.
- Enforces invariant checks for escrowed IOU/MPT amounts.
- Extends GatewayBalances RPC to report locked (escrowed) balances.
This commit is contained in:
Denis Angell
2025-06-03 18:51:55 +02:00
committed by GitHub
parent 7e24adbdd0
commit 053e1af7ff
39 changed files with 6420 additions and 766 deletions

View File

@@ -3651,10 +3651,10 @@ private:
// Can't pay into AMM with escrow.
testAMM([&](AMM& ammAlice, Env& env) {
auto const baseFee = env.current()->fees().base;
env(escrow(carol, ammAlice.ammAccount(), XRP(1)),
condition(cb1),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
escrow::cancel_time(env.now() + 2s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
});

View File

@@ -335,26 +335,11 @@ public:
env(check::cancel(becky, checkId));
env.close();
// Lambda to create an escrow.
auto escrowCreate = [](jtx::Account const& account,
jtx::Account const& to,
STAmount const& amount,
NetClock::time_point const& cancelAfter) {
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Account] = account.human();
jv[jss::Destination] = to.human();
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[sfFinishAfter.jsonName] =
cancelAfter.time_since_epoch().count() + 1;
jv[sfCancelAfter.jsonName] =
cancelAfter.time_since_epoch().count() + 2;
return jv;
};
using namespace std::chrono_literals;
std::uint32_t const escrowSeq{env.seq(alice)};
env(escrowCreate(alice, becky, XRP(333), env.now() + 2s));
env(escrow::create(alice, becky, XRP(333)),
escrow::finish_time(env.now() + 3s),
escrow::cancel_time(env.now() + 4s));
env.close();
// alice and becky should be unable to delete their accounts because
@@ -366,17 +351,39 @@ public:
// Now cancel the escrow, but create a payment channel between
// alice and becky.
// Lambda to cancel an escrow.
auto escrowCancel =
[](Account const& account, Account const& from, std::uint32_t seq) {
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCancel;
jv[jss::Account] = account.human();
jv[sfOwner.jsonName] = from.human();
jv[sfOfferSequence.jsonName] = seq;
return jv;
};
env(escrowCancel(becky, alice, escrowSeq));
bool const withTokenEscrow =
env.current()->rules().enabled(featureTokenEscrow);
if (withTokenEscrow)
{
Account const gw1("gw1");
Account const carol("carol");
auto const USD = gw1["USD"];
env.fund(XRP(100000), carol, gw1);
env(fset(gw1, asfAllowTrustLineLocking));
env.close();
env.trust(USD(10000), carol);
env.close();
env(pay(gw1, carol, USD(100)));
env.close();
std::uint32_t const escrowSeq{env.seq(carol)};
env(escrow::create(carol, becky, USD(1)),
escrow::finish_time(env.now() + 3s),
escrow::cancel_time(env.now() + 4s));
env.close();
incLgrSeqForAccDel(env, gw1);
env(acctdelete(gw1, becky),
fee(acctDelFee),
ter(tecHAS_OBLIGATIONS));
env.close();
env(escrow::cancel(becky, carol, escrowSeq));
env.close();
}
env(escrow::cancel(becky, alice, escrowSeq));
env.close();
Keylet const alicePayChanKey{

View File

@@ -714,12 +714,12 @@ struct DepositPreauth_test : public beast::unit_test::suite
if (!supportsPreauth)
{
auto const seq1 = env.seq(alice);
env(escrow(alice, becky, XRP(100)),
finish_time(env.now() + 1s));
env(escrow::create(alice, becky, XRP(100)),
escrow::finish_time(env.now() + 1s));
env.close();
// Failed as rule is disabled
env(finish(gw, alice, seq1),
env(escrow::finish(gw, alice, seq1),
fee(1500),
ter(tecNO_PERMISSION));
env.close();
@@ -1387,12 +1387,13 @@ struct DepositPreauth_test : public beast::unit_test::suite
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s));
env(escrow::create(alice, bob, XRP(1000)),
escrow::finish_time(env.now() + 1s));
env.close();
// zelda can't finish escrow with invalid credentials
{
env(finish(zelda, alice, seq),
env(escrow::finish(zelda, alice, seq),
credentials::ids({}),
ter(temMALFORMED));
env.close();
@@ -1404,14 +1405,14 @@ struct DepositPreauth_test : public beast::unit_test::suite
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
"01E034";
env(finish(zelda, alice, seq),
env(escrow::finish(zelda, alice, seq),
credentials::ids({invalidIdx}),
ter(tecBAD_CREDENTIALS));
env.close();
}
{ // Ledger closed, time increased, zelda can't finish escrow
env(finish(zelda, alice, seq),
env(escrow::finish(zelda, alice, seq),
credentials::ids({credIdx}),
fee(1500),
ter(tecEXPIRED));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1694,15 +1694,6 @@ class MPToken_test : public beast::unit_test::suite
jv[jss::SendMax] = mpt.getJson(JsonOptions::none);
test(jv, jss::SendMax.c_str());
}
// EscrowCreate
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Account] = alice.human();
jv[jss::Destination] = carol.human();
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// OfferCreate
{
Json::Value jv = offer(alice, USD(100), mpt);

View File

@@ -39,6 +39,7 @@
#include <test/jtx/deposit.h>
#include <test/jtx/did.h>
#include <test/jtx/domain.h>
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/invoice_id.h>

View File

@@ -472,6 +472,15 @@ public:
PrettyAmount
balance(Account const& account, Issue const& issue) const;
PrettyAmount
balance(Account const& account, MPTIssue const& mptIssue) const;
/** Returns the IOU limit on an account.
Returns 0 if the trust line does not exist.
*/
PrettyAmount
limit(Account const& account, Issue const& issue) const;
/** Return the number of objects owned by an account.
* Returns 0 if the account does not exist.
*/

View File

@@ -233,127 +233,6 @@ expectLedgerEntryRoot(
Account const& acct,
STAmount const& expectedValue);
/* Escrow */
/******************************************************************************/
Json::Value
escrow(AccountID const& account, AccountID const& to, STAmount const& amount);
inline Json::Value
escrow(Account const& account, Account const& to, STAmount const& amount)
{
return escrow(account.id(), to.id(), amount);
}
Json::Value
finish(AccountID const& account, AccountID const& from, std::uint32_t seq);
inline Json::Value
finish(Account const& account, Account const& from, std::uint32_t seq)
{
return finish(account.id(), from.id(), seq);
}
Json::Value
cancel(AccountID const& account, Account const& from, std::uint32_t seq);
inline Json::Value
cancel(Account const& account, Account const& from, std::uint32_t seq)
{
return cancel(account.id(), from, seq);
}
std::array<std::uint8_t, 39> constexpr cb1 = {
{0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC,
0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24,
0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95,
0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}};
// A PreimageSha256 fulfillments and its associated condition.
std::array<std::uint8_t, 4> const fb1 = {{0xA0, 0x02, 0x80, 0x00}};
/** Set the "FinishAfter" time tag on a JTx */
struct finish_time
{
private:
NetClock::time_point value_;
public:
explicit finish_time(NetClock::time_point const& value) : value_(value)
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count();
}
};
/** Set the "CancelAfter" time tag on a JTx */
struct cancel_time
{
private:
NetClock::time_point value_;
public:
explicit cancel_time(NetClock::time_point const& value) : value_(value)
{
}
void
operator()(jtx::Env&, jtx::JTx& jt) const
{
jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count();
}
};
struct condition
{
private:
std::string value_;
public:
explicit condition(Slice const& cond) : value_(strHex(cond))
{
}
template <size_t N>
explicit condition(std::array<std::uint8_t, N> const& c)
: condition(makeSlice(c))
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfCondition.jsonName] = value_;
}
};
struct fulfillment
{
private:
std::string value_;
public:
explicit fulfillment(Slice condition) : value_(strHex(condition))
{
}
template <size_t N>
explicit fulfillment(std::array<std::uint8_t, N> f)
: fulfillment(makeSlice(f))
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfFulfillment.jsonName] = value_;
}
};
/* Payment Channel */
/******************************************************************************/

185
src/test/jtx/escrow.h Normal file
View File

@@ -0,0 +1,185 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_ESCROW_H_INCLUDED
#define RIPPLE_TEST_JTX_ESCROW_H_INCLUDED
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/owners.h>
#include <test/jtx/rate.h>
#include <xrpl/protocol/Indexes.h>
namespace ripple {
namespace test {
namespace jtx {
/** Escrow operations. */
namespace escrow {
Json::Value
create(AccountID const& account, AccountID const& to, STAmount const& amount);
inline Json::Value
create(Account const& account, Account const& to, STAmount const& amount)
{
return create(account.id(), to.id(), amount);
}
Json::Value
finish(AccountID const& account, AccountID const& from, std::uint32_t seq);
inline Json::Value
finish(Account const& account, Account const& from, std::uint32_t seq)
{
return finish(account.id(), from.id(), seq);
}
Json::Value
cancel(AccountID const& account, Account const& from, std::uint32_t seq);
inline Json::Value
cancel(Account const& account, Account const& from, std::uint32_t seq)
{
return cancel(account.id(), from, seq);
}
Rate
rate(Env& env, Account const& account, std::uint32_t const& seq);
// A PreimageSha256 fulfillments and its associated condition.
std::array<std::uint8_t, 4> const fb1 = {{0xA0, 0x02, 0x80, 0x00}};
std::array<std::uint8_t, 39> const cb1 = {
{0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC,
0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24,
0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95,
0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}};
// Another PreimageSha256 fulfillments and its associated condition.
std::array<std::uint8_t, 7> const fb2 = {
{0xA0, 0x05, 0x80, 0x03, 0x61, 0x61, 0x61}};
std::array<std::uint8_t, 39> const cb2 = {
{0xA0, 0x25, 0x80, 0x20, 0x98, 0x34, 0x87, 0x6D, 0xCF, 0xB0,
0x5C, 0xB1, 0x67, 0xA5, 0xC2, 0x49, 0x53, 0xEB, 0xA5, 0x8C,
0x4A, 0xC8, 0x9B, 0x1A, 0xDF, 0x57, 0xF2, 0x8F, 0x2F, 0x9D,
0x09, 0xAF, 0x10, 0x7E, 0xE8, 0xF0, 0x81, 0x01, 0x03}};
// Another PreimageSha256 fulfillment and its associated condition.
std::array<std::uint8_t, 8> const fb3 = {
{0xA0, 0x06, 0x80, 0x04, 0x6E, 0x69, 0x6B, 0x62}};
std::array<std::uint8_t, 39> const cb3 = {
{0xA0, 0x25, 0x80, 0x20, 0x6E, 0x4C, 0x71, 0x45, 0x30, 0xC0,
0xA4, 0x26, 0x8B, 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D,
0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD,
0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}};
/** Set the "FinishAfter" time tag on a JTx */
struct finish_time
{
private:
NetClock::time_point value_;
public:
explicit finish_time(NetClock::time_point const& value) : value_(value)
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count();
}
};
/** Set the "CancelAfter" time tag on a JTx */
struct cancel_time
{
private:
NetClock::time_point value_;
public:
explicit cancel_time(NetClock::time_point const& value) : value_(value)
{
}
void
operator()(jtx::Env&, jtx::JTx& jt) const
{
jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count();
}
};
struct condition
{
private:
std::string value_;
public:
explicit condition(Slice const& cond) : value_(strHex(cond))
{
}
template <size_t N>
explicit condition(std::array<std::uint8_t, N> const& c)
: condition(makeSlice(c))
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfCondition.jsonName] = value_;
}
};
struct fulfillment
{
private:
std::string value_;
public:
explicit fulfillment(Slice condition) : value_(strHex(condition))
{
}
template <size_t N>
explicit fulfillment(std::array<std::uint8_t, N> f)
: fulfillment(makeSlice(f))
{
}
void
operator()(Env&, JTx& jt) const
{
jt.jv[sfFulfillment.jsonName] = value_;
}
};
} // namespace escrow
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -96,6 +96,9 @@ private:
case asfDisallowIncomingTrustline:
mask_ |= lsfDisallowIncomingTrustline;
break;
case asfAllowTrustLineLocking:
mask_ |= lsfAllowTrustLineLocking;
break;
default:
Throw<std::runtime_error>("unknown flag");
}

View File

@@ -199,6 +199,48 @@ Env::balance(Account const& account, Issue const& issue) const
return {amount, lookup(issue.account).name()};
}
PrettyAmount
Env::balance(Account const& account, MPTIssue const& mptIssue) const
{
MPTID const id = mptIssue.getMptID();
if (!id)
return {STAmount(mptIssue, 0), account.name()};
AccountID const issuer = mptIssue.getIssuer();
if (account.id() == issuer)
{
// Issuer balance
auto const sle = le(keylet::mptIssuance(id));
if (!sle)
return {STAmount(mptIssue, 0), account.name()};
STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)};
return {amount, lookup(issuer).name()};
}
else
{
// Holder balance
auto const sle = le(keylet::mptoken(id, account));
if (!sle)
return {STAmount(mptIssue, 0), account.name()};
STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)};
return {amount, lookup(issuer).name()};
}
}
PrettyAmount
Env::limit(Account const& account, Issue const& issue) const
{
auto const sle = le(keylet::line(account.id(), issue));
if (!sle)
return {STAmount(issue, 0), account.name()};
auto const aHigh = account.id() > issue.account;
if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit))
return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()};
return {STAmount(issue, 0), account.name()};
}
std::uint32_t
Env::ownerCount(Account const& account) const
{

View File

@@ -211,42 +211,6 @@ expectLedgerEntryRoot(
return accountBalance(env, acct) == to_string(expectedValue.xrp());
}
/* Escrow */
/******************************************************************************/
Json::Value
escrow(AccountID const& account, AccountID const& to, STAmount const& amount)
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::none);
return jv;
}
Json::Value
finish(AccountID const& account, AccountID const& from, std::uint32_t seq)
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowFinish;
jv[jss::Account] = to_string(account);
jv[sfOwner.jsonName] = to_string(from);
jv[sfOfferSequence.jsonName] = seq;
return jv;
}
Json::Value
cancel(AccountID const& account, Account const& from, std::uint32_t seq)
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCancel;
jv[jss::Account] = to_string(account);
jv[sfOwner.jsonName] = from.human();
jv[sfOfferSequence.jsonName] = seq;
return jv;
}
/* Payment Channel */
/******************************************************************************/
Json::Value

View File

@@ -0,0 +1,82 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 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 <test/jtx/escrow.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
namespace jtx {
/** Escrow operations. */
namespace escrow {
Json::Value
create(AccountID const& account, AccountID const& to, STAmount const& amount)
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Flags] = tfFullyCanonicalSig;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::none);
return jv;
}
Json::Value
finish(AccountID const& account, AccountID const& from, std::uint32_t seq)
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowFinish;
jv[jss::Flags] = tfFullyCanonicalSig;
jv[jss::Account] = to_string(account);
jv[sfOwner.jsonName] = to_string(from);
jv[sfOfferSequence.jsonName] = seq;
return jv;
}
Json::Value
cancel(AccountID const& account, Account const& from, std::uint32_t seq)
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCancel;
jv[jss::Flags] = tfFullyCanonicalSig;
jv[jss::Account] = to_string(account);
jv[sfOwner.jsonName] = from.human();
jv[sfOfferSequence.jsonName] = seq;
return jv;
}
Rate
rate(Env& env, Account const& account, std::uint32_t const& seq)
{
auto const sle = env.le(keylet::escrow(account.id(), seq));
if (sle->isFieldPresent(sfTransferRate))
return ripple::Rate((*sle)[sfTransferRate]);
return Rate{0};
}
} // namespace escrow
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -731,21 +731,6 @@ class Invariants_test : public beast::unit_test::suite
using namespace test::jtx;
testcase << "no zero escrow";
doInvariantCheck(
{{"Cannot return non-native STAmount as XRPAmount"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
// escrow with nonnative amount
auto const sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
auto sleNew = std::make_shared<SLE>(
keylet::escrow(A1, (*sle)[sfSequence] + 2));
STAmount nonNative(A2["USD"](51));
sleNew->setFieldAmount(sfAmount, nonNative);
ac.view().insert(sleNew);
return true;
});
doInvariantCheck(
{{"XRP net change of -1000000 doesn't match fee 0"},
{"escrow specifies invalid amount"}},

View File

@@ -17,6 +17,8 @@
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/STAmount.h>
@@ -668,6 +670,366 @@ public:
}
}
void
testCanAddXRP()
{
testcase("can add xrp");
// Adding zero
{
STAmount amt1(XRPAmount(0));
STAmount amt2(XRPAmount(1000));
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding zero
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(XRPAmount(0));
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding two positive XRP amounts
{
STAmount amt1(XRPAmount(500));
STAmount amt2(XRPAmount(1500));
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding two negative XRP amounts
{
STAmount amt1(XRPAmount(-500));
STAmount amt2(XRPAmount(-1500));
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding a positive and a negative XRP amount
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(XRPAmount(-1000));
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Overflow check for max XRP amounts
{
STAmount amt1(std::numeric_limits<XRPAmount::value_type>::max());
STAmount amt2(XRPAmount(1));
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Overflow check for min XRP amounts
{
STAmount amt1(std::numeric_limits<XRPAmount::value_type>::max());
amt1 += XRPAmount(1);
STAmount amt2(XRPAmount(-1));
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
}
void
testCanAddIOU()
{
testcase("can add iou");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)};
// Adding two IOU amounts
{
STAmount amt1(usd, 500);
STAmount amt2(usd, 1500);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding a positive and a negative IOU amount
{
STAmount amt1(usd, 1000);
STAmount amt2(usd, -1000);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Overflow check for max IOU amounts
{
STAmount amt1(usd, std::numeric_limits<int64_t>::max());
STAmount amt2(usd, 1);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Overflow check for min IOU amounts
{
STAmount amt1(usd, std::numeric_limits<std::int64_t>::min());
STAmount amt2(usd, -1);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Adding XRP and IOU
{
STAmount amt1(XRPAmount(1));
STAmount amt2(usd, 1);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Adding different IOU issues (non zero)
{
STAmount amt1(usd, 1000);
STAmount amt2(eur, 500);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Adding different IOU issues (zero)
{
STAmount amt1(usd, 0);
STAmount amt2(eur, 500);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
}
void
testCanAddMPT()
{
testcase("can add mpt");
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}};
// Adding zero
{
STAmount amt1(mpt, 0);
STAmount amt2(mpt, 1000);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding zero
{
STAmount amt1(mpt, 1000);
STAmount amt2(mpt, 0);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding two positive MPT amounts
{
STAmount amt1(mpt, 500);
STAmount amt2(mpt, 1500);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding two negative MPT amounts
{
STAmount amt1(mpt, -500);
STAmount amt2(mpt, -1500);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Adding a positive and a negative MPT amount
{
STAmount amt1(mpt, 1000);
STAmount amt2(mpt, -1000);
BEAST_EXPECT(canAdd(amt1, amt2) == true);
}
// Overflow check for max MPT amounts
{
STAmount amt1(
mpt, std::numeric_limits<MPTAmount::value_type>::max());
STAmount amt2(mpt, 1);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Overflow check for min MPT amounts
// Note: Cannot check min MPT overflow because you cannot initialize the
// STAmount with a negative MPT amount.
// Adding MPT and XRP
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(mpt, 1000);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Adding different MPT issues (non zero)
{
STAmount amt1(mpt2, 500);
STAmount amt2(mpt, 500);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
// Adding different MPT issues (non zero)
{
STAmount amt1(mpt2, 0);
STAmount amt2(mpt, 500);
BEAST_EXPECT(canAdd(amt1, amt2) == false);
}
}
void
testCanSubtractXRP()
{
testcase("can subtract xrp");
// Subtracting zero
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(XRPAmount(0));
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting zero
{
STAmount amt1(XRPAmount(0));
STAmount amt2(XRPAmount(1000));
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting two positive XRP amounts
{
STAmount amt1(XRPAmount(1500));
STAmount amt2(XRPAmount(500));
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting two negative XRP amounts
{
STAmount amt1(XRPAmount(-1500));
STAmount amt2(XRPAmount(-500));
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting a positive and a negative XRP amount
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(XRPAmount(-1000));
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Underflow check for min XRP amounts
{
STAmount amt1(std::numeric_limits<XRPAmount::value_type>::max());
amt1 += XRPAmount(1);
STAmount amt2(XRPAmount(1));
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Overflow check for max XRP amounts
{
STAmount amt1(std::numeric_limits<XRPAmount::value_type>::max());
STAmount amt2(XRPAmount(-1));
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
}
void
testCanSubtractIOU()
{
testcase("can subtract iou");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)};
// Subtracting two IOU amounts
{
STAmount amt1(usd, 1500);
STAmount amt2(usd, 500);
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting XRP and IOU
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(usd, 1000);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting different IOU issues (non zero)
{
STAmount amt1(usd, 1000);
STAmount amt2(eur, 500);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting different IOU issues (zero)
{
STAmount amt1(usd, 0);
STAmount amt2(eur, 500);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
}
void
testCanSubtractMPT()
{
testcase("can subtract mpt");
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}};
// Subtracting zero
{
STAmount amt1(mpt, 1000);
STAmount amt2(mpt, 0);
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting zero
{
STAmount amt1(mpt, 0);
STAmount amt2(mpt, 1000);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting two positive MPT amounts
{
STAmount amt1(mpt, 1500);
STAmount amt2(mpt, 500);
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting two negative MPT amounts
{
STAmount amt1(mpt, -1500);
STAmount amt2(mpt, -500);
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Subtracting a positive and a negative MPT amount
{
STAmount amt1(mpt, 1000);
STAmount amt2(mpt, -1000);
BEAST_EXPECT(canSubtract(amt1, amt2) == true);
}
// Underflow check for min MPT amounts
// Note: Cannot check min MPT underflow because you cannot initialize
// the STAmount with a negative MPT amount.
// Overflow check for max positive MPT amounts (should fail)
{
STAmount amt1(
mpt, std::numeric_limits<MPTAmount::value_type>::max());
STAmount amt2(mpt, -2);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting MPT and XRP
{
STAmount amt1(XRPAmount(1000));
STAmount amt2(mpt, 1000);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting different MPT issues (non zero)
{
STAmount amt1(mpt, 1000);
STAmount amt2(mpt2, 500);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
// Subtracting different MPT issues (zero)
{
STAmount amt1(mpt, 0);
STAmount amt2(mpt2, 500);
BEAST_EXPECT(canSubtract(amt1, amt2) == false);
}
}
//--------------------------------------------------------------------------
void
@@ -681,6 +1043,12 @@ public:
testRounding();
testConvertXRP();
testConvertIOU();
testCanAddXRP();
testCanAddIOU();
testCanAddMPT();
testCanSubtractXRP();
testCanSubtractIOU();
testCanSubtractMPT();
}
};

View File

@@ -573,21 +573,6 @@ public:
env.fund(XRP(10000), alice, becky, gw1);
env.close();
// A couple of helper lambdas
auto escrow = [&env](
Account const& account,
Account const& to,
STAmount const& amount) {
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Account] = account.human();
jv[jss::Destination] = to.human();
jv[jss::Amount] = amount.getJson(JsonOptions::none);
NetClock::time_point finish = env.now() + 1s;
jv[sfFinishAfter.jsonName] = finish.time_since_epoch().count();
return jv;
};
auto payChan = [](Account const& account,
Account const& to,
STAmount const& amount,
@@ -623,8 +608,10 @@ public:
env.close();
// Escrow, in each direction
env(escrow(alice, becky, XRP(1000)));
env(escrow(becky, alice, XRP(1000)));
env(escrow::create(alice, becky, XRP(1000)),
escrow::finish_time(env.now() + 1s));
env(escrow::create(becky, alice, XRP(1000)),
escrow::finish_time(env.now() + 1s));
// Pay channels, in each direction
env(payChan(alice, becky, XRP(1000), 100s, alice.pk()));

View File

@@ -99,6 +99,12 @@ public:
// is tested elsewhere.
continue;
}
if (flag == asfAllowTrustLineLocking)
{
// These flags are part of the AllowTokenLocking amendment
// and are tested elsewhere
continue;
}
if (std::find(goodFlags.begin(), goodFlags.end(), flag) !=
goodFlags.end())