Files
rippled/src/test/app/PayChan_test.cpp

1937 lines
81 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <test/jtx.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/ledger/Dir.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
namespace test {
using namespace jtx::paychan;
struct PayChan_test : public beast::unit_test::suite
{
static std::pair<uint256, std::shared_ptr<SLE const>>
channelKeyAndSle(ReadView const& view, jtx::Account const& account, jtx::Account const& dst)
{
auto const sle = view.read(keylet::account(account));
if (!sle)
return {};
auto const k = keylet::payChan(account, dst, (*sle)[sfSequence] - 1);
return {k.key, view.read(k)};
}
static Buffer
signClaimAuth(
PublicKey const& pk,
SecretKey const& sk,
uint256 const& channel,
STAmount const& authAmt)
{
Serializer msg;
serializePayChanAuthorization(msg, channel, authAmt.xrp());
return sign(pk, sk, msg.slice());
}
static STAmount
channelAmount(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return XRPAmount{-1};
return (*slep)[sfAmount];
}
static std::optional<std::int64_t>
channelExpiration(ReadView const& view, uint256 const& chan)
{
auto const slep = view.read({ltPAYCHAN, chan});
if (!slep)
return std::nullopt;
if (auto const r = (*slep)[~sfExpiration])
return r.value();
return std::nullopt;
}
void
testSimple(FeatureBitset features)
{
testcase("simple");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto USDA = alice["USD"];
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
{
auto const preAlice = env.balance(alice);
env(fund(alice, chan, XRP(1000)));
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
}
auto chanBal = channelBalance(*env.current(), chan);
auto chanAmt = channelAmount(*env.current(), chan);
BEAST_EXPECT(chanBal == XRP(0));
BEAST_EXPECT(chanAmt == XRP(2000));
{
// bad amounts (non-xrp, negative amounts)
env(create(alice, bob, USDA(1000), settleDelay, pk), ter(temBAD_AMOUNT));
env(fund(alice, chan, USDA(1000)), ter(temBAD_AMOUNT));
env(create(alice, bob, XRP(-1000), settleDelay, pk), ter(temBAD_AMOUNT));
env(fund(alice, chan, XRP(-1000)), ter(temBAD_AMOUNT));
}
// invalid account
env(create(alice, "noAccount", XRP(1000), settleDelay, pk), ter(tecNO_DST));
// can't create channel to the same account
env(create(alice, alice, XRP(1000), settleDelay, pk), ter(temDST_IS_SRC));
// invalid channel
env(fund(alice, channel(alice, "noAccount", env.seq(alice) - 1), XRP(1000)),
ter(tecNO_ENTRY));
// not enough funds
env(create(alice, bob, XRP(10000), settleDelay, pk), ter(tecUNFUNDED));
{
// No signature claim with bad amounts (negative and non-xrp)
auto const iou = USDA(100).value();
auto const negXRP = XRP(-100).value();
auto const posXRP = XRP(100).value();
env(claim(alice, chan, iou, iou), ter(temBAD_AMOUNT));
env(claim(alice, chan, posXRP, iou), ter(temBAD_AMOUNT));
env(claim(alice, chan, iou, posXRP), ter(temBAD_AMOUNT));
env(claim(alice, chan, negXRP, negXRP), ter(temBAD_AMOUNT));
env(claim(alice, chan, posXRP, negXRP), ter(temBAD_AMOUNT));
env(claim(alice, chan, negXRP, posXRP), ter(temBAD_AMOUNT));
}
{
// No signature claim more than authorized
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(-100);
assert(reqBal <= chanAmt);
env(claim(alice, chan, reqBal, authAmt), ter(temBAD_AMOUNT));
}
{
// No signature needed since the owner is claiming
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
env(claim(alice, chan, reqBal, authAmt));
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob + delta);
chanBal = reqBal;
}
{
// Claim with signature
auto preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()));
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
chanBal = reqBal;
// claim again
preBob = env.balance(bob);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()),
ter(tecUNFUNDED_PAYMENT));
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
}
{
// Try to claim more than authorized
auto const preBob = env.balance(bob);
STAmount const authAmt = chanBal + XRP(500);
STAmount const reqAmt = authAmt + STAmount{1};
assert(reqAmt <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), ter(temBAD_AMOUNT));
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// Dst tries to fund the channel
env(fund(bob, chan, XRP(1000)), ter(tecNO_PERMISSION));
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
{
// Wrong signing key
auto const sig = signClaimAuth(bob.pk(), bob.sk(), chan, XRP(1500));
env(claim(bob, chan, XRP(1500).value(), XRP(1500).value(), Slice(sig), bob.pk()),
ter(temBAD_SIGNER));
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
}
{
// Bad signature
auto const sig = signClaimAuth(bob.pk(), bob.sk(), chan, XRP(1500));
env(claim(bob, chan, XRP(1500).value(), XRP(1500).value(), Slice(sig), alice.pk()),
ter(temBAD_SIGNATURE));
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
}
{
// Dst closes channel
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
env(claim(bob, chan), txflags(tfClose));
BEAST_EXPECT(!channelExists(*env.current(), chan));
auto const feeDrops = env.current()->fees().base;
auto const delta = chanAmt - chanBal;
assert(delta > beast::zero);
BEAST_EXPECT(env.balance(alice) == preAlice + delta);
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
}
}
void
testDisallowIncoming(FeatureBitset features)
{
testcase("Disallow Incoming Flag");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const cho = Account("cho");
env.fund(XRP(10000), alice, bob, cho);
auto const pk = alice.pk();
auto const settleDelay = 100s;
// set flag on bob only
env(fset(bob, asfDisallowIncomingPayChan));
env.close();
// channel creation from alice to bob is disallowed
{
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk), ter(tecNO_PERMISSION));
BEAST_EXPECT(!channelExists(*env.current(), chan));
}
// set flag on alice also
env(fset(alice, asfDisallowIncomingPayChan));
env.close();
// channel creation from bob to alice is now disallowed
{
auto const chan = channel(bob, alice, env.seq(bob));
env(create(bob, alice, XRP(1000), settleDelay, pk), ter(tecNO_PERMISSION));
BEAST_EXPECT(!channelExists(*env.current(), chan));
}
// remove flag from bob
env(fclear(bob, asfDisallowIncomingPayChan));
env.close();
// now the channel between alice and bob can exist
{
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk), ter(tesSUCCESS));
BEAST_EXPECT(channelExists(*env.current(), chan));
}
// a channel from cho to alice isn't allowed
{
auto const chan = channel(cho, alice, env.seq(cho));
env(create(cho, alice, XRP(1000), settleDelay, pk), ter(tecNO_PERMISSION));
BEAST_EXPECT(!channelExists(*env.current(), chan));
}
// remove flag from alice
env(fclear(alice, asfDisallowIncomingPayChan));
env.close();
// now a channel from cho to alice is allowed
{
auto const chan = channel(cho, alice, env.seq(cho));
env(create(cho, alice, XRP(1000), settleDelay, pk), ter(tesSUCCESS));
BEAST_EXPECT(channelExists(*env.current(), chan));
}
}
void
testCancelAfter(FeatureBitset features)
{
testcase("cancel after");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
{
// If dst claims after cancel after, channel closes
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 100s;
NetClock::time_point const cancelAfter =
env.current()->header().parentCloseTime + 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(channelExists(*env.current(), chan));
env.close(cancelAfter);
{
// dst cannot claim after cancelAfter
auto const chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto preAlice = env.balance(alice);
auto preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()));
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(!channelExists(*env.current(), chan));
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds);
}
}
{
// Third party can close after cancel after
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
auto const pk = alice.pk();
auto const settleDelay = 100s;
NetClock::time_point const cancelAfter =
env.current()->header().parentCloseTime + 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(channelExists(*env.current(), chan));
// third party close before cancelAfter
env(claim(carol, chan), txflags(tfClose), ter(tecNO_PERMISSION));
BEAST_EXPECT(channelExists(*env.current(), chan));
env.close(cancelAfter);
// third party close after cancelAfter
auto const preAlice = env.balance(alice);
env(claim(carol, chan), txflags(tfClose));
BEAST_EXPECT(!channelExists(*env.current(), chan));
BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds);
}
// fixPayChanCancelAfter
// CancelAfter should be greater than close time
{
for (bool const withFixPayChan : {true, false})
{
auto const amend = withFixPayChan ? features : features - fixPayChanCancelAfter;
Env env{*this, amend};
env.fund(XRP(10000), alice, bob);
env.close();
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const channelFunds = XRP(1000);
NetClock::time_point const cancelAfter =
env.current()->header().parentCloseTime - 1s;
auto const txResult = withFixPayChan ? ter(tecEXPIRED) : ter(tesSUCCESS);
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter), txResult);
}
}
// fixPayChanCancelAfter
// CancelAfter can be equal to the close time
{
for (bool const withFixPayChan : {true, false})
{
auto const amend = withFixPayChan ? features : features - fixPayChanCancelAfter;
Env env{*this, amend};
env.fund(XRP(10000), alice, bob);
env.close();
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const channelFunds = XRP(1000);
NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime;
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter),
ter(tesSUCCESS));
}
}
}
void
testExpiration(FeatureBitset features)
{
testcase("expiration");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, bob, carol);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const closeTime = env.current()->header().parentCloseTime;
auto const minExpiration = closeTime + settleDelay;
NetClock::time_point const cancelAfter = closeTime + 7200s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(channelExists(*env.current(), chan));
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
// Owner closes, will close after settleDelay
env(claim(alice, chan), txflags(tfClose));
auto counts = [](auto const& t) { return t.time_since_epoch().count(); };
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration));
// increase the expiration time
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 100s}));
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 100);
// decrease the expiration, but still above minExpiration
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 50s}));
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 50);
// decrease the expiration below minExpiration
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}),
ter(temBAD_EXPIRATION));
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 50);
env(claim(bob, chan), txflags(tfRenew), ter(tecNO_PERMISSION));
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 50);
env(claim(alice, chan), txflags(tfRenew));
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
// decrease the expiration below minExpiration
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}),
ter(temBAD_EXPIRATION));
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration}));
env.close(minExpiration);
// Try to extend the expiration after the expiration has already passed
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 1000s}));
BEAST_EXPECT(!channelExists(*env.current(), chan));
}
void
testSettleDelay(FeatureBitset features)
{
testcase("settle delay");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
NetClock::time_point const settleTimepoint =
env.current()->header().parentCloseTime + settleDelay;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan));
// Owner closes, will close after settleDelay
env(claim(alice, chan), txflags(tfClose));
BEAST_EXPECT(channelExists(*env.current(), chan));
env.close(settleTimepoint - settleDelay / 2);
{
// receiver can still claim
auto const chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()));
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
}
env.close(settleTimepoint);
{
// past settleTime, channel will close
auto const chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto const preAlice = env.balance(alice);
auto preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()));
BEAST_EXPECT(!channelExists(*env.current(), chan));
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(alice) == preAlice + chanAmt - chanBal);
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
}
}
void
testCloseDry(FeatureBitset features)
{
testcase("close dry");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan));
// Owner tries to close channel, but it will remain open (settle delay)
env(claim(alice, chan), txflags(tfClose));
BEAST_EXPECT(channelExists(*env.current(), chan));
{
// claim the entire amount
auto const preBob = env.balance(bob);
env(claim(alice, chan, channelFunds.value(), channelFunds.value()));
BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds);
BEAST_EXPECT(env.balance(bob) == preBob + channelFunds);
}
auto const preAlice = env.balance(alice);
// Channel is now dry, can close before expiration date
env(claim(alice, chan), txflags(tfClose));
BEAST_EXPECT(!channelExists(*env.current(), chan));
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(alice) == preAlice - feeDrops);
}
void
testDefaultAmount(FeatureBitset features)
{
// auth amount defaults to balance if not present
testcase("default amount");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan));
// Owner tries to close channel, but it will remain open (settle delay)
env(claim(alice, chan), txflags(tfClose));
BEAST_EXPECT(channelExists(*env.current(), chan));
{
auto chanBal = channelBalance(*env.current(), chan);
auto chanAmt = channelAmount(*env.current(), chan);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, reqBal);
env(claim(bob, chan, reqBal, std::nullopt, Slice(sig), alice.pk()));
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
chanBal = reqBal;
}
{
// Claim again
auto chanBal = channelBalance(*env.current(), chan);
auto chanAmt = channelAmount(*env.current(), chan);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, reqBal);
env(claim(bob, chan, reqBal, std::nullopt, Slice(sig), alice.pk()));
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
chanBal = reqBal;
}
}
void
testDisallowXRP(FeatureBitset features)
{
// auth amount defaults to balance if not present
testcase("Disallow XRP");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
{
// Create a channel where dst disallows XRP. Ignore that flag,
// since it's just advisory.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env(fset(bob, asfDisallowXRP));
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), 3600s, alice.pk()));
BEAST_EXPECT(channelExists(*env.current(), chan));
}
{
// Claim to a channel where dst disallows XRP (channel is
// created before disallow xrp is set). Ignore that flag
// since it is just advisory.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), 3600s, alice.pk()));
BEAST_EXPECT(channelExists(*env.current(), chan));
env(fset(bob, asfDisallowXRP));
auto const reqBal = XRP(500).value();
env(claim(alice, chan, reqBal, reqBal));
}
}
void
testDstTag(FeatureBitset features)
{
// auth amount defaults to balance if not present
testcase("Dst Tag");
using namespace jtx;
using namespace std::literals::chrono_literals;
// Create a channel where dst disallows XRP
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env(fset(bob, asfRequireDest));
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
{
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk), ter(tecDST_TAG_NEEDED));
BEAST_EXPECT(!channelExists(*env.current(), chan));
}
{
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, std::nullopt, 1));
BEAST_EXPECT(channelExists(*env.current(), chan));
}
}
void
testDepositAuth(FeatureBitset features)
{
testcase("Deposit Authorization");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto USDA = alice["USD"];
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
env(fset(bob, asfDepositAuth));
env.close();
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
// alice can add more funds to the channel even though bob has
// asfDepositAuth set.
env(fund(alice, chan, XRP(1000)));
env.close();
// alice claims. Fails because bob's lsfDepositAuth flag is set.
env(claim(alice, chan, XRP(500).value(), XRP(500).value()), ter(tecNO_PERMISSION));
env.close();
// Claim with signature
auto const baseFee = env.current()->fees().base;
auto const preBob = env.balance(bob);
{
auto const delta = XRP(500).value();
auto const sig = signClaimAuth(pk, alice.sk(), chan, delta);
// alice claims with signature. Fails since bob has
// lsfDepositAuth flag set.
env(claim(alice, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob);
// bob claims but omits the signature. Fails because only
// alice can claim without a signature.
env(claim(bob, chan, delta, delta), ter(temBAD_SIGNATURE));
env.close();
// bob claims with signature. Succeeds even though bob's
// lsfDepositAuth flag is set since bob submitted the
// transaction.
env(claim(bob, chan, delta, delta, Slice(sig), pk));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob + delta - baseFee);
}
{
// Explore the limits of deposit pre-authorization.
auto const delta = XRP(600).value();
auto const sig = signClaimAuth(pk, alice.sk(), chan, delta);
// carol claims and fails. Only channel participants (bob or
// alice) may claim.
env(claim(carol, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION));
env.close();
// bob preauthorizes carol for deposit. But after that carol
// still can't claim since only channel participants may claim.
env(deposit::auth(bob, carol));
env.close();
env(claim(carol, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION));
// Since alice is not preauthorized she also may not claim
// for bob.
env(claim(alice, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION));
env.close();
// However if bob preauthorizes alice for deposit then she can
// successfully submit a claim.
env(deposit::auth(bob, alice));
env.close();
env(claim(alice, chan, delta, delta, Slice(sig), pk));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob + delta - (3 * baseFee));
}
{
// bob removes pre-authorization of alice. Once again she
// cannot submit a claim.
auto const delta = XRP(800).value();
env(deposit::unauth(bob, alice));
env.close();
// alice claims and fails since she is no longer preauthorized.
env(claim(alice, chan, delta, delta), ter(tecNO_PERMISSION));
env.close();
// bob clears lsfDepositAuth. Now alice can claim.
env(fclear(bob, asfDepositAuth));
env.close();
// alice claims successfully.
env(claim(alice, chan, delta, delta));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob + XRP(800) - (5 * baseFee));
}
}
}
void
testDepositAuthCreds()
{
testcase("Deposit Authorization with Credentials");
using namespace jtx;
using namespace std::literals::chrono_literals;
char const credType[] = "abcde";
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
Account const dillon("dillon");
Account const zelda("zelda");
{
Env env{*this};
env.fund(XRP(10000), alice, bob, carol, dillon, zelda);
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
// alice add funds to the channel
env(fund(alice, chan, XRP(1000)));
env.close();
std::string const credBadIdx =
"D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6"
"E1";
auto const delta = XRP(500).value();
{ // create credentials
auto jv = credentials::create(alice, carol, credType);
uint32_t const t =
env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
jv[sfExpiration.jsonName] = t;
env(jv);
env.close();
}
auto const jv = credentials::ledgerEntry(env, alice, carol, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// Bob require pre-authorization
env(fset(bob, asfDepositAuth));
env.close();
// Fail, credentials not accepted
env(claim(alice, chan, delta, delta),
credentials::ids({credIdx}),
ter(tecBAD_CREDENTIALS));
env.close();
env(credentials::accept(alice, carol, credType));
env.close();
// Fail, no depositPreauth object
env(claim(alice, chan, delta, delta),
credentials::ids({credIdx}),
ter(tecNO_PERMISSION));
env.close();
// Setup deposit authorization
env(deposit::authCredentials(bob, {{carol, credType}}));
env.close();
// Fail, credentials doesnt belong to root account
env(claim(dillon, chan, delta, delta),
credentials::ids({credIdx}),
ter(tecBAD_CREDENTIALS));
// Fails because bob's lsfDepositAuth flag is set.
env(claim(alice, chan, delta, delta), ter(tecNO_PERMISSION));
// Fail, bad credentials index.
env(claim(alice, chan, delta, delta),
credentials::ids({credBadIdx}),
ter(tecBAD_CREDENTIALS));
// Fail, empty credentials
env(claim(alice, chan, delta, delta), credentials::ids({}), ter(temMALFORMED));
{
// claim fails cause of expired credentials
// Every cycle +10sec.
for (int i = 0; i < 10; ++i)
env.close();
env(claim(alice, chan, delta, delta), credentials::ids({credIdx}), ter(tecEXPIRED));
env.close();
}
{ // create credentials once more
env(credentials::create(alice, carol, credType));
env.close();
env(credentials::accept(alice, carol, credType));
env.close();
auto const jv = credentials::ledgerEntry(env, alice, carol, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// Success
env(claim(alice, chan, delta, delta), credentials::ids({credIdx}));
}
}
{
Env env{*this};
env.fund(XRP(10000), alice, bob, carol, dillon, zelda);
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
// alice add funds to the channel
env(fund(alice, chan, XRP(1000)));
env.close();
auto const delta = XRP(500).value();
{ // create credentials
env(credentials::create(alice, carol, credType));
env.close();
env(credentials::accept(alice, carol, credType));
env.close();
}
auto const jv = credentials::ledgerEntry(env, alice, carol, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// Succeed, lsfDepositAuth is not set
env(claim(alice, chan, delta, delta), credentials::ids({credIdx}));
env.close();
}
{
// Credentials amendment not enabled
Env env(*this, testable_amendments() - featureCredentials);
env.fund(XRP(5000), "alice", "bob");
env.close();
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
env(fund(alice, chan, XRP(1000)));
env.close();
std::string const credIdx =
"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B"
"E4";
// can't claim with old DepositPreauth because rule is not enabled.
env(fset(bob, asfDepositAuth));
env.close();
env(deposit::auth(bob, alice));
env.close();
env(claim(alice, chan, XRP(500).value(), XRP(500).value()),
credentials::ids({credIdx}),
ter(temDISABLED));
}
}
void
testMultiple(FeatureBitset features)
{
// auth amount defaults to balance if not present
testcase("Multiple channels to the same account");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1 = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan1));
auto const chan2 = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan2));
BEAST_EXPECT(chan1 != chan2);
}
void
testAccountChannelsRPC(FeatureBitset features)
{
testcase("AccountChannels RPC");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const charlie = Account("charlie", KeyType::ed25519);
env.fund(XRP(10000), alice, bob, charlie);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
env.close();
{
// test account non-string
auto testInvalidAccountParam = [&](auto const& param) {
Json::Value params;
params[jss::account] = param;
auto jrr = env.rpc("json", "account_channels", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
};
testInvalidAccountParam(1);
testInvalidAccountParam(1.1);
testInvalidAccountParam(true);
testInvalidAccountParam(Json::Value(Json::nullValue));
testInvalidAccountParam(Json::Value(Json::objectValue));
testInvalidAccountParam(Json::Value(Json::arrayValue));
}
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str);
BEAST_EXPECT(r[jss::result][jss::validated]);
}
{
auto const r = env.rpc("account_channels", alice.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str);
BEAST_EXPECT(r[jss::result][jss::validated]);
}
{
auto const r = env.rpc("account_channels", bob.human(), alice.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 0);
BEAST_EXPECT(r[jss::result][jss::validated]);
}
auto const chan2Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
env.close();
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 2);
BEAST_EXPECT(r[jss::result][jss::validated]);
BEAST_EXPECT(chan1Str != chan2Str);
for (auto const& c : {chan1Str, chan2Str})
BEAST_EXPECT(
r[jss::result][jss::channels][0u][jss::channel_id] == c ||
r[jss::result][jss::channels][1u][jss::channel_id] == c);
}
}
void
testAccountChannelsRPCMarkers(FeatureBitset features)
{
testcase("Account channels RPC markers");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bobs = []() -> std::vector<Account> {
int const n = 10;
std::vector<Account> r;
r.reserve(n);
for (int i = 0; i < n; ++i)
{
r.emplace_back("bob"s + std::to_string(i));
}
return r;
}();
Env env{*this, features};
env.fund(XRP(10000), alice);
for (auto const& a : bobs)
{
env.fund(XRP(10000), a);
env.close();
}
{
// create a channel from alice to every bob account
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1);
for (auto const& b : bobs)
{
env(create(alice, b, channelFunds, settleDelay, alice.pk()));
}
}
auto testLimit = [](test::jtx::Env& env,
test::jtx::Account const& src,
std::optional<int> limit = std::nullopt,
Json::Value const& marker = Json::nullValue,
std::optional<test::jtx::Account> const& dst = std::nullopt) {
Json::Value jvc;
jvc[jss::account] = src.human();
if (dst)
jvc[jss::destination_account] = dst->human();
if (limit)
jvc[jss::limit] = *limit;
if (marker)
jvc[jss::marker] = marker;
return env.rpc("json", "account_channels", to_string(jvc))[jss::result];
};
{
// No marker
auto const r = testLimit(env, alice);
BEAST_EXPECT(r.isMember(jss::channels));
BEAST_EXPECT(r[jss::channels].size() == bobs.size());
}
auto const bobsB58 = [&bobs]() -> std::set<std::string> {
std::set<std::string> r;
for (auto const& a : bobs)
r.insert(a.human());
return r;
}();
for (int limit = 1; limit < bobs.size() + 1; ++limit)
{
auto leftToFind = bobsB58;
auto const numFull = bobs.size() / limit;
auto const numNonFull = bobs.size() % limit ? 1 : 0;
Json::Value marker = Json::nullValue;
auto const testIt = [&](bool expectMarker, int expectedBatchSize) {
auto const r = testLimit(env, alice, limit, marker);
BEAST_EXPECT(!expectMarker || r.isMember(jss::marker));
if (r.isMember(jss::marker))
marker = r[jss::marker];
BEAST_EXPECT(r[jss::channels].size() == expectedBatchSize);
auto const c = r[jss::channels];
auto const s = r[jss::channels].size();
for (int j = 0; j < s; ++j)
{
auto const dstAcc = c[j][jss::destination_account].asString();
BEAST_EXPECT(leftToFind.count(dstAcc));
leftToFind.erase(dstAcc);
}
};
for (int i = 0; i < numFull; ++i)
{
bool const expectMarker = (numNonFull != 0 || i < numFull - 1);
testIt(expectMarker, limit);
}
if (numNonFull)
{
testIt(false, bobs.size() % limit);
}
BEAST_EXPECT(leftToFind.empty());
}
{
// degenerate case
auto const r = testLimit(env, alice, 0);
BEAST_EXPECT(r.isMember(jss::error_message));
}
}
void
testAccountChannelsRPCSenderOnly(FeatureBitset features)
{
// Check that the account_channels command only returns channels owned
// by the account
testcase("Account channels RPC owner only");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
// Create a channel from alice to bob and from bob to alice
// When retrieving alice's channels, it should only retrieve the
// channels where alice is the source, not the destination
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
env(create(alice, bob, channelFunds, settleDelay, alice.pk()));
env(create(bob, alice, channelFunds, settleDelay, bob.pk()));
auto const r = [&] {
Json::Value jvc;
jvc[jss::account] = alice.human();
return env.rpc("json", "account_channels", to_string(jvc))[jss::result];
}();
BEAST_EXPECT(r.isMember(jss::channels));
BEAST_EXPECT(r[jss::channels].size() == 1);
BEAST_EXPECT(r[jss::channels][0u][jss::destination_account].asString() == bob.human());
}
void
testAccountChannelAuthorize(FeatureBitset features)
{
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const charlie = Account("charlie", KeyType::ed25519);
env.fund(XRP(10000), alice, bob, charlie);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
env.close();
Json::Value args{Json::objectValue};
args[jss::channel_id] = chan1Str;
args[jss::key_type] = "ed255191";
args[jss::seed] = "snHq1rzQoN2qiUkC3XF5RyxBzUtN";
args[jss::amount] = 51110000;
// test for all api versions
forAllApiVersions([&, this](unsigned apiVersion) {
testcase("PayChan Channel_Auth RPC Api " + std::to_string(apiVersion));
args[jss::api_version] = apiVersion;
auto const rs =
env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
auto const error = apiVersion < 2u ? "invalidParams" : "badKeyType";
BEAST_EXPECT(rs[jss::error] == error);
});
}
void
testAuthVerifyRPC(FeatureBitset features)
{
testcase("PayChan Auth/Verify RPC");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const charlie = Account("charlie", KeyType::ed25519);
env.fund(XRP(10000), alice, bob, charlie);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
env.close();
std::string chan1PkStr;
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str);
BEAST_EXPECT(r[jss::result][jss::validated]);
chan1PkStr = r[jss::result][jss::channels][0u][jss::public_key].asString();
}
{
auto const r = env.rpc("account_channels", alice.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str);
BEAST_EXPECT(r[jss::result][jss::validated]);
chan1PkStr = r[jss::result][jss::channels][0u][jss::public_key].asString();
}
{
auto const r = env.rpc("account_channels", bob.human(), alice.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 0);
BEAST_EXPECT(r[jss::result][jss::validated]);
}
auto const chan2Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
env.close();
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 2);
BEAST_EXPECT(r[jss::result][jss::validated]);
BEAST_EXPECT(chan1Str != chan2Str);
for (auto const& c : {chan1Str, chan2Str})
BEAST_EXPECT(
r[jss::result][jss::channels][0u][jss::channel_id] == c ||
r[jss::result][jss::channels][1u][jss::channel_id] == c);
}
auto sliceToHex = [](Slice const& slice) {
std::string s;
s.reserve(2 * slice.size());
for (int i = 0; i < slice.size(); ++i)
{
s += "0123456789ABCDEF"[((slice[i] & 0xf0) >> 4)];
s += "0123456789ABCDEF"[((slice[i] & 0x0f) >> 0)];
}
return s;
};
{
// Verify chan1 auth
auto const rs = env.rpc("channel_authorize", "alice", chan1Str, "1000");
auto const sig = rs[jss::result][jss::signature].asString();
BEAST_EXPECT(!sig.empty());
{
auto const rv = env.rpc("channel_verify", chan1PkStr, chan1Str, "1000", sig);
BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool());
}
{
// use pk hex to verify
auto const pkAsHex = sliceToHex(pk.slice());
auto const rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000", sig);
BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool());
}
{
// malformed amount
auto const pkAsHex = sliceToHex(pk.slice());
auto rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000x", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 ", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x1000", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, " ", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 1000", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1,000", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, " 1000", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
rv = env.rpc("channel_verify", pkAsHex, chan1Str, "", sig);
BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed");
}
{
// malformed channel
auto const pkAsHex = sliceToHex(pk.slice());
auto chan1StrBad = chan1Str;
chan1StrBad.pop_back();
auto rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
chan1StrBad = chan1Str;
chan1StrBad.push_back('0');
rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
chan1StrBad = chan1Str;
chan1StrBad.back() = 'x';
rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig);
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000");
BEAST_EXPECT(rv[jss::error] == "channelMalformed");
}
{
// give an ill formed base 58 public key
auto illFormedPk = chan1PkStr.substr(0, chan1PkStr.size() - 1);
auto const rv = env.rpc("channel_verify", illFormedPk, chan1Str, "1000", sig);
BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool());
}
{
// give an ill formed hex public key
auto const pkAsHex = sliceToHex(pk.slice());
auto illFormedPk = pkAsHex.substr(0, chan1PkStr.size() - 1);
auto const rv = env.rpc("channel_verify", illFormedPk, chan1Str, "1000", sig);
BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool());
}
}
{
// Try to verify chan2 auth with chan1 key
auto const rs = env.rpc("channel_authorize", "alice", chan2Str, "1000");
auto const sig = rs[jss::result][jss::signature].asString();
BEAST_EXPECT(!sig.empty());
{
auto const rv = env.rpc("channel_verify", chan1PkStr, chan1Str, "1000", sig);
BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool());
}
{
// use pk hex to verify
auto const pkAsHex = sliceToHex(pk.slice());
auto const rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000", sig);
BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool());
}
}
{
// Try to explicitly specify secp256k1 and Ed25519 keys:
auto const chan = to_string(channel(charlie, alice, env.seq(charlie)));
env(create(charlie, alice, channelFunds, settleDelay, charlie.pk()));
env.close();
std::string cpk;
{
auto const r = env.rpc("account_channels", charlie.human(), alice.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan);
BEAST_EXPECT(r[jss::result][jss::validated]);
cpk = r[jss::result][jss::channels][0u][jss::public_key].asString();
}
// Try to authorize without specifying a key type, expect an error:
auto const rs = env.rpc("channel_authorize", "charlie", chan, "1000");
auto const sig = rs[jss::result][jss::signature].asString();
BEAST_EXPECT(!sig.empty());
{
auto const rv = env.rpc("channel_verify", cpk, chan, "1000", sig);
BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool());
}
// Try to authorize using an unknown key type, except an error:
auto const rs1 = env.rpc("channel_authorize", "charlie", "nyx", chan, "1000");
BEAST_EXPECT(rs1[jss::error] == "badKeyType");
// Try to authorize using secp256k1; the authorization _should_
// succeed but the verification should fail:
auto const rs2 = env.rpc("channel_authorize", "charlie", "secp256k1", chan, "1000");
auto const sig2 = rs2[jss::result][jss::signature].asString();
BEAST_EXPECT(!sig2.empty());
{
auto const rv = env.rpc("channel_verify", cpk, chan, "1000", sig2);
BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool());
}
// Try to authorize using Ed25519; expect success:
auto const rs3 = env.rpc("channel_authorize", "charlie", "ed25519", chan, "1000");
auto const sig3 = rs3[jss::result][jss::signature].asString();
BEAST_EXPECT(!sig3.empty());
{
auto const rv = env.rpc("channel_verify", cpk, chan, "1000", sig3);
BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool());
}
}
{
// send malformed amounts rpc requests
auto rs = env.rpc("channel_authorize", "alice", chan1Str, "1000x");
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
rs = env.rpc("channel_authorize", "alice", chan1Str, "x1000");
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
rs = env.rpc("channel_authorize", "alice", chan1Str, "x");
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
{
// Missing channel_id
Json::Value args{Json::objectValue};
args[jss::amount] = "2000";
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "invalidParams");
}
{
// Missing amount
Json::Value args{Json::objectValue};
args[jss::channel_id] = chan1Str;
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "invalidParams");
}
{
// Missing key_type and no secret.
Json::Value args{Json::objectValue};
args[jss::amount] = "2000";
args[jss::channel_id] = chan1Str;
args[jss::passphrase] = "passphrase_can_be_anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "invalidParams");
}
{
// Both passphrase and seed specified.
Json::Value args{Json::objectValue};
args[jss::amount] = "2000";
args[jss::channel_id] = chan1Str;
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
args[jss::seed] = "seed can be anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "invalidParams");
}
{
// channel_id is not exact hex.
Json::Value args{Json::objectValue};
args[jss::amount] = "2000";
args[jss::channel_id] = chan1Str + "1";
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "channelMalformed");
}
{
// amount is not a string
Json::Value args{Json::objectValue};
args[jss::amount] = 2000;
args[jss::channel_id] = chan1Str;
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
}
{
// Amount is not a decimal string.
Json::Value args{Json::objectValue};
args[jss::amount] = "TwoThousand";
args[jss::channel_id] = chan1Str;
args[jss::key_type] = "secp256k1";
args[jss::passphrase] = "passphrase_can_be_anything";
rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result];
BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed");
}
}
}
void
testOptionalFields(FeatureBitset features)
{
testcase("Optional Fields");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const dan = Account("dan");
env.fund(XRP(10000), alice, bob, carol, dan);
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
std::optional<NetClock::time_point> cancelAfter;
{
auto const chan = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
auto const r = env.rpc("account_channels", alice.human(), bob.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan);
BEAST_EXPECT(!r[jss::result][jss::channels][0u].isMember(jss::destination_tag));
}
{
std::uint32_t dstTag = 42;
auto const chan = to_string(channel(alice, carol, env.seq(alice)));
env(create(alice, carol, channelFunds, settleDelay, pk, cancelAfter, dstTag));
auto const r = env.rpc("account_channels", alice.human(), carol.human());
BEAST_EXPECT(r[jss::result][jss::channels].size() == 1);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan);
BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::destination_tag] == dstTag);
}
}
void
testMalformedPK(FeatureBitset features)
{
testcase("malformed pk");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto USDA = alice["USD"];
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
auto jv = create(alice, bob, XRP(1000), settleDelay, pk);
auto const pkHex = strHex(pk.slice());
jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2);
env(jv, ter(temMALFORMED));
jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2);
env(jv, ter(temMALFORMED));
auto badPrefix = pkHex;
badPrefix[0] = 'f';
badPrefix[1] = 'f';
jv["PublicKey"] = badPrefix;
env(jv, ter(temMALFORMED));
jv["PublicKey"] = pkHex;
env(jv);
auto const authAmt = XRP(100);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
jv = claim(bob, chan, authAmt.value(), authAmt.value(), Slice(sig), alice.pk());
jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2);
env(jv, ter(temMALFORMED));
jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2);
env(jv, ter(temMALFORMED));
badPrefix = pkHex;
badPrefix[0] = 'f';
badPrefix[1] = 'f';
jv["PublicKey"] = badPrefix;
env(jv, ter(temMALFORMED));
// missing public key
jv.removeMember("PublicKey");
env(jv, ter(temMALFORMED));
{
auto const txn = R"*(
{
"channel_id":"5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3",
"signature":
"304402204EF0AFB78AC23ED1C472E74F4299C0C21F1B21D07EFC0A3838A420F76D783A400220154FB11B6F54320666E4C36CA7F686C16A3A0456800BBC43746F34AF50290064",
"public_key":
"aKijDDiC2q2gXjMpM7i4BUS6cmixgsEe18e7CjsUxwihKfuoFgS5",
"amount": "1000000"
}
)*";
auto const r = env.rpc("json", "channel_verify", txn);
BEAST_EXPECT(r["result"]["error"] == "publicMalformed");
}
}
void
testMetaAndOwnership(FeatureBitset features)
{
testcase("Metadata & Ownership");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const settleDelay = 100s;
auto const pk = alice.pk();
auto inOwnerDir = [](ReadView const& view,
Account const& acc,
std::shared_ptr<SLE const> const& chan) -> bool {
xrpl::Dir const ownerDir(view, keylet::ownerDir(acc.id()));
return std::find(ownerDir.begin(), ownerDir.end(), chan) != ownerDir.end();
};
auto ownerDirCount = [](ReadView const& view, Account const& acc) -> std::size_t {
xrpl::Dir const ownerDir(view, keylet::ownerDir(acc.id()));
return std::distance(ownerDir.begin(), ownerDir.end());
};
{
// Test with adding the paychan to the recipient's owner directory
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob);
BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
// close the channel
env(claim(bob, chan), txflags(tfClose));
BEAST_EXPECT(!channelExists(*env.current(), chan));
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
}
{
// Test removing paychans created before adding to the recipient's
// owner directory
Env env(*this, features);
env.fund(XRP(10000), alice, bob);
// create the channel before the amendment activates
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob);
BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
env(claim(bob, chan), txflags(tfClose));
BEAST_EXPECT(!channelExists(*env.current(), chan));
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle));
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
}
}
void
testAccountDelete(FeatureBitset features)
{
testcase("Account Delete");
using namespace test::jtx;
using namespace std::literals::chrono_literals;
auto rmAccount =
[this](
Env& env, Account const& toRm, Account const& dst, TER expectedTer = tesSUCCESS) {
// only allow an account to be deleted if the account's sequence
// number is at least 256 less than the current ledger sequence
for (auto minRmSeq = env.seq(toRm) + 257; env.current()->seq() < minRmSeq;
env.close())
{
}
env(acctdelete(toRm, dst),
fee(drops(env.current()->fees().increment)),
ter(expectedTer));
env.close();
this->BEAST_EXPECT(
isTesSuccess(expectedTer) == !env.closed()->exists(keylet::account(toRm.id())));
};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol);
env.close();
// Create a channel from alice to bob
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
rmAccount(env, alice, carol, tecHAS_OBLIGATIONS);
rmAccount(env, bob, carol, TER(tecHAS_OBLIGATIONS));
auto const feeDrops = env.current()->fees().base;
auto chanBal = channelBalance(*env.current(), chan);
auto chanAmt = channelAmount(*env.current(), chan);
BEAST_EXPECT(chanBal == XRP(0));
BEAST_EXPECT(chanAmt == XRP(1000));
auto preBob = env.balance(bob);
auto const delta = XRP(50);
auto reqBal = chanBal + delta;
auto authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
env(claim(alice, chan, reqBal, authAmt));
env.close();
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob + delta);
chanBal = reqBal;
auto const preAlice = env.balance(alice);
env(fund(alice, chan, XRP(1000)));
env.close();
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt + XRP(1000));
chanAmt = chanAmt + XRP(1000);
{
// Owner closes, will close after settleDelay
env(claim(alice, chan), txflags(tfClose));
env.close();
// settle delay hasn't elapsed. Channels should exist.
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const closeTime = env.current()->header().parentCloseTime;
auto const minExpiration = closeTime + settleDelay;
env.close(minExpiration);
env(claim(alice, chan), txflags(tfClose));
BEAST_EXPECT(!channelExists(*env.current(), chan));
}
}
}
void
testUsingTickets(FeatureBitset features)
{
testcase("using tickets");
using namespace jtx;
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto USDA = alice["USD"];
env.fund(XRP(10000), alice, bob);
// alice and bob grab enough tickets for all of the following
// transactions. Note that once the tickets are acquired alice's
// and bob's account sequence numbers should not advance.
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
std::uint32_t const aliceSeq{env.seq(alice)};
std::uint32_t bobTicketSeq{env.seq(bob) + 1};
env(ticket::create(bob, 10));
std::uint32_t const bobSeq{env.seq(bob)};
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, aliceTicketSeq);
env(create(alice, bob, XRP(1000), settleDelay, pk), ticket::use(aliceTicketSeq++));
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
{
auto const preAlice = env.balance(alice);
env(fund(alice, chan, XRP(1000)), ticket::use(aliceTicketSeq++));
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
}
auto chanBal = channelBalance(*env.current(), chan);
auto chanAmt = channelAmount(*env.current(), chan);
BEAST_EXPECT(chanBal == XRP(0));
BEAST_EXPECT(chanAmt == XRP(2000));
{
// No signature needed since the owner is claiming
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
env(claim(alice, chan, reqBal, authAmt), ticket::use(aliceTicketSeq++));
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob + delta);
chanBal = reqBal;
}
{
// Claim with signature
auto preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()),
ticket::use(bobTicketSeq++));
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
chanBal = reqBal;
// claim again
preBob = env.balance(bob);
// A transaction that generates a tec still consumes its ticket.
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()),
ticket::use(bobTicketSeq++),
ter(tecUNFUNDED_PAYMENT));
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
}
{
// Try to claim more than authorized
auto const preBob = env.balance(bob);
STAmount const authAmt = chanBal + XRP(500);
STAmount const reqAmt = authAmt + drops(1);
assert(reqAmt <= chanAmt);
// Note that since claim() returns a tem (neither tec nor tes),
// the ticket is not consumed. So we don't increment bobTicket.
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()),
ticket::use(bobTicketSeq),
ter(temBAD_AMOUNT));
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob);
}
// Dst tries to fund the channel
env(fund(bob, chan, XRP(1000)), ticket::use(bobTicketSeq++), ter(tecNO_PERMISSION));
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
{
// Dst closes channel
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
env(claim(bob, chan), txflags(tfClose), ticket::use(bobTicketSeq++));
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(!channelExists(*env.current(), chan));
auto const feeDrops = env.current()->fees().base;
auto const delta = chanAmt - chanBal;
assert(delta > beast::zero);
BEAST_EXPECT(env.balance(alice) == preAlice + delta);
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
}
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
}
void
testWithFeats(FeatureBitset features)
{
testSimple(features);
testDisallowIncoming(features);
testCancelAfter(features);
testSettleDelay(features);
testExpiration(features);
testCloseDry(features);
testDefaultAmount(features);
testDisallowXRP(features);
testDstTag(features);
testDepositAuth(features);
testMultiple(features);
testAccountChannelsRPC(features);
testAccountChannelsRPCMarkers(features);
testAccountChannelsRPCSenderOnly(features);
testAccountChannelAuthorize(features);
testAuthVerifyRPC(features);
testOptionalFields(features);
testMalformedPK(features);
testMetaAndOwnership(features);
testAccountDelete(features);
testUsingTickets(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testable_amendments()};
testWithFeats(all);
testDepositAuthCreds();
testMetaAndOwnership(all - fixIncludeKeyletFields);
}
};
BEAST_DEFINE_TESTSUITE(PayChan, app, xrpl);
} // namespace test
} // namespace xrpl