mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
1937 lines
81 KiB
C++
1937 lines
81 KiB
C++
#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 doesn’t 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
|