Compare commits

...

4 Commits

Author SHA1 Message Date
Richard Holland
b43a100efc Merge branch 'dev' into EscrowCancel 2026-10-03 15:40:10 +10:00
Richard Holland
81bd320775 more tests 2026-10-03 15:38:31 +10:00
Richard Holland
f8b5520506 test coverage 2026-10-02 14:34:43 +10:00
Richard Holland
dfb58c1b4d escrow cancel amendment 2026-10-02 13:54:12 +10:00
7 changed files with 454 additions and 58 deletions

View File

@@ -34,6 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -4722,6 +4722,359 @@ struct Escrow_test : public beast::unit_test::suite
}
}
void
testDestinationCancel(FeatureBitset features)
{
using namespace jtx;
using namespace std::chrono;
bool const enabled = features[featureEscrowDestinationCancel];
TER const destResult =
enabled ? TER{tesSUCCESS} : TER{tecNO_PERMISSION};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const USD = gw["USD"];
auto const ownerDirEmpty = [](Env& env, Account const& acct) {
ripple::Dir dir(*env.current(), keylet::ownerDir(acct.id()));
return std::distance(dir.begin(), dir.end()) == 0;
};
{
testcase("Destination Cancel: before CancelAfter");
Env env{*this, features};
env.fund(XRP(5000), alice, bob, carol);
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)),
finish_time(env.now() + 100s),
cancel_time(env.now() + 200s));
env.close();
// Neither the owner nor a third party may cancel early.
env(cancel(alice, alice, seq), ter(tecNO_PERMISSION));
env(cancel(carol, alice, seq), ter(tecNO_PERMISSION));
env.close();
STAmount const preAlice = env.balance(alice);
env(cancel(bob, alice, seq), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
// Funds go back to the owner, not the destination.
env.require(balance(alice, preAlice + XRP(1000)));
BEAST_EXPECT(env.ownerCount(alice) == 0);
BEAST_EXPECT(ownerDirEmpty(env, alice));
BEAST_EXPECT(ownerDirEmpty(env, bob));
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
// Rejected outright: nothing moves.
env.require(balance(alice, preAlice));
BEAST_EXPECT(env.ownerCount(alice) == 1);
BEAST_EXPECT(!ownerDirEmpty(env, bob));
}
}
{
testcase("Destination Cancel: after FinishAfter");
Env env{*this, features};
env.fund(XRP(5000), alice, bob);
env.close();
// Finishable, and with no CancelAfter, otherwise never cancellable.
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s));
env.close(5s);
env(cancel(alice, alice, seq), ter(tecNO_PERMISSION));
env(cancel(bob, alice, seq), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
env(finish(bob, alice, seq));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
}
}
{
testcase("Destination Cancel: conditional");
Env env{*this, features};
env.fund(XRP(5000), alice, bob);
env.close();
// The destination does not need the fulfillment to decline.
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)),
condition(cb1),
cancel_time(env.now() + 200s));
env.close();
env(cancel(bob, alice, seq), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
}
}
{
testcase("Destination Cancel: self-escrow stays locked");
Env env{*this, features};
env.fund(XRP(5000), alice);
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, alice, XRP(1000)),
finish_time(env.now() + 100s),
cancel_time(env.now() + 200s));
env.close();
env(cancel(alice, alice, seq), ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
}
{
testcase("Destination Cancel: EscrowID");
Env env{*this, features};
env.fund(XRP(5000), alice, bob);
env.close();
uint256 const escrowId{keylet::escrow(alice, env.seq(alice)).key};
env(escrow(alice, bob, XRP(1000)),
finish_time(env.now() + 100s),
cancel_time(env.now() + 200s));
env.close();
env(cancel(bob, alice), escrow_id(escrowId), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::unchecked(escrowId)));
}
else
{
BEAST_EXPECT(env.le(keylet::unchecked(escrowId)));
}
}
{
testcase("Destination Cancel: owner hook cannot block");
Env env{*this, features};
env.fund(XRP(5000), alice, bob, carol);
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s));
env.close();
env.close();
// Past CancelAfter, so only the owner's hook stands in the way.
env(jtx::hook(alice, {{hso(jtx::genesis::RollbackHook)}}, 0),
fee(XRP(2)));
env.close();
// The owner stays a strong tsh when a third party cancels.
env(cancel(carol, alice, seq), fee(XRP(1)), ter(tecHOOK_REJECTED));
env.close();
if (enabled)
{
env(cancel(bob, alice, seq), fee(XRP(1)));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
}
else
{
env(cancel(bob, alice, seq),
fee(XRP(1)),
ter(tecHOOK_REJECTED));
env.close();
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
}
}
{
testcase("Destination Cancel: IOU");
Env env{*this, features};
env.fund(XRP(5000), alice, bob, gw);
env.close();
env.trust(USD(10000), alice, bob);
env.close();
env(pay(gw, alice, USD(5000)));
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 100s),
cancel_time(env.now() + 200s));
env.close();
BEAST_EXPECT(-lockedAmount(env, alice, gw, USD) == USD(1000));
env(cancel(bob, alice, seq), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
BEAST_EXPECT(-lockedAmount(env, alice, gw, USD) == USD(0));
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
BEAST_EXPECT(-lockedAmount(env, alice, gw, USD) == USD(1000));
}
env.require(balance(alice, USD(5000)));
env.require(balance(bob, USD(0)));
}
{
testcase("Destination Cancel: issuer-owned IOU");
Env env{*this, features};
env.fund(XRP(5000), alice, gw);
env.close();
env.trust(USD(10000), alice);
env.close();
// An issuer escrowing its own IOU locks nothing, so the cancel
// only has to unwind the escrow itself.
auto const seq = env.seq(gw);
env(escrow(gw, alice, USD(1000)),
finish_time(env.now() + 100s),
cancel_time(env.now() + 200s));
env.close();
BEAST_EXPECT(env.ownerCount(gw) == 1);
env(cancel(gw, gw, seq), ter(tecNO_PERMISSION));
env(cancel(alice, gw, seq), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::escrow(gw.id(), seq)));
BEAST_EXPECT(env.ownerCount(gw) == 0);
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(gw.id(), seq)));
BEAST_EXPECT(env.ownerCount(gw) == 1);
}
env.require(balance(alice, USD(0)));
}
{
testcase("Destination Cancel: owner hook chain not charged");
Env env{*this, features};
env.fund(XRP(5000), alice, bob, carol);
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s));
env.close();
env.close();
auto const feeFor = [&](Account const& acct) {
return calculateBaseFee(
*env.current(), *env.jt(cancel(acct, alice, seq)).stx);
};
XRPAmount const bobBefore = feeFor(bob);
XRPAmount const carolBefore = feeFor(carol);
env(jtx::hook(alice, {{hso(jtx::genesis::RollbackHook)}}, 0),
fee(XRP(2)));
env.close();
// A third party still pays for the owner's (strong) hook chain.
BEAST_EXPECT(feeFor(carol) > carolBefore);
// The destination does too, but only without the amendment.
if (enabled)
{
BEAST_EXPECT(feeFor(bob) == bobBefore);
}
else
{
BEAST_EXPECT(feeFor(bob) > bobBefore);
}
}
{
// fix1571 is still a live (DefaultYes) amendment on Xahau, so the
// legacy CancelAfter check in EscrowCancel remains reachable.
testcase("Destination Cancel: without fix1571");
Env env{*this, features - fix1571};
env.fund(XRP(5000), alice, bob, carol);
env.close();
// No CancelAfter.
auto const seq1 = env.seq(alice);
env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 100s));
// CancelAfter in the future.
auto const seq2 = env.seq(alice);
env(escrow(alice, bob, XRP(1000)),
finish_time(env.now() + 100s),
cancel_time(env.now() + 200s));
// CancelAfter about to pass.
auto const seq3 = env.seq(alice);
env(escrow(alice, bob, XRP(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s));
env.close();
env(cancel(alice, alice, seq1), ter(tecNO_PERMISSION));
env(cancel(carol, alice, seq2), ter(tecNO_PERMISSION));
env(cancel(bob, alice, seq1), ter(destResult));
env(cancel(bob, alice, seq2), ter(destResult));
env.close();
if (enabled)
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq1)));
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq2)));
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq1)));
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq2)));
}
// Past CancelAfter anyone may cancel, with or without the
// amendment.
env(cancel(carol, alice, seq3));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq3)));
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -4770,12 +5123,17 @@ public:
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments() | featureCredentials};
testWithFeats(all - featurePaychanAndEscrowForTokens);
testWithFeats(all);
testIOUWithFeats(all - featureClawback);
testIOUWithFeats(all);
// The pre-existing suites assert that the destination cannot cancel
// early, so they run without featureEscrowDestinationCancel.
FeatureBitset const legacy{all - featureEscrowDestinationCancel};
testWithFeats(legacy - featurePaychanAndEscrowForTokens);
testWithFeats(legacy);
testIOUWithFeats(legacy - featureClawback);
testIOUWithFeats(legacy);
testEscrowID(all);
testCredentials(all);
testDestinationCancel(legacy);
testDestinationCancel(all);
}
};

View File

@@ -1962,7 +1962,8 @@ private:
// | A | A | S | S | S | S | S
// | A | D | N | N | S | S | S
// | D | D | S | S | N/A | S | S
// | D | A | S | S | N/A | S | S
// | D | A | W* | W* | N/A | S | S
// * S without featureEscrowDestinationCancel
static uint256
getEscrowIndex(AccountID const& account, std::uint32_t uSequence)
@@ -2117,7 +2118,7 @@ private:
// otxn: dest
// tsh account
// w/s: strong
// w/s: weak (strong without featureEscrowDestinationCancel)
for (bool const testStrong : {true, false})
{
test::jtx::Env env{
@@ -2125,6 +2126,11 @@ private:
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
bool const withDestCancel =
env.current()->rules().enabled(featureEscrowDestinationCancel);
auto const account = Account("alice");
auto const dest = Account("bob");
env.fund(XRP(1000), account, dest);
@@ -2154,7 +2160,10 @@ private:
env.close();
// verify tsh hook triggered
testTSHStrongWeak(env, tshSTRONG, __LINE__);
auto const expected = !withDestCancel ? tshSTRONG
: !testStrong && withIOUIssuerWeakTSH ? tshWEAK
: tshNONE;
testTSHStrongWeak(env, expected, __LINE__);
}
// otxn: account
@@ -2373,7 +2382,7 @@ private:
// otxn: dest
// tsh account
// w/s: strong
// w/s: weak (strong without featureEscrowDestinationCancel)
for (bool const testStrong : {true, false})
{
test::jtx::Env env{
@@ -2381,6 +2390,11 @@ private:
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
bool const withDestCancel =
env.current()->rules().enabled(featureEscrowDestinationCancel);
auto const account = Account("alice");
auto const dest = Account("bob");
env.fund(XRP(1000), account, dest);
@@ -2413,7 +2427,9 @@ private:
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
auto const expected = !withDestCancel ? tshSTRONG
: !testStrong && withIOUIssuerWeakTSH ? tshWEAK
: tshNONE;
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -8519,8 +8535,9 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 2> const feats{
static std::array<FeatureBitset, 3> const feats{
all,
all - featureEscrowDestinationCancel,
all - featureIOUIssuerWeakTSH,
};
@@ -8550,10 +8567,12 @@ public:
} \
};
SETHOOKTSH_TEST(1, true)
SETHOOKTSH_TEST(1, false)
SETHOOKTSH_TEST(2, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH1, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH2, app, ripple, 2);
} // namespace test
} // namespace ripple

View File

@@ -143,6 +143,25 @@ static const std::vector<uint8_t> AcceptHook = {
0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
0x20U, 0x01U, 0x0BU};
// rollback_wasm from SetHook_test.cpp: rolls back every transaction.
static const std::vector<uint8_t> RollbackHook = {
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x13U, 0x03U,
0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU,
0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x19U, 0x02U, 0x03U,
0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU, 0x00U,
0x01U, 0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U,
0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0x90U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U,
0x41U, 0x8EU, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU,
0x00U, 0x41U, 0x90U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U,
0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x02U,
0x0AU, 0xA1U, 0x80U, 0x00U, 0x01U, 0x9DU, 0x80U, 0x00U, 0x00U, 0x41U, 0x01U,
0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, 0x80U,
0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x0EU, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U,
0x80U, 0x80U, 0x00U, 0x0BU, 0x0BU, 0x15U, 0x01U, 0x00U, 0x41U, 0x80U, 0x08U,
0x0BU, 0x0EU, 0x48U, 0x6FU, 0x6FU, 0x6BU, 0x20U, 0x52U, 0x65U, 0x6AU, 0x65U,
0x63U, 0x74U, 0x65U, 0x64U, 0x00U};
struct GenMint
{
std::optional<std::string> dest;

View File

@@ -1136,29 +1136,10 @@ class Simulate_test : public beast::unit_test::suite
{
testcase("simulate Hook rollback");
const std::vector<uint8_t> rollbackHook = {
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U,
0x13U, 0x03U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U,
0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU,
0x01U, 0x7EU, 0x02U, 0x19U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x02U, 0x5FU, 0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU,
0x00U, 0x01U, 0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, 0x01U,
0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0x90U,
0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x8EU, 0x08U, 0x0BU,
0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U,
0x90U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U,
0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU,
0x00U, 0x02U, 0x0AU, 0xA1U, 0x80U, 0x00U, 0x01U, 0x9DU, 0x80U,
0x00U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U,
0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U,
0x00U, 0x41U, 0x0EU, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U,
0x80U, 0x00U, 0x0BU, 0x0BU, 0x15U, 0x01U, 0x00U, 0x41U, 0x80U,
0x08U, 0x0BU, 0x0EU, 0x48U, 0x6FU, 0x6FU, 0x6BU, 0x20U, 0x52U,
0x65U, 0x6AU, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U, 0x00U,
};
env(hook(alice, {{hso(rollbackHook, overrideFlag)}}, 0),
env(hook(
alice,
{{hso(jtx::genesis::RollbackHook, overrideFlag)}},
0),
fee(XRP(1)));
env.close();

View File

@@ -382,9 +382,16 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
AccountID const src = escrow->getAccountID(sfAccount);
AccountID const dst = escrow->getAccountID(sfDestination);
// the source account is a strong transacitonal stakeholder for
// fin and can
ADD_TSH(src, tshSTRONG);
// featureEscrowDestinationCancel: an account can always cancel an
// escrow destined for it, so when the destination cancels, the
// source is only a weak tsh and its hooks cannot roll back.
bool const destinationCancel = tt == ttESCROW_CANCEL &&
rv.rules().enabled(featureEscrowDestinationCancel) &&
*otxnAcc == dst && src != dst;
// otherwise the source account is a strong transactional
// stakeholder for fin and can
ADD_TSH(src, destinationCancel ? tshWEAK : tshSTRONG);
// the dest acc is a strong tsh for fin and weak for can
if (src != dst)

View File

@@ -756,28 +756,39 @@ EscrowCancel::doApply()
if (slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
return tecINTERNAL;
if (ctx_.view().rules().enabled(fix1571))
{
auto const now = ctx_.view().info().parentCloseTime;
// No cancel time specified: can't execute at all.
if (!(*slep)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
}
else
{
// Too soon?
if (!(*slep)[~sfCancelAfter] ||
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
(*slep)[sfCancelAfter])
return tecNO_PERMISSION;
}
AccountID const account = (*slep)[sfAccount];
// The destination of an escrow may decline it at any time, returning the
// funds to the owner. Self-escrows are excluded so that a timelock on
// one's own funds remains binding.
bool const destinationCancel =
ctx_.view().rules().enabled(featureEscrowDestinationCancel) &&
account_ == slep->getAccountID(sfDestination) && account_ != account;
if (!destinationCancel)
{
if (ctx_.view().rules().enabled(fix1571))
{
auto const now = ctx_.view().info().parentCloseTime;
// No cancel time specified: can't execute at all.
if (!(*slep)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
}
else
{
// Too soon?
if (!(*slep)[~sfCancelAfter] ||
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
(*slep)[sfCancelAfter])
return tecNO_PERMISSION;
}
}
auto const sle = ctx_.view().peek(keylet::account(account));
auto const amount = slep->getFieldAmount(sfAmount);
bool const isIssuer = amount.getIssuer() == account;