mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
819 lines
33 KiB
C++
819 lines
33 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <BeastConfig.h>
|
|
#include <test/support/jtx.h>
|
|
#include <ripple/app/tx/applySteps.h>
|
|
#include <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/Indexes.h>
|
|
#include <ripple/protocol/JsonFields.h>
|
|
#include <ripple/protocol/TxFlags.h>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
struct SusPay_test : public beast::unit_test::suite
|
|
{
|
|
// An Ed25519 conditional trigger fulfillment and its
|
|
// condition
|
|
std::array<std::uint8_t, 99> const fb1 =
|
|
{{
|
|
0x00, 0x04, 0x60, 0x3B, 0x6A, 0x27, 0xBC, 0xCE, 0xB6, 0xA4, 0x2D, 0x62,
|
|
0xA3, 0xA8, 0xD0, 0x2A, 0x6F, 0x0D, 0x73, 0x65, 0x32, 0x15, 0x77, 0x1D,
|
|
0xE2, 0x43, 0xA6, 0x3A, 0xC0, 0x48, 0xA1, 0x8B, 0x59, 0xDA, 0x29, 0x8F,
|
|
0x89, 0x5B, 0x3C, 0xAF, 0xE2, 0xC9, 0x50, 0x60, 0x39, 0xD0, 0xE2, 0xA6,
|
|
0x63, 0x82, 0x56, 0x80, 0x04, 0x67, 0x4F, 0xE8, 0xD2, 0x37, 0x78, 0x50,
|
|
0x92, 0xE4, 0x0D, 0x6A, 0xAF, 0x48, 0x3E, 0x4F, 0xC6, 0x01, 0x68, 0x70,
|
|
0x5F, 0x31, 0xF1, 0x01, 0x59, 0x61, 0x38, 0xCE, 0x21, 0xAA, 0x35, 0x7C,
|
|
0x0D, 0x32, 0xA0, 0x64, 0xF4, 0x23, 0xDC, 0x3E, 0xE4, 0xAA, 0x3A, 0xBF,
|
|
0x53, 0xF8, 0x03,
|
|
}};
|
|
|
|
std::array<std::uint8_t, 39> const cb1 =
|
|
{{
|
|
0x00, 0x04, 0x01, 0x20, 0x20, 0x3B, 0x6A, 0x27, 0xBC, 0xCE, 0xB6, 0xA4,
|
|
0x2D, 0x62, 0xA3, 0xA8, 0xD0, 0x2A, 0x6F, 0x0D, 0x73, 0x65, 0x32, 0x15,
|
|
0x77, 0x1D, 0xE2, 0x43, 0xA6, 0x3A, 0xC0, 0x48, 0xA1, 0x8B, 0x59, 0xDA,
|
|
0x29, 0x01, 0x60
|
|
}};
|
|
|
|
// A prefix.prefix.ed25519 conditional trigger fulfillment:
|
|
std::array<std::uint8_t, 106> const fb2 =
|
|
{{
|
|
0x00, 0x01, 0x67, 0x03, 0x61, 0x62, 0x63, 0x00, 0x04, 0x60, 0x76, 0xA1,
|
|
0x59, 0x20, 0x44, 0xA6, 0xE4, 0xF5, 0x11, 0x26, 0x5B, 0xCA, 0x73, 0xA6,
|
|
0x04, 0xD9, 0x0B, 0x05, 0x29, 0xD1, 0xDF, 0x60, 0x2B, 0xE3, 0x0A, 0x19,
|
|
0xA9, 0x25, 0x76, 0x60, 0xD1, 0xF5, 0xAE, 0xC6, 0xAB, 0x6A, 0x91, 0x22,
|
|
0xAF, 0xF0, 0xF7, 0xDC, 0xB9, 0x66, 0x7F, 0xF6, 0x13, 0x13, 0x68, 0x94,
|
|
0x73, 0x2B, 0x6E, 0x78, 0xC2, 0x6F, 0x5B, 0x67, 0x31, 0x01, 0xE2, 0x67,
|
|
0xFE, 0x2E, 0x2B, 0x65, 0xFA, 0x4D, 0x53, 0xDA, 0xD4, 0x78, 0xA1, 0xAD,
|
|
0xA6, 0x4D, 0x50, 0xFD, 0x1D, 0xFD, 0xB7, 0xD9, 0x49, 0x20, 0xDC, 0x3E,
|
|
0x1A, 0x56, 0x4A, 0x64, 0x7B, 0x1C, 0xBA, 0x35, 0x60, 0x01,
|
|
}};
|
|
|
|
std::array<std::uint8_t, 39> const cb2 =
|
|
{{
|
|
|
|
0x00, 0x01, 0x01, 0x25, 0x20, 0x28, 0x7A, 0x8B, 0xD8, 0xAD, 0xAE, 0x8A,
|
|
0xCA, 0x0C, 0x87, 0x1C, 0xE7, 0xC2, 0x5F, 0xBA, 0xA5, 0xA8, 0xBE, 0x10,
|
|
0xD0, 0xE4, 0xDB, 0x1F, 0x56, 0xAE, 0xEE, 0x8B, 0xB3, 0xAD, 0xCE, 0xE5,
|
|
0x5B, 0x01, 0x64
|
|
}};
|
|
|
|
// A prefix+preimage conditional trigger fulfillment
|
|
std::array<std::uint8_t, 7> const fb3 =
|
|
{{
|
|
0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00,
|
|
}};
|
|
|
|
std::array<std::uint8_t, 39> const cb3 =
|
|
{{
|
|
|
|
0x00, 0x01, 0x01, 0x07, 0x20, 0x62, 0x36, 0xB7, 0xA8, 0x58, 0xFB, 0x35,
|
|
0x2F, 0xD5, 0xC3, 0x01, 0x3B, 0x68, 0x98, 0xCF, 0x26, 0x8B, 0x3E, 0xB8,
|
|
0x50, 0xB3, 0x4A, 0xD2, 0x65, 0x24, 0xB0, 0xF8, 0x56, 0xC3, 0x72, 0xD9,
|
|
0x73, 0x01, 0x01
|
|
}};
|
|
|
|
static
|
|
Json::Value
|
|
condpay (jtx::Account const& account, jtx::Account const& to,
|
|
STAmount const& amount, Slice condition,
|
|
NetClock::time_point const& cancelAfter)
|
|
{
|
|
using namespace jtx;
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = "SuspendedPaymentCreate";
|
|
jv[jss::Flags] = tfUniversal;
|
|
jv[jss::Account] = account.human();
|
|
jv[jss::Destination] = to.human();
|
|
jv[jss::Amount] = amount.getJson(0);
|
|
jv["CancelAfter"] =
|
|
cancelAfter.time_since_epoch().count();
|
|
jv["Condition"] = strHex(condition);
|
|
return jv;
|
|
}
|
|
|
|
static
|
|
Json::Value
|
|
condpay (jtx::Account const& account, jtx::Account const& to,
|
|
STAmount const& amount, Slice condition,
|
|
NetClock::time_point const& cancelAfter,
|
|
NetClock::time_point const& finishAfter)
|
|
{
|
|
auto jv = condpay (account, to, amount, condition, cancelAfter);
|
|
jv ["FinishAfter"] = finishAfter.time_since_epoch().count();
|
|
return jv;
|
|
}
|
|
|
|
static
|
|
Json::Value
|
|
lockup (jtx::Account const& account, jtx::Account const& to,
|
|
STAmount const& amount, NetClock::time_point const& expiry)
|
|
{
|
|
using namespace jtx;
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = "SuspendedPaymentCreate";
|
|
jv[jss::Flags] = tfUniversal;
|
|
jv[jss::Account] = account.human();
|
|
jv[jss::Destination] = to.human();
|
|
jv[jss::Amount] = amount.getJson(0);
|
|
jv["FinishAfter"] =
|
|
expiry.time_since_epoch().count();
|
|
return jv;
|
|
}
|
|
|
|
static
|
|
Json::Value
|
|
lockup (jtx::Account const& account, jtx::Account const& to,
|
|
STAmount const& amount, Slice condition,
|
|
NetClock::time_point const& expiry)
|
|
{
|
|
using namespace jtx;
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = "SuspendedPaymentCreate";
|
|
jv[jss::Flags] = tfUniversal;
|
|
jv[jss::Account] = account.human();
|
|
jv[jss::Destination] = to.human();
|
|
jv[jss::Amount] = amount.getJson(0);
|
|
jv["FinishAfter"] =
|
|
expiry.time_since_epoch().count();
|
|
jv["Condition"] = strHex(condition);
|
|
return jv;
|
|
}
|
|
|
|
static
|
|
Json::Value
|
|
finish (jtx::Account const& account,
|
|
jtx::Account const& from, std::uint32_t seq)
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = "SuspendedPaymentFinish";
|
|
jv[jss::Flags] = tfUniversal;
|
|
jv[jss::Account] = account.human();
|
|
jv["Owner"] = from.human();
|
|
jv["OfferSequence"] = seq;
|
|
return jv;
|
|
}
|
|
|
|
static
|
|
Json::Value
|
|
finish (jtx::Account const& account,
|
|
jtx::Account const& from, std::uint32_t seq,
|
|
Slice condition, Slice fulfillment)
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = "SuspendedPaymentFinish";
|
|
jv[jss::Flags] = tfUniversal;
|
|
jv[jss::Account] = account.human();
|
|
jv["Owner"] = from.human();
|
|
jv["OfferSequence"] = seq;
|
|
jv["Condition"] = strHex(condition);
|
|
jv["Fulfillment"] = strHex(fulfillment);
|
|
return jv;
|
|
}
|
|
|
|
static
|
|
Json::Value
|
|
cancel (jtx::Account const& account,
|
|
jtx::Account const& from, std::uint32_t seq)
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = "SuspendedPaymentCancel";
|
|
jv[jss::Flags] = tfUniversal;
|
|
jv[jss::Account] = account.human();
|
|
jv["Owner"] = from.human();
|
|
jv["OfferSequence"] = seq;
|
|
return jv;
|
|
}
|
|
|
|
void
|
|
testEnablement()
|
|
{
|
|
testcase ("Enablement");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
|
|
{ // SusPay enabled
|
|
Env env(*this, features(featureSusPay));
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
env(lockup("alice", "bob", XRP(1000), env.now() + 1s));
|
|
}
|
|
|
|
{ // SusPay not enabled
|
|
Env env(*this);
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
env(lockup("alice", "bob", XRP(1000), env.now() + 1s), ter(temDISABLED));
|
|
env(finish("bob", "alice", 1), ter(temDISABLED));
|
|
env(cancel("bob", "alice", 1), ter(temDISABLED));
|
|
}
|
|
|
|
{ // SusPay enabled, CryptoConditions disabled
|
|
Env env(*this,
|
|
features(featureSusPay));
|
|
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
|
|
auto const seq = env.seq("alice");
|
|
|
|
// Fail: no cryptoconditions allowed
|
|
env(condpay("alice", "bob", XRP(1000),
|
|
makeSlice (cb1), env.now() + 1s), ter(temDISABLED));
|
|
|
|
// Succeed: doesn't have a cryptocondition
|
|
env(lockup("alice", "bob", XRP(1000),
|
|
env.now() + 1s));
|
|
|
|
// Fail: can't specify conditional finishes if
|
|
// cryptoconditions aren't allowed.
|
|
{
|
|
auto f = finish("bob", "alice", seq,
|
|
makeSlice(cb1), makeSlice(fb1));
|
|
env (f, ter(temDISABLED));
|
|
|
|
auto fnc = f;
|
|
fnc.removeMember ("Condition");
|
|
env (fnc, ter(temDISABLED));
|
|
|
|
auto fnf = f;
|
|
fnf.removeMember ("Fulfillment");
|
|
env (fnf, ter(temDISABLED));
|
|
|
|
}
|
|
|
|
// Succeeds
|
|
env.close();
|
|
env(finish("bob", "alice", seq));
|
|
}
|
|
|
|
{ // SusPay enabled, CryptoConditions enabled
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
|
|
auto const seq = env.seq("alice");
|
|
|
|
env(condpay("alice", "bob", XRP(1000),
|
|
makeSlice (cb1), env.now() + 1s), fee(1500));
|
|
env(finish("bob", "alice", seq,
|
|
makeSlice(cb1), makeSlice(fb1)), fee(1500));
|
|
}
|
|
}
|
|
|
|
void
|
|
testTags()
|
|
{
|
|
testcase ("Tags");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
|
|
{
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
|
|
auto const alice = Account("alice");
|
|
env.fund(XRP(5000), alice, "bob");
|
|
|
|
auto const seq = env.seq(alice);
|
|
// set source and dest tags
|
|
env(condpay(alice, "bob", XRP(1000),
|
|
makeSlice (cb1), env.now() + 1s),
|
|
stag(1), dtag(2));
|
|
auto const sle = env.le(keylet::susPay(alice.id(), seq));
|
|
BEAST_EXPECT((*sle)[sfSourceTag] == 1);
|
|
BEAST_EXPECT((*sle)[sfDestinationTag] == 2);
|
|
}
|
|
}
|
|
|
|
void
|
|
testFails()
|
|
{
|
|
testcase ("Failure Cases");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
env.close();
|
|
|
|
// Expiration in the past
|
|
env(condpay("alice", "bob", XRP(1000),
|
|
makeSlice(cb1), env.now() - 1s), ter(tecNO_PERMISSION));
|
|
|
|
// no destination account
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
makeSlice(cb1), env.now() + 1s), ter(tecNO_DST));
|
|
|
|
env.fund(XRP(5000), "carol");
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
makeSlice(cb1), env.now() + 1s), stag(2));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
makeSlice(cb1), env.now() + 1s), stag(3), dtag(4));
|
|
env(fset("carol", asfRequireDest));
|
|
|
|
// missing destination tag
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
makeSlice(cb1), env.now() + 1s), ter(tecDST_TAG_NEEDED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
makeSlice(cb1), env.now() + 1s), dtag(1));
|
|
|
|
// Using non-XRP:
|
|
env (lockup("alice", "carol", Account("alice")["USD"](500),
|
|
env.now() + 1s), ter(temBAD_AMOUNT));
|
|
|
|
// Sending zero or no XRP:
|
|
env (lockup("alice", "carol", XRP(0),
|
|
env.now() + 1s), ter(temBAD_AMOUNT));
|
|
env (lockup("alice", "carol", XRP(-1000),
|
|
env.now() + 1s), ter(temBAD_AMOUNT));
|
|
|
|
// Fail if neither CancelAfter nor FinishAfter are specified:
|
|
{
|
|
auto j1 = lockup("alice", "carol", XRP(1), env.now() + 1s);
|
|
j1.removeMember ("FinishAfter");
|
|
env (j1, ter(temBAD_EXPIRATION));
|
|
|
|
auto j2 = condpay("alice", "carol", XRP(1), makeSlice(cb1), env.now() + 1s);
|
|
j2.removeMember ("CancelAfter");
|
|
env (j2, ter(temBAD_EXPIRATION));
|
|
}
|
|
|
|
// Fail if FinishAfter has already passed:
|
|
env (lockup("alice", "carol", XRP(1), env.now() - 1s), ter (tecNO_PERMISSION));
|
|
|
|
// Both CancelAfter and FinishAfter
|
|
env(condpay("alice", "carol", XRP(1), makeSlice(cb1),
|
|
env.now() + 10s, env.now() + 10s), ter (temBAD_EXPIRATION));
|
|
env(condpay("alice", "carol", XRP(1), makeSlice(cb1),
|
|
env.now() + 10s, env.now() + 15s), ter (temBAD_EXPIRATION));
|
|
|
|
// Fail if the sender wants to send more than he has:
|
|
auto const accountReserve =
|
|
drops(env.current()->fees().reserve);
|
|
auto const accountIncrement =
|
|
drops(env.current()->fees().increment);
|
|
|
|
env.fund (accountReserve + accountIncrement + XRP(50), "daniel");
|
|
env(lockup("daniel", "bob", XRP(51), env.now() + 1s), ter (tecUNFUNDED));
|
|
|
|
env.fund (accountReserve + accountIncrement + XRP(50), "evan");
|
|
env(lockup("evan", "bob", XRP(50), env.now() + 1s), ter (tecUNFUNDED));
|
|
|
|
env.fund (accountReserve, "frank");
|
|
env(lockup("frank", "bob", XRP(1), env.now() + 1s), ter (tecINSUFFICIENT_RESERVE));
|
|
|
|
// Respect the "asfDisallowXRP" account flag:
|
|
env.fund (accountReserve + accountIncrement, "george");
|
|
env(fset("george", asfDisallowXRP));
|
|
env(lockup("bob", "george", XRP(10), env.now() + 1s), ter (tecNO_TARGET));
|
|
|
|
{ // Specify incorrect sequence number
|
|
env.fund (XRP(5000), "hannah");
|
|
auto const seq = env.seq("hannah");
|
|
env(lockup("hannah", "hannah", XRP(10), env.now() + 1s));
|
|
env(finish ("hannah", "hannah", seq + 7), ter (tecNO_TARGET));
|
|
}
|
|
|
|
{ // Try to specify a condition for a non-conditional payment
|
|
env.fund (XRP(5000), "ivan");
|
|
auto const seq = env.seq("ivan");
|
|
|
|
auto j = lockup("ivan", "ivan", XRP(10), env.now() + 1s);
|
|
j["CancelAfter"] = j.removeMember ("FinishAfter");
|
|
env (j);
|
|
env(finish("ivan", "ivan", seq,
|
|
makeSlice(cb1), makeSlice(fb1)), fee(1500), ter (tecCRYPTOCONDITION_ERROR));
|
|
}
|
|
}
|
|
|
|
void
|
|
testLockup()
|
|
{
|
|
testcase ("Lockup");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
|
|
{ // Unconditional
|
|
Env env(*this, features(featureSusPay));
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
auto const seq = env.seq("alice");
|
|
env(lockup("alice", "alice", XRP(1000), env.now() + 1s));
|
|
env.require(balance("alice", XRP(4000) - drops(10)));
|
|
|
|
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
env(finish("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
env(finish("bob", "alice", seq));
|
|
}
|
|
|
|
{ // Conditional
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
env.fund(XRP(5000), "alice", "bob");
|
|
auto const seq = env.seq("alice");
|
|
env(lockup("alice", "alice", XRP(1000), makeSlice(cb2), env.now() + 1s));
|
|
env.require(balance("alice", XRP(4000) - drops(10)));
|
|
|
|
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
env(finish("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
env(finish("bob", "alice", seq,
|
|
makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR));
|
|
env.close();
|
|
|
|
env(finish("bob", "alice", seq,
|
|
makeSlice(cb2), makeSlice(fb2)), fee(1500));
|
|
}
|
|
}
|
|
|
|
void
|
|
testCondPay()
|
|
{
|
|
testcase ("Conditional Payments");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
using S = seconds;
|
|
|
|
{ // Test cryptoconditions
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
auto T = [&env](NetClock::duration const& d)
|
|
{ return env.now() + d; };
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
auto const seq = env.seq("alice");
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
|
env(condpay("alice", "carol", XRP(1000), makeSlice(cb1), T(S{1})));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env.require(balance("alice", XRP(4000) - drops(10)));
|
|
env.require(balance("carol", XRP(5000)));
|
|
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
|
|
// Attempt to finish without a fulfillment
|
|
env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
|
|
// Attempt to finish with a condition instead of a fulfillment
|
|
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb1)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb2)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb3)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
|
|
// Attempt to finish with an incorrect condition and various
|
|
// combinations of correct and incorrect fulfillments.
|
|
env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb1)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb3)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
|
|
// Attempt to finish with the correct condition & fulfillment
|
|
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(fb1)), fee(1500));
|
|
// SLE removed on finish
|
|
BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq)));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
|
env.require(balance("carol", XRP(6000)));
|
|
env(cancel("bob", "alice", seq), ter(tecNO_TARGET));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
|
env(cancel("bob", "carol", 1), ter(tecNO_TARGET));
|
|
env.close();
|
|
}
|
|
|
|
{ // Test cancel when condition is present
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
auto T = [&env](NetClock::duration const& d)
|
|
{ return env.now() + d; };
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
auto const seq = env.seq("alice");
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
|
env(condpay("alice", "carol", XRP(1000), makeSlice(cb2), T(S{1})));
|
|
env.close();
|
|
env.require(balance("alice", XRP(4000) - drops(10)));
|
|
// balance restored on cancel
|
|
env(cancel("bob", "alice", seq));
|
|
env.require(balance("alice", XRP(5000) - drops(10)));
|
|
// SLE removed on cancel
|
|
BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq)));
|
|
}
|
|
|
|
{
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
auto T = [&env](NetClock::duration const& d)
|
|
{ return env.now() + d; };
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
env.close();
|
|
auto const seq = env.seq("alice");
|
|
env(condpay("alice", "carol", XRP(1000), makeSlice(cb3), T(S{1})));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
// cancel fails before expiration
|
|
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env.close();
|
|
// finish fails after expiration
|
|
env(finish("bob", "alice", seq, makeSlice(cb3), makeSlice(fb3)),
|
|
fee(1500), ter(tecNO_PERMISSION));
|
|
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
|
env.require(balance("carol", XRP(5000)));
|
|
}
|
|
|
|
{ // Test long & short conditions during creation
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
auto T = [&env](NetClock::duration const& d)
|
|
{ return env.now() + d; };
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
|
|
std::vector<std::uint8_t> v;
|
|
v.resize(cb1.size() + 2, 0x78);
|
|
std::memcpy (v.data() + 1, cb1.data(), cb1.size());
|
|
|
|
auto const p = v.data();
|
|
auto const s = v.size();
|
|
|
|
// All these are expected to fail, because the
|
|
// condition we pass in is malformed in some way
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p, s}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p, s - 1}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p, s - 2}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p + 1, s - 1}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p + 1, s - 3}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p + 2, s - 2}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p + 2, s - 3}, T(S{1})), ter(temMALFORMED));
|
|
|
|
auto const seq = env.seq("alice");
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{p + 1, s - 2}, T(S{1})), fee(100));
|
|
env(finish("bob", "alice", seq,
|
|
makeSlice(cb1), makeSlice(fb1)), fee(1500));
|
|
env.require(balance("alice", XRP(4000) - drops(100)));
|
|
env.require(balance("bob", XRP(5000) - drops(1500)));
|
|
env.require(balance("carol", XRP(6000)));
|
|
}
|
|
|
|
{ // Test long and short conditions & fulfillments during finish
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
auto T = [&env](NetClock::duration const& d)
|
|
{ return env.now() + d; };
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
|
|
std::vector<std::uint8_t> cv;
|
|
cv.resize(cb2.size() + 2, 0x78);
|
|
std::memcpy (cv.data() + 1, cb2.data(), cb2.size());
|
|
|
|
auto const cp = cv.data();
|
|
auto const cs = cv.size();
|
|
|
|
std::vector<std::uint8_t> fv;
|
|
fv.resize(fb2.size() + 2, 0x13);
|
|
std::memcpy(fv.data() + 1, fb2.data(), fb2.size());
|
|
|
|
auto const fp = fv.data();
|
|
auto const fs = fv.size();
|
|
|
|
// All these are expected to fail, because the
|
|
// condition we pass in is malformed in some way
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp, cs}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp, cs - 1}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp, cs - 2}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp + 1, cs - 1}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp + 1, cs - 3}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp + 2, cs - 2}, T(S{1})), ter(temMALFORMED));
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp + 2, cs - 3}, T(S{1})), ter(temMALFORMED));
|
|
|
|
auto const seq = env.seq("alice");
|
|
env(condpay("alice", "carol", XRP(1000),
|
|
Slice{cp + 1, cs - 2}, T(S{1})), fee(100));
|
|
|
|
// Now, try to fulfill using the same sequence of
|
|
// malformed conditions.
|
|
env(finish("bob", "alice", seq, Slice{cp, cs}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp, cs - 1}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp, cs - 2}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 1}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 3}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 2, cs - 2}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 2, cs - 3}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
|
|
// Now, using the correct condition, try malformed
|
|
// fulfillments:
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs - 1}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs - 2}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 1}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 3}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 3}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 2, fs - 2}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 2, fs - 3}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
|
|
// Now try for the right one
|
|
env(finish("bob", "alice", seq,
|
|
makeSlice(cb2), makeSlice(fb2)), fee(1500));
|
|
env.require(balance("alice", XRP(4000) - drops(100)));
|
|
env.require(balance("carol", XRP(6000)));
|
|
}
|
|
|
|
{ // Test empty condition during creation and
|
|
// empty condition & fulfillment during finish
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
auto T = [&env](NetClock::duration const& d)
|
|
{ return env.now() + d; };
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
|
|
env(condpay("alice", "carol", XRP(1000), {}, T(S{1})), ter(temMALFORMED));
|
|
|
|
auto const seq = env.seq("alice");
|
|
env(condpay("alice", "carol", XRP(1000), makeSlice(cb3), T(S{1})));
|
|
|
|
env(finish("bob", "alice", seq, {}, {}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, makeSlice(cb3), {}),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
env(finish("bob", "alice", seq, {}, makeSlice(fb3)),
|
|
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
|
|
|
auto correctFinish = finish("bob", "alice", seq,
|
|
makeSlice(cb3), makeSlice(fb3));
|
|
|
|
// Manually assemble finish that is missing the
|
|
// Condition or the Fulfillment (either both must
|
|
// be present, or neither can):
|
|
{
|
|
auto finishNoCondition = correctFinish;
|
|
finishNoCondition.removeMember ("Condition");
|
|
env (finishNoCondition, ter(temMALFORMED));
|
|
|
|
auto finishNoFulfillment = correctFinish;
|
|
finishNoFulfillment.removeMember ("Fulfillment");
|
|
env (finishNoFulfillment, ter(temMALFORMED));
|
|
}
|
|
|
|
env(correctFinish, fee(1500));
|
|
env.require(balance ("carol", XRP(6000)));
|
|
env.require(balance ("alice", XRP(4000) - drops(10)));
|
|
}
|
|
}
|
|
|
|
void
|
|
testMeta()
|
|
{
|
|
testcase ("Metadata");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
|
|
env.fund(XRP(5000), "alice", "bob", "carol");
|
|
env(condpay("alice", "carol", XRP(1000), makeSlice(cb1), env.now() + 1s));
|
|
auto const m = env.meta();
|
|
BEAST_EXPECT((*m)[sfTransactionResult] == tesSUCCESS);
|
|
}
|
|
|
|
void testConsequences()
|
|
{
|
|
testcase ("Consequences");
|
|
|
|
using namespace jtx;
|
|
using namespace std::chrono;
|
|
Env env(*this,
|
|
features(featureSusPay),
|
|
features(featureCryptoConditions));
|
|
|
|
env.memoize("alice");
|
|
env.memoize("bob");
|
|
env.memoize("carol");
|
|
|
|
{
|
|
auto const jtx = env.jt(
|
|
condpay("alice", "carol", XRP(1000),
|
|
makeSlice(cb1), env.now() + 1s),
|
|
seq(1), fee(10));
|
|
auto const pf = preflight(env.app(), env.current()->rules(),
|
|
*jtx.stx, tapNONE, env.journal);
|
|
BEAST_EXPECT(pf.ter == tesSUCCESS);
|
|
auto const conseq = calculateConsequences(pf);
|
|
BEAST_EXPECT(conseq.category == TxConsequences::normal);
|
|
BEAST_EXPECT(conseq.fee == drops(10));
|
|
BEAST_EXPECT(conseq.potentialSpend == XRP(1000));
|
|
}
|
|
|
|
{
|
|
auto const jtx = env.jt(cancel("bob", "alice", 3),
|
|
seq(1), fee(10));
|
|
auto const pf = preflight(env.app(), env.current()->rules(),
|
|
*jtx.stx, tapNONE, env.journal);
|
|
BEAST_EXPECT(pf.ter == tesSUCCESS);
|
|
auto const conseq = calculateConsequences(pf);
|
|
BEAST_EXPECT(conseq.category == TxConsequences::normal);
|
|
BEAST_EXPECT(conseq.fee == drops(10));
|
|
BEAST_EXPECT(conseq.potentialSpend == XRP(0));
|
|
}
|
|
|
|
{
|
|
auto const jtx = env.jt(
|
|
finish("bob", "alice", 3,
|
|
makeSlice(cb1), makeSlice(fb1)),
|
|
seq(1), fee(10));
|
|
auto const pf = preflight(env.app(), env.current()->rules(),
|
|
*jtx.stx, tapNONE, env.journal);
|
|
BEAST_EXPECT(pf.ter == tesSUCCESS);
|
|
auto const conseq = calculateConsequences(pf);
|
|
BEAST_EXPECT(conseq.category == TxConsequences::normal);
|
|
BEAST_EXPECT(conseq.fee == drops(10));
|
|
BEAST_EXPECT(conseq.potentialSpend == XRP(0));
|
|
}
|
|
}
|
|
|
|
void run() override
|
|
{
|
|
testEnablement();
|
|
testTags();
|
|
testFails();
|
|
testLockup();
|
|
testCondPay();
|
|
testMeta();
|
|
testConsequences();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(SusPay,app,ripple);
|
|
|
|
} // test
|
|
} // ripple
|