diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py old mode 100755 new mode 100644 diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 5aa5214b1fb..e29a5116d71 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -6,13 +6,132 @@ #include #include #include +#include #include #include +#include #include +#include #include namespace xrpl { +template +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +inline TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = + directSendNoFee(view, sender, issuer, amount, !amount.holds(), journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +inline TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = lockEscrowMPT(view, sender, amount, journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template +TER +escrowUnlockPreclaimHelper( + ReadView const& view, + AccountID const& account, + STAmount const& amount, + bool checkFreeze = true); + +template <> +inline TER +escrowUnlockPreclaimHelper( + ReadView const& view, + AccountID const& account, + STAmount const& amount, + bool checkFreeze) +{ + AccountID const& issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == account) + return tesSUCCESS; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(view, amount.get(), account); !isTesSuccess(ter)) + return ter; + + // If the issuer has deep frozen the destination, return tecFROZEN + if (checkFreeze && + isDeepFrozen(view, account, amount.get().currency, amount.getIssuer())) + return tecFROZEN; + + return tesSUCCESS; +} + +template <> +inline TER +escrowUnlockPreclaimHelper( + ReadView const& view, + AccountID const& account, + STAmount const& amount, + bool checkFreeze) +{ + AccountID const& issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == account) + return tesSUCCESS; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth); + !isTesSuccess(ter)) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (checkFreeze && isFrozen(view, account, mptIssue)) + return tecLOCKED; + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + template TER escrowUnlockApplyHelper( @@ -46,6 +165,7 @@ escrowUnlockApplyHelper( bool const recvLow = issuer > receiver; bool const senderIssuer = issuer == sender; bool const receiverIssuer = issuer == receiver; + bool const lineExisted = view.exists(trustLineKey); if (senderIssuer) return tecINTERNAL; // LCOV_EXCL_LINE @@ -53,7 +173,7 @@ escrowUnlockApplyHelper( if (receiverIssuer) return tesSUCCESS; - if (!view.exists(trustLineKey) && createAsset) + if (!lineExisted && createAsset) { // Can the account cover the trust line's reserve? if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; @@ -120,9 +240,8 @@ escrowUnlockApplyHelper( finalAmt = amount.value() - xferFee; } - // validate the line limit if the account submitting txn is not the receiver - // of the funds - if (!createAsset) + // validate the line limit for pre-existing trust lines + if (lineExisted) { auto const sleRippleState = view.peek(trustLineKey); if (!sleRippleState) diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 24838f1331e..2eba3a0dfb6 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -12,6 +13,7 @@ closeChannel( std::shared_ptr const& slep, ApplyView& view, uint256 const& key, + AccountID const& txAccount, beast::Journal j); } // namespace xrpl diff --git a/include/xrpl/protocol/PayChan.h b/include/xrpl/protocol/PayChan.h index d8f4e0f5273..9dd2586b70b 100644 --- a/include/xrpl/protocol/PayChan.h +++ b/include/xrpl/protocol/PayChan.h @@ -1,8 +1,14 @@ #pragma once #include +#include #include +#include +#include +#include +#include #include +#include #include namespace xrpl { @@ -15,4 +21,58 @@ serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount con msg.add64(amt.drops()); } +inline void +serializePayChanAuthorization( + Serializer& msg, + uint256 const& key, + IOUAmount const& amt, + Currency const& cur, + AccountID const& iss) +{ + msg.add32(HashPrefix::PaymentChannelClaim); + msg.addBitString(key); + if (amt == beast::kZERO) + msg.add64(STAmount::kISSUED_CURRENCY); + else if (amt.signum() == -1) // 512 = not native + msg.add64( + amt.mantissa() | (static_cast(amt.exponent() + 512 + 97) << (64 - 10))); + else // 256 = positive + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 256 + 97) << (64 - 10))); + msg.addBitString(cur); + msg.addBitString(iss); +} + +inline void +serializePayChanAuthorization( + Serializer& msg, + uint256 const& key, + MPTAmount const& amt, + MPTID const& mptID, + AccountID const& iss) +{ + msg.add32(HashPrefix::PaymentChannelClaim); + msg.addBitString(key); + msg.add64(amt.value()); + msg.addBitString(mptID); + msg.addBitString(iss); +} + +inline void +serializePayChanAuthorization(Serializer& msg, uint256 const& key, STAmount const& amt) +{ + if (amt.native()) + serializePayChanAuthorization(msg, key, amt.xrp()); + else if (amt.holds()) + serializePayChanAuthorization( + msg, key, amt.iou(), amt.get().currency, amt.get().account); + else if (amt.holds()) + { + auto const mpt = amt.get(); + auto const mptID = mpt.getMptID(); + serializePayChanAuthorization(msg, key, amt.mpt(), mptID, amt.getIssuer()); + } +} + } // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 78b78e132e9..9aecfbcbacc 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Security3_1_3, Supported::No, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index bf641862d16..231228ea132 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -364,6 +364,8 @@ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({ {sfPreviousTxnID, SoeRequired}, {sfPreviousTxnLgrSeq, SoeRequired}, {sfDestinationNode, SoeOptional}, + {sfTransferRate, SoeOptional}, + {sfIssuerNode, SoeOptional}, })) /** The ledger object which tracks the AMM. diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 9bac9ef7dbb..560f8ab6996 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -191,7 +191,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, NoPriv, ({ {sfDestination, SoeRequired}, - {sfAmount, SoeRequired}, + {sfAmount, SoeRequired, SoeMptSupported}, {sfSettleDelay, SoeRequired}, {sfPublicKey, SoeRequired}, {sfCancelAfter, SoeOptional}, @@ -208,7 +208,7 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, NoPriv, ({ {sfChannel, SoeRequired}, - {sfAmount, SoeRequired}, + {sfAmount, SoeRequired, SoeMptSupported}, {sfExpiration, SoeOptional}, })) @@ -222,8 +222,8 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, NoPriv, ({ {sfChannel, SoeRequired}, - {sfAmount, SoeOptional}, - {sfBalance, SoeOptional}, + {sfAmount, SoeOptional, SoeMptSupported}, + {sfBalance, SoeOptional, SoeMptSupported}, {sfSignature, SoeOptional}, {sfPublicKey, SoeOptional}, {sfCredentialIDs, SoeOptional}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 8a2a1125427..58a5a84a0d9 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -243,6 +243,7 @@ JSS(errored); // JSS(error_code); // out: error JSS(error_exception); // out: Submit JSS(error_message); // out: error +JSS(escrowed); // out: escrowed JSS(expand); // in: handler/Ledger JSS(expected_date); // out: any (warnings) JSS(expected_date_UTC); // out: any (warnings) diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index 31c206d85b6..e0cbf244c7b 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -6,13 +6,19 @@ #include #include #include +#include #include +#include +#include #include +#include #include +#include #include #include #include +#include namespace xrpl { @@ -21,6 +27,7 @@ closeChannel( std::shared_ptr const& slep, ApplyView& view, uint256 const& key, + AccountID const& txAccount, beast::Journal j) { AccountID const src = (*slep)[sfAccount]; @@ -37,9 +44,9 @@ closeChannel( } // Remove PayChan from recipient's owner directory, if present. + AccountID const dst = (*slep)[sfDestination]; if (auto const page = (*slep)[~sfDestinationNode]) { - auto const dst = (*slep)[sfDestination]; if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true)) { // LCOV_EXCL_START @@ -56,7 +63,61 @@ closeChannel( XRPL_ASSERT( (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; + + auto const reqDelta = (*slep)[sfAmount] - (*slep)[sfBalance]; + auto const issuer = reqDelta.getIssuer(); + + // Only update the balance if there is a positive delta. + if (reqDelta > beast::kZERO) + { + if (isXRP(reqDelta)) + (*sle)[sfBalance] = (*sle)[sfBalance] + reqDelta; + else + { + if (!view.rules().enabled(featureTokenPaychan)) + return temDISABLED; + + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockPreclaimHelper(view, src, reqDelta, false); + }, + reqDelta.asset().value()); + !isTesSuccess(ret)) + return ret; + + bool const createAsset = src == txAccount; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + view, + kPARITY_RATE, + sle, + (*sle)[sfBalance], + reqDelta, + issuer, + src, + src, + createAsset, + j); + }, + reqDelta.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } + + // Remove PayChan from issuer's owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!view.dirRemove(keylet::ownerDir(issuer), *optPage, key, true)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Could not remove paychan from issuer owner directory"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + adjustOwnerCount(view, sle, -1, j); view.update(sle); diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 4f25a91bdf0..0e04a5667cb 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -130,7 +130,8 @@ XRPNotCreated::visitEntry( drops_ -= (*before)[sfBalance].xrp().drops(); break; case ltPAYCHAN: - drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); break; case ltESCROW: if (isXRP((*before)[sfAmount])) @@ -149,7 +150,7 @@ XRPNotCreated::visitEntry( drops_ += (*after)[sfBalance].xrp().drops(); break; case ltPAYCHAN: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops(); break; case ltESCROW: diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 9b8838f145d..369def41ec0 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -252,6 +252,13 @@ ValidMPTIssuance::finalize( return true; } + if (tx.getTxnType() == ttPAYCHAN_CLAIM) + { + if (mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensDeleted_ == 0 && + mptokensCreated_ <= 1) + return true; + } + if (hasPrivilege(tx, MayDeleteMpt) && ((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) && mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index 05d6a294542..a9d9ba0ee8c 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -34,60 +34,6 @@ EscrowCancel::preflight(PreflightContext const& ctx) return tesSUCCESS; } -template -static TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount); - -template <> -TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount) -{ - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecINTERNAL - if (issuer == account) - return tecINTERNAL; // LCOV_EXCL_LINE - - // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, amount.get(), account); !isTesSuccess(ter)) - return ter; - - return tesSUCCESS; -} - -template <> -TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount) -{ - AccountID const issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecINTERNAL - if (issuer == account) - return tecINTERNAL; // LCOV_EXCL_LINE - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the account is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - !isTesSuccess(ter)) - return ter; - - return tesSUCCESS; -} - TER EscrowCancel::preclaim(PreclaimContext const& ctx) { @@ -105,7 +51,7 @@ EscrowCancel::preclaim(PreclaimContext const& ctx) { if (auto const ret = std::visit( [&](T const&) { - return escrowCancelPreclaimHelper(ctx, account, amount); + return escrowUnlockPreclaimHelper(ctx.view, account, amount, false); }, amount.asset().value()); !isTesSuccess(ret)) diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index dd9c1b84b44..bcc32cf4293 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -186,61 +187,48 @@ escrowCreatePreclaimHelper( { Issue const& issue = amount.get(); AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecNO_PERMISSION if (issuer == account) return tecNO_PERMISSION; - // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION auto const sleIssuer = ctx.view.read(keylet::account(issuer)); if (!sleIssuer) return tecNO_ISSUER; if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) return tecNO_PERMISSION; - // If the account does not have a trustline to the issuer, return tecNO_LINE auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, issue.currency)); if (!sleRippleState) return tecNO_LINE; STAmount const balance = (*sleRippleState)[sfBalance]; - // If balance is positive, issuer must have higher address than account if (balance > beast::kZERO && issuer < account) return tecNO_PERMISSION; // LCOV_EXCL_LINE - // If balance is negative, issuer must have lower address than account if (balance < beast::kZERO && issuer > account) return tecNO_PERMISSION; // LCOV_EXCL_LINE - // If the issuer has requireAuth set, check if the account is authorized if (auto const ter = requireAuth(ctx.view, issue, account); !isTesSuccess(ter)) return ter; - // If the issuer has requireAuth set, check if the destination is authorized if (auto const ter = requireAuth(ctx.view, issue, dest); !isTesSuccess(ter)) return ter; - // If the issuer has frozen the account, return tecFROZEN if (isFrozen(ctx.view, account, issue)) return tecFROZEN; - // If the issuer has frozen the destination, return tecFROZEN if (isFrozen(ctx.view, dest, issue)) return tecFROZEN; STAmount const spendableAmount = accountHolds( ctx.view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, ctx.j); - // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS if (spendableAmount <= beast::kZERO) return tecINSUFFICIENT_FUNDS; - // If the spendable amount is less than the amount, return - // tecINSUFFICIENT_FUNDS if (spendableAmount < amount) return tecINSUFFICIENT_FUNDS; - // If the amount is not addable to the balance, return tecPRECISION_LOSS if (!canAdd(spendableAmount, amount)) return tecPRECISION_LOSS; @@ -256,51 +244,38 @@ escrowCreatePreclaimHelper( STAmount const& amount) { AccountID const issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecNO_PERMISSION if (issuer == account) return tecNO_PERMISSION; - // If the mpt does not exist, return tecOBJECT_NOT_FOUND auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); auto const sleIssuance = ctx.view.read(issuanceKey); if (!sleIssuance) return tecOBJECT_NOT_FOUND; - // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION if (!sleIssuance->isFlag(lsfMPTCanEscrow)) return tecNO_PERMISSION; - // If the issuer is not the same as the issuer of the mpt, return - // tecNO_PERMISSION if (sleIssuance->getAccountID(sfIssuer) != issuer) return tecNO_PERMISSION; // LCOV_EXCL_LINE - // If the account does not have the mpt, return tecOBJECT_NOT_FOUND if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) return tecOBJECT_NOT_FOUND; - // If the issuer has requireAuth set, check if the account is - // authorized auto const& mptIssue = amount.get(); if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); !isTesSuccess(ter)) return ter; - // If the issuer has requireAuth set, check if the destination is - // authorized if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter)) return ter; - // If the issuer has frozen the account, return tecLOCKED if (isFrozen(ctx.view, account, mptIssue)) return tecLOCKED; - // If the issuer has frozen the destination, return tecLOCKED if (isFrozen(ctx.view, dest, mptIssue)) return tecLOCKED; - // If the mpt cannot be transferred, return tecNO_AUTH if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter)) return ter; @@ -312,12 +287,9 @@ escrowCreatePreclaimHelper( AuthHandling::IgnoreAuth, ctx.j); - // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS if (spendableAmount <= beast::kZERO) return tecINSUFFICIENT_FUNDS; - // If the spendable amount is less than the amount, return - // tecINSUFFICIENT_FUNDS if (spendableAmount < amount) return tecINSUFFICIENT_FUNDS; @@ -358,54 +330,6 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -template -static TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal); - -template <> -TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal) -{ - // Defensive: Issuer cannot create an escrow - if (issuer == sender) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ter = - directSendNoFee(view, sender, issuer, amount, !amount.holds(), journal); - if (!isTesSuccess(ter)) - return ter; // LCOV_EXCL_LINE - return tesSUCCESS; -} - -template <> -TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal) -{ - // Defensive: Issuer cannot create an escrow - if (issuer == sender) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ter = lockEscrowMPT(view, sender, amount, journal); - if (!isTesSuccess(ter)) - return ter; // LCOV_EXCL_LINE - return tesSUCCESS; -} - TER EscrowCreate::doApply() { diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index fb6c77c61a6..5fa926ed129 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -130,68 +130,6 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) return Transactor::calculateBaseFee(view, tx) + extraFee; } -template -static TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount); - -template <> -TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount) -{ - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tesSUCCESS - if (issuer == dest) - return tesSUCCESS; - - // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, amount.get(), dest); !isTesSuccess(ter)) - return ter; - - // If the issuer has deep frozen the destination, return tecFROZEN - if (isDeepFrozen(ctx.view, dest, amount.get().currency, amount.getIssuer())) - return tecFROZEN; - - return tesSUCCESS; -} - -template <> -TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount) -{ - AccountID const& issuer = amount.getIssuer(); - // If the issuer is the same as the dest, return tesSUCCESS - if (issuer == dest) - return tesSUCCESS; - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the destination is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - !isTesSuccess(ter)) - return ter; - - // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) - return tecLOCKED; - - return tesSUCCESS; -} - TER EscrowFinish::preclaim(PreclaimContext const& ctx) { @@ -216,7 +154,7 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) { if (auto const ret = std::visit( [&](T const&) { - return escrowFinishPreclaimHelper(ctx, dest, amount); + return escrowUnlockPreclaimHelper(ctx.view, dest, amount); }, amount.asset().value()); !isTesSuccess(ret)) diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index 13c6a9b235e..2ab52a4a5ce 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -4,14 +4,17 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -25,6 +28,7 @@ #include #include #include +#include namespace xrpl { @@ -44,11 +48,11 @@ NotTEC PaymentChannelClaim::preflight(PreflightContext const& ctx) { auto const bal = ctx.tx[~sfBalance]; - if (bal && (!isXRP(*bal) || *bal <= beast::kZERO)) + if (bal && *bal <= beast::kZERO) return temBAD_AMOUNT; auto const amt = ctx.tx[~sfAmount]; - if (amt && (!isXRP(*amt) || *amt <= beast::kZERO)) + if (amt && *amt <= beast::kZERO) return temBAD_AMOUNT; if (bal && amt && *bal > *amt) @@ -70,8 +74,8 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) // The signature isn't needed if txAccount == src, but if it's // present, check it - auto const reqBalance = bal->xrp(); - auto const authAmt = amt ? amt->xrp() : reqBalance; + auto const reqBalance = bal; + auto const authAmt = amt ? amt : reqBalance; if (reqBalance > authAmt) return temBAD_AMOUNT; @@ -82,7 +86,7 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) PublicKey const pk(ctx.tx[sfPublicKey]); Serializer msg; - serializePayChanAuthorization(msg, k.key, authAmt); + serializePayChanAuthorization(msg, k.key, *authAmt); if (!verify(pk, msg.slice(), *sig)) return temBAD_SIGNATURE; } @@ -103,6 +107,27 @@ PaymentChannelClaim::preclaim(PreclaimContext const& ctx) !isTesSuccess(err)) return err; + Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + if (!isXRP(amount) && ctx.tx.isFieldPresent(sfBalance)) + { + if (!ctx.view.rules().enabled(featureTokenPaychan)) + return temDISABLED; + + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockPreclaimHelper(ctx.view, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; } @@ -116,7 +141,6 @@ PaymentChannelClaim::doApply() AccountID const src = (*slep)[sfAccount]; AccountID const dst = (*slep)[sfDestination]; - AccountID const txAccount = ctx_.tx[sfAccount]; auto const curExpiration = (*slep)[~sfExpiration]; { @@ -124,19 +148,20 @@ PaymentChannelClaim::doApply() auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); if ((cancelAfter && closeTime >= *cancelAfter) || (curExpiration && closeTime >= *curExpiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel( + slep, ctx_.view(), k.key, account_, ctx_.registry.get().getJournal("View")); } - if (txAccount != src && txAccount != dst) + if (account_ != src && account_ != dst) return tecNO_PERMISSION; if (ctx_.tx[~sfBalance]) { - auto const chanBalance = slep->getFieldAmount(sfBalance).xrp(); - auto const chanFunds = slep->getFieldAmount(sfAmount).xrp(); - auto const reqBalance = ctx_.tx[sfBalance].xrp(); + auto const chanBalance = slep->getFieldAmount(sfBalance); + auto const chanFunds = slep->getFieldAmount(sfAmount); + auto const reqBalance = ctx_.tx[sfBalance]; - if (txAccount == dst && !ctx_.tx[~sfSignature]) + if (account_ == dst && !ctx_.tx[~sfSignature]) return temBAD_SIGNATURE; if (ctx_.tx[~sfSignature]) @@ -160,22 +185,54 @@ PaymentChannelClaim::doApply() return tecNO_DST; if (auto err = - verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal); + verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, dst, sled, ctx_.journal); !isTesSuccess(err)) return err; (*slep)[sfBalance] = ctx_.tx[sfBalance]; - XRPAmount const reqDelta = reqBalance - chanBalance; + STAmount const reqDelta = reqBalance - chanBalance; XRPL_ASSERT( reqDelta >= beast::kZERO, "xrpl::PaymentChannelClaim::doApply : minimum balance delta"); - (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + + // Transfer amount to destination + if (isXRP(reqDelta)) + (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + else + { + if (!ctx_.view().rules().enabled(featureTokenPaychan)) + return temDISABLED; + + Rate lockedRate = slep->isFieldPresent(sfTransferRate) + ? xrpl::Rate(slep->getFieldU32(sfTransferRate)) + : kPARITY_RATE; + auto const issuer = reqDelta.getIssuer(); + bool const createAsset = dst == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + lockedRate, + sled, + preFeeBalance_, + reqDelta, + issuer, + src, + dst, + createAsset, + j_); + }, + reqDelta.asset().value()); + !isTesSuccess(ret)) + return ret; + } + ctx_.view().update(sled); ctx_.view().update(slep); } if ((ctx_.tx.getFlags() & tfRenew) != 0u) { - if (src != txAccount) + if (src != account_) return tecNO_PERMISSION; (*slep)[~sfExpiration] = std::nullopt; ctx_.view().update(slep); @@ -184,8 +241,9 @@ PaymentChannelClaim::doApply() if ((ctx_.tx.getFlags() & tfClose) != 0u) { // Channel will close immediately if dry or the receiver closes - if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + if (dst == account_ || (*slep)[sfBalance] == (*slep)[sfAmount]) + return closeChannel( + slep, ctx_.view(), k.key, account_, ctx_.registry.get().getJournal("View")); auto const settleExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() + diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index bbc8d9f13ae..0385cd3f7f3 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -7,11 +7,21 @@ #include #include #include +#include +#include +#include +#include +#include #include #include +#include #include #include +#include +#include +#include #include +#include #include #include #include @@ -22,6 +32,7 @@ #include #include +#include namespace xrpl { @@ -47,17 +58,268 @@ namespace xrpl { //------------------------------------------------------------------------------ +template +static NotTEC +payChanCreatePreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +payChanCreatePreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::kZERO) + return temBAD_AMOUNT; + + if (badCurrency() == amount.get().currency) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +payChanCreatePreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{kMAX_MP_TOKEN_AMOUNT} || amount <= beast::kZERO) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +template +static TER +payChanCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +payChanCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + Issue const& issue = amount.get(); + AccountID const& issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + // If the account does not have a trustline to the issuer, return tecNO_LINE + auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, issue.currency)); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than account + if (balance > beast::kZERO && issuer < account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than account + if (balance < beast::kZERO && issuer > account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, issue, account); !isTesSuccess(ter)) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, issue, dest); !isTesSuccess(ter)) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(ctx.view, account, issue)) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(ctx.view, dest, issue)) + return tecFROZEN; + + STAmount const spendableAmount = accountHolds( + ctx.view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::kZERO) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +TER +payChanCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID const issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt, return + // tecNO_PERMISSION + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the account does not have the mpt, return tecOBJECT_NOT_FOUND + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); + !isTesSuccess(ter)) + return ter; + + // If the issuer has requireAuth set, check if the destination is + // authorized + if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); + !isTesSuccess(ter)) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (isFrozen(ctx.view, account, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter)) + return ter; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.get(), + FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::kZERO) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +template +static TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create a payment channel + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = + directSendNoFee(view, sender, issuer, amount, !amount.holds(), journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create a payment channel + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = lockEscrowMPT(view, sender, amount, journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + TxConsequences PaymentChannelCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::kZERO}; } NotTEC PaymentChannelCreate::preflight(PreflightContext const& ctx) { - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZERO)) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenPaychan)) + return temBAD_AMOUNT; + + if (auto const ret = std::visit( + [&](T const&) { return payChanCreatePreflightHelper(ctx); }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::kZERO) + return temBAD_AMOUNT; + } if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) return temDST_IS_SRC; @@ -76,6 +338,8 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) if (!sle) return terNO_ACCOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + // Check reserve and funds availability { auto const balance = (*sle)[sfBalance]; @@ -84,15 +348,15 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx.tx[sfAmount]) + if (isXRP(amount) && balance < reserve + ctx.tx[sfAmount]) return tecUNFUNDED; } - auto const dst = ctx.tx[sfDestination]; + auto const dest = ctx.tx[sfDestination]; { // Check destination account - auto const sled = ctx.view.read(keylet::account(dst)); + auto const sled = ctx.view.read(keylet::account(dest)); if (!sled) return tecNO_DST; @@ -115,6 +379,17 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return payChanCreatePreclaimHelper(ctx, account, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; } @@ -122,6 +397,7 @@ TER PaymentChannelCreate::doApply() { auto const account = ctx_.tx[sfAccount]; + STAmount const amount{ctx_.tx[sfAmount]}; auto const sle = ctx_.view().peek(keylet::account(account)); if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE @@ -133,21 +409,21 @@ PaymentChannelCreate::doApply() return tecEXPIRED; } - auto const dst = ctx_.tx[sfDestination]; + auto const dest = ctx_.tx[sfDestination]; // Create PayChan in ledger. // // Note that we use the value from the sequence or ticket as the // payChan sequence. For more explanation see comments in SeqProxy.h. - Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue()); + Keylet const payChanKeylet = keylet::payChan(account, dest, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(payChanKeylet); // Funds held in this channel - (*slep)[sfAmount] = ctx_.tx[sfAmount]; + (*slep)[sfAmount] = amount; // Amount channel has already paid - (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed(); + (*slep)[sfBalance] = amount.zeroed(); (*slep)[sfAccount] = account; - (*slep)[sfDestination] = dst; + (*slep)[sfDestination] = dest; (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay]; (*slep)[sfPublicKey] = ctx_.tx[sfPublicKey]; (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; @@ -158,6 +434,13 @@ PaymentChannelCreate::doApply() (*slep)[sfSequence] = ctx_.tx.getSeqValue(); } + if (ctx_.view().rules().enabled(featureTokenPaychan) && !isXRP(amount)) + { + auto const xferRate = transferRate(ctx_.view(), amount); + if (xferRate != kPARITY_RATE) + (*slep)[sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add PayChan to owner directory @@ -172,14 +455,37 @@ PaymentChannelCreate::doApply() // Add PayChan to the recipient's owner directory { auto const page = - ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst)); + ctx_.view().dirInsert(keylet::ownerDir(dest), payChanKeylet, describeOwnerDir(dest)); if (!page) return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfDestinationNode] = *page; } + // Add PayChan to the issuer's owner directory, if applicable + AccountID const issuer = amount.getIssuer(); + if (!isXRP(amount) && issuer != account_ && issuer != dest && !amount.holds()) + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(issuer), payChanKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfIssuerNode] = *page; + } + // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + if (auto const ret = std::visit( + [&](T const&) { + return payChanLockApplyHelper(ctx_.view(), issuer, account_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index e392df213ac..5cb0332e0f7 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -4,11 +4,18 @@ #include #include #include +#include #include +#include #include +#include +#include #include #include #include +#include +#include +#include #include #include #include @@ -19,20 +26,114 @@ #include #include +#include namespace xrpl { +template +static NotTEC +payChanFundPreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +payChanFundPreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::kZERO) + return temBAD_AMOUNT; + + if (badCurrency() == amount.get().currency) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +payChanFundPreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{kMAX_MP_TOKEN_AMOUNT} || amount <= beast::kZERO) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +template +static TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = + directSendNoFee(view, sender, issuer, amount, !amount.holds(), journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +TER +payChanLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = lockEscrowMPT(view, sender, amount, journal); + if (!isTesSuccess(ter)) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + TxConsequences PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::kZERO}; } NotTEC PaymentChannelFund::preflight(PreflightContext const& ctx) { - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZERO)) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenPaychan)) + return temBAD_AMOUNT; + + if (auto const ret = std::visit( + [&](T const&) { return payChanFundPreflightHelper(ctx); }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::kZERO) + return temBAD_AMOUNT; + } return tesSUCCESS; } @@ -46,17 +147,18 @@ PaymentChannelFund::doApply() return tecNO_ENTRY; AccountID const src = (*slep)[sfAccount]; - auto const txAccount = ctx_.tx[sfAccount]; + AccountID const dst = (*slep)[sfDestination]; auto const expiration = (*slep)[~sfExpiration]; { auto const cancelAfter = (*slep)[~sfCancelAfter]; auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel( + slep, ctx_.view(), k.key, account_, ctx_.registry.get().getJournal("View")); } - if (src != txAccount) + if (src != account_) { // only the owner can add funds or extend return tecNO_PERMISSION; @@ -75,10 +177,11 @@ PaymentChannelFund::doApply() ctx_.view().update(slep); } - auto const sle = ctx_.view().peek(keylet::account(txAccount)); + auto const sle = ctx_.view().peek(keylet::account(account_)); if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE + STAmount const amount{ctx_.tx[sfAmount]}; { // Check reserve and funds availability auto const balance = (*sle)[sfBalance]; @@ -87,20 +190,51 @@ PaymentChannelFund::doApply() if (balance < reserve) return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx_.tx[sfAmount]) + if (isXRP(amount) && balance < reserve + amount) return tecUNFUNDED; } // do not allow adding funds if dst does not exist - if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst))) + if (!ctx_.view().read(keylet::account(dst))) { return tecNO_DST; } - (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; - ctx_.view().update(slep); + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) -> TER { + auto const& iss = amount.get(); + if (isFrozen(ctx_.view(), account_, iss)) + { + if constexpr (std::is_same_v) + return tecFROZEN; + else + return tecLOCKED; + } + return TER{tesSUCCESS}; + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + AccountID const issuer = amount.getIssuer(); + if (auto const ret = std::visit( + [&](T const&) { + return payChanLockApplyHelper(ctx_.view(), issuer, account_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + + (*slep)[sfAmount] = (*slep)[sfAmount] + amount; + ctx_.view().update(slep); ctx_.view().update(sle); return tesSUCCESS; diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index bf7cf90bf24..ee683050800 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -1613,7 +1613,6 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta); BEAST_EXPECT(env.balance(bob, usd) == usd(10'100)); } - // test rate change - lower { Env env{*this, features}; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index a9c36d93d20..62551be05ec 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1959,8 +1959,67 @@ class MPToken_test : public beast::unit_test::Suite setMPTFields(field, jv); test(jv, field.fieldName); }; - ammBid(sfBidMin); - ammBid(sfBidMax); + for (SField const& field : + {std::cref(static_cast(sfBidMin)), + std::cref(static_cast(sfBidMax)), + std::cref(static_cast(sfAsset)), + std::cref(static_cast(sfAsset2))}) + ammBid(field); + // AMMClawback + auto ammClawback = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMClawback; + jv[jss::Account] = alice.human(); + jv[jss::Holder] = carol.human(); + setMPTFields(field, jv); + test(jv, field.fieldName); + }; + for (SField const& field : + {std::cref(static_cast(sfAmount)), + std::cref(static_cast(sfAsset)), + std::cref(static_cast(sfAsset2))}) + ammClawback(field); + // AMMDelete + auto ammDelete = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMDelete; + jv[jss::Account] = alice.human(); + setMPTFields(field, jv, false); + test(jv, field.fieldName); + }; + ammDelete(sfAsset); + ammDelete(sfAsset2); + // AMMVote + auto ammVote = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::AMMVote; + jv[jss::Account] = alice.human(); + jv[jss::TradingFee] = 100; + setMPTFields(field, jv, false); + test(jv, field.fieldName); + }; + ammVote(sfAsset); + ammVote(sfAsset2); + // CheckCash + auto checkCash = [&](SField const& field) { + json::Value jv; + jv[jss::TransactionType] = jss::CheckCash; + jv[jss::Account] = alice.human(); + jv[sfCheckID.fieldName] = to_string(uint256{1}); + jv[field.fieldName] = mpt.getJson(JsonOptions::KNone); + test(jv, field.fieldName); + }; + checkCash(sfAmount); + checkCash(sfDeliverMin); + // CheckCreate + { + json::Value jv; + jv[jss::TransactionType] = jss::CheckCreate; + jv[jss::Account] = alice.human(); + jv[jss::Destination] = carol.human(); + jv[jss::SendMax] = mpt.getJson(JsonOptions::KNone); + test(jv, jss::SendMax.cStr()); + } // PaymentChannelCreate { json::Value jv; @@ -1990,6 +2049,13 @@ class MPToken_test : public beast::unit_test::Suite jv[jss::Amount] = mpt.getJson(JsonOptions::KNone); test(jv, jss::Amount.cStr()); } + // OfferCreate + { + json::Value jv = offer(alice, usd(100), mpt); + test(jv, jss::TakerPays.cStr()); + jv = offer(alice, mpt, usd(100)); + test(jv, jss::TakerGets.cStr()); + } // NFTokenCreateOffer { json::Value jv; diff --git a/src/test/app/PayChanToken_test.cpp b/src/test/app/PayChanToken_test.cpp new file mode 100644 index 00000000000..2bd128094bb --- /dev/null +++ b/src/test/app/PayChanToken_test.cpp @@ -0,0 +1,3670 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { +namespace test { +struct PayChanToken_test : public beast::unit_test::Suite +{ + void + testIOUEnablement(FeatureBitset features) + { + testcase("IOU Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenPaychan : {false, true}) + { + auto const amend = withTokenPaychan ? features : features - featureTokenPaychan; + Env env{*this, amend}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const openResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(temBAD_AMOUNT); + auto const closeResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(tecNO_TARGET); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk), openResult); + env.close(); + env(paychan::fund(alice, chan, USD(1'000)), openResult); + env.close(); + env(paychan::claim(bob, chan), Txflags(tfClose), closeResult); + env.close(); + } + } + + void + testIOUAllowLockingFlag(FeatureBitset features) + { + testcase("IOU Allow Locking Flag"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + // Create PayChan + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + // Clear the asfAllowTrustLineLocking flag + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + env.require(Nflags(gw, asfAllowTrustLineLocking)); + + // Cannot Create PayChan without asfAllowTrustLineLocking + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + + // Can Fund PayChan without asfAllowTrustLineLocking + env(paychan::fund(alice, chan, USD(1'000)), Ter(tesSUCCESS)); + env.close(); + + // Can claim the paychan created before the flag was cleared + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, USD(1'000)); + env(paychan::claim(bob, chan, USD(1'000), USD(1'000), Slice(sig), alice.pk()), + Ter(tesSUCCESS)); + env.close(); + } + + void + testIOUCreatePreflight(FeatureBitset features) + { + testcase("IOU Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + // temBAD_FEE: Exercises invalid preflight1. + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5'000), alice, bob, gw); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, USD(1), settleDelay, pk), + Fee(XRP(-1)), + Ter(temBAD_FEE)); + env.close(); + } + + // temBAD_AMOUNT: amount <= 0 + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5'000), alice, bob, gw); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, USD(-1), settleDelay, pk), Ter(temBAD_AMOUNT)); + env.close(); + } + + // temBAD_CURRENCY: badCurrency() == amount.getCurrency() + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const BAD = IOU(gw, badCurrency()); + env.fund(XRP(5'000), alice, bob, gw); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, BAD(1), settleDelay, pk), Ter(temBAD_CURRENCY)); + env.close(); + } + } + + void + testIOUCreatePreclaim(FeatureBitset features) + { + testcase("IOU Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(paychan::create(gw, alice, USD(1), 100s, alice.pk()), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_ISSUER: Issuer does not exist + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob); + env.close(); + env.memoize(gw); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecNO_ISSUER)); + env.close(); + } + + // tecNO_PERMISSION: asfAllowTrustLineLocking is not set + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + env(paychan::create(gw, alice, USD(1), 100s, alice.pk()), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_LINE: account does not have a trustline to the issuer + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecNO_LINE)); + env.close(); + } + + // tecNO_PERMISSION: Not testable + // tecNO_PERMISSION: Not testable + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: account is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecFROZEN)); + env.close(); + } + + // tecFROZEN: dest is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecFROZEN)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + env(paychan::create(alice, bob, USD(10'001), 100s, alice.pk()), + Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecPRECISION_LOSS + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create paychan for 1/10 iou - precision loss + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tecPRECISION_LOSS)); + env.close(); + } + } + + void + testIOUClaimPreclaim(FeatureBitset features) + { + testcase("IOU Claim Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, USD(10'000))); + env(trust(gw, bobUSD(0)), Txflags(tfSetfAuth)); + env(trust(bob, USD(0))); + env.close(); + + env.trust(USD(10'000), bob); + env.close(); + + // bob cannot claim because he is not authorized + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, USD(1)); + env(paychan::claim(bob, chan, USD(1), USD(1), Slice(sig), alice.pk()), Ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: issuer has deep frozen the dest + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + + // bob cannot claim because of deep freeze + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, USD(1)); + env(paychan::claim(bob, chan, USD(1), USD(1), Slice(sig), alice.pk()), Ter(tecFROZEN)); + env.close(); + } + } + + void + testIOUClaimDoApply(FeatureBitset features) + { + testcase("IOU Claim Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // bob cannot claim because insufficient reserve to create line + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, USD(1)); + env(paychan::claim(bob, chan, USD(1), USD(1), Slice(sig), alice.pk()), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + } + + // tecNO_LINE: alice submits; claim IOU not created + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + // alice cannot claim because bob does not have a trustline + env(paychan::claim(alice, chan, USD(1), USD(1)), Ter(tecNO_LINE)); + env.close(); + } + + // tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(5), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // alice cannot claim because bobs limit is too low + env(paychan::claim(alice, chan, USD(5), USD(5)), Ter(tecLIMIT_EXCEEDED)); + env.close(); + } + + // tesSUCCESS: bob submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(5), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + auto const bobPreLimit = env.limit(bob, USD); + + // bob cannot claim if bobs limit is too low + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, USD(5)); + env(paychan::claim(bob, chan, USD(5), USD(5), Slice(sig), alice.pk()), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + + // bobs limit is not changed + BEAST_EXPECT(env.limit(bob, USD) == bobPreLimit); + } + } + + void + testIOUBalances(FeatureBitset features) + { + testcase("IOU Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const outstandingUSD = USD(10'000); + + // Create & Claim (Dest) PayChan + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(paychan::create(alice, bob, USD(1'000), 1s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD - USD(1'000)); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD); + BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD - USD(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(1'000)); + } + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + auto const sig = paychan::signClaimAuth(alice.pk(), alice.sk(), chan, USD(1'000)); + env(paychan::claim(bob, chan, USD(1'000), USD(1'000), Slice(sig), alice.pk()), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD + USD(1'000)); + BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0)); + } + + // Create & Claim (Account) PayChan + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(paychan::create(alice, bob, USD(1'000), 100s, alice.pk()), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD - USD(1'000)); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD); + BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD - USD(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(1'000)); + } + { + auto const preAliceUSD = env.balance(alice, USD); + auto const preBobUSD = env.balance(bob, USD); + env(paychan::claim(alice, chan2, USD(1'000), USD(1'000)), + Txflags(tfClose), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, USD) == preBobUSD + USD(1'000)); + BEAST_EXPECT(issuerBalance(env, gw, USD) == outstandingUSD); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0)); + } + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + 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"]; + { + testcase("IOU Metadata to other"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + auto const pk = alice.pk(); + auto const pk2 = bob.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + env(paychan::create(bob, carol, USD(1'000), settleDelay, pk2)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + + auto const ab = env.le(keylet::payChan(alice.id(), bob.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::payChan(bob.id(), carol.id(), bseq)); + BEAST_EXPECT(bc); + + { + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) != aod.end()); + + xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT(std::find(cod.begin(), cod.end(), bc) != cod.end()); + + xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) != iod.end()); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + auto const chan_ab = paychan::channel(alice, bob, aseq); + env(paychan::claim(alice, chan_ab, USD(1'000), USD(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + + xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(); + auto const chan_bc = paychan::channel(bob, carol, bseq); + env(paychan::claim(bob, chan_bc, USD(1'000), USD(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) == bod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), bc) == iod.end()); + } + } + + { + testcase("IOU Metadata to issuer"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, gw, USD(1'000), settleDelay, pk)); + + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + env(paychan::create(gw, carol, USD(1'000), settleDelay, alice.pk()), + Ter(tecNO_PERMISSION)); + env.close(); + + auto const ag = env.le(keylet::payChan(alice.id(), gw.id(), aseq)); + BEAST_EXPECT(ag); + + { + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ag) != aod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ag) != iod.end()); + } + + auto const chan_ag = paychan::channel(alice, gw, aseq); + env(paychan::claim(alice, chan_ag, USD(1'000), USD(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChan(alice.id(), gw.id(), aseq))); + + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ag) == aod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + xrpl::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2); + BEAST_EXPECT(std::find(iod.begin(), iod.end(), ag) == iod.end()); + } + } + } + + void + testIOURippleState(FeatureBitset features) + { + testcase("IOU RippleState"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + Account gw; + bool hasTrustline; + bool negative; + }; + + std::array tests = {{ + // src > dst && src > issuer && dst no trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true}, + // src < dst && src < issuer && dst no trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false}, + // dst > src && dst > issuer && dst no trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true}, + // dst < src && dst < issuer && dst no trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false}, + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env(fset(t.gw, asfAllowTrustLineLocking)); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100'000), t.src, t.dst); + else + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10'000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10'000))); + env.close(); + + // src can create paychan + auto const seq1 = env.seq(t.src); + auto const delta = USD(1'000); + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + env(paychan::create(t.src, t.dst, delta, settleDelay, pk)); + env.close(); + + // dst can claim paychan + auto const preSrc = env.balance(t.src, USD); + auto const preDst = env.balance(t.dst, USD); + + auto const chan = paychan::channel(t.src, t.dst, seq1); + auto const sig = paychan::signClaimAuth(pk, t.src.sk(), chan, delta); + env(paychan::claim(t.dst, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(t.src, USD) == preSrc); + BEAST_EXPECT(env.balance(t.dst, USD) == preDst + delta); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU Gateway"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.close(); + + env(pay(gw, alice, USD(10'000))); + env.close(); + + // issuer cannot create paychan + auto const pk = gw.pk(); + auto const settleDelay = 100s; + env(paychan::create(gw, alice, USD(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + struct TestAccountData + { + Account src; + Account dst; + bool hasTrustline; + }; + + std::array gwDstTests = {{ + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, true}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true}, + }}; + + // issuer is destination + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.dst, asfAllowTrustLineLocking)); + env.close(); + + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10'000))); + env.close(); + + // issuer can receive paychan + auto const seq1 = env.seq(t.src); + auto const preSrc = env.balance(t.src, USD); + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + env(paychan::create(t.src, t.dst, USD(1'000), settleDelay, pk)); + env.close(); + + // issuer can claim paychan, no dest trustline + auto const chan = paychan::channel(t.src, t.dst, seq1); + auto const sig = paychan::signClaimAuth(pk, t.src.sk(), chan, USD(1'000)); + env(paychan::claim(t.dst, chan, USD(1'000), USD(1'000), Slice(sig), pk)); + env.close(); + auto const preAmount = 10'000; + BEAST_EXPECT(preSrc == USD(preAmount)); + auto const postAmount = 9000; + BEAST_EXPECT(env.balance(t.src, USD) == USD(postAmount)); + BEAST_EXPECT(env.balance(t.dst, USD) == USD(0)); + } + } + + void + testIOULockedRate(FeatureBitset features) + { + testcase("IOU Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + 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"]; + + // test locked rate + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto const transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can claim paychan + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + // test rate change - higher + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can claim paychan - rate unchanged + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + // test rate change - lower + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // bob can claim paychan - rate changed + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10125)); + } + + // test claim/close doesnt charge rate + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + auto transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // alice can close paychan - rate is not charged + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice); + BEAST_EXPECT(env.balance(bob, USD) == USD(10000)); + } + } + + void + testIOULimitAmount(FeatureBitset features) + { + testcase("IOU Limit"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test LimitAmount + { + Env env{*this, features}; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1'000))); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create paychan + auto seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // bob can claim + auto const preBobLimit = env.limit(bob, USD); + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + auto const postBobLimit = env.limit(bob, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOURequireAuth(FeatureBitset features) + { + testcase("IOU Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + 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 aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + Env env{*this, features}; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth)); + env(trust(alice, USD(10'000))); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, alice, USD(1'000))); + env.close(); + + // alice cannot create paychan - fails without auth + auto seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth)); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create paychan - bob has auth + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // bob can claim + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + } + + void + testIOUFreeze(FeatureBitset features) + { + testcase("IOU Freeze"); + using namespace test::jtx; + using namespace std::literals; + + 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"]; + + // test Global Freeze + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob claim paychan success regardless of frozen assets + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // alice close paychan success regardless of frozen assets + auto const chan2 = paychan::channel(alice, bob, seq1); + env(paychan::claim(alice, chan2, delta, delta), Txflags(tfClose)); + env.close(); + } + + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob claim paychan success regardless of frozen assets + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + // reset freeze on bob and alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // alice close paychan success regardless of frozen assets + auto const chan2 = paychan::channel(alice, bob, seq1); + env(paychan::claim(alice, chan2, delta, delta), Txflags(tfClose)); + env.close(); + } + + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, delta, settleDelay, pk), Ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob claim paychan fails because of deep frozen assets + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk), Ter(tecFROZEN)); + env.close(); + + // reset freeze on alice and bob trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create paychan success + seq1 = env.seq(alice); + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob close paychan success regardless of deep frozen assets + auto const chan2 = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan2), Txflags(tfClose)); + env.close(); + } + } + + void + testIOUINSF(FeatureBitset features) + { + testcase("IOU Insufficient Funds"); + using namespace test::jtx; + using namespace std::literals; + + 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"]; + { + // test tecPATH_PARTIAL + // ie. has 10'000, paychan 1'000 then try to pay 10'000 + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // create paychan success + auto const delta = USD(1'000); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + env(pay(alice, gw, USD(10'000)), Ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10'000 paychan 1'000 then try to paychan 10'000 + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const delta = USD(1'000); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, delta, settleDelay, pk)); + env.close(); + + env(paychan::create(alice, bob, USD(10'000), settleDelay, pk), + Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test min create precision loss + { + Env env(*this, features); + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create paychan for 1/10 iou - precision loss + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, USD(1), settleDelay, pk), Ter(tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create paychan for 1'000 iou + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + // bob claim paychan success + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, USD(1'000)); + env(paychan::claim(bob, chan, USD(1'000), USD(1'000), Slice(sig), pk)); + env.close(); + } + } + + void + testMPTEnablement(FeatureBitset features) + { + testcase("MPT Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenPaychan : {false, true}) + { + auto const amend = withTokenPaychan ? features : features - featureTokenPaychan; + Env env{*this, amend}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const openResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(temBAD_AMOUNT); + auto const closeResult = withTokenPaychan ? Ter(tesSUCCESS) : Ter(tecNO_TARGET); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk), openResult); + env.close(); + env(paychan::fund(alice, chan, MPT(1'000)), openResult); + env.close(); + env(paychan::claim(bob, chan), Txflags(tfClose), closeResult); + env.close(); + } + } + + void + testMPTCreatePreflight(FeatureBitset features) + { + testcase("MPT Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + for (bool const withMPT : {true, false}) + { + auto const amend = withMPT ? features : features - featureMPTokensV1; + Env env{*this, amend}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(1'000), alice, bob, gw); + + json::Value jv = paychan::create(alice, bob, XRP(1), 100s, alice.pk()); + jv.removeMember(jss::Amount); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + jv[jss::Amount][jss::value] = "-1"; + + auto const result = withMPT ? Ter(temBAD_AMOUNT) : Ter(temDISABLED); + env(jv, result); + env.close(); + } + + // temBAD_AMOUNT: amount < 0 + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(-1), settleDelay, pk), Ter(temBAD_AMOUNT)); + env.close(); + } + } + + void + testMPTCreatePreclaim(FeatureBitset features) + { + testcase("MPT Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const pk = gw.pk(); + auto const settleDelay = 100s; + env(paychan::create(gw, alice, MPT(1), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: mpt does not exist + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), alice, bob, gw); + env.close(); + + auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice)); + json::Value jv = paychan::create(alice, bob, mpt(2), 100s, alice.pk()); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + env(jv, Ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_PERMISSION: tfMPTCanEscrow is not enabled + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(3), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: account does not have the mpt + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + auto const MPT = mptGw["MPT"]; + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(4), settleDelay, pk), Ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // unauthorize account + mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(5), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // unauthorize dest + mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(6), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the account + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock account + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(7), settleDelay, pk), Ter(tecLOCKED)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(8), settleDelay, pk), Ter(tecLOCKED)); + env.close(); + } + + // tecNO_AUTH: mpt cannot be transferred + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(9), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is zero + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, bob, MPT(10))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(11), settleDelay, pk), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is less than the amount + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10))); + env(pay(gw, bob, MPT(10))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(11), settleDelay, pk), Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testMPTClaimPreclaim(FeatureBitset features) + { + testcase("MPT Claim Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + // unauthorize dest + mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(10)); + env(paychan::claim(bob, chan, MPT(10), MPT(10), Slice(sig), pk), Ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(8), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(8)); + env(paychan::claim(bob, chan, MPT(8), MPT(8), Slice(sig), pk), Ter(tecLOCKED)); + env.close(); + } + } + + void + testMPTClaimDoApply(FeatureBitset features) + { + testcase("MPT Claim Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(10)); + env(paychan::claim(bob, chan, MPT(10), MPT(10), Slice(sig), pk), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + // tesSUCCESS: bob submits; claim MPT created + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(10)); + env(paychan::claim(bob, chan, MPT(10), MPT(10), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + } + + // tecNO_PERMISSION: alice submits; claim MPT not created + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + env(paychan::claim(alice, chan, MPT(10), MPT(10)), Ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTBalances(FeatureBitset features) + { + testcase("MPT Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice, carol}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + + auto outstandingMPT = env.balance(gw, MPT); + + // Create & Claim (Dest) PayChan + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const pk = alice.pk(); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(1'000)); + env(paychan::claim(bob, chan, MPT(1'000), MPT(1'000), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Create & Claim (Account) PayChan + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(paychan::claim(alice, chan2, MPT(1'000), MPT(1'000)), + Txflags(tfClose), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Multiple PayChans + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const preCarolMPT = env.balance(carol, MPT); + auto const pk = alice.pk(); + auto const pk2 = carol.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + env(paychan::create(carol, bob, MPT(1'000), settleDelay, pk2), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(carol, MPT) == preCarolMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, carol, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 2'000); + } + + // Max MPT Amount Issued (PayChan 1 MPT) + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(kMAX_MP_TOKEN_AMOUNT))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(1), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(1)); + env(paychan::claim(bob, chan, MPT(1), MPT(1), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT( + !env.le(keylet::mptoken(MPT.mpt(), alice))->isFieldPresent(sfLockedAmount)); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptIssuance(MPT.mpt()))->isFieldPresent(sfLockedAmount)); + } + + // Max MPT Amount Issued (PayChan Max MPT) + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(kMAX_MP_TOKEN_AMOUNT))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + // PayChan Max MPT - 10 + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan1 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(kMAX_MP_TOKEN_AMOUNT - 10), settleDelay, pk)); + env.close(); + + // PayChan 10 MPT + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(kMAX_MP_TOKEN_AMOUNT)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == kMAX_MP_TOKEN_AMOUNT); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == kMAX_MP_TOKEN_AMOUNT); + + auto const sig1 = + paychan::signClaimAuth(pk, alice.sk(), chan1, MPT(kMAX_MP_TOKEN_AMOUNT - 10)); + env(paychan::claim( + bob, + chan1, + MPT(kMAX_MP_TOKEN_AMOUNT - 10), + MPT(kMAX_MP_TOKEN_AMOUNT - 10), + Slice(sig1), + pk), + Ter(tesSUCCESS)); + env.close(); + + auto const sig2 = paychan::signClaimAuth(pk, alice.sk(), chan2, MPT(10)); + env(paychan::claim(bob, chan2, MPT(10), MPT(10), Slice(sig2), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(kMAX_MP_TOKEN_AMOUNT)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(kMAX_MP_TOKEN_AMOUNT)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + } + + void + testMPTMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + { + testcase("MPT Metadata to other"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + auto const pk = alice.pk(); + auto const pk2 = bob.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + env(paychan::create(bob, carol, MPT(1'000), settleDelay, pk2)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); + env.close(); + + auto const ab = env.le(keylet::payChan(alice.id(), bob.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::payChan(bob.id(), carol.id(), bseq)); + BEAST_EXPECT(bc); + + { + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) != aod.end()); + + xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT(std::find(cod.begin(), cod.end(), bc) != cod.end()); + } + + auto const chan_ab = paychan::channel(alice, bob, aseq); + env(paychan::claim(alice, chan_ab, MPT(1'000), MPT(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) != bod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + } + + env.close(); + auto const chan_bc = paychan::channel(bob, carol, bseq); + env(paychan::claim(bob, chan_bc, MPT(1'000), MPT(1'000)), Txflags(tfClose)); + { + BEAST_EXPECT(!env.le(keylet::payChan(alice.id(), bob.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::payChan(bob.id(), carol.id(), bseq))); + + xrpl::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT(std::find(aod.begin(), aod.end(), ab) == aod.end()); + + xrpl::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT(std::find(bod.begin(), bod.end(), bc) == bod.end()); + + xrpl::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + } + } + } + + void + testMPTGateway(FeatureBitset features) + { + testcase("MPT Gateway Balances"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // issuer cannot create paychan + auto const pk = gw.pk(); + auto const settleDelay = 100s; + env(paychan::create(gw, alice, MPT(1'000), settleDelay, pk), Ter(tecNO_PERMISSION)); + env.close(); + } + + // issuer is dest; alice w/ authorization + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // issuer can be destination + auto const preAliceMPT = env.balance(alice, MPT); + auto const preOutstanding = env.balance(gw, MPT); + auto const preEscrowed = issuerMPTEscrowed(env, MPT); + BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preEscrowed == 0); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, gw, env.seq(alice)); + env(paychan::create(alice, gw, MPT(1'000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed + 1'000); + + // issuer (dest) can claim paychan + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(1'000)); + env(paychan::claim(gw, chan, MPT(1'000), MPT(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); + } + } + + void + testMPTLockedRate(FeatureBitset features) + { + testcase("MPT Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + // test locked rate: claim + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(125), settleDelay, pk)); + env.close(); + auto const transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can claim paychan + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, delta); + env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100)); + } + + // test locked rate: close + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const preBob = env.balance(bob, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(125), settleDelay, pk)); + env.close(); + auto const transferRate = paychan::rate(env, alice, bob, seq1); + BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can close paychan + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice); + BEAST_EXPECT(env.balance(bob, MPT) == preBob); + } + } + + // void + // testMPTRequireAuth(FeatureBitset features) + // { + // testcase("MPT Require Auth"); + // using namespace test::jtx; + // using namespace std::literals; + + // Env env{*this, features}; + // auto const baseFee = env.current()->fees().base; + // auto const alice = Account("alice"); + // auto const bob = Account("bob"); + // auto const gw = Account("gw"); + + // MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + // mptGw.create( + // {.ownerCount = 1, + // .holderCount = 0, + // .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + // mptGw.authorize({.account = alice}); + // mptGw.authorize({.account = gw, .holder = alice}); + // mptGw.authorize({.account = bob}); + // mptGw.authorize({.account = gw, .holder = bob}); + // auto const MPT = mptGw["MPT"]; + // env(pay(gw, alice, MPT(10'000))); + // env.close(); + + // auto seq = env.seq(alice); + // auto const delta = MPT(125); + // // alice can create escrow - is authorized + // env(escrow::create(alice, bob, MPT(100)), + // escrow::condition(escrow::cb1), + // escrow::finish_time(env.now() + 1s), + // Fee(baseFee * 150)); + // env.close(); + + // // bob can finish escrow - is authorized + // env(escrow::finish(bob, alice, seq), + // escrow::condition(escrow::cb1), + // escrow::fulfillment(escrow::fb1), + // Fee(baseFee * 150)); + // env.close(); + // } + + void + testMPTLock(FeatureBitset features) + { + testcase("MPT Lock"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice create paychan + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(100), settleDelay, pk)); + env.close(); + + // lock account & dest + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + // bob cannot claim + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(100)); + env(paychan::claim(bob, chan, MPT(100), MPT(100), Slice(sig), pk), Ter(tecLOCKED)); + env.close(); + + // bob can claim/close + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + } + + void + testMPTCanTransfer(FeatureBitset features) + { + testcase("MPT Can Transfer"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice cannot create paychan to non issuer + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(100), settleDelay, pk), Ter(tecNO_AUTH)); + env.close(); + + // PayChan Create & Claim + { + // alice can create paychan to issuer + auto const chan = paychan::channel(alice, gw, env.seq(alice)); + env(paychan::create(alice, gw, MPT(100), settleDelay, pk)); + env.close(); + + // gw can claim + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(100)); + env(paychan::claim(gw, chan, MPT(100), MPT(100), Slice(sig), pk)); + env.close(); + } + + // PayChan Create & Close + { + // alice can create paychan to issuer + auto const chan = paychan::channel(alice, gw, env.seq(alice)); + env(paychan::create(alice, gw, MPT(100), settleDelay, pk)); + env.close(); + + // gw can claim/close + env(paychan::claim(gw, chan), Txflags(tfClose)); + env.close(); + } + } + + void + testMPTDestroy(FeatureBitset features) + { + testcase("MPT Destroy"); + using namespace test::jtx; + using namespace std::literals; + + // tecHAS_OBLIGATIONS: issuer cannot destroy issuance + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk)); + env.close(); + + env(pay(alice, gw, MPT(10'000)), Ter(tecPATH_PARTIAL)); + env(pay(alice, gw, MPT(9'990))); + env(pay(bob, gw, MPT(10'000))); + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(10)); + mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 1, .err = tecHAS_OBLIGATIONS}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(10)); + env(paychan::claim(bob, chan, MPT(10), MPT(10), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, MPT(10))); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0}); + } + + // tecHAS_OBLIGATIONS: holder cannot destroy mptoken + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, MPT(10), settleDelay, pk), Ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, MPT(9'990))); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + mptGw.authorize( + {.account = alice, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS}); + + auto const sig = paychan::signClaimAuth(pk, alice.sk(), chan, MPT(10)); + env(paychan::claim(bob, chan, MPT(10), MPT(10), Slice(sig), pk), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice))); + } + } + + void + testIOUClawbackInteraction(FeatureBitset features) + { + testcase("IOU Clawback Interaction"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // Attempt to shelter funds from clawback by locking in channel + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(4'000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == USD(1'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(4'000)); + + env(claw(gw, alice["USD"](1'000))); + env.close(); + BEAST_EXPECT(env.balance(alice, USD) == USD(0)); + + auto const chan = paychan::channel(alice, bob, seq1); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == USD(4'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(4'000)); + + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + // Alice recovered 4000 USD that survived the clawback + BEAST_EXPECT(env.balance(alice, USD) == USD(4'000)); + BEAST_EXPECT(issuerEscrowed(env, gw, USD) == USD(0)); + } + + // Clawback from dest with active channel (claim after clawback) + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + env(claw(gw, bob["USD"](5'000))); + env.close(); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(1'000)); + env(paychan::claim( + bob, chan, USD(1'000), USD(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, USD) == USD(1'000)); + } + } + + void + testIOUFundAfterFreeze(FeatureBitset features) + { + testcase("IOU Fund After Freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // Fund channel after destination is frozen + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + env(trust(gw, USD(100'000), bob, tfSetFreeze)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, USD(1'000)), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + paychan::channelAmount(*env.current(), chan) == USD(2'000)); + + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(500)); + env(paychan::claim( + bob, chan, USD(500), USD(500), Slice(sig), pk), + Ter(tesSUCCESS)); + env.close(); + } + + // Fund channel after sender frozen + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + env(trust(gw, USD(100'000), alice, tfSetFreeze)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::fund(alice, chan, USD(1'000)), + Ter(tecFROZEN)); + env.close(); + BEAST_EXPECT( + paychan::channelAmount(*env.current(), chan) == USD(1'000)); + } + + // Close channel refund with deep frozen sender + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + auto const preAlice = env.balance(alice, USD); + + env(trust(gw, USD(100'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice + USD(1'000)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + } + } + + void + testIOUDeepFreezeAfterCreate(FeatureBitset features) + { + testcase("IOU Deep Freeze After Create"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // Create channel, deep freeze sender, bob claims (sender freeze + // doesn't block claim since funds already locked) + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + // Deep freeze alice's trust line + env(trust(gw, USD(100'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // Bob claims - only dest freeze is checked, not sender + auto const chan = paychan::channel(alice, bob, seq1); + auto const preBob = env.balance(bob, USD); + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(500)); + env(paychan::claim( + bob, chan, USD(500), USD(500), Slice(sig), pk), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(bob, USD) == preBob + USD(500)); + } + } + + void + testIOUMultiChannelDrain(FeatureBitset features) + { + testcase("IOU Multi Channel Drain"); + using namespace test::jtx; + using namespace std::literals; + + 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"]; + + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5'000))); + env(pay(gw, bob, USD(5'000))); + env(pay(gw, carol, USD(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(3'000), settleDelay, pk)); + env.close(); + BEAST_EXPECT(env.balance(alice, USD) == USD(2'000)); + + auto const seq2 = env.seq(alice); + env(paychan::create(alice, carol, USD(2'000), settleDelay, pk)); + env.close(); + BEAST_EXPECT(env.balance(alice, USD) == USD(0)); + + env(paychan::create(alice, bob, USD(1), settleDelay, pk), + Ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + auto const chan1 = paychan::channel(alice, bob, seq1); + auto sig = + paychan::signClaimAuth(pk, alice.sk(), chan1, USD(3'000)); + env(paychan::claim( + bob, chan1, USD(3'000), USD(3'000), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, USD) == USD(8'000)); + + auto const chan2 = paychan::channel(alice, carol, seq2); + sig = paychan::signClaimAuth(pk, alice.sk(), chan2, USD(2'000)); + env(paychan::claim( + carol, chan2, USD(2'000), USD(2'000), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) == USD(7'000)); + BEAST_EXPECT(env.balance(alice, USD) == USD(0)); + } + } + + void + testIOUTransferRatePartialClaims(FeatureBitset features) + { + testcase("IOU Transfer Rate Partial Claims"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // Partial claim at high rate, rate drops, second claim at lower rate + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + auto const preBob = env.balance(bob, USD); + auto const chan = paychan::channel(alice, bob, seq1); + + auto sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(500)); + env(paychan::claim( + bob, chan, USD(500), USD(500), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, USD) == preBob + USD(400)); + + env(rate(gw, 1.0)); + env.close(); + + sig = paychan::signClaimAuth(pk, alice.sk(), chan, USD(1'000)); + env(paychan::claim( + bob, chan, USD(1'000), USD(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, USD) == preBob + USD(900)); + } + + // Create at parity, issuer raises rate, claim uses locked parity + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + env(rate(gw, 2.0)); + env.close(); + + auto const preBob = env.balance(bob, USD); + auto const chan = paychan::channel(alice, bob, seq1); + + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(1'000)); + env(paychan::claim( + bob, chan, USD(1'000), USD(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, USD) == preBob + USD(1'000)); + } + } + + void + testIOUTrustLineLimitClaim(FeatureBitset features) + { + testcase("IOU Trust Line Limit Claim"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // Claim that would exceed bob's trust line limit + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(500))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(200))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + + // Bob claims 250 for self — succeeds (200 + 250 = 450 < 500) + auto sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(250)); + env(paychan::claim( + bob, chan, USD(250), USD(250), Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob, USD) == USD(450)); + + // Bob claims more — fails, would exceed limit (450 + 350 > 500) + sig = paychan::signClaimAuth(pk, alice.sk(), chan, USD(600)); + env(paychan::claim( + bob, chan, USD(600), USD(600), Slice(sig), pk), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + BEAST_EXPECT(env.balance(bob, USD) == USD(450)); + + // Alice closing on bob's behalf also blocked by limit + env(paychan::claim(alice, chan, USD(600), USD(600)), + Txflags(tfClose), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + } + } + + void + testIOUAllowLockingClearedClaim(FeatureBitset features) + { + testcase("IOU Allow Locking Cleared Claim"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + { + Env env{*this, features}; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk)); + env.close(); + + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + + auto const preBob = env.balance(bob, USD); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, USD(1'000)); + env(paychan::claim( + bob, chan, USD(1'000), USD(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, USD) == preBob + USD(1'000)); + + env(paychan::create(alice, bob, USD(1'000), settleDelay, pk), + Ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTClawbackInteraction(FeatureBitset features) + { + testcase("MPT Clawback Interaction"); + using namespace test::jtx; + using namespace std::literals; + + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanClawback}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(5'000))); + env(pay(gw, bob, MPT(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, MPT(4'000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 4'000); + + mptGw.claw(gw, alice, 1'000); + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + + auto const chan = paychan::channel(alice, bob, seq1); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 4'000); + + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(4'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + } + } + + void + testMPTClaimAutoCreate(FeatureBitset features) + { + testcase("MPT Claim Auto Create"); + using namespace test::jtx; + using namespace std::literals; + + // Claim auto-creates MPToken for receiver without authorize + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), bob))); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, MPT(500)); + env(paychan::claim( + bob, chan, MPT(500), MPT(500), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.le(keylet::mptoken(MPT.mpt(), bob))); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(500)); + } + + // requireAuth blocks claim even with auto-create + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | + tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk), + Ter(tecNO_AUTH)); + env.close(); + } + } + + void + testMPTFreezeClaimClose(FeatureBitset features) + { + testcase("MPT Freeze Claim Close"); + using namespace test::jtx; + using namespace std::literals; + + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk)); + env.close(); + + auto const preAlice = env.balance(alice, MPT); + + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, MPT(500)); + + env(paychan::claim( + bob, chan, MPT(500), MPT(500), Slice(sig), pk), + Ter(tesSUCCESS)); + env.close(); + + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + auto const sig2 = + paychan::signClaimAuth(pk, alice.sk(), chan, MPT(1'000)); + env(paychan::claim( + bob, chan, MPT(1'000), MPT(1'000), Slice(sig2), pk), + Ter(tecLOCKED)); + env.close(); + + env(paychan::claim(bob, chan), Txflags(tfClose)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice + MPT(500)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); + } + } + + void + testMPTCanEscrowRequired(FeatureBitset features) + { + testcase("MPT CanEscrow Required"); + using namespace test::jtx; + using namespace std::literals; + + // Without canEscrow flag, channel creation fails + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk), + Ter(tecNO_PERMISSION)); + env.close(); + } + + // With canEscrow flag, channel works and claim succeeds + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(5'000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const seq1 = env.seq(alice); + env(paychan::create(alice, bob, MPT(1'000), settleDelay, pk)); + env.close(); + + auto const chan = paychan::channel(alice, bob, seq1); + auto const preBob = env.balance(bob, MPT); + auto const sig = + paychan::signClaimAuth(pk, alice.sk(), chan, MPT(1'000)); + env(paychan::claim( + bob, chan, MPT(1'000), MPT(1'000), Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob, MPT) == preBob + MPT(1'000)); + } + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + testIOUAllowLockingFlag(features); + testIOUCreatePreflight(features); + testIOUCreatePreclaim(features); + testIOUClaimPreclaim(features); + testIOUClaimDoApply(features); + // testIOUClaimClosePreclaim(features); + testIOUBalances(features); + testIOUMetaAndOwnership(features); + testIOURippleState(features); + testIOUGateway(features); + testIOULockedRate(features); + testIOULimitAmount(features); + testIOURequireAuth(features); + testIOUFreeze(features); + testIOUINSF(features); + testIOUPrecisionLoss(features); + testIOUClawbackInteraction(features); + testIOUFundAfterFreeze(features); + testIOUDeepFreezeAfterCreate(features); + testIOUMultiChannelDrain(features); + testIOUTransferRatePartialClaims(features); + testIOUTrustLineLimitClaim(features); + testIOUAllowLockingClearedClaim(features); + } + + void + testMPTWithFeats(FeatureBitset features) + { + testMPTEnablement(features); + testMPTCreatePreflight(features); + testMPTCreatePreclaim(features); + testMPTClaimPreclaim(features); + testMPTClaimDoApply(features); + // testMPTClaimClosePreclaim(features); + testMPTBalances(features); + testMPTMetaAndOwnership(features); + testMPTGateway(features); + testMPTLockedRate(features); + // testMPTRequireAuth(features); + testMPTLock(features); + testMPTCanTransfer(features); + testMPTDestroy(features); + testMPTClawbackInteraction(features); + testMPTClaimAutoCreate(features); + testMPTFreezeClaimClose(features); + testMPTCanEscrowRequired(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testableAmendments()}; + testIOUWithFeats(all); + testMPTWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(PayChanToken, app, xrpl); +} // namespace test +} // namespace xrpl diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index c775dd2b6a8..cf9773fe5c4 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -79,15 +79,6 @@ struct PayChan_test : public beast::unit_test::Suite 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 channelExpiration(ReadView const& view, uint256 const& chan) { @@ -112,20 +103,20 @@ struct PayChan_test : public beast::unit_test::Suite 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 chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000)); { auto const preAlice = env.balance(alice); - env(fund(alice, chan, XRP(1000))); + env(paychan::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); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(2000)); @@ -175,9 +166,9 @@ struct PayChan_test : public beast::unit_test::Suite 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); + env(paychan::claim(alice, chan, reqBal, authAmt)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; } @@ -337,14 +328,14 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(paychan::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 const chanBal = paychan::channelBalance(*env.current(), chan); + auto const chanAmt = paychan::channelAmount(*env.current(), chan); auto preAlice = env.balance(alice); auto preBob = env.balance(bob); auto const delta = XRP(500); @@ -354,7 +345,7 @@ struct PayChan_test : public beast::unit_test::Suite 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(!paychan::channelExists(*env.current(), chan)); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds); } @@ -368,9 +359,9 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); // third party close before cancelAfter env(claim(carol, chan), Txflags(tfClose), Ter(tecNO_PERMISSION)); BEAST_EXPECT(channelExists(*env.current(), chan)); @@ -437,9 +428,9 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); // Owner closes, will close after settleDelay env(claim(alice, chan), Txflags(tfClose)); @@ -468,7 +459,7 @@ struct PayChan_test : public beast::unit_test::Suite 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(paychan::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})); @@ -490,17 +481,17 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::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 const chanBal = paychan::channelBalance(*env.current(), chan); + auto const chanAmt = paychan::channelAmount(*env.current(), chan); auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; @@ -516,8 +507,8 @@ struct PayChan_test : public beast::unit_test::Suite env.close(settleTimepoint); { // past settleTime, channel will close - auto const chanBal = channelBalance(*env.current(), chan); - auto const chanAmt = channelAmount(*env.current(), chan); + auto const chanBal = paychan::channelBalance(*env.current(), chan); + auto const chanAmt = paychan::channelAmount(*env.current(), chan); auto const preAlice = env.balance(alice); auto preBob = env.balance(bob); auto const delta = XRP(500); @@ -546,17 +537,17 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::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); + env(paychan::claim(alice, chan, channelFunds.value(), channelFunds.value())); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == channelFunds); BEAST_EXPECT(env.balance(bob) == preBob + channelFunds); } auto const preAlice = env.balance(alice); @@ -581,15 +572,15 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::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 chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); auto const preBob = env.balance(bob); auto const delta = XRP(500); @@ -604,8 +595,8 @@ struct PayChan_test : public beast::unit_test::Suite } { // Claim again - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); auto const preBob = env.balance(bob); auto const delta = XRP(500); @@ -636,9 +627,9 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk())); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); } { @@ -647,13 +638,13 @@ struct PayChan_test : public beast::unit_test::Suite // 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk())); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); auto const reqBal = XRP(500).value(); - env(claim(alice, chan, reqBal, reqBal)); + env(paychan::claim(alice, chan, reqBal, reqBal)); } } @@ -705,16 +696,16 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::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)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::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(paychan::fund(alice, chan, XRP(1000))); env.close(); // alice claims. Fails because bob's lsfDepositAuth flag is set. @@ -742,7 +733,7 @@ struct PayChan_test : public beast::unit_test::Suite // 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(paychan::claim(bob, chan, delta, delta, Slice(sig), pk)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + delta - baseFee); } @@ -773,7 +764,7 @@ struct PayChan_test : public beast::unit_test::Suite env(deposit::auth(bob, alice)); env.close(); - env(claim(alice, chan, delta, delta, Slice(sig), pk)); + env(paychan::claim(alice, chan, delta, delta, Slice(sig), pk)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + delta - (3 * baseFee)); @@ -795,7 +786,7 @@ struct PayChan_test : public beast::unit_test::Suite env.close(); // alice claims successfully. - env(claim(alice, chan, delta, delta)); + env(paychan::claim(alice, chan, delta, delta)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + XRP(800) - (5 * baseFee)); } @@ -823,12 +814,12 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); // alice add funds to the channel - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); std::string const credBadIdx = @@ -877,7 +868,7 @@ struct PayChan_test : public beast::unit_test::Suite credentials::Ids({credIdx}), Ter(tecBAD_CREDENTIALS)); - // Fails because bob's lsfDepositAuth flag is set. + // Fails because bob’s lsfDepositAuth flag is set. env(claim(alice, chan, delta, delta), Ter(tecNO_PERMISSION)); // Fail, bad credentials index. @@ -919,12 +910,12 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); // alice add funds to the channel - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); auto const delta = XRP(500).value(); @@ -952,11 +943,11 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - env(fund(alice, chan, XRP(1000))); + env(paychan::fund(alice, chan, XRP(1000))); env.close(); std::string const credIdx = "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" @@ -988,12 +979,12 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan1 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan1)); + auto const chan2 = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(paychan::channelExists(*env.current(), chan2)); BEAST_EXPECT(chan1 != chan2); } @@ -1012,8 +1003,8 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { // test account non-string @@ -1069,8 +1060,8 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -1120,7 +1111,7 @@ struct PayChan_test : public beast::unit_test::Suite auto const channelFunds = XRP(1); for (auto const& b : bobs) { - env(create(alice, b, channelFunds, settleDelay, alice.pk())); + env(paychan::create(alice, b, channelFunds, settleDelay, alice.pk())); } } @@ -1219,8 +1210,8 @@ struct PayChan_test : public beast::unit_test::Suite // 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())); + env(paychan::create(alice, bob, channelFunds, settleDelay, alice.pk())); + env(paychan::create(bob, alice, channelFunds, settleDelay, bob.pk())); auto const r = [&] { json::Value jvc; @@ -1247,8 +1238,8 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); json::Value args{json::ObjectValue}; @@ -1282,8 +1273,8 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); std::string chan1PkStr; { @@ -1305,8 +1296,8 @@ struct PayChan_test : public beast::unit_test::Suite 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)); + auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice))); + env(paychan::create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -1602,8 +1593,8 @@ struct PayChan_test : public beast::unit_test::Suite 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 chan = paychan::channel(alice, bob, env.seq(alice)); + auto jv = paychan::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)); @@ -1682,7 +1673,7 @@ struct PayChan_test : public beast::unit_test::Suite // 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(paychan::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)); @@ -1704,7 +1695,7 @@ struct PayChan_test : public beast::unit_test::Suite 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(paychan::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)); @@ -1712,6 +1703,7 @@ struct PayChan_test : public beast::unit_test::Suite BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + // close the channel after the amendment activates env(claim(bob, chan), Txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); @@ -1757,18 +1749,18 @@ struct PayChan_test : public beast::unit_test::Suite // 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)); + auto const chan = paychan::channel(alice, bob, env.seq(alice)); + env(paychan::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)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::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); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(1000)); @@ -1832,15 +1824,15 @@ struct PayChan_test : public beast::unit_test::Suite auto const pk = alice.pk(); auto const settleDelay = 100s; - auto const chan = channel(alice, bob, aliceTicketSeq); + auto const chan = paychan::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)); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000)); { auto const preAlice = env.balance(alice); @@ -1853,8 +1845,8 @@ struct PayChan_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); } - auto chanBal = channelBalance(*env.current(), chan); - auto chanAmt = channelAmount(*env.current(), chan); + auto chanBal = paychan::channelBalance(*env.current(), chan); + auto chanAmt = paychan::channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(2000)); @@ -1870,8 +1862,8 @@ struct PayChan_test : public beast::unit_test::Suite 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(paychan::channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; } @@ -1889,8 +1881,8 @@ struct PayChan_test : public beast::unit_test::Suite 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); + BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); chanBal = reqBal; @@ -1905,8 +1897,8 @@ struct PayChan_test : public beast::unit_test::Suite 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(paychan::channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } { @@ -1916,7 +1908,7 @@ struct PayChan_test : public beast::unit_test::Suite 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 kINCREMENT bobTicket. + // 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), @@ -1925,8 +1917,8 @@ struct PayChan_test : public beast::unit_test::Suite 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(paychan::channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob); } @@ -1936,8 +1928,8 @@ struct PayChan_test : public beast::unit_test::Suite 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(paychan::channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt); { // Dst closes channel @@ -1948,7 +1940,7 @@ struct PayChan_test : public beast::unit_test::Suite env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); - BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(!paychan::channelExists(*env.current(), chan)); auto const feeDrops = env.current()->fees().base; auto const delta = chanAmt - chanBal; assert(delta > beast::kZERO); @@ -1994,6 +1986,7 @@ struct PayChan_test : public beast::unit_test::Suite using namespace test::jtx; FeatureBitset const all{testableAmendments()}; testWithFeats(all); + testWithFeats(all - featureTokenEscrow); testDepositAuthCreds(); testMetaAndOwnership(all - fixIncludeKeyletFields); } diff --git a/src/test/jtx.h b/src/test/jtx.h index d4b88b0b9ef..f84dd396030 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index de0017bfa96..6a23fdfd8cd 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -345,6 +346,20 @@ checkArraySize(json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +/* Token (IOU/MPT) Locking */ +/******************************************************************************/ +uint64_t +mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt); + +uint64_t +issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt); + +jtx::PrettyAmount +issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue); + +jtx::PrettyAmount +issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue); + [[nodiscard]] inline bool checkVL(Slice const& result, std::string const& expected) @@ -552,66 +567,6 @@ accountBalance(Env& env, Account const& acct); [[nodiscard]] bool expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedValue); -/* Payment Channel */ -/******************************************************************************/ -namespace paychan { - -json::Value -create( - AccountID const& account, - AccountID const& to, - STAmount const& amount, - NetClock::duration const& settleDelay, - PublicKey const& pk, - std::optional const& cancelAfter = std::nullopt, - std::optional const& dstTag = std::nullopt); - -inline json::Value -create( - Account const& account, - Account const& to, - STAmount const& amount, - NetClock::duration const& settleDelay, - PublicKey const& pk, - std::optional const& cancelAfter = std::nullopt, - std::optional const& dstTag = std::nullopt) -{ - return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag); -} - -json::Value -fund( - AccountID const& account, - uint256 const& channel, - STAmount const& amount, - std::optional const& expiration = std::nullopt); - -json::Value -claim( - AccountID const& account, - uint256 const& channel, - std::optional const& balance = std::nullopt, - std::optional const& amount = std::nullopt, - std::optional const& signature = std::nullopt, - std::optional const& pk = std::nullopt); - -uint256 -channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue); - -inline uint256 -channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue) -{ - return channel(account.id(), dst.id(), seqProxyValue); -} - -STAmount -channelBalance(ReadView const& view, uint256 const& chan); - -bool -channelExists(ReadView const& view, uint256 const& chan); - -} // namespace paychan - /* Crossing Limits */ /******************************************************************************/ diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 6be3220220a..48576422849 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -94,6 +94,54 @@ ownerCount(Env const& env, Account const& account) return env.ownerCount(account); } +/* Token (IOU/MPT) Locking */ +/******************************************************************************/ +uint64_t +mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt) +{ + auto const sle = env.le(keylet::mptoken(mpt.mpt(), account)); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; +} + +uint64_t +issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt) +{ + auto const sle = env.le(keylet::mptIssuance(mpt.mpt())); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; +} + +jtx::PrettyAmount +issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue) +{ + json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const obligations = result[jss::obligations][to_string(issue.currency)]; + if (obligations.isNull()) + return {STAmount(issue, 0), account.name()}; + STAmount const amount = amountFromString(issue, obligations.asString()); + return {amount, account.name()}; +} + +jtx::PrettyAmount +issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue) +{ + json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const locked = result[jss::locked][to_string(issue.currency)]; + if (locked.isNull()) + return {STAmount(issue, 0), account.name()}; + STAmount const amount = amountFromString(issue, locked.asString()); + return {amount, account.name()}; +} + /* Path finding */ /******************************************************************************/ void @@ -481,100 +529,6 @@ expectLedgerEntryRoot(Env& env, Account const& acct, STAmount const& expectedVal return accountBalance(env, acct) == to_string(expectedValue.xrp()); } -/* Payment Channel */ -/******************************************************************************/ -namespace paychan { - -json::Value -create( - AccountID const& account, - AccountID const& to, - STAmount const& amount, - NetClock::duration const& settleDelay, - PublicKey const& pk, - std::optional const& cancelAfter, - std::optional const& dstTag) -{ - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Account] = to_string(account); - jv[jss::Destination] = to_string(to); - jv[jss::Amount] = amount.getJson(JsonOptions::KNone); - jv[jss::SettleDelay] = settleDelay.count(); - jv[sfPublicKey.fieldName] = strHex(pk.slice()); - if (cancelAfter) - jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count(); - if (dstTag) - jv[sfDestinationTag.fieldName] = *dstTag; - return jv; -} - -json::Value -fund( - AccountID const& account, - uint256 const& channel, - STAmount const& amount, - std::optional const& expiration) -{ - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Account] = to_string(account); - jv[sfChannel.fieldName] = to_string(channel); - jv[jss::Amount] = amount.getJson(JsonOptions::KNone); - if (expiration) - jv[sfExpiration.fieldName] = expiration->time_since_epoch().count(); - return jv; -} - -json::Value -claim( - AccountID const& account, - uint256 const& channel, - std::optional const& balance, - std::optional const& amount, - std::optional const& signature, - std::optional const& pk) -{ - json::Value jv; - jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Account] = to_string(account); - jv["Channel"] = to_string(channel); - if (amount) - jv[jss::Amount] = amount->getJson(JsonOptions::KNone); - if (balance) - jv["Balance"] = balance->getJson(JsonOptions::KNone); - if (signature) - jv["Signature"] = strHex(*signature); - if (pk) - jv["PublicKey"] = strHex(pk->slice()); - return jv; -} - -uint256 -channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue) -{ - auto const k = keylet::payChan(account, dst, seqProxyValue); - return k.key; -} - -STAmount -channelBalance(ReadView const& view, uint256 const& chan) -{ - auto const slep = view.read({ltPAYCHAN, chan}); - if (!slep) - return XRPAmount{-1}; - return (*slep)[sfBalance]; -} - -bool -channelExists(ReadView const& view, uint256 const& chan) -{ - auto const slep = view.read({ltPAYCHAN, chan}); - return bool(slep); -} - -} // namespace paychan - /* Crossing Limits */ /******************************************************************************/ diff --git a/src/test/jtx/impl/paychan.cpp b/src/test/jtx/impl/paychan.cpp new file mode 100644 index 00000000000..5d274e6500c --- /dev/null +++ b/src/test/jtx/impl/paychan.cpp @@ -0,0 +1,159 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include + +namespace xrpl { +namespace test { +namespace jtx { + +/** Paychan operations. */ +namespace paychan { + +json::Value +create( + AccountID const& account, + AccountID const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + std::optional const& cancelAfter, + std::optional const& dstTag) +{ + json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelCreate; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[jss::Destination] = to_string(to); + jv[jss::Amount] = amount.getJson(JsonOptions::KNone); + jv[jss::SettleDelay] = settleDelay.count(); + jv[sfPublicKey.fieldName] = strHex(pk.slice()); + if (cancelAfter) + jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count(); + if (dstTag) + jv[sfDestinationTag.fieldName] = *dstTag; + return jv; +} + +json::Value +fund( + AccountID const& account, + uint256 const& channel, + STAmount const& amount, + std::optional const& expiration) +{ + json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelFund; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfChannel.fieldName] = to_string(channel); + jv[jss::Amount] = amount.getJson(JsonOptions::KNone); + if (expiration) + jv[sfExpiration.fieldName] = expiration->time_since_epoch().count(); + return jv; +} + +json::Value +claim( + AccountID const& account, + uint256 const& channel, + std::optional const& balance, + std::optional const& amount, + std::optional const& signature, + std::optional const& pk) +{ + json::Value jv; + jv[jss::TransactionType] = jss::PaymentChannelClaim; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv["Channel"] = to_string(channel); + if (amount) + jv[jss::Amount] = amount->getJson(JsonOptions::KNone); + if (balance) + jv["Balance"] = balance->getJson(JsonOptions::KNone); + if (signature) + jv["Signature"] = strHex(*signature); + if (pk) + jv["PublicKey"] = strHex(pk->slice()); + return jv; +} + +uint256 +channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue) +{ + auto const k = keylet::payChan(account, dst, seqProxyValue); + return k.key; +} + +STAmount +channelBalance(ReadView const& view, uint256 const& chan) +{ + auto const slep = view.read({ltPAYCHAN, chan}); + if (!slep) + return XRPAmount{-1}; + return (*slep)[sfBalance]; +} + +STAmount +channelAmount(ReadView const& view, uint256 const& chan) +{ + auto const slep = view.read({ltPAYCHAN, chan}); + if (!slep) + return XRPAmount{-1}; + return (*slep)[sfAmount]; +} + +bool +channelExists(ReadView const& view, uint256 const& chan) +{ + auto const slep = view.read({ltPAYCHAN, chan}); + return bool(slep); +} + +Buffer +signClaimAuth( + PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt) +{ + Serializer msg; + serializePayChanAuthorization(msg, channel, authAmt); + return sign(pk, sk, msg.slice()); +} + +Rate +rate(Env& env, Account const& account, Account const& dest, std::uint32_t const& seq) +{ + auto const sle = env.le(keylet::payChan(account.id(), dest.id(), seq)); + if (sle->isFieldPresent(sfTransferRate)) + return xrpl::Rate((*sle)[sfTransferRate]); + return Rate{0}; +} + +} // namespace paychan + +} // namespace jtx + +} // namespace test +} // namespace xrpl diff --git a/src/test/jtx/paychan.h b/src/test/jtx/paychan.h new file mode 100644 index 00000000000..22a686d241b --- /dev/null +++ b/src/test/jtx/paychan.h @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_PAYCHAN_H_INCLUDED +#define RIPPLE_TEST_JTX_PAYCHAN_H_INCLUDED + +#include +#include +#include +#include + +#include + +namespace xrpl { +namespace test { +namespace jtx { + +/** Paychan operations. */ +namespace paychan { + +json::Value +create( + AccountID const& account, + AccountID const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + std::optional const& cancelAfter = std::nullopt, + std::optional const& dstTag = std::nullopt); + +inline json::Value +create( + Account const& account, + Account const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + std::optional const& cancelAfter = std::nullopt, + std::optional const& dstTag = std::nullopt) +{ + return create(account.id(), to.id(), amount, settleDelay, pk, cancelAfter, dstTag); +} + +json::Value +fund( + AccountID const& account, + uint256 const& channel, + STAmount const& amount, + std::optional const& expiration = std::nullopt); + +json::Value +claim( + AccountID const& account, + uint256 const& channel, + std::optional const& balance = std::nullopt, + std::optional const& amount = std::nullopt, + std::optional const& signature = std::nullopt, + std::optional const& pk = std::nullopt); + +uint256 +channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue); + +inline uint256 +channel(Account const& account, Account const& dst, std::uint32_t seqProxyValue) +{ + return channel(account.id(), dst.id(), seqProxyValue); +} + +STAmount +channelBalance(ReadView const& view, uint256 const& chan); + +STAmount +channelAmount(ReadView const& view, uint256 const& chan); + +bool +channelExists(ReadView const& view, uint256 const& chan); + +Buffer +signClaimAuth( + PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt); + +Rate +rate(Env& env, Account const& account, Account const& dest, std::uint32_t const& seq); + +} // namespace paychan + +} // namespace jtx + +} // namespace test +} // namespace xrpl + +#endif diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index 7b6d6ca07b6..a8b3973df82 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -147,22 +147,53 @@ doGatewayBalances(RPC::JsonContext& context) forEachItem(*ledger, accountID, [&](std::shared_ptr const& sle) { if (sle->getType() == ltESCROW) { - auto const& escrow = sle->getFieldAmount(sfAmount); - // Gateway Balance should not include MPTs - if (escrow.holds()) + auto const& amount = sle->getFieldAmount(sfAmount); + if (amount.native() || amount.holds()) return; - auto& bal = locked[escrow.get().currency]; + auto& bal = locked[amount.get().currency]; if (bal == beast::kZERO) { // This is needed to set the currency code correctly - bal = escrow; + bal = amount; } else { try { - bal += escrow; + bal += amount; + } + catch (std::runtime_error const&) + { + // Presumably the exception was caused by overflow. + // On overflow return the largest valid STAmount. + // Very large sums of STAmount are approximations + // anyway. + bal = + STAmount(bal.get(), STAmount::kMAX_VALUE, STAmount::kMAX_OFFSET); + } + } + } + + if (sle->getType() == ltPAYCHAN) + { + auto const& amount = sle->getFieldAmount(sfAmount); + if (amount.native() || amount.holds()) + return; + + auto const& balance = sle->getFieldAmount(sfBalance); + auto const& netAmount = amount - balance; + auto& bal = locked[netAmount.get().currency]; + if (bal == beast::kZERO) + { + // This is needed to set the currency code correctly + bal = netAmount; + } + else + { + try + { + bal += netAmount; } catch (std::runtime_error const&) {