From b07dd3b483aa787ebd19019202c3bb0c8fafdc95 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Wed, 19 Feb 2025 12:25:05 -0500 Subject: [PATCH] support AccountPermission --- include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/Indexes.h | 11 + include/xrpl/protocol/Permissions.h | 92 + include/xrpl/protocol/STTx.h | 6 + include/xrpl/protocol/STTxDelegated.h | 256 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 12 + include/xrpl/protocol/detail/sfields.macro | 6 + .../xrpl/protocol/detail/transactions.macro | 6 + include/xrpl/protocol/jss.h | 8 + src/libxrpl/protocol/BuildInfo.cpp | 2 +- src/libxrpl/protocol/Indexes.cpp | 18 + src/libxrpl/protocol/InnerObjectFormats.cpp | 4 + src/libxrpl/protocol/Permissions.cpp | 96 + src/libxrpl/protocol/STParsedJSON.cpp | 43 +- src/libxrpl/protocol/STTx.cpp | 24 + src/libxrpl/protocol/TxFormats.cpp | 3 + src/test/app/AccountPermission_test.cpp | 4577 +++++++++++++++++ src/test/app/Check_test.cpp | 278 +- src/test/app/Credentials_test.cpp | 60 +- src/test/app/DID_test.cpp | 39 +- src/test/app/DepositAuth_test.cpp | 33 +- src/test/app/NFTokenBurn_test.cpp | 2 + src/test/app/PayChan_test.cpp | 9 - src/test/jtx/AMM.h | 42 +- src/test/jtx/AccountPermission.h | 46 + src/test/jtx/JTx.h | 1 + src/test/jtx/Oracle.h | 6 + src/test/jtx/TestHelpers.h | 68 + src/test/jtx/check.h | 72 +- src/test/jtx/credentials.h | 12 + src/test/jtx/deposit.h | 16 +- src/test/jtx/did.h | 5 +- src/test/jtx/flags.h | 12 + src/test/jtx/impl/AMM.cpp | 114 +- src/test/jtx/impl/AccountPermission.cpp | 73 + src/test/jtx/impl/Env.cpp | 2 + src/test/jtx/impl/Oracle.cpp | 15 +- src/test/jtx/impl/TestHelpers.cpp | 9 + src/test/jtx/impl/check.cpp | 35 +- src/test/jtx/impl/credentials.cpp | 18 + src/test/jtx/impl/deposit.cpp | 33 +- src/test/jtx/impl/did.cpp | 11 +- src/test/jtx/impl/mpt.cpp | 54 +- src/test/jtx/impl/paths.cpp | 8 +- src/test/jtx/impl/token.cpp | 21 + src/test/jtx/impl/utility.cpp | 14 + src/test/jtx/mpt.h | 12 + src/test/jtx/paths.h | 10 + src/test/jtx/token.h | 3 + src/test/jtx/utility.h | 4 + src/test/ledger/Invariants_test.cpp | 1 + src/xrpld/app/ledger/AcceptedLedgerTx.cpp | 2 +- src/xrpld/app/ledger/detail/LedgerToJson.cpp | 2 +- src/xrpld/app/ledger/detail/LocalTxs.cpp | 2 +- src/xrpld/app/misc/CredentialHelpers.cpp | 2 +- src/xrpld/app/misc/NetworkOPs.cpp | 4 +- src/xrpld/app/misc/detail/TxQ.cpp | 4 +- src/xrpld/app/tx/apply.h | 1 + src/xrpld/app/tx/applySteps.h | 16 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 7 +- .../app/tx/detail/AccountPermissionSet.cpp | 155 + .../app/tx/detail/AccountPermissionSet.h | 56 + src/xrpld/app/tx/detail/ApplyContext.cpp | 6 +- src/xrpld/app/tx/detail/ApplyContext.h | 7 +- src/xrpld/app/tx/detail/CashCheck.cpp | 2 +- src/xrpld/app/tx/detail/Change.cpp | 14 +- src/xrpld/app/tx/detail/CreateCheck.cpp | 2 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 5 +- src/xrpld/app/tx/detail/CreateTicket.cpp | 6 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 16 + src/xrpld/app/tx/detail/Escrow.cpp | 4 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 39 +- src/xrpld/app/tx/detail/InvariantCheck.h | 42 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceSet.cpp | 16 +- .../app/tx/detail/NFTokenCreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenMint.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenUtils.cpp | 4 +- src/xrpld/app/tx/detail/NFTokenUtils.h | 2 +- src/xrpld/app/tx/detail/PayChan.cpp | 6 +- src/xrpld/app/tx/detail/Payment.cpp | 27 +- src/xrpld/app/tx/detail/SetAccount.cpp | 47 +- src/xrpld/app/tx/detail/SetSignerList.cpp | 4 +- src/xrpld/app/tx/detail/SetTrust.cpp | 36 + src/xrpld/app/tx/detail/Transactor.cpp | 212 +- src/xrpld/app/tx/detail/Transactor.h | 24 +- src/xrpld/app/tx/detail/XChainBridge.cpp | 8 +- src/xrpld/app/tx/detail/applySteps.cpp | 68 +- src/xrpld/rpc/MPTokenIssuanceID.h | 4 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 9 +- src/xrpld/rpc/handlers/LedgerEntry.cpp | 38 + 92 files changed, 6743 insertions(+), 467 deletions(-) create mode 100644 include/xrpl/protocol/Permissions.h create mode 100644 include/xrpl/protocol/STTxDelegated.h create mode 100644 src/libxrpl/protocol/Permissions.cpp create mode 100644 src/test/app/AccountPermission_test.cpp create mode 100644 src/test/jtx/AccountPermission.h create mode 100644 src/test/jtx/impl/AccountPermission.cpp create mode 100644 src/xrpld/app/tx/detail/AccountPermissionSet.cpp create mode 100644 src/xrpld/app/tx/detail/AccountPermissionSet.h diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 1c476df617f..b6477baa02d 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 88; +static constexpr std::size_t numFeatures = 89; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index bbed5395927..6fc0e8017a0 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -279,6 +279,17 @@ amm(Asset const& issue1, Asset const& issue2) noexcept; Keylet amm(uint256 const& amm) noexcept; +/** An AccountPermission */ +/** @{ */ +Keylet +accountPermission( + AccountID const& account, + AccountID const& authorizedAccount) noexcept; + +Keylet +accountPermission(uint256 const& key) noexcept; +/** @} */ + Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h new file mode 100644 index 00000000000..bb62af85e29 --- /dev/null +++ b/include/xrpl/protocol/Permissions.h @@ -0,0 +1,92 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_PROTOCOL_PERMISSION_H_INCLUDED +#define RIPPLE_PROTOCOL_PERMISSION_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +/** + * We have transaction type permissions and granular type + * permissions. Since we will reuse the TransactionFormats to parse the + * Transaction Permissions, we only define the GranularPermissionType here. + */ + +enum GranularPermissionType : std::uint32_t { + TrustlineAuthorize = 65537, + + TrustlineFreeze = 65538, + + TrustlineUnfreeze = 65539, + + AccountDomainSet = 65540, + + AccountEmailHashSet = 65541, + + AccountMessageKeySet = 65542, + + AccountTransferRateSet = 65543, + + AccountTickSizeSet = 65544, + + PaymentMint = 65545, + + PaymentBurn = 65546, + + MPTokenIssuanceLock = 65547, + + MPTokenIssuanceUnlock = 65548, +}; + +class Permission +{ +private: + Permission(); + + std::unordered_map + granularPermissionMap; + + std::unordered_map granularTxTypeMap; + +public: + static Permission const& + getInstance(); + + Permission(const Permission&) = delete; + Permission& + operator=(const Permission&) = delete; + + std::optional + getGranularValue(std::string const& name) const; + + std::optional + getGranularTxType(GranularPermissionType const& gpType) const; + + bool + isProhibited(std::string const& name) const; +}; + +} // namespace ripple + +#endif diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 08b9a1bad10..943c11ded4b 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -101,6 +101,9 @@ class STTx final : public STObject, public CountedObject SeqProxy getSeqProxy() const; + SeqProxy + getDelegateSeqProxy() const; + boost::container::flat_set getMentionedAccounts() const; @@ -139,6 +142,9 @@ class STTx final : public STObject, public CountedObject char status, std::string const& escapedMetaData) const; + AccountID + getEffectiveAccountID() const; + private: Expected checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; diff --git a/include/xrpl/protocol/STTxDelegated.h b/include/xrpl/protocol/STTxDelegated.h new file mode 100644 index 00000000000..8ef4b071b81 --- /dev/null +++ b/include/xrpl/protocol/STTxDelegated.h @@ -0,0 +1,256 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_PROTOCOL_STTX_WRAPPER_H_INCLUDED +#define RIPPLE_PROTOCOL_STTX_WRAPPER_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +// This class is a wrapper to deal with delegating in AccountPermission +// amendment. It wraps STTx, and delegates to STTx methods. The key change is +// getAccountID and operator[]. We need to first check if the transaction is +// delegated by another account by checking if the sfOnBehalfOf field is +// present. If it is present, we need to return the sfOnBehalfOf field as the +// account when calling getAccountID(sfAccount) and tx[sfAccount]. +class STTxDelegated +{ +private: + const STTx& tx_; // Wrap an instance of STTx + bool isDelegated_; // if the transaction is delegated by another account + +public: + explicit STTxDelegated(STTx const& tx, bool isDelegated) + : tx_(tx), isDelegated_(isDelegated) + { + } + + const STTx& + getSTTx() const + { + return tx_; + } + + bool + isDelegated() const + { + return isDelegated_; + } + + AccountID + getSenderAccount() const + { + return tx_.getAccountID(sfAccount); + } + + std::uint32_t + getEffectiveSeq() const + { + return isDelegated_ ? tx_.getDelegateSeqProxy().value() + : tx_.getSeqProxy().value(); + } + + AccountID + getAccountID(SField const& field) const + { + if (field == sfAccount) + return tx_.isFieldPresent(sfOnBehalfOf) ? *tx_[~sfOnBehalfOf] + : tx_[sfAccount]; + return tx_.getAccountID(field); + } + + template + requires(!std::is_same, SF_ACCOUNT>::value) + typename T::value_type + operator[](TypedField const& f) const + { + return tx_[f]; + } + + // When Type is SF_ACCOUNT and also field name is sfAccount, we need to + // check if the transaction is delegated by another account. If it is, + // return sfOnBehalfOf field instead. + template + requires std::is_same, SF_ACCOUNT>::value + AccountID + operator[](TypedField const& f) const + { + if (f == sfAccount) + return tx_.isFieldPresent(sfOnBehalfOf) ? *tx_[~sfOnBehalfOf] + : tx_[sfAccount]; + return tx_[f]; + } + + template + std::optional> + operator[](OptionaledField const& of) const + { + return tx_[of]; + } + + template + requires(!std::is_same, SF_ACCOUNT>::value) + typename T::value_type + at(TypedField const& f) const + { + return tx_.at(f); + } + + // When Type is SF_ACCOUNT and also field name is sfAccount, we need to + // check if the transaction is delegated by another account. If it is, + // return sfOnBehalfOf field instead. + template + requires std::is_same, SF_ACCOUNT>::value + AccountID + at(TypedField const& f) const + { + if (f == sfAccount) + return tx_.isFieldPresent(sfOnBehalfOf) ? *tx_[~sfOnBehalfOf] + : tx_[sfAccount]; + return tx_.at(f); + } + + template + std::optional> + at(OptionaledField const& of) const + { + return tx_.at(of); + } + + uint256 + getTransactionID() const + { + return tx_.getTransactionID(); + } + + TxType + getTxnType() const + { + return tx_.getTxnType(); + } + + std::uint32_t + getFlags() const + { + return tx_.getFlags(); + } + + bool + isFieldPresent(SField const& field) const + { + return tx_.isFieldPresent(field); + } + + Json::Value + getJson(JsonOptions options) const + { + return tx_.getJson(options); + } + + void + add(Serializer& s) const + { + tx_.add(s); + } + + unsigned char + getFieldU8(SField const& field) const + { + return tx_.getFieldU8(field); + } + + std::uint32_t + getFieldU32(SField const& field) const + { + return tx_.getFieldU32(field); + } + + uint256 + getFieldH256(SField const& field) const + { + return tx_.getFieldH256(field); + } + + Blob + getFieldVL(SField const& field) const + { + return tx_.getFieldVL(field); + } + + STAmount const& + getFieldAmount(SField const& field) const + { + return tx_.getFieldAmount(field); + } + + STPathSet const& + getFieldPathSet(SField const& field) const + { + return tx_.getFieldPathSet(field); + } + + const STVector256& + getFieldV256(SField const& field) const + { + return tx_.getFieldV256(field); + } + + const STArray& + getFieldArray(SField const& field) const + { + return tx_.getFieldArray(field); + } + + Blob + getSigningPubKey() const + { + return tx_.getSigningPubKey(); + } + + Blob + getSignature() const + { + return tx_.getSignature(); + } + + bool + isFlag(std::uint32_t f) const + { + return tx_.isFlag(f); + } + + SeqProxy + getSeqProxy() const + { + return tx_.getSeqProxy(); + } + + SeqProxy + getDelegateSeqProxy() const + { + return tx_.getDelegateSeqProxy(); + } +}; + +} // namespace ripple + +#endif diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 7b120c0b8d2..a217ca2d0a3 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(AccountPermission, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5a652baf4f7..861528bf0d5 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -463,3 +463,15 @@ LEDGER_ENTRY(ltPERMISSIONED_DOMAIN, 0x0082, PermissionedDomain, permissioned_dom #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE + +/** A ledger object representing permissions an account has delegated to another account. + \sa keylet::accountPermission + */ +LEDGER_ENTRY(ltACCOUNT_PERMISSION, 0x0083, AccountPermission, account_permission, ({ + {sfAccount, soeREQUIRED}, + {sfAuthorize, soeREQUIRED}, + {sfPermissions, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, +})) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3217bab9134..15f66f973b3 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -112,6 +112,9 @@ TYPED_SFIELD(sfEmitGeneration, UINT32, 46) TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) +TYPED_SFIELD(sfPermissionValue, UINT32, 52) +TYPED_SFIELD(sfDelegateSequence, UINT32, 53) +TYPED_SFIELD(sfDelegateTicketSequence, UINT32, 54) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -278,6 +281,7 @@ TYPED_SFIELD(sfRegularKey, ACCOUNT, 8) TYPED_SFIELD(sfNFTokenMinter, ACCOUNT, 9) TYPED_SFIELD(sfEmitCallback, ACCOUNT, 10) TYPED_SFIELD(sfHolder, ACCOUNT, 11) +TYPED_SFIELD(sfOnBehalfOf, ACCOUNT, 12) // account (uncommon) TYPED_SFIELD(sfHookAccount, ACCOUNT, 16) @@ -327,6 +331,7 @@ UNTYPED_SFIELD(sfSignerEntry, OBJECT, 11) UNTYPED_SFIELD(sfNFToken, OBJECT, 12) UNTYPED_SFIELD(sfEmitDetails, OBJECT, 13) UNTYPED_SFIELD(sfHook, OBJECT, 14) +UNTYPED_SFIELD(sfPermission, OBJECT, 15) // inner object (uncommon) UNTYPED_SFIELD(sfSigner, OBJECT, 16) @@ -377,3 +382,4 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) +UNTYPED_SFIELD(sfPermissions, ARRAY, 29) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dd3ac42325d..049e3d88db9 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -465,6 +465,12 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, ({ {sfDomainID, soeREQUIRED}, })) +/** This transaction type delegates authorized account specified permissions */ +TRANSACTION(ttACCOUNT_PERMISSION_SET, 64, AccountPermissionSet, ({ + {sfAuthorize, soeREQUIRED}, + {sfPermissions, soeREQUIRED}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 483b69a962f..d3f41ecdd98 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -55,10 +55,13 @@ JSS(AssetClass); // in: Oracle JSS(AssetPrice); // in: Oracle JSS(AuthAccount); // in: AMM Auction Slot JSS(AuthAccounts); // in: AMM Auction Slot +JSS(Authorize); // in: account_permission JSS(BaseAsset); // in: Oracle JSS(BidMax); // in: AMM Bid JSS(BidMin); // in: AMM Bid JSS(ClearFlag); // field. +JSS(DelegateTicketSequence); // in/out: OnBehalfOf; field. +JSS(DelegateSequence); // in/out: OnBehalfOf; field. JSS(DeliverMax); // out: alias to Amount JSS(DeliverMin); // in: TransactionSign JSS(Destination); // in: TransactionSign; field. @@ -76,9 +79,13 @@ JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens JSS(LPToken); // out: AMM Liquidity Provider tokens info JSS(OfferSequence); // field. +JSS(OnBehalfOf); // in: AccountPermission JSS(OracleDocumentID); // field JSS(Owner); // field JSS(Paths); // in/out: TransactionSign +JSS(Permission); // in: AccountPermission +JSS(Permissions); // in: AccountPermission +JSS(PermissionValue); // in: AccountPermission JSS(PriceDataSeries); // field. JSS(PriceData); // field. JSS(Provider); // field. @@ -145,6 +152,7 @@ JSS(attestations); JSS(attestation_reward_account); JSS(auction_slot); // out: amm_info JSS(authorized); // out: AccountLines +JSS(authorize); // out: account_permission JSS(authorized_credentials); // in: ledger_entry DepositPreauth JSS(auth_accounts); // out: amm_info JSS(auth_change); // out: AccountInfo diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 93a38d062ab..6384f4de8db 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -33,7 +33,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.4.0-b3" +char const* const versionString = "2.4.0-rc1" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 046be444224..df4717d149c 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -79,6 +79,7 @@ enum class LedgerNameSpace : std::uint16_t { MPTOKEN = 't', CREDENTIAL = 'D', PERMISSIONED_DOMAIN = 'm', + ACCOUNT_PERMISSION = 'E', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -437,6 +438,23 @@ amm(uint256 const& id) noexcept return {ltAMM, id}; } +Keylet +accountPermission( + AccountID const& account, + AccountID const& authorizedAccount) noexcept +{ + return { + ltACCOUNT_PERMISSION, + indexHash( + LedgerNameSpace::ACCOUNT_PERMISSION, account, authorizedAccount)}; +} + +Keylet +accountPermission(uint256 const& key) noexcept +{ + return {ltACCOUNT_PERMISSION, key}; +} + Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType) { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 87abcc23516..ecfca9743dd 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -154,6 +154,10 @@ InnerObjectFormats::InnerObjectFormats() {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, }); + + add(sfPermission.jsonName.c_str(), + sfPermission.getCode(), + {{sfPermissionValue, soeREQUIRED}}); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp new file mode 100644 index 00000000000..ef91420e5dc --- /dev/null +++ b/src/libxrpl/protocol/Permissions.cpp @@ -0,0 +1,96 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 + +namespace ripple { + +Permission::Permission() +{ + granularPermissionMap = { + {"TrustlineAuthorize", TrustlineAuthorize}, + {"TrustlineFreeze", TrustlineFreeze}, + {"TrustlineUnfreeze", TrustlineUnfreeze}, + {"AccountDomainSet", AccountDomainSet}, + {"AccountEmailHashSet", AccountEmailHashSet}, + {"AccountMessageKeySet", AccountMessageKeySet}, + {"AccountTransferRateSet", AccountTransferRateSet}, + {"AccountTickSizeSet", AccountTickSizeSet}, + {"PaymentMint", PaymentMint}, + {"PaymentBurn", PaymentBurn}, + {"MPTokenIssuanceLock", MPTokenIssuanceLock}, + {"MPTokenIssuanceUnlock", MPTokenIssuanceUnlock}}; + + granularTxTypeMap = { + {TrustlineAuthorize, ttTRUST_SET}, + {TrustlineFreeze, ttTRUST_SET}, + {TrustlineUnfreeze, ttTRUST_SET}, + {AccountDomainSet, ttACCOUNT_SET}, + {AccountEmailHashSet, ttACCOUNT_SET}, + {AccountMessageKeySet, ttACCOUNT_SET}, + {AccountTransferRateSet, ttACCOUNT_SET}, + {AccountTickSizeSet, ttACCOUNT_SET}, + {PaymentMint, ttPAYMENT}, + {PaymentBurn, ttPAYMENT}, + {MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET}, + {MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET}}; +} + +Permission const& +Permission::getInstance() +{ + static Permission const instance; + return instance; +} + +std::optional +Permission::getGranularValue(std::string const& name) const +{ + auto const it = granularPermissionMap.find(name); + if (it != granularPermissionMap.end()) + return static_cast(it->second); + + return std::nullopt; +} + +std::optional +Permission::getGranularTxType(GranularPermissionType const& gpType) const +{ + auto const it = granularTxTypeMap.find(gpType); + if (it != granularTxTypeMap.end()) + return it->second; + + return std::nullopt; +} + +bool +Permission::isProhibited(std::string const& name) const +{ + // We do not allow delegating the following transaction permissions to other + // accounts for security reason. + if (name == "AccountSet" || name == "SetRegularKey" || + name == "SignerListSet" || name == "AccountPermissionSet" || + name == "AccountDelete") + return true; + + return false; +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 7d08993a8ba..a471753e475 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -360,10 +361,44 @@ parseLeaf( { if (value.isString()) { - ret = detail::make_stvar( - field, - beast::lexicalCastThrow( - value.asString())); + if (field == sfPermissionValue) + { + std::string const strValue = value.asString(); + auto const granularPermission = + Permission::getInstance().getGranularValue( + strValue); + if (!granularPermission) + { + // if it's not granular permission, parse as + // transaction type permission. + if (Permission::getInstance().isProhibited( + strValue)) + { + // we do not allow delegating some transaction + // type permissions to other accounts for + // security reason. + error = invalid_data(json_name, fieldName); + return ret; + } + else + ret = detail::make_stvar( + field, + static_cast( + TxFormats::getInstance().findTypeByName( + strValue) + + 1)); + } + else + ret = detail::make_stvar( + field, *granularPermission); + } + else + { + ret = detail::make_stvar( + field, + beast::lexicalCastThrow( + value.asString())); + } } else if (value.isInt()) { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index bd1c461c8c7..081431db82a 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -197,6 +197,23 @@ STTx::getSeqProxy() const return SeqProxy{SeqProxy::ticket, *ticketSeq}; } +SeqProxy +STTx::getDelegateSeqProxy() const +{ + std::uint32_t const seq{getFieldU32(sfDelegateSequence)}; + if (seq != 0) + return SeqProxy::sequence(seq); + + std::optional const ticketSeq{operator[]( + ~sfDelegateTicketSequence)}; + if (!ticketSeq) + // No DelegateTicketSequence specified. Return the delegateSequence, + // whatever it is. + return SeqProxy::sequence(seq); + + return SeqProxy{SeqProxy::ticket, *ticketSeq}; +} + void STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) { @@ -438,6 +455,13 @@ STTx::checkMultiSign( return {}; } +AccountID +STTx::getEffectiveAccountID() const +{ + return isFieldPresent(sfOnBehalfOf) ? getAccountID(sfOnBehalfOf) + : getAccountID(sfAccount); +} + //------------------------------------------------------------------------------ static bool diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 76b1ae8ad4f..da2c4fdf0ae 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -45,6 +45,9 @@ TxFormats::TxFormats() {sfTxnSignature, soeOPTIONAL}, {sfSigners, soeOPTIONAL}, // submit_multisigned {sfNetworkID, soeOPTIONAL}, + {sfOnBehalfOf, soeOPTIONAL}, + {sfDelegateSequence, soeOPTIONAL}, + {sfDelegateTicketSequence, soeOPTIONAL}, }; #pragma push_macro("UNWRAP") diff --git a/src/test/app/AccountPermission_test.cpp b/src/test/app/AccountPermission_test.cpp new file mode 100644 index 00000000000..ea5d9015b7b --- /dev/null +++ b/src/test/app/AccountPermission_test.cpp @@ -0,0 +1,4577 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +class AccountPermission_test : public beast::unit_test::suite +{ + void + testFeatureDisabled(FeatureBitset features) + { + testcase("test featureAccountPermission is not enabled"); + using namespace jtx; + + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000), gw, alice, bob); + env.close(); + + // can not set account permission when feature disabled + env(account_permission::accountPermissionSet(gw, alice, {"Payment"}), + ter(temDISABLED)); + + // can not send transaction on behalf of other account when feature + // disabled + env(pay(bob, alice, XRP(50)), onBehalfOf(gw), ter(temDISABLED)); + } + + void + testInvalidRequest(FeatureBitset features) + { + testcase("test invalid request"); + using namespace jtx; + + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(100000), gw, alice); + env.close(); + + // when permissions size exceeds the limit 10, should return + // temARRAY_TOO_LARGE. + { + env(account_permission::accountPermissionSet( + gw, + alice, + {"Payment", + "EscrowCreate", + "EscrowFinish", + "EscrowCancel", + "CheckCreate", + "CheckCash", + "CheckCancel", + "DepositPreauth", + "TrustSet", + "NFTokenMint", + "NFTokenBurn"}), + ter(temARRAY_TOO_LARGE)); + } + + // alice can not authorize herself + { + env(account_permission::accountPermissionSet( + alice, alice, {"Payment"}), + ter(temMALFORMED)); + } + + // when provided permissions contains some permission which does not + // exists. + { + try + { + env(account_permission::accountPermissionSet( + gw, alice, {"Payment1"})); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + e.what() == + std::string("invalidParamsError at " + "'tx_json.Permissions.[0].Permission'. Field " + "'tx_json.Permissions.[0].Permission." + "PermissionValue' has invalid data.")); + } + } + + // when provided permissions contains duplicate values, should return + // temMALFORMED. + { + env(account_permission::accountPermissionSet( + gw, + alice, + {"Payment", + "EscrowCreate", + "EscrowFinish", + "TrustlineAuthorize", + "CheckCreate", + "TrustlineAuthorize"}), + ter(temMALFORMED)); + } + + // when authorizing account which does not exist, should return + // terNO_ACCOUNT. + { + env(account_permission::accountPermissionSet( + gw, Account("unknown"), {"Payment"}), + ter(terNO_ACCOUNT)); + } + + // for security reasons, AccountSet, SetRegularKey, SignerListSet, + // AccountPermissionSet are prohibited to be delegated to other accounts + { + auto testProhibitedTrans = [&](std::string const& permission) { + try + { + env(account_permission::accountPermissionSet( + gw, alice, {permission})); + } + catch (std::exception const& e) + { + BEAST_EXPECT( + e.what() == + std::string( + "invalidParamsError at " + "'tx_json.Permissions.[0].Permission'. Field " + "'tx_json.Permissions.[0].Permission." + "PermissionValue' has invalid data.")); + } + }; + + testProhibitedTrans("SetRegularKey"); + testProhibitedTrans("AccountSet"); + testProhibitedTrans("SignerListSet"); + testProhibitedTrans("AccountPermissionSet"); + testProhibitedTrans("AccountDelete"); + } + } + + void + testReserve(FeatureBitset features) + { + testcase("test reserve"); + using namespace jtx; + + // test reserve for AccountPermissionSet + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + + env.fund(drops(env.current()->fees().accountReserve(0)), alice); + env.fund( + drops(env.current()->fees().accountReserve(1)), bob, carol); + env.close(); + + // alice does not have enough reserve to create account permission + env(account_permission::accountPermissionSet( + alice, bob, {"Payment"}), + ter(tecINSUFFICIENT_RESERVE)); + + // bob has enough reserve + env(account_permission::accountPermissionSet( + bob, alice, {"Payment"})); + env.close(); + + // now bob create another account permission, he does not have + // enough reserve + env(account_permission::accountPermissionSet( + bob, carol, {"Payment"}), + ter(tecINSUFFICIENT_RESERVE)); + } + + // test reserve when sending transaction on behalf of other account + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + + env.fund(drops(env.current()->fees().accountReserve(1)), alice); + env.fund(drops(env.current()->fees().accountReserve(2)), bob); + env.close(); + + // alice gives bob permission + env(account_permission::accountPermissionSet( + alice, bob, {"DIDSet", "DIDDelete"})); + + // bob set DID on behalf of alice, but alice does not have enough + // reserve + env(did::set(bob), + did::uri("uri"), + onBehalfOf(alice), + ter(tecINSUFFICIENT_RESERVE)); + + // bob can set DID for himself because he has enough reserve + env(did::set(bob), did::uri("uri")); + env.close(); + } + } + + void + testAccountDelete(FeatureBitset features) + { + testcase("test delete account"); + using namespace jtx; + + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + env(account_permission::accountPermissionSet(alice, bob, {"Payment"})); + env.close(); + BEAST_EXPECT(env.closed()->exists( + keylet::accountPermission(alice.id(), bob.id()))); + + for (std::uint32_t i = 0; i < 256; ++i) + env.close(); + + auto const aliceBalance = env.balance(alice); + auto const bobBalance = env.balance(bob); + + // alice deletes account + auto const deleteFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), fee(deleteFee)); + env.close(); + + BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id()))); + BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(alice.id()))); + BEAST_EXPECT(env.balance(bob) == bobBalance + aliceBalance - deleteFee); + + BEAST_EXPECT(!env.closed()->exists( + keylet::accountPermission(alice.id(), bob.id()))); + } + + void + testAccountPermissionSet(FeatureBitset features) + { + testcase("test valid request creating, updating, deleting permissions"); + using namespace jtx; + + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(100000), gw, alice); + env.close(); + + auto const permissions = std::list{ + "Payment", + "EscrowCreate", + "EscrowFinish", + "TrustlineAuthorize", + "CheckCreate"}; + env(account_permission::accountPermissionSet(gw, alice, permissions)); + env.close(); + + // this lambda function is used to error message when the user tries to + // get ledger entry with invalid parameters. + auto testInvalidParams = + [&](std::optional const& account, + std::optional const& authorize) -> std::string { + Json::Value jvParams; + std::string error; + jvParams[jss::ledger_index] = jss::validated; + if (account) + jvParams[jss::account_permission][jss::account] = *account; + if (authorize) + jvParams[jss::account_permission][jss::authorize] = *authorize; + auto const& response = + env.rpc("json", "ledger_entry", to_string(jvParams)); + if (response[jss::result].isMember(jss::error)) + error = response[jss::result][jss::error].asString(); + return error; + }; + + // get ledger entry with invalid parameters should return error. + BEAST_EXPECT( + testInvalidParams(std::nullopt, alice.human()) == + "malformedRequest"); + BEAST_EXPECT( + testInvalidParams(gw.human(), std::nullopt) == "malformedRequest"); + BEAST_EXPECT( + testInvalidParams("-", alice.human()) == "malformedAccount"); + BEAST_EXPECT( + testInvalidParams(gw.human(), "-") == "malformedAuthorize"); + + // this lambda function is used to compare the json value of ledger + // entry response with the given list of permission strings. + auto comparePermissions = [&](Json::Value const& jle, + std::list const& permissions, + Account const& account, + Account const& authorize) { + BEAST_EXPECT( + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node)); + BEAST_EXPECT( + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::AccountPermission); + BEAST_EXPECT( + jle[jss::result][jss::node][jss::Account] == account.human()); + BEAST_EXPECT( + jle[jss::result][jss::node][jss::Authorize] == + authorize.human()); + + auto const& jPermissions = + jle[jss::result][jss::node][jss::Permissions]; + unsigned i = 0; + for (auto const& permission : permissions) + { + auto const granularVal = + Permission::getInstance().getGranularValue(permission); + if (granularVal) + BEAST_EXPECT( + jPermissions[i][jss::Permission] + [jss::PermissionValue] == *granularVal); + else + { + auto const transVal = + TxFormats::getInstance().findTypeByName(permission); + BEAST_EXPECT( + jPermissions[i][jss::Permission] + [jss::PermissionValue] == transVal + 1); + } + i++; + } + }; + + // get ledger entry with valid parameter + comparePermissions( + account_permission::ledgerEntry(env, gw, alice), + permissions, + gw, + alice); + + // gw update permission + auto const newPermissions = std::list{ + "Payment", "AMMCreate", "AMMDeposit", "AMMWithdraw"}; + env(account_permission::accountPermissionSet( + gw, alice, newPermissions)); + env.close(); + + // get ledger entry again, permissions should be updated to + // newPermissions + comparePermissions( + account_permission::ledgerEntry(env, gw, alice), + newPermissions, + gw, + alice); + + // gw delete all permissions delegated to alice, this will delete the + // ledger entry + env(account_permission::accountPermissionSet(gw, alice, {})); + env.close(); + auto const jle = account_permission::ledgerEntry(env, gw, alice); + BEAST_EXPECT(jle[jss::result][jss::error] == "entryNotFound"); + + // alice can delegate permissions to gw as well + env(account_permission::accountPermissionSet(alice, gw, permissions)); + env.close(); + comparePermissions( + account_permission::ledgerEntry(env, alice, gw), + permissions, + alice, + gw); + auto const response = account_permission::ledgerEntry(env, gw, alice); + // alice is not delegated any permissions by gw, should return + // entryNotFound + BEAST_EXPECT(response[jss::result][jss::error] == "entryNotFound"); + } + + void + testDelegateSequenceAndTicket(FeatureBitset features) + { + testcase("test delegating sequence and ticket"); + using namespace jtx; + + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"CheckCreate"})); + env.close(); + + // add initial sequences and add sequence distance between alice and bob + for (int i = 0; i < 20; i++) + { + env(check::create(alice, carol, XRP(1))); + } + env(check::create(bob, carol, XRP(1))); + env.close(); + auto aliceSequence = env.seq(alice); + auto bobSequence = env.seq(bob); + + // non existing delegating account + Account bad{"bad"}; + env(check::create(bob, carol, XRP(1)), + onBehalfOf(bad), + delegateSequence(1), + ter(terNO_ACCOUNT)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // missing delegating sequence + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(none), + ter(temBAD_SEQUENCE)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // delegating sequence smaller than current + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(1), + ter(tefPAST_SEQ)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // delegating sequence larger than current + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(100), + ter(terPRE_SEQ)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // delegating sequence is consumed after transaction success + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(aliceSequence), + ter(tesSUCCESS)); + env.close(); + aliceSequence++; + bobSequence++; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // delegating sequence is consumed if transaction calls + // Transactor::reset(XRPAmount) and return some special tec codes + env(check::create(bob, carol, XRP(1)), + check::expiration(env.now()), + onBehalfOf(alice), + delegateSequence(autofill), + ter(tecEXPIRED)); + env.close(); + aliceSequence++; + bobSequence++; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // use both delegating sequence and delegating ticket + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(autofill), + delegateTicketSequence(aliceSequence), + ter(temSEQ_AND_TICKET)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // set delegating sequence to 0 without delegating tickcet + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(0), + ter(tefPAST_SEQ)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // use current or future sequence as delegating ticket + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceSequence), + ter(terPRE_TICKET)); + env.close(); + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceSequence + 1), + ter(terPRE_TICKET)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + // proceed one sequence so terPRE_TICKET won't be retried + env(check::create(alice, carol, XRP(1))); + aliceSequence += 1; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + + // degelating ticket is consumed after transaction success + env(ticket::create(alice, 1)); + env.close(); + auto aliceTicket = aliceSequence + 1; + aliceSequence += 2; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceTicket), + ter(tesSUCCESS)); + env.close(); + bobSequence++; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // delegating ticket is consumed if transaction calls + // Transactor::reset(XRPAmount) and return some special tec codes + env(ticket::create(alice, 1)); + env.close(); + aliceTicket = aliceSequence + 1; + aliceSequence += 2; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + env(check::create(bob, carol, XRP(1)), + check::expiration(env.now()), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceTicket), + ter(tecEXPIRED)); + env.close(); + bobSequence++; + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + + // use an already consumed delegating ticket + env(check::create(bob, carol, XRP(1)), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceTicket), + ter(tefNO_TICKET)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSequence); + BEAST_EXPECT(env.seq(bob) == bobSequence); + } + + void + testAMM(FeatureBitset features) + { + testcase( + "test AMMCreate, AMMDeposit, AMMWithdraw, AMMClawback, AMMVote, " + "AMMDelete and AMMBid"); + using namespace jtx; + + // test AMMCreate, AMMDeposit, AMMWithdraw, AMMClawback + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000000), gw, alice, bob); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + auto const USD = gw["USD"]; + env.trust(USD(10000), alice); + env(pay(gw, alice, USD(3000))); + env.trust(USD(10000), bob); + env(pay(gw, bob, USD(3000))); + env.close(); + + // alice delegates AMMCreate, AMMDeposit, AMMWithdraw to bob + env(account_permission::accountPermissionSet( + alice, bob, {"AMMCreate", "AMMDeposit", "AMMWithdraw"})); + env.close(); + + auto aliceXrpBalance = env.balance(alice, XRP); + auto bobXrpBalance = env.balance(bob, XRP); + + AMM amm(env, bob, USD(1000), XRP(2000), alice, ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(1000), XRP(2000), IOUAmount{1414213562373095, -9})); + + // bob sends the AMMCreate on behalf of alice, so alice holds all + // the lptokens, bob holds 0. + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{1414213562373095, -9})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + + // alice initially has 3000USD, 1000USD is deducted to create the + // AMM pool, 2000USD left + env.require(balance(alice, USD(2000))); + env.require(balance(bob, USD(3000))); + + // alice spent 2000XRP to create the AMM + env.require(balance(alice, aliceXrpBalance - XRP(2000))); + // bob sent the transaction, bob pays the fee + env.require(balance(bob, bobXrpBalance - XRP(50))); + + // update alice and bob balance variables + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // bob deposit 1000USD/2000XRP on behalf of alice + amm.deposit( + bob, + USD(1000), + XRP(2000), + std::nullopt, + std::nullopt, + ter(tesSUCCESS), + alice); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(2000), XRP(4000), IOUAmount{2828427124746190, -9})); + + // alice holds all the lptokens, and bob has 0 in the pool + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{2828427124746190, -9})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + + // alice spent another 1000USD and 2000XRP to deposit + env.require(balance(alice, USD(1000))); + env.require(balance(bob, USD(3000))); + env.require(balance(alice, aliceXrpBalance - XRP(2000))); + // bob sent the transaction, bob pays another 10 drop XRP fee + env.require(balance(bob, bobXrpBalance - drops(10))); + + // update alice and bob balance variables + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // bob can deposit for himself + amm.deposit(bob, USD(1000), XRP(2000)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(3000), XRP(6000), IOUAmount{4242640687119285, -9})); + ; + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{2828427124746190, -9})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); + + env.require(balance(alice, USD(1000))); + env.require(balance(bob, USD(2000))); + + // alice's XRP balance keeps the same + + env.require(balance(alice, aliceXrpBalance)); + // bob spent 2000XRP to deposit and also pays 10 drops fee + env.require(balance(bob, bobXrpBalance - XRP(2000) - drops(10))); + + // update alice and bob balance variables + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // bob withdraw 1000USD/2000XRP on behalf of alice + amm.withdraw( + bob, + USD(1000), + XRP(2000), + std::nullopt, + ter(tesSUCCESS), + alice); + env.close(); + + // the 1000USD/2000XRP is withdrawn from alice, so alice's + // lptoken is deducted by half, bob's lptoken balance remains the + // same. + BEAST_EXPECT(amm.expectBalances( + USD(2000), XRP(4000), IOUAmount{2828427124746190, -9})); + ; + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{1414213562373095, -9})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); + + // alice gets 1000 USD back so she has 2000 USD now + env.require(balance(alice, USD(2000))); + env.require(balance(bob, USD(2000))); + + // alice gets 2000 XRP back + env.require(balance(alice, aliceXrpBalance + XRP(2000))); + // bob pays 10 drops fee + env.require(balance(bob, bobXrpBalance - drops(10))); + + // update alice and bob balance variables + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // bob can withdraw 1000USD/2000XRP for himself + amm.withdraw(bob, USD(1000), XRP(2000)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(1000), XRP(2000), IOUAmount{1414213562373095, -9})); + ; + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{1414213562373095, -9})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + env.require(balance(alice, USD(2000))); + env.require(balance(bob, USD(3000))); + env.require(balance(alice, aliceXrpBalance)); + // bob gets 2000XRP back and pays 10 drops fee + env.require(balance(bob, bobXrpBalance + XRP(2000) - drops(10))); + + // alice can not AMMClawback from herself on behalf of gw + env(amm::ammClawback(alice, alice, USD, XRP, USD(1000), gw), + ter(tecNO_PERMISSION)); + env.close(); + + // gw give permission to alice for AMMClawback transaction + env(account_permission::accountPermissionSet( + gw, alice, {"AMMClawback"})); + env.close(); + + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // now alice can AMMClawback from herself onbehalf of gw + env(amm::ammClawback(alice, alice, USD, XRP, USD(500), gw)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(500), XRP(1000), IOUAmount{7071067811865475, -10})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -10})); + env.require(balance(alice, USD(2000))); + // alice gets 1000 XRP back and pays 10 drops fee as the sender + env.require( + balance(alice, aliceXrpBalance + XRP(1000) - drops(10))); + + // bob deposit for himself + amm.deposit(bob, USD(1000), XRP(2000)); + env.close(); + + // there's some rounding happening + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1499999999999999), -12}, + XRP(3000), + IOUAmount{2121320343559642, -9})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -10})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1414213562373094, -9})); + env.require(balance(alice, USD(2000))); + env.require( + balance(bob, STAmount{USD, UINT64_C(2000000000000001), -12})); + env.require(balance(bob, bobXrpBalance - XRP(2000) - drops(10))); + + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // alice AMMClawback all bob's USD on behalf of gw + env(amm::ammClawback(alice, bob, USD, XRP, std::nullopt, gw)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(5000000000000001), -13}, + XRP(1000), + IOUAmount{7071067811865480, -10})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -10})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + env.require(balance(alice, USD(2000))); + env.require( + balance(bob, STAmount{USD, UINT64_C(2000000000000001), -12})); + env.require(balance(alice, aliceXrpBalance - drops(10))); + env.require(balance(bob, bobXrpBalance + XRP(2000))); + } + + // test AMMVote + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000000), gw, alice, bob); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(10000), alice); + env(pay(gw, alice, USD(3000))); + env.trust(USD(10000), bob); + env(pay(gw, bob, USD(3000))); + env.close(); + + // alice delegates AMMVote to bob + env(account_permission::accountPermissionSet( + alice, bob, {"AMMVote"})); + env.close(); + + AMM amm(env, alice, USD(1000), XRP(2000), ter(tesSUCCESS)); + env.close(); + + auto aliceXrpBalance = env.balance(alice, XRP); + auto bobXrpBalance = env.balance(bob, XRP); + + BEAST_EXPECT(amm.expectTradingFee(0)); + amm.vote(alice, 100); + env.close(); + BEAST_EXPECT(amm.expectTradingFee(100)); + // alice is the sender who pays the fee + env.require(balance(alice, aliceXrpBalance - drops(10))); + env.require(balance(bob, bobXrpBalance)); + + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // bob vote onbehalf of alice + amm.vote( + bob, + 500, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tesSUCCESS), + alice); + env.close(); + BEAST_EXPECT(amm.expectTradingFee(500)); + // bob is the sender who pays the fee + env.require(balance(alice, aliceXrpBalance)); + env.require(balance(bob, bobXrpBalance - drops(10))); + + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + + // bob vote again onbehalf of alice + amm.vote( + bob, + 1000, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tesSUCCESS), + alice); + env.close(); + BEAST_EXPECT(amm.expectTradingFee(1000)); + env.require(balance(alice, aliceXrpBalance)); + env.require(balance(bob, bobXrpBalance - drops(10))); + } + + // test AMMDelete + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(1000000000), gw, alice); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(10000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + + // gw delegates AMMDelete to alice + env(account_permission::accountPermissionSet( + gw, alice, {"AMMDelete"})); + env.close(); + + AMM amm(env, gw, USD(1000), XRP(2000), ter(tesSUCCESS)); + env.close(); + // create a lot of trust lines with the lptoken issuer + for (auto i = 0; i < maxDeletableAMMTrustLines * 2 + 10; ++i) + { + Account const a{std::to_string(i)}; + env.fund(XRP(1'000), a); + env(trust(a, STAmount{amm.lptIssue(), 10'000})); + env.close(); + } + + // there are lots of trustlines so the amm still exists + amm.withdrawAll(gw); + BEAST_EXPECT(amm.ammExists()); + + auto gwXrpBalance = env.balance(gw, XRP); + auto aliceXrpBalance = env.balance(alice, XRP); + + // gw delete amm, but at most 512 trustlines are deleted at once, so + // it's incomplete + amm.ammDelete(gw, ter(tecINCOMPLETE)); + BEAST_EXPECT(amm.ammExists()); + // alice is the sender who pays the fee + env.require(balance(gw, gwXrpBalance - drops(10))); + env.require(balance(alice, aliceXrpBalance)); + + gwXrpBalance = env.balance(gw, XRP); + aliceXrpBalance = env.balance(alice, XRP); + + // alice delete amm onbehalf of gw + amm.ammDelete(alice, ter(tesSUCCESS), gw); + BEAST_EXPECT(!amm.ammExists()); + BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount()))); + env.require(balance(gw, gwXrpBalance)); + // alice is the sender who pays the fee + env.require(balance(alice, aliceXrpBalance - drops(10))); + + // Try redundant delete + amm.ammDelete(alice, ter(terNO_AMM)); + } + + // test AMMBid + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1000000000), gw, alice, bob, carol); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(10000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + + // alice delegates AMMBid to bob + env(account_permission::accountPermissionSet( + alice, bob, {"AMMBid"})); + env.close(); + + AMM amm(env, gw, USD(1000), XRP(2000), ter(tesSUCCESS)); + env.close(); + + auto aliceXrpBalance = env.balance(alice, XRP); + auto bobXrpBalance = env.balance(bob, XRP); + + env(amm.bid( + {.account = gw, .bidMin = 110, .authAccounts = {alice}})); + BEAST_EXPECT(amm.expectAuctionSlot(0, 0, IOUAmount{110})); + BEAST_EXPECT(amm.expectAuctionSlot({alice})); + + amm.deposit(alice, 1'000'000); + + // because bob is not lp, can not bid + env(amm.bid({.account = bob, .authAccounts = {bob}}), + ter(tecAMM_INVALID_TOKENS)); + + // but bob can bid onbehalf of alice who is the lp + env(amm.bid( + {.account = bob, + .authAccounts = {alice, bob, carol}, + .onBehalfOf = alice})); + env.close(); + BEAST_EXPECT(amm.expectAuctionSlot(0, 0, IOUAmount(1155, -1))); + BEAST_EXPECT(amm.expectAuctionSlot({alice, bob, carol})); + } + } + + void + testCheck(FeatureBitset features) + { + testcase("test CheckCreate, CheckCash and CheckCancel"); + using namespace jtx; + + // test create and cash check of XRP on behalf of another account + { + Env env(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + STAmount const startBalance{XRP(1000000).value()}; + + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(startBalance, alice, bob, carol); + env.close(); + + // bob can not write a check to himself + env(check::create(bob, bob, XRP(10)), ter(temREDUNDANT)); + env.close(); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); + + // alice delegates CheckCreate to bob + env(account_permission::accountPermissionSet( + alice, bob, {"CheckCreate"})); + env.close(); + + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance)); + + // now bob send a check on behalf of alice to alice, + // this should fail as well + env(check::create(bob, alice, XRP(10)), + onBehalfOf(alice), + ter(temREDUNDANT)); + env.close(); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance)); + env.require(balance(carol, startBalance)); + + // now bob send a check on behalf of alice to bob himself, + // this should succeed because it's alice->bob + uint256 const aliceToBob = keylet::check(alice, env.seq(alice)).key; + env(check::create(bob, bob, XRP(10)), onBehalfOf(alice)); + env.close(); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); + // alice owns the account permission and check + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + env.require(balance(carol, startBalance)); + + // bob send a check on behalf of alice to carol, the check is + // actually alice->carol + uint256 const aliceToCarol = + keylet::check(alice, env.seq(alice)).key; + env(check::create(bob, carol, XRP(100)), onBehalfOf(alice)); + env.close(); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 1); + // alice owns the account permission and 2 checks + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 0); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance - drops(baseFee * 2))); + env.require(balance(carol, startBalance)); + + // bob cash the check + env(check::cash(bob, aliceToBob, XRP(10))); + env.close(); + env.require( + balance(alice, startBalance - XRP(10) - drops(baseFee))); + env.require( + balance(bob, startBalance + XRP(10) - drops(baseFee * 3))); + env.require(balance(carol, startBalance)); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 1); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 0); + + env(check::cash(bob, aliceToCarol, XRP(10)), ter(tecNO_PERMISSION)); + env.require( + balance(bob, startBalance + XRP(10) - drops(baseFee * 4))); + + // carol delegates CheckCash to bob + env(account_permission::accountPermissionSet( + carol, bob, {"CheckCash"})); + env.close(); + env.require( + balance(bob, startBalance + XRP(10) - drops(baseFee * 4))); + env.require(balance(carol, startBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, carol) == 1); + + // bob cash the check on behalf of carol + env(check::cash(bob, aliceToCarol, XRP(100), carol)); + env.close(); + + env.require( + balance(alice, startBalance - XRP(110) - drops(baseFee))); + env.require( + balance(bob, startBalance + XRP(10) - drops(baseFee * 5))); + env.require( + balance(carol, startBalance + XRP(100) - drops(baseFee))); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 0); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 1); + } + + // test create/cash/cancel check of USD on behalf of another account + { + Env env(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + STAmount const startBalance{XRP(1000000).value()}; + + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(startBalance, gw, alice, bob, carol); + env.close(); + + auto const USD = gw["USD"]; + + // alice give CheckCreate permission to bob + env(account_permission::accountPermissionSet( + alice, bob, {"CheckCreate"})); + env.close(); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance)); + + // bob writes 10USD check on behalf of alice when alice does not + // have USD + uint256 const aliceToCarol = + keylet::check(alice, env.seq(alice)).key; + env(check::create(bob, carol, USD(10)), onBehalfOf(alice)); + env.close(); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 1); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 0); + + // carol give CheckCash permission to bob + env(account_permission::accountPermissionSet( + carol, bob, {"CheckCash"})); + env.close(); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + env.require(balance(carol, startBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 1); + + // bob cash the check on behalf of carol should fail bacause alice + // does not have USD + env(check::cash(bob, aliceToCarol, USD(10), carol), + ter(tecPATH_PARTIAL)); + env.close(); + env.require(balance(alice, startBalance - drops(baseFee))); + env.require(balance(bob, startBalance - drops(2 * baseFee))); + env.require(balance(carol, startBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 1); + + // alice does not have enough USD + env(trust(alice, USD(100))); + env(pay(gw, alice, USD(9.5))); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env(check::cash(bob, aliceToCarol, USD(10), carol), + ter(tecPATH_PARTIAL)); + env.close(); + env.require(balance(bob, startBalance - drops(3 * baseFee))); + env.require(balance(alice, USD(9.5))); + BEAST_EXPECT(ownerCount(env, alice) == 3); + + // now alice have enough USD + env(pay(gw, alice, USD(0.5))); + env.close(); + + // bob cash 9.9 USD on behalf of carol + env(check::cash(bob, aliceToCarol, USD(9.9), carol)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance - drops(4 * baseFee))); + env.require(balance(carol, startBalance - drops(baseFee))); + env.require(balance(alice, USD(0.1))); + env.require(balance(carol, USD(9.9))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + // cashing the check automatically creats a trustline for carol + BEAST_EXPECT(ownerCount(env, carol) == 2); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 0); + + // bob trying to cash the same check on behalf of carol should fail + env(check::cash(bob, aliceToCarol, USD(10), carol), + ter(tecNO_ENTRY)); + env.require(balance(bob, startBalance - drops(5 * baseFee))); + + // carol does not have permission yet. + env(check::create(carol, alice, USD(10)), + onBehalfOf(bob), + ter(tecNO_PERMISSION)); + // fail again + env(check::create(carol, alice, USD(10)), + onBehalfOf(bob), + ter(tecNO_PERMISSION)); + env.require(balance(carol, startBalance - drops(3 * baseFee))); + + // bob allows carol to send CheckCreate on behalf of himself + env(account_permission::accountPermissionSet( + bob, carol, {"CheckCreate"})); + env.close(); + env.require(balance(bob, startBalance - drops(6 * baseFee))); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(ownerCount(env, carol) == 2); + + // carol writes two checks on behalf of bob to alice + uint256 const checkId1 = keylet::check(bob, env.seq(bob)).key; + env(check::create(carol, alice, USD(20)), onBehalfOf(bob)); + uint256 const checkId2 = keylet::check(bob, env.seq(bob)).key; + env(check::create(carol, alice, USD(10)), onBehalfOf(bob)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance - drops(6 * baseFee))); + env.require(balance(carol, startBalance - drops(5 * baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(ownerCount(env, carol) == 2); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 0); + + // alice allows bob to cash check on behalf of herself + env(account_permission::accountPermissionSet( + alice, bob, {"CheckCash"})); + env.close(); + env.require(balance(alice, startBalance - drops(3 * baseFee))); + // alice already owns AccountPermission object for "alice + // delegating bob" + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // alice allows bob to cancel check on behalf of herself. + env(account_permission::accountPermissionSet( + alice, bob, {"CheckCash", "CheckCancel"})); + env.close(); + env.require(balance(alice, startBalance - drops(4 * baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + + env(trust(bob, USD(10))); + env(pay(gw, bob, USD(10))); + env.close(); + env.require(balance(bob, startBalance - drops(7 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 4); + + // bob cash check2 on behalf of alice + env(check::cash(bob, checkId2, USD(10), alice)); + env.close(); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, carol).size() == 0); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(ownerCount(env, carol) == 2); + env.require(balance(alice, startBalance - drops(4 * baseFee))); + env.require(balance(bob, startBalance - drops(8 * baseFee))); + env.require(balance(carol, startBalance - drops(5 * baseFee))); + env.require(balance(alice, USD(10.1))); + env.require(balance(bob, USD(0))); + + // bob cancel check1 on behalf of alice + env(check::cancel(bob, checkId1, alice)); + env.close(); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 2); + } + } + + void + testClawback(FeatureBitset features) + { + testcase("test Clawback"); + using namespace jtx; + + Env env(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + STAmount const startBalance{XRP(1000000).value()}; + + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(startBalance, gw, alice, bob); + env.close(); + + // set asfAllowTrustLineClawback + env(fset(gw, asfAllowTrustLineClawback)); + env(fset(alice, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + env.require(flags(alice, asfAllowTrustLineClawback)); + env.require(balance(gw, startBalance - drops(baseFee))); + env.require(balance(alice, startBalance - drops(baseFee))); + + // gw issues bob 1000USD + auto const USD = gw["USD"]; + env.trust(USD(10000), bob); + env(pay(gw, bob, USD(1000))); + env.close(); + env.require(balance(gw, startBalance - drops(2 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 1); + env.require(balance(bob, USD(1000))); + + // alice clawback from bob on behalf of gw should fail + // because she does not have permission. + env(claw(alice, bob["USD"](100)), + onBehalfOf(gw), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance)); + env.require(balance(gw, startBalance - drops(2 * baseFee))); + env.require(balance(bob, USD(1000))); + + // now gw give permission to alice + env(account_permission::accountPermissionSet(gw, alice, {"Clawback"})); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance)); + env.require(balance(gw, startBalance - drops(3 * baseFee))); + BEAST_EXPECT(ownerCount(env, gw) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + + // now alice can claw on behalf gw + env(claw(alice, bob["USD"](100)), onBehalfOf(gw)); + env.close(); + env.require(balance(alice, startBalance - drops(3 * baseFee))); + env.require(balance(bob, startBalance)); + env.require(balance(gw, startBalance - drops(3 * baseFee))); + BEAST_EXPECT(ownerCount(env, gw) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + env.require(balance(bob, USD(900))); + + // gw claw another 200USD from bob by himself + env(claw(gw, bob["USD"](200))); + env.close(); + env.require(balance(alice, startBalance - drops(3 * baseFee))); + env.require(balance(bob, startBalance)); + env.require(balance(gw, startBalance - drops(4 * baseFee))); + BEAST_EXPECT(ownerCount(env, gw) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + env.require(balance(bob, USD(700))); + + // update limit + env(trust(bob, USD(0), 0)); + env.close(); + env.require(balance(bob, startBalance - drops(baseFee))); + + // alice claw the remaining balance from bob on behalf gw + env(claw(alice, bob["USD"](700)), onBehalfOf(gw)); + env.close(); + env.require(balance(alice, startBalance - drops(4 * baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + env.require(balance(gw, startBalance - drops(4 * baseFee))); + BEAST_EXPECT(ownerCount(env, gw) == 1); + // the trustline got deleted + BEAST_EXPECT(ownerCount(env, bob) == 0); + } + + void + testCredentials(FeatureBitset features) + { + testcase("test crendentials"); + using namespace jtx; + Account const subject{"subject"}; + + { + Env env(*this, features); + Account alice{"alice"}; + Account issuer{"issuer"}; + Account subject{"subject"}; + env.fund(XRP(5'000), alice, issuer, subject); + env.close(); + + const char credType[] = "abcde"; + const char uri[] = "uri"; + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); + + // create credential on behalf of another account + { + // alice creating credential on behalf of issuer is not + // permitted + env(credentials::create(subject, alice, credType), + credentials::uri(uri), + onBehalfOf(issuer), + ter(tecNO_PERMISSION)); + + env(account_permission::accountPermissionSet( + issuer, alice, {"CredentialCreate"})); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // alice creates credential on behalf of issuer successfully + env(credentials::create(subject, alice, credType), + credentials::uri(uri), + onBehalfOf(issuer)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 2); + + auto const sleCred = env.le(credKey); + BEAST_EXPECT(sleCred); + BEAST_EXPECT(sleCred->getAccountID(sfSubject) == subject.id()); + BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); + BEAST_EXPECT(!sleCred->getFieldU32(sfFlags)); + BEAST_EXPECT( + credentials::checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(credentials::checkVL(sleCred, sfURI, uri)); + } + + // accept credential on behalf of another account + { + env(account_permission::accountPermissionSet( + subject, alice, {"CredentialAccept"})); + env.close(); + BEAST_EXPECT(ownerCount(env, subject) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // alice accept credential on behalf of subject + env(credentials::accept(alice, issuer, credType), + onBehalfOf(subject)); + env.close(); + // owner of credential now is subject, not issuer + BEAST_EXPECT(ownerCount(env, subject) == 2); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + auto const sleCred = env.le(credKey); + BEAST_EXPECT(sleCred); + BEAST_EXPECT(sleCred->getAccountID(sfSubject) == subject.id()); + BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); + BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted); + BEAST_EXPECT( + credentials::checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(credentials::checkVL(sleCred, sfURI, uri)); + } + + // delete credential on behalf of another account + { + env(account_permission::accountPermissionSet( + subject, alice, {"CredentialDelete"})); + env.close(); + BEAST_EXPECT(ownerCount(env, subject) == 2); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + + env(credentials::deleteCred(alice, subject, issuer, credType), + onBehalfOf(subject)); + env.close(); + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(ownerCount(env, subject) == 1); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + } + + // create and delete credential on behalf of issuer for the issuer + // himself + { + env(account_permission::accountPermissionSet( + issuer, alice, {"CredentialCreate", "CredentialDelete"})); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + + env(credentials::create(issuer, alice, credType), + credentials::uri(uri), + onBehalfOf(issuer)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 2); + + auto const credKey = + credentials::credentialKeylet(issuer, issuer, credType); + + auto sleCred = env.le(credKey); + BEAST_EXPECT(sleCred); + BEAST_EXPECT(sleCred->getAccountID(sfSubject) == issuer.id()); + BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); + BEAST_EXPECT( + credentials::checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(credentials::checkVL(sleCred, sfURI, uri)); + BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted); + + env(credentials::deleteCred(alice, issuer, issuer, credType), + onBehalfOf(issuer)); + env.close(); + BEAST_EXPECT(!env.le(credKey)); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + } + } + } + + void + testDepositPreauth(FeatureBitset features) + { + testcase("test DepositPreauth"); + using namespace jtx; + + { + Env env(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + STAmount const startBalance{XRP(1000000).value()}; + + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(startBalance, gw, alice, bob, carol); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(10000), alice); + env.trust(USD(10000), bob); + env.trust(USD(10000), carol); + env.close(); + + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env(pay(gw, carol, USD(1000))); + env.close(); + env.require(balance(alice, startBalance)); + env.require(balance(bob, startBalance)); + env.require(balance(carol, startBalance)); + env.require(balance(alice, USD(1000))); + env.require(balance(bob, USD(1000))); + env.require(balance(carol, USD(1000))); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(ownerCount(env, carol) == 1); + + // bob requiress authorization for deposits + env(fset(bob, asfDepositAuth)); + env.close(); + env.require(balance(bob, startBalance - drops(baseFee))); + + // alice and carol can not pay bob + env(pay(alice, bob, XRP(100)), ter(tecNO_PERMISSION)); + env(pay(alice, bob, USD(100)), ter(tecNO_PERMISSION)); + env(pay(carol, bob, XRP(100)), ter(tecNO_PERMISSION)); + env(pay(carol, bob, USD(100)), ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + env.require(balance(carol, startBalance - drops(2 * baseFee))); + + // bob preauthorizes carol for deposit + env(deposit::auth(bob, carol)); + env.close(); + env.require(balance(bob, startBalance - drops(2 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + // carol can pay bob + env(pay(carol, bob, XRP(100))); + env(pay(carol, bob, USD(100))); + // alice still can not pay + env(pay(alice, bob, XRP(100)), ter(tecNO_PERMISSION)); + env(pay(alice, bob, USD(100)), ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(alice, startBalance - drops(4 * baseFee))); + env.require( + balance(bob, startBalance + XRP(100) - drops(2 * baseFee))); + env.require( + balance(carol, startBalance - XRP(100) - drops(4 * baseFee))); + env.require(balance(alice, USD(1000))); + env.require(balance(bob, USD(1100))); + env.require(balance(carol, USD(900))); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, carol) == 1); + + // bob give permission to carol to preauthorize other accounts for + // deposit + env(account_permission::accountPermissionSet( + bob, carol, {"DepositPreauth"})); + env.close(); + env.require( + balance(bob, startBalance + XRP(100) - drops(3 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(ownerCount(env, carol) == 1); + + // now carol send DepositPreauth on behalf of bob to allow alice to + // deposit + env(deposit::auth(carol, alice, bob)); + env.close(); + env.require(balance(alice, startBalance - drops(4 * baseFee))); + env.require( + balance(bob, startBalance + XRP(100) - drops(3 * baseFee))); + env.require( + balance(carol, startBalance - XRP(100) - drops(5 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 4); + + // now alice can pay bob + env(pay(alice, bob, XRP(100))); + env(pay(alice, bob, USD(100))); + env.close(); + env.require( + balance(alice, startBalance - XRP(100) - drops(6 * baseFee))); + env.require( + balance(bob, startBalance + XRP(200) - drops(3 * baseFee))); + env.require( + balance(carol, startBalance - XRP(100) - drops(5 * baseFee))); + env.require(balance(alice, USD(900))); + env.require(balance(bob, USD(1200))); + env.require(balance(carol, USD(900))); + + // bob give permission to alice to auth/unauth on behalf of himself + env(account_permission::accountPermissionSet( + bob, alice, {"DepositPreauth"})); + env.close(); + env.require( + balance(bob, startBalance + XRP(200) - drops(4 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 5); + + // now alice unauthorize carol to pay bob on behalf of bob + env(deposit::unauth(alice, carol, bob)); + env.close(); + env.require( + balance(alice, startBalance - XRP(100) - drops(7 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 4); + + // carol can not pay bob + env(pay(carol, bob, XRP(100)), ter(tecNO_PERMISSION)); + env(pay(carol, bob, USD(100)), ter(tecNO_PERMISSION)); + env.close(); + env.require( + balance(carol, startBalance - XRP(100) - drops(7 * baseFee))); + + // alice can still pay bob + env(pay(alice, bob, XRP(100))); + env(pay(alice, bob, USD(100))); + env.close(); + env.require( + balance(alice, startBalance - XRP(200) - drops(9 * baseFee))); + env.require( + balance(bob, startBalance + XRP(300) - drops(4 * baseFee))); + env.require(balance(alice, USD(800))); + env.require(balance(bob, USD(1300))); + + // alice unauth herself to pay bob on behalf of bob + env(deposit::unauth(alice, alice, bob)); + env.close(); + env.require( + balance(alice, startBalance - XRP(200) - drops(10 * baseFee))); + env.require( + balance(bob, startBalance + XRP(300) - drops(4 * baseFee))); + env.require( + balance(carol, startBalance - XRP(100) - drops(7 * baseFee))); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // now alice can not pay bob + env(pay(alice, bob, XRP(100)), ter(tecNO_PERMISSION)); + env(pay(alice, bob, USD(100)), ter(tecNO_PERMISSION)); + // carol still can not pay bob + env(pay(carol, bob, XRP(100)), ter(tecNO_PERMISSION)); + env(pay(carol, bob, USD(100)), ter(tecNO_PERMISSION)); + env.require( + balance(alice, startBalance - XRP(200) - drops(12 * baseFee))); + env.require( + balance(carol, startBalance - XRP(100) - drops(9 * baseFee))); + + env(fclear(bob, asfDepositAuth)); + env.close(); + + // now alice and carol can pay bob + env(pay(alice, bob, XRP(100))); + env(pay(alice, bob, USD(100))); + env(pay(carol, bob, XRP(100))); + env(pay(carol, bob, USD(100))); + env.close(); + } + + { + const char credType[] = "abcde"; + const char uri[] = "uri"; + Env env(*this, features); + + Account alice{"alice"}; + Account bob{"bob"}; + Account issuer{"issuer"}; + Account subject{"subject"}; + env.fund(XRP(5000), alice, bob, issuer, subject); + env.close(); + + env(fset(bob, asfDepositAuth)); + env.close(); + + env(account_permission::accountPermissionSet( + issuer, alice, {"CredentialCreate"})); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + // alice creates credential on behalf of issuer successfully + env(credentials::create(subject, alice, credType), + credentials::uri(uri), + onBehalfOf(issuer)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 2); + + // Get the index of the credentials + auto const jv = + credentials::ledgerEntry(env, subject, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + env(account_permission::accountPermissionSet( + bob, alice, {"DepositPreauth"})); + env.close(); + + // alice send DepositPreauth on behalf of bob. + // bob will accept payements from accounts with credentials signed + // by issuer + env(deposit::authCredentials(alice, {{issuer, credType}}), + onBehalfOf(bob)); + env.close(); + + auto const jDP = deposit::ledgerEntryDepositPreauth( + env, bob, {{issuer, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + !jDP[jss::result].isMember(jss::error) && + jDP[jss::result].isMember(jss::node) && + jDP[jss::result][jss::node].isMember("LedgerEntryType") && + jDP[jss::result][jss::node]["LedgerEntryType"] == + jss::DepositPreauth); + + // credentials are not accepted yet + env(pay(subject, bob, XRP(100)), + credentials::ids({credIdx}), + ter(tecBAD_CREDENTIALS)); + env.close(); + + // alice accept credentials on behalf of subject + env(account_permission::accountPermissionSet( + subject, alice, {"CredentialAccept"})); + env.close(); + + env(credentials::accept(alice, issuer, credType), + onBehalfOf(subject)); + env.close(); + + // now subject can pay bob + env(pay(subject, bob, XRP(100)), credentials::ids({credIdx})); + env.close(); + + // subject can pay alice because alice did not enable depositAuth + env(pay(subject, alice, XRP(250)), credentials::ids({credIdx})); + env.close(); + + Account carol{"carol"}; + env.fund(XRP(5000), carol); + env.close(); + + env(fset(carol, asfDepositAuth)); + env.close(); + + // carol did not setup DepositPreauth + env(pay(subject, carol, XRP(100)), + credentials::ids({credIdx}), + ter(tecNO_PERMISSION)); + + // bob setup depositPreauth on behalf of carol + env(account_permission::accountPermissionSet( + carol, bob, {"DepositPreauth"})); + env.close(); + + env(deposit::authCredentials(bob, {{issuer, credType}}), + onBehalfOf(carol)); + env.close(); + + const char credType2[] = "fghij"; + env(credentials::create(subject, issuer, credType2)); + env.close(); + env(credentials::accept(subject, issuer, credType2)); + env.close(); + auto const jv2 = + credentials::ledgerEntry(env, subject, issuer, credType2); + std::string const credIdx2 = + jv2[jss::result][jss::index].asString(); + + // unable to pay with invalid set of credentials + env(pay(subject, carol, XRP(100)), + credentials::ids({credIdx, credIdx2}), + ter(tecNO_PERMISSION)); + + env(pay(subject, carol, XRP(100)), credentials::ids({credIdx})); + env.close(); + } + } + + void + testDID(FeatureBitset features) + { + testcase("test DIDSet, DIDDelete"); + using namespace jtx; + + Env env(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + STAmount const startBalance{XRP(1000000).value()}; + + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(startBalance, alice, bob, carol); + env.close(); + + // alice give permission to bob and carol for DIDSet and DIDDelete + env(account_permission::accountPermissionSet( + alice, bob, {"DIDSet", "DIDDelete"})); + env(account_permission::accountPermissionSet( + alice, carol, {"DIDSet", "DIDDelete"})); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + + // bob set uri and doc on behalf of alice + std::string const uri = "uri"; + std::string const doc = "doc"; + std::string const data = "data"; + env(did::set(bob), + did::uri(uri), + did::document(doc), + onBehalfOf(alice)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + env.require(balance(carol, startBalance)); + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 0); + auto sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(sleDID); + BEAST_EXPECT(did::checkVL((*sleDID)[sfURI], uri)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfDIDDocument], doc)); + BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); + + // carol set data, update document and remove uri on behalf of alice + std::string const doc2 = "doc2"; + env(did::set(carol), + did::uri(""), + did::document(doc2), + did::data(data), + onBehalfOf(alice)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance - drops(baseFee))); + env.require(balance(carol, startBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 0); + sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(sleDID); + BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfDIDDocument], doc2)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfData], data)); + + // bob delete DID on behalf of alice + env(did::del(bob, alice)); + env.close(); + env.require(balance(alice, startBalance - drops(2 * baseFee))); + env.require(balance(bob, startBalance - drops(2 * baseFee))); + env.require(balance(carol, startBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, carol) == 0); + sleDID = env.le(keylet::did(alice.id())); + BEAST_EXPECT(!sleDID); + } + + void + testEscrow(FeatureBitset features) + { + std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; + + std::array const cb1 = { + {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, + 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, + 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, + 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; + + testcase("test EscrowCreate, EscrowCancel, EscrowFinish"); + using namespace jtx; + + Env env(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + STAmount aliceXrpBalance, bobXrpBalance, carolXrpBalance; + auto UpdateXrpBalances = [&]() { + aliceXrpBalance = env.balance(alice, XRP); + bobXrpBalance = env.balance(bob, XRP); + carolXrpBalance = env.balance(carol, XRP); + }; + + env(account_permission::accountPermissionSet( + alice, bob, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env(account_permission::accountPermissionSet( + alice, carol, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env(account_permission::accountPermissionSet( + bob, alice, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env(account_permission::accountPermissionSet( + bob, carol, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env(account_permission::accountPermissionSet( + carol, alice, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env(account_permission::accountPermissionSet( + carol, bob, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, carol) == 2); + + // test send basic EscrowCreate, EscrowCancel, EscrowFinish transactions + // on behalf of others + { + UpdateXrpBalances(); + auto const ts = env.now() + std::chrono::seconds(90); + // bob creates escrow on behalf of alice, destination is carol + // (alice->carol) + auto const seq1 = env.seq(alice); + env(escrow(bob, carol, XRP(1000)), + onBehalfOf(alice), + finish_time(ts)); + env.close(); + env.require(balance(alice, aliceXrpBalance - XRP(1000))); + env.require(balance(bob, bobXrpBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 3); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, carol) == 2); + + UpdateXrpBalances(); + // carol creates escrow on behalf of alice, destination is bob + // (alice->bob) + auto const seq2 = env.seq(alice); + env(escrow(carol, bob, XRP(2000)), + onBehalfOf(alice), + cancel_time(ts), + condition(cb1)); + env.close(); + env.require(balance(alice, aliceXrpBalance - XRP(2000))); + env.require(balance(bob, bobXrpBalance)); + env.require(balance(carol, carolXrpBalance - drops(baseFee))); + BEAST_EXPECT(ownerCount(env, alice) == 4); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, carol) == 2); + + UpdateXrpBalances(); + // bob creates escrow on behalf of alice again, destination is carol + // (alice->carol) + auto const seq3 = env.seq(alice); + env(escrow(bob, carol, XRP(3000)), + onBehalfOf(alice), + finish_time(ts)); + env.close(); + env.require(balance(alice, aliceXrpBalance - XRP(3000))); + env.require(balance(bob, bobXrpBalance - drops(baseFee))); + env.require(balance(carol, carolXrpBalance)); + BEAST_EXPECT(ownerCount(env, alice) == 5); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, carol) == 2); + + // finish and cancel won't complete prematurely. + for (; env.now() <= ts; env.close()) + { + // alice finish seq1 on behalf of bob, the escrow's owner is + // alice + env(finish(alice, alice, seq1), + onBehalfOf(carol), + fee(1500), + ter(tecNO_PERMISSION)); + + // alice cancel seq2 on behalf of bob, the escrow's owner is + // alice + env(cancel(alice, alice, seq1), + onBehalfOf(bob), + fee(1500), + ter(tecNO_PERMISSION)); + + // bob finish seq3 on behalf of carol, the escrow's owner is + // alice + env(finish(bob, alice, seq3), + onBehalfOf(carol), + fee(1500), + ter(tecNO_PERMISSION)); + } + + UpdateXrpBalances(); + // alice finish escrow seq1 on behalf of carol. + // alice is the owner. + env(finish(alice, alice, seq1), + onBehalfOf(carol), + fee(1500), + ter(tesSUCCESS)); + env.close(); + env.require(balance(alice, aliceXrpBalance - drops(1500))); + env.require(balance(bob, bobXrpBalance)); + env.require(balance(carol, carolXrpBalance + XRP(1000))); + BEAST_EXPECT(ownerCount(env, alice) == 4); + + UpdateXrpBalances(); + // finish won't work for escrow seq2 + env(finish(alice, alice, seq2), + condition(cb1), + fulfillment(fb1), + onBehalfOf(bob), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(alice, aliceXrpBalance - drops(1500))); + env.require(balance(bob, bobXrpBalance)); + env.require(balance(carol, carolXrpBalance)); + BEAST_EXPECT(ownerCount(env, alice) == 4); + + UpdateXrpBalances(); + // alice cancel escrow seq2 on behalf of bob + env(cancel(alice, alice, seq2), onBehalfOf(bob), fee(1500)); + env.close(); + env.require( + balance(alice, aliceXrpBalance + XRP(2000) - drops(1500))); + env.require(balance(bob, bobXrpBalance)); + env.require(balance(carol, carolXrpBalance)); + BEAST_EXPECT(ownerCount(env, alice) == 3); + + UpdateXrpBalances(); + // bob finish escrow seq3 on behalf of carol + env(finish(bob, alice, seq3), + onBehalfOf(carol), + fee(1500), + ter(tesSUCCESS)); + env.close(); + env.require(balance(alice, aliceXrpBalance)); + env.require(balance(bob, bobXrpBalance - drops(1500))); + env.require(balance(carol, carolXrpBalance + XRP(3000))); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // test escrow with FinishAfter earlier than CancelAfter + { + auto const fts = env.now() + std::chrono::seconds(117); + auto const cts = env.now() + std::chrono::seconds(192); + + UpdateXrpBalances(); + // alice creates escrow on behalf of carol, destination is bob + // (carol->bob) + auto const seq = env.seq(carol); + env(escrow(alice, bob, XRP(1000)), + onBehalfOf(carol), + finish_time(fts), + cancel_time(cts), + stag(1), + dtag(2)); + env.close(); + + auto const sle = env.le(keylet::escrow(carol.id(), seq)); + BEAST_EXPECT(sle); + BEAST_EXPECT((*sle)[sfSourceTag] == 1); + BEAST_EXPECT((*sle)[sfDestinationTag] == 2); + + env.require(balance(alice, aliceXrpBalance - drops(baseFee))); + env.require(balance(carol, carolXrpBalance - XRP(1000))); + + // finish and cancel won't complete prematurely. + for (; env.now() <= fts; env.close()) + { + // bob finish escrow seq on behalf of carol + env(finish(bob, carol, seq), + onBehalfOf(carol), + fee(1500), + ter(tecNO_PERMISSION)); + + // bob cancel escrow seq on behalf of carol + env(cancel(bob, carol, seq), + onBehalfOf(carol), + fee(1500), + ter(tecNO_PERMISSION)); + } + + UpdateXrpBalances(); + // still can not cancel before CancelAfter time + env(cancel(alice, carol, seq), + onBehalfOf(bob), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(alice, aliceXrpBalance - drops(1500))); + env.require(balance(bob, bobXrpBalance)); + env.require(balance(carol, carolXrpBalance)); + + // can finish after FinishAfter time + env(finish(alice, carol, seq), onBehalfOf(bob), fee(1500)); + env.close(); + env.require(balance(alice, aliceXrpBalance - drops(3000))); + env.require(balance(bob, bobXrpBalance + XRP(1000))); + env.require(balance(carol, carolXrpBalance)); + } + + // test escrow with asfDepositAuth + { + Account gw("gw"); + Account david{"david"}; + Account emma{"emma"}; + Account frank{"frank"}; + env.fund(XRP(5000), gw, david, emma, frank); + env(fset(david, asfDepositAuth)); + env.close(); + env(deposit::auth(david, emma)); + env.close(); + + auto const seq = env.seq(gw); + auto const fts = env.now() + std::chrono::seconds(5); + env(escrow(gw, david, XRP(1000)), finish_time(fts)); + env.require(balance(gw, XRP(4000) - drops(baseFee))); + env.close(); + + env(account_permission::accountPermissionSet( + emma, frank, {"EscrowCreate", "EscrowCancel", "EscrowFinish"})); + env.close(); + + while (env.now() <= fts) + env.close(); + + // gw has no permission + env(finish(gw, gw, seq), ter(tecNO_PERMISSION)); + + auto davidXrpBalance = env.balance(david, XRP); + // but frank can finish onbehalf of emma because emma is + // preauthorized + env(finish(frank, gw, seq), onBehalfOf(emma)); + env.close(); + env.require(balance(david, davidXrpBalance + XRP(1000))); + } + } + + void + testMPToken(FeatureBitset features) + { + testcase("test MPT transactions"); + using namespace jtx; + + // test create, authorize on behalf of others + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1000000), alice, bob, carol); + env.close(); + + // sender is alice, bob is the issuer + MPTTester mpt(env, alice, bob); + env.close(); + + env(account_permission::accountPermissionSet( + bob, + alice, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize"})); + + env(account_permission::accountPermissionSet( + bob, + carol, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize"})); + + env(account_permission::accountPermissionSet( + alice, carol, {"MPTokenAuthorize"})); + env.close(); + + // bob owns AccountPermission and MPTokenIssuance + mpt.create( + {.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807 + .assetScale = 1, + .transferFee = 10, + .metadata = "123", + .ownerCount = 3, + .flags = tfMPTCanLock | tfMPTCanEscrow | tfMPTCanTrade | + tfMPTCanTransfer | tfMPTCanClawback, + .onBehalfOf = bob}); + + // Get the hash for the most recent transaction. + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const result = env.rpc("tx", txHash)[jss::result]; + BEAST_EXPECT( + result[sfMaximumAmount.getJsonName()] == "9223372036854775807"); + env.close(); + + // carol does not have the permission to authorize on behalf of bob + mpt.authorize( + {.account = carol, .onBehalfOf = bob, .err = tecNO_PERMISSION}); + + // alice has permission, but bob can not hold onto his own token + mpt.authorize( + {.account = alice, .onBehalfOf = bob, .err = tecNO_PERMISSION}); + + // alice holds the mptoken object, sender is carol + mpt.authorize({.account = carol, .onBehalfOf = alice}); + + // alice cannot create the mptoken again + mpt.authorize({.account = alice, .err = tecDUPLICATE}); + + // bob pays alice 100 tokens + mpt.pay(bob, alice, 100); + + // alice hold token, can not unauthorize + mpt.authorize( + {.account = carol, + .flags = tfMPTUnauthorize, + .onBehalfOf = alice, + .err = tecHAS_OBLIGATIONS}); + + // alice pays back 100 tokens + mpt.pay(alice, bob, 100); + + // now alice can unauthorize, carol sent the request on behalf of + // her + mpt.authorize( + {.account = carol, + .flags = tfMPTUnauthorize, + .onBehalfOf = alice}); + + // now if alice tries to unauthorize by herself, it will fail + mpt.authorize( + {.account = alice, + .holderCount = 0, + .flags = tfMPTUnauthorize, + .err = tecOBJECT_NOT_FOUND}); + } + + // test create, destroy, claw with tfMPTRequireAuth + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(100000), alice, bob, carol); + env.close(); + + // alice gives bob permissions + env(account_permission::accountPermissionSet( + alice, + bob, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize"})); + env.close(); + + // sender is bob, alice is the issuer + MPTTester mpt(env, bob, alice); + env.close(); + + // alice owns the mptokenissuance and the account permission + mpt.create( + {.ownerCount = 2, + .flags = tfMPTRequireAuth | tfMPTCanClawback, + .onBehalfOf = alice}); + env.close(); + + // bob creates mptoken + mpt.authorize({.account = bob, .holderCount = 1}); + + // bob authorize himself on behalf of alice + mpt.authorize({.account = bob, .holder = bob, .onBehalfOf = alice}); + + mpt.pay(alice, bob, 200); + mpt.claw(alice, bob, 100); + mpt.pay(bob, alice, 100); + + // bob unauthorize bob's mptoken on behalf of alice + mpt.authorize( + {.account = bob, + .holder = bob, + .holderCount = 1, + .flags = tfMPTUnauthorize, + .onBehalfOf = alice}); + + // bob gives carol permissions + env(account_permission::accountPermissionSet( + bob, + carol, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize"})); + env.close(); + + mpt.authorize( + {.account = carol, + .holderCount = 0, + .flags = tfMPTUnauthorize, + .onBehalfOf = bob}); + + // bob destroys the mpt issuance on behalf of alice + // issuer is alice, she still owns the account permission, so + // ownerCount is 1. + mpt.destroy({.issuer = bob, .ownerCount = 1, .onBehalfOf = alice}); + } + + // MPTokenIssuanceSet on behalf of other account + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(100000), alice, bob, carol); + env.close(); + + // alice gives bob permissions + env(account_permission::accountPermissionSet( + alice, + bob, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize", + "MPTokenIssuanceSet"})); + env.close(); + + // sender is bob, alice is the issuer + MPTTester mpt(env, bob, alice); + env.close(); + + // alice create with tfMPTCanLock by herself + // alice owns account permission and mpt issuance + mpt.create( + {.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanLock}); + + env(account_permission::accountPermissionSet( + bob, + carol, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize"})); + env.close(); + + // carol send auth on behalf of bob + mpt.authorize( + {.account = carol, .holderCount = 1, .onBehalfOf = bob}); + + env(account_permission::accountPermissionSet( + alice, + carol, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenIssuanceSet"})); + env.close(); + + // carol locks bob's mptoken on behalf of alice + mpt.set( + {.account = carol, + .holder = bob, + .flags = tfMPTLock, + .onBehalfOf = alice}); + + // alice locks bob's mptoken again, it remains locked + mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + + // bob locks mptissuance on behalf of alice + mpt.set({.account = bob, .flags = tfMPTLock, .onBehalfOf = alice}); + + // carol unlock bob's mptoken on behalf of alice + mpt.set( + {.account = carol, + .holder = bob, + .flags = tfMPTUnlock, + .onBehalfOf = alice}); + + // alice unlock mptissuance by herself + mpt.set({.account = alice, .flags = tfMPTUnlock}); + + // alice locks mptissuance + mpt.set({.account = alice, .flags = tfMPTLock}); + + // carol unlock mptissuance on behalf of alice + mpt.set( + {.account = carol, .flags = tfMPTUnlock, .onBehalfOf = alice}); + } + + // DepositPreauth and credential + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + Account david{"david"}; + env.fund(XRP(100000), alice, bob, carol, david); + env.close(); + const char credType[] = "abcde"; + + // alice gives bob permissions + env(account_permission::accountPermissionSet( + alice, + bob, + {"MPTokenIssuanceCreate", + "MPTokenIssuanceDestroy", + "MPTokenAuthorize", + "MPTokenIssuanceSet"})); + env.close(); + + // sender is bob, alice is the issuer + MPTTester mpt(env, bob, alice); + env.close(); + + // alice owns the mptokenissuance and the account permission + mpt.create( + {.ownerCount = 2, + .flags = tfMPTRequireAuth | tfMPTCanTransfer, + .onBehalfOf = alice}); + env.close(); + + mpt.authorize({.account = bob}); + // bob authorize himself on behalf of alice + mpt.authorize({.account = bob, .holder = bob, .onBehalfOf = alice}); + + // bob require preauthorization + env(fset(bob, asfDepositAuth)); + env.close(); + + // alice try to send 100 MPT to bob, not authorized + mpt.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + env(account_permission::accountPermissionSet( + david, carol, {"CredentialCreate", "CredentialAccept"})); + env.close(); + + env(account_permission::accountPermissionSet( + alice, carol, {"CredentialCreate", "CredentialAccept"})); + env.close(); + + // Create credentials + env(credentials::create(alice, carol, credType), onBehalfOf(david)); + env.close(); + env(credentials::accept(carol, david, credType), onBehalfOf(alice)); + env.close(); + auto const jv = + credentials::ledgerEntry(env, alice, david, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + // alice sends 100 MPT to bob with credentials, not authorized + mpt.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}}); + env.close(); + + // bob setup depositPreauth on behalf of carol + env(account_permission::accountPermissionSet( + bob, carol, {"DepositPreauth"})); + env.close(); + + // bob authorize credentials + env(deposit::authCredentials(carol, {{david, credType}}), + onBehalfOf(bob)); + env.close(); + + // alice try to send 100 MPT to bob, not authorized + mpt.pay(alice, bob, 100, tecNO_PERMISSION); + env.close(); + + // alice sends 100 MPT to bob with credentials + mpt.pay(alice, bob, 100, tesSUCCESS, {{credIdx}}); + env.close(); + } + } + + void + testMPTokenIssuanceSetGranular(FeatureBitset features) + { + testcase("test MPTokenIssuanceSet granular"); + using namespace jtx; + + // test MPTokenIssuanceUnlock and MPTokenIssuanceLock permissions + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceUnlock + env(account_permission::accountPermissionSet( + alice, bob, {"MPTokenIssuanceUnlock"})); + env.close(); + // bob does not have lock permission + mpt.set( + {.account = bob, + .flags = tfMPTLock, + .onBehalfOf = alice, + .err = tecNO_PERMISSION}); + // bob now has lock permission, but does not have unlock permission + env(account_permission::accountPermissionSet( + alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + mpt.set({.account = bob, .flags = tfMPTLock, .onBehalfOf = alice}); + mpt.set( + {.account = bob, + .flags = tfMPTUnlock, + .onBehalfOf = alice, + .err = tecNO_PERMISSION}); + + // now bob can lock and unlock + env(account_permission::accountPermissionSet( + alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"})); + env.close(); + mpt.set( + {.account = bob, .flags = tfMPTUnlock, .onBehalfOf = alice}); + mpt.set({.account = bob, .flags = tfMPTLock, .onBehalfOf = alice}); + env.close(); + } + + // test mix of granular and transaction level permission + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceLock + env(account_permission::accountPermissionSet( + alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + mpt.set({.account = bob, .flags = tfMPTLock, .onBehalfOf = alice}); + // bob does not have unlock permission + mpt.set( + {.account = bob, + .flags = tfMPTUnlock, + .onBehalfOf = alice, + .err = tecNO_PERMISSION}); + + // alice gives bob some unrelated permission with + // MPTokenIssuanceLock + env(account_permission::accountPermissionSet( + alice, + bob, + {"NFTokenMint", "MPTokenIssuanceLock", "NFTokenBurn"})); + env.close(); + // bob can not unlock + mpt.set( + {.account = bob, + .flags = tfMPTUnlock, + .onBehalfOf = alice, + .err = tecNO_PERMISSION}); + + // alice add MPTokenIssuanceSet to permissions + env(account_permission::accountPermissionSet( + alice, + bob, + {"NFTokenMint", + "MPTokenIssuanceLock", + "NFTokenBurn", + "MPTokenIssuanceSet"})); + mpt.set( + {.account = bob, .flags = tfMPTUnlock, .onBehalfOf = alice}); + // alice can lock by herself + mpt.set({.account = alice, .flags = tfMPTLock}); + mpt.set( + {.account = bob, .flags = tfMPTUnlock, .onBehalfOf = alice}); + mpt.set({.account = bob, .flags = tfMPTLock, .onBehalfOf = alice}); + } + } + + void + testNFToken(FeatureBitset features) + { + testcase("test NFT transactions"); + using namespace jtx; + using UriTaxtonPair = std::pair; + + // test mint on behalf of another account + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000), alice, bob); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"NFTokenMint"})); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + + std::vector entries; + for (std::size_t i = 0; i < 100; i++) + { + entries.emplace_back( + token::randURI(), rand_int()); + } + + // bob mint 100 nfts on behalf of alice + for (UriTaxtonPair const& entry : entries) + { + if (entry.first.empty()) + env(token::mint(bob, entry.second), onBehalfOf(alice)); + else + env(token::mint(bob, entry.second), + token::uri(entry.first), + onBehalfOf(alice)); + + env.close(); + } + + // bob does not own anything + BEAST_EXPECT(ownerCount(env, bob) == 0); + + // check alice's NFTs are accurate + Json::Value aliceNFTs = [&env, &alice]() { + Json::Value params; + params[jss::account] = alice.human(); + params[jss::type] = "state"; + return env.rpc("json", "account_nfts", to_string(params)); + }(); + + auto const& nfts = aliceNFTs[jss::result][jss::account_nfts]; + BEAST_EXPECT(nfts.size() == entries.size()); + + std::vector sortedNFTs; + sortedNFTs.reserve(nfts.size()); + for (std::size_t i = 0; i < nfts.size(); ++i) + sortedNFTs.push_back(nfts[i]); + std::sort( + sortedNFTs.begin(), + sortedNFTs.end(), + [](Json::Value const& lhs, Json::Value const& rhs) { + return lhs[jss::nft_serial] < rhs[jss::nft_serial]; + }); + + for (std::size_t i = 0; i < entries.size(); ++i) + { + UriTaxtonPair const& entry = entries[i]; + Json::Value const& ret = sortedNFTs[i]; + + BEAST_EXPECT(entry.second == ret[sfNFTokenTaxon.jsonName]); + if (entry.first.empty()) + BEAST_EXPECT(!ret.isMember(sfURI.jsonName)); + else + BEAST_EXPECT(strHex(entry.first) == ret[sfURI.jsonName]); + } + } + + // mint on behalf of an authroized minter, create offer and accept offer + // on behalf of another account, burn nft on behalf of another account + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + Account minter{"minter"}; + Account const buyer{"buyer"}; + env.fund(XRP(1000000), alice, bob, carol, minter, buyer); + env.close(); + + // alice selects minter as her minter. + env(token::setMinter(alice, minter)); + env.close(); + + // minter authroizes bob + env(account_permission::accountPermissionSet( + minter, + bob, + {"NFTokenMint", "NFTokenBurn", "NFTokenCreateOffer"})); + env.close(); + + // buyer authroizes alice + env(account_permission::accountPermissionSet( + buyer, + alice, + {"NFTokenMint", "NFTokenBurn", "NFTokenAcceptOffer"})); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + + auto buyNFT = [&](std::uint32_t flags) { + uint256 const nftID{token::getNextID(env, alice, 0u, flags)}; + + // bob mint nft on behalf of minter + env(token::mint(bob, 0u), + token::issuer(alice), + onBehalfOf(minter), + txflags(flags)); + env.close(); + + uint256 const offerIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + + // bob create offer on behalf of minter + env(token::createOffer(bob, nftID, XRP(0)), + txflags(tfSellNFToken), + onBehalfOf(minter)); + env.close(); + + // bob accepts offer on behalf of buyer + env(token::acceptSellOffer(alice, offerIndex), + onBehalfOf(buyer)); + env.close(); + + return nftID; + }; + + // no flagBurnable, can only be burned by owner + { + uint256 const nftID = buyNFT(0); + env(token::burn(bob, nftID), + onBehalfOf(alice), + token::owner(buyer), + ter(tecNO_PERMISSION)); + env.close(); + env(token::burn(bob, nftID), + onBehalfOf(minter), + token::owner(buyer), + ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + env(token::burn(alice, nftID), + token::owner(buyer), + onBehalfOf(buyer)); + env.close(); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } + + // enable tfBurnable, issuer alice can burn the nft + { + uint256 const nftID = buyNFT(tfBurnable); + env(account_permission::accountPermissionSet( + alice, carol, {"NFTokenMint", "NFTokenBurn"})); + env.close(); + + BEAST_EXPECT(ownerCount(env, buyer) == 2); + env(token::burn(carol, nftID), + onBehalfOf(alice), + token::owner(buyer)); + env.close(); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } + + // alice set bob as minter and carol burn nft on behalf of bob + { + uint256 const nftID = buyNFT(tfBurnable); + env(token::setMinter(alice, bob)); + env.close(); + + env(account_permission::accountPermissionSet( + bob, carol, {"NFTokenMint", "NFTokenBurn"})); + env.close(); + + BEAST_EXPECT(ownerCount(env, buyer) == 2); + + // carol burn nft on behalf of bob + env(token::burn(carol, nftID), + onBehalfOf(bob), + token::owner(buyer)); + env.close(); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } + } + + // mint with flagTransferable + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + Account const buyer{"buyer"}; + env.fund(XRP(1000000), alice, bob, carol, buyer); + env.close(); + + // alice mint nft by herself + uint256 const nftAliceID{ + token::getNextID(env, alice, 0u, tfTransferable)}; + env(token::mint(alice, 0u), txflags(tfTransferable)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + + env(account_permission::accountPermissionSet( + alice, + bob, + {"NFTokenMint", + "NFTokenBurn", + "NFTokenCreateOffer", + "NFTokenAcceptOffer"})); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 2); + + env(account_permission::accountPermissionSet( + bob, + carol, + {"NFTokenMint", + "NFTokenBurn", + "NFTokenCreateOffer", + "NFTokenAcceptOffer", + "NFTokenCancelOffer"})); + env.close(); + BEAST_EXPECT(ownerCount(env, bob) == 1); + + // bob creates offer on behalf of alice + uint256 const aliceSellOfferIndex = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(bob, nftAliceID, XRP(20)), + onBehalfOf(alice), + txflags(tfSellNFToken)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 3); + + // carol creates offer on behalf of bob + uint256 const bobBuyOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(carol, nftAliceID, XRP(21)), + onBehalfOf(bob), + token::owner(alice)); + env.close(); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + // carol accepts offer on behalf of bob + env(token::acceptSellOffer(carol, aliceSellOfferIndex), + onBehalfOf(bob)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(ownerCount(env, carol) == 0); + + // bob offers to sell the nft by himself + uint256 const bobSellOfferIndex = + keylet::nftoffer(bob, env.seq(bob)).key; + env(token::createOffer(bob, nftAliceID, XRP(22)), + txflags(tfSellNFToken)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 4); + BEAST_EXPECT(ownerCount(env, carol) == 0); + + env(account_permission::accountPermissionSet( + buyer, + alice, + {"NFTokenMint", + "NFTokenBurn", + "NFTokenCreateOffer", + "NFTokenAcceptOffer"})); + env.close(); + + // alice accepts the offer on behalf of buyer + env(token::acceptSellOffer(alice, bobSellOfferIndex), + onBehalfOf(buyer)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + + // alice sells the nft on behalf of buyer + uint256 const buyerSellOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(alice, nftAliceID, XRP(23)), + onBehalfOf(buyer), + txflags(tfSellNFToken)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, buyer) == 3); + + // alice buys back the nft by herself + env(token::acceptSellOffer(alice, buyerSellOfferIndex)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 2); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + + // carol cancel bob's offer on behalf of bob + env(token::cancelOffer(carol, {bobBuyOfferIndex}), onBehalfOf(bob)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } + + // buy and sell nft using IOU + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + Account const buyer{"buyer"}; + env.fund(XRP(1000000), gw, alice, bob, carol, buyer); + env.close(); + + auto const USD = gw["USD"]; + env(trust(alice, USD(1000))); + env(trust(bob, USD(1000))); + env.close(); + env(pay(gw, alice, USD(500))); + env(pay(gw, bob, USD(500))); + env.close(); + + std::uint16_t transferFee = 5000; + + // alice mint nft by herself + uint256 const nftAliceID{ + token::getNextID(env, alice, 0u, tfTransferable, transferFee)}; + env(token::mint(alice, 0u), + token::xferFee(transferFee), + txflags(tfTransferable)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 2); + + env(account_permission::accountPermissionSet( + alice, + bob, + {"NFTokenMint", + "NFTokenBurn", + "NFTokenCreateOffer", + "NFTokenAcceptOffer"})); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 3); + + env(account_permission::accountPermissionSet( + bob, + carol, + {"NFTokenMint", + "NFTokenBurn", + "NFTokenCreateOffer", + "NFTokenAcceptOffer"})); + env.close(); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + // bob sells the nft for 200 USD on behalf of alice + uint256 const aliceSellOfferIndex = + keylet::nftoffer(alice, env.seq(alice)).key; + env(token::createOffer(bob, nftAliceID, USD(200)), + onBehalfOf(alice), + txflags(tfSellNFToken)); + env.close(); + + // carol accept the sell offer on behalf of bob + env(token::acceptSellOffer(carol, aliceSellOfferIndex), + onBehalfOf(bob)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == USD(700)); + + // can not sell for CAD + env(token::createOffer(carol, nftAliceID, gw["CAD"](50)), + onBehalfOf(bob), + txflags(tfSellNFToken), + ter(tecNO_LINE)); + env.close(); + } + } + + void + testOracle(FeatureBitset features) + { + testcase("test oracle"); + using namespace jtx; + + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1'000), alice, bob); + + env(account_permission::accountPermissionSet( + bob, alice, {"OracleSet", "OracleDelete"})); + env.close(std::chrono::seconds(maxLastUpdateTimeDelta + 100)); + + // alice create oracle on behalf of bob + oracle::Oracle oracle( + env, + {.series = {{"XRP", "USD", 740, 1}}, + .onBehalfOf = bob, + .sender = alice}); + BEAST_EXPECT(oracle.exists()); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 2); + // bob delete oracle himself + oracle.remove({}); + BEAST_EXPECT(!oracle.exists()); + BEAST_EXPECT(ownerCount(env, bob) == 1); + + // alice create oracle2 on behalf of bob + oracle::Oracle oracle2(env, {.onBehalfOf = bob, .sender = alice}); + BEAST_EXPECT(oracle2.exists()); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + // alice updates oracle2 on behalf of bob + oracle2.set(oracle::UpdateArg{ + .series = {{"XRP", "USD", 740, 2}}, + .onBehalfOf = bob, + .sender = alice}); + BEAST_EXPECT(oracle2.expectPrice({{"XRP", "USD", 740, 2}})); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + oracle2.set(oracle::UpdateArg{ + .series = {{"XRP", "EUR", 700, 2}}, + .onBehalfOf = bob, + .sender = alice}); + BEAST_EXPECT(oracle2.expectPrice( + {{"XRP", "USD", 0, 0}, {"XRP", "EUR", 700, 2}})); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + // bob updates oracle2 himself + oracle2.set(oracle::UpdateArg{ + .series = {{"XRP", "USD", 741, 2}, {"XRP", "EUR", 710, 2}}}); + BEAST_EXPECT(oracle2.expectPrice( + {{"XRP", "USD", 741, 2}, {"XRP", "EUR", 710, 2}})); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + // alice updates oracle2 on behalf of bob + oracle2.set(oracle::UpdateArg{ + .series = + { + {"BTC", "USD", 741, 2}, + {"ETH", "EUR", 710, 2}, + {"YAN", "EUR", 710, 2}, + {"CAN", "EUR", 710, 2}, + }, + .onBehalfOf = bob, + .sender = alice}); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + oracle2.set(oracle::UpdateArg{ + .series = {{"BTC", "USD", std::nullopt, std::nullopt}}}); + + oracle2.set(oracle::UpdateArg{ + .series = + {{"XRP", "USD", 742, 2}, + {"XRP", "EUR", 711, 2}, + {"ETH", "EUR", std::nullopt, std::nullopt}, + {"YAN", "EUR", std::nullopt, std::nullopt}, + {"CAN", "EUR", std::nullopt, std::nullopt}}, + .onBehalfOf = bob, + .sender = alice}); + BEAST_EXPECT(oracle2.expectPrice( + {{"XRP", "USD", 742, 2}, {"XRP", "EUR", 711, 2}})); + + BEAST_EXPECT(ownerCount(env, bob) == 2); + + auto const index = env.closed()->seq(); + auto const hash = env.closed()->info().hash; + for (int i = 0; i < 256; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + + // deleting account bob deletes oracle2 + env(acctdelete(bob, alice), fee(acctDelFee)); + env.close(); + BEAST_EXPECT(!oracle2.exists()); + + // can still get the oracles via the ledger index or hash + auto verifyLedgerData = [&](auto const& field, auto const& value) { + Json::Value jvParams; + jvParams[field] = value; + jvParams[jss::binary] = false; + jvParams[jss::type] = jss::oracle; + Json::Value jrr = env.rpc( + "json", + "ledger_data", + boost::lexical_cast(jvParams)); + BEAST_EXPECT(jrr[jss::result][jss::state].size() == 1); + }; + verifyLedgerData(jss::ledger_index, index); + verifyLedgerData(jss::ledger_hash, to_string(hash)); + } + + void + testTrustSet(FeatureBitset features) + { + testcase("test TrustSet"); + using namespace jtx; + + // test create trustline + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1'000), gw, alice, bob); + + env(account_permission::accountPermissionSet( + bob, alice, {"TrustSet"})); + + // alice send trustset on behalf of bob + env(trust(alice, gw["USD"](50), 0), onBehalfOf(bob)); + env.close(); + + env.require(lines(gw, 1)); + env.require(lines(bob, 1)); + + Json::Value jv; + jv["account"] = bob.human(); + auto bobLines = env.rpc("json", "account_lines", to_string(jv)); + + jv["account"] = gw.human(); + auto gwLines = env.rpc("json", "account_lines", to_string(jv)); + + BEAST_EXPECT(bobLines[jss::result][jss::lines].size() == 1); + BEAST_EXPECT(gwLines[jss::result][jss::lines].size() == 1); + + // pay exceeding trustline limit + env(pay(gw, bob, gw["USD"](200)), ter(tecPATH_PARTIAL)); + env.close(); + + // smaller payments should succeed + env(pay(gw, bob, gw["USD"](20)), ter(tesSUCCESS)); + env.close(); + + env.require(balance(bob, gw["USD"](20))); + env.require(balance(gw, bob["USD"](-20))); + } + + // test requireAuth + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1'000), gw, alice, bob, carol); + + env(fset(gw, asfRequireAuth)); + env.close(); + env.require(flags(gw, asfRequireAuth)); + + env(account_permission::accountPermissionSet( + bob, alice, {"TrustSet"})); + env(account_permission::accountPermissionSet( + gw, alice, {"TrustSet"})); + env.close(); + + // alice send trustset on behalf of gw, but source can not be the + // same as destination + env(trust(alice, gw["USD"](50), 0), + onBehalfOf(gw), + ter(temDST_IS_SRC)); + env.close(); + + // alice send trustset on behalf of bob + env(trust(alice, gw["USD"](50), 0), onBehalfOf(bob)); + env.close(); + + env(pay(gw, bob, gw["USD"](10)), ter(tecPATH_DRY)); + env.close(); + + // alice authorizes bob to hold gw["USD"] on behalf of gw + env(trust(alice, gw["USD"](0), bob, tfSetfAuth), onBehalfOf(gw)); + env.close(); + + env.require(lines(gw, 1)); + env.require(lines(bob, 1)); + + Json::Value jv; + jv["account"] = bob.human(); + auto bobLines = env.rpc("json", "account_lines", to_string(jv)); + + jv["account"] = gw.human(); + auto gwLines = env.rpc("json", "account_lines", to_string(jv)); + + BEAST_EXPECT(bobLines[jss::result][jss::lines].size() == 1); + BEAST_EXPECT(gwLines[jss::result][jss::lines].size() == 1); + + // alice resets trust line limit to 0 on behalf of bob + // this will delete the trust line + env(trust(alice, gw["USD"](0), 0), onBehalfOf(bob)); + env.close(); + + env.require(lines(gw, 0)); + env.require(lines(bob, 0)); + } + + // create trustline to each other + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1'000), gw, alice, bob, carol); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"TrustSet"})); + env(account_permission::accountPermissionSet( + bob, alice, {"TrustSet"})); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 1); + + // alice creates trustline to alice on behalf of bob + env(trust(alice, alice["USD"](100)), onBehalfOf(bob)); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 2); + + env.require(lines(alice, 1)); + env.require(lines(bob, 1)); + + env(pay(alice, bob, alice["USD"](20)), ter(tesSUCCESS)); + env.close(); + env.require(balance(bob, alice["USD"](20))); + env.require(balance(alice, bob["USD"](-20))); + + env(pay(bob, alice, bob["USD"](10)), ter(tesSUCCESS)); + env.close(); + env.require(balance(bob, alice["USD"](10))); + env.require(balance(alice, bob["USD"](-10))); + + env(pay(bob, alice, bob["USD"](11)), ter(tecPATH_PARTIAL)); + env.close(); + env.require(balance(bob, alice["USD"](10))); + env.require(balance(alice, bob["USD"](-10))); + + env(pay(bob, alice, bob["USD"](10)), ter(tesSUCCESS)); + env.close(); + env.require(balance(bob, alice["USD"](0))); + env.require(balance(alice, bob["USD"](0))); + + env(trust(bob, bob["USD"](100)), onBehalfOf(alice)); + env.close(); + env(pay(bob, alice, bob["USD"](5)), ter(tesSUCCESS)); + env.close(); + + env.require(lines(alice, 1)); + env.require(lines(bob, 1)); + + env.require(balance(bob, alice["USD"](-5))); + env.require(balance(alice, bob["USD"](5))); + } + + // create trustline when asfDisallowIncomingTrustline is set + // create trustline with tfSetNoRipple + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1'000), gw, alice, bob, carol); + env.close(); + + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + env(account_permission::accountPermissionSet( + bob, alice, {"TrustSet"})); + env(account_permission::accountPermissionSet( + gw, alice, {"TrustSet"})); + env.close(); + + // can not create trustline when asfDisallowIncomingTrustline is set + auto const USD = gw["USD"]; + env(trust(alice, USD(1000)), + onBehalfOf(bob), + ter(tecNO_PERMISSION)); + env.close(); + + env(fclear(gw, asfDisallowIncomingTrustline)); + env.close(); + + // alice can create trustline on behalf of bob when + // asfDisallowIncomingTrustline is cleared + env(trust(alice, USD(1000)), onBehalfOf(bob)); + env.close(); + + env(pay(gw, bob, USD(200))); + env.close(); + env.require(balance(gw, bob["USD"](-200))); + env.require(balance(bob, gw["USD"](200))); + + // alice create trustline on behalf of gw to carol with + // tfSetNoRipple flag + env(trust(alice, USD(2000), carol, tfSetNoRipple), onBehalfOf(gw)); + env.close(); + + Json::Value carolJson; + carolJson[jss::account] = carol.human(); + Json::Value response = + env.rpc("json", "account_lines", to_string(carolJson)); + auto const& line = response[jss::result][jss::lines][0u]; + BEAST_EXPECT(line[jss::no_ripple_peer].asBool() == true); + } + } + + void + testXChain(FeatureBitset features) + { + testcase("test XChain transactions"); + using namespace jtx; + + // create two chains + Env env(*this, features); + Env envX(*this, features); + XRPAmount const baseFee{env.current()->fees().base}; + + // fund initial accounts + Account door = Account("door"); + Account alice = Account("alice"); + Account bob = Account("bob"); + env.fund(XRP(100000), door, alice, bob); + env.close(); + Account attesterX = Account("attesterX"); + Account signerX = Account("signerX"); + Account rewardX = Account("rewardX"); + Account aliceX = Account("aliceX"); + Account bobX = Account("bobX"); + Account carolX = Account{"carolX"}; + envX.fund(XRP(100000), attesterX, signerX, rewardX, bobX, carolX); + envX.close(); + std::vector signerXs = {jtx::signer(signerX)}; + + auto doorBalance = env.balance(door, XRP); + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + // door on the side chain has to be master account for XRP + auto doorXBalance = envX.balance(Account::master, XRP); + auto attesterXBalance = envX.balance(attesterX, XRP); + auto signerXBalance = envX.balance(signerX, XRP); + auto rewardXBalance = envX.balance(rewardX, XRP); + auto aliceXBalance = envX.balance(aliceX, XRP); + auto bobXBalance = envX.balance(bobX, XRP); + auto carolXBalance = envX.balance(carolX, XRP); + + // XChainCreateBridge + Json::Value jvBridge = + bridge(door, xrpIssue(), Account::master, xrpIssue()); + { + env(bridge_create(bob, jvBridge, XRP(1), XRP(100)), + onBehalfOf(door), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + env(account_permission::accountPermissionSet( + door, bob, {"XChainCreateBridge"})); + env.close(); + env.require(balance(door, doorBalance - drops(baseFee))); + doorBalance = env.balance(door, XRP); + + env(bridge_create(bob, jvBridge, XRP(1), XRP(100)), + onBehalfOf(door)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + } + { + envX( + bridge_create(bobX, jvBridge, XRP(1), XRP(100)), + onBehalfOf(Account::master), + ter(tecNO_PERMISSION)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + envX(account_permission::accountPermissionSet( + Account::master, bobX, {"XChainCreateBridge"})); + envX.close(); + envX.require( + balance(Account::master, doorXBalance - drops(baseFee))); + doorXBalance = envX.balance(Account::master, XRP); + + envX( + bridge_create(bobX, jvBridge, XRP(1), XRP(100)), + onBehalfOf(Account::master)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + // set up signer on envX + envX(jtx::signers(Account::master, 1, signerXs)); + envX.close(); + envX.require( + balance(Account::master, doorXBalance - drops(baseFee))); + doorXBalance = envX.balance(Account::master, XRP); + } + + // XChainModifyBridge + { + env(bridge_modify(bob, jvBridge, XRP(2), XRP(200)), + onBehalfOf(door), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + env(account_permission::accountPermissionSet( + door, bob, {"XChainModifyBridge"})); + env.close(); + env.require(balance(door, doorBalance - drops(baseFee))); + doorBalance = env.balance(door, XRP); + + env(bridge_modify(bob, jvBridge, XRP(2), XRP(200)), + onBehalfOf(door)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + } + { + envX( + bridge_modify(bobX, jvBridge, XRP(2), XRP(200)), + onBehalfOf(Account::master), + ter(tecNO_PERMISSION)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + envX(account_permission::accountPermissionSet( + Account::master, bobX, {"XChainModifyBridge"})); + envX.close(); + envX.require( + balance(Account::master, doorXBalance - drops(baseFee))); + doorXBalance = envX.balance(Account::master, XRP); + + envX( + bridge_modify(bobX, jvBridge, XRP(2), XRP(200)), + onBehalfOf(Account::master)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + } + + // XChainAccountCreateCommit + { + env(sidechain_xchain_account_create( + bob, jvBridge, aliceX, XRP(10000), XRP(2)), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + env(account_permission::accountPermissionSet( + alice, bob, {"XChainAccountCreateCommit"})); + env.close(); + env.require(balance(alice, aliceBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + + env(sidechain_xchain_account_create( + bob, jvBridge, aliceX, XRP(10000), XRP(2)), + onBehalfOf(alice)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(alice, aliceBalance - XRP(10000) - XRP(2))); + env.require(balance(door, doorBalance + XRP(10000) + XRP(2))); + bobBalance = env.balance(bob, XRP); + aliceBalance = env.balance(alice, XRP); + doorBalance = env.balance(door, XRP); + } + + // XChainAddAccountCreateAttestation + { + envX( + create_account_attestation( + bobX, + jvBridge, + alice, + XRP(10000), + XRP(2), + rewardX, + true, + 1, + aliceX, + signerXs[0]), + onBehalfOf(attesterX), + ter(tecNO_PERMISSION)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + envX(account_permission::accountPermissionSet( + attesterX, bobX, {"XChainAddAccountCreateAttestation"})); + envX.close(); + envX.require(balance(attesterX, attesterXBalance - drops(baseFee))); + attesterXBalance = envX.balance(attesterX, XRP); + + envX( + create_account_attestation( + bobX, + jvBridge, + alice, + XRP(10000), + XRP(2), + rewardX, + true, + 1, + aliceX, + signerXs[0]), + onBehalfOf(attesterX)); + envX.close(); + BEAST_EXPECT(envX.le(aliceX)); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + envX.require( + balance(Account::master, doorXBalance - XRP(10000) - XRP(2))); + envX.require(balance(aliceX, aliceXBalance + XRP(10000))); + envX.require(balance(rewardX, rewardXBalance + XRP(2))); + bobXBalance = envX.balance(bobX, XRP); + doorXBalance = envX.balance(Account::master, XRP); + aliceXBalance = envX.balance(aliceX, XRP); + rewardXBalance = envX.balance(rewardX, XRP); + } + envX.memoize(aliceX); + + // XChainCreateClaimID + { + envX( + xchain_create_claim_id(bobX, jvBridge, XRP(2), alice), + onBehalfOf(carolX), + ter(tecNO_PERMISSION)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + envX(account_permission::accountPermissionSet( + carolX, bobX, {"XChainCreateClaimID"})); + envX.close(); + envX.require(balance(carolX, carolXBalance - drops(baseFee))); + carolXBalance = envX.balance(carolX, XRP); + + envX( + xchain_create_claim_id(bobX, jvBridge, XRP(2), alice), + onBehalfOf(carolX)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + BEAST_EXPECT( + !!envX.le(keylet::xChainClaimID(STXChainBridge(jvBridge), 1))); + } + + // XChainCommit + { + env(xchain_commit(bob, jvBridge, 1, XRP(20000), std::nullopt), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + env(account_permission::accountPermissionSet( + alice, bob, {"XChainCommit"})); + env.close(); + env.require(balance(alice, aliceBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + + env(xchain_commit(bob, jvBridge, 1, XRP(20000), std::nullopt), + onBehalfOf(alice)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(alice, aliceBalance - XRP(20000))); + env.require(balance(door, doorBalance + XRP(20000))); + bobBalance = env.balance(bob, XRP); + aliceBalance = env.balance(alice, XRP); + doorBalance = env.balance(door, XRP); + } + + // XChainAddClaimAttestation + { + envX( + claim_attestation( + bobX, + jvBridge, + alice, + XRP(20000), + rewardX, + true, + 1, + std::nullopt, + signerX), + onBehalfOf(attesterX), + ter(tecNO_PERMISSION)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + envX(account_permission::accountPermissionSet( + attesterX, bobX, {"XChainAddClaimAttestation"})); + envX.close(); + envX.require(balance(attesterX, attesterXBalance - drops(baseFee))); + attesterXBalance = envX.balance(attesterX, XRP); + + envX( + claim_attestation( + bobX, + jvBridge, + alice, + XRP(20000), + rewardX, + true, + 1, + std::nullopt, + signerX), + onBehalfOf(attesterX)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + } + + // XChainClaim + { + envX( + xchain_claim(bobX, jvBridge, 1, XRP(20000), aliceX), + onBehalfOf(carolX), + ter(tecNO_PERMISSION)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + bobXBalance = envX.balance(bobX, XRP); + + envX(account_permission::accountPermissionSet( + carolX, bobX, {"XChainClaim"})); + envX.close(); + envX.require(balance(carolX, carolXBalance - drops(baseFee))); + carolXBalance = envX.balance(carolX, XRP); + + envX( + xchain_claim(bobX, jvBridge, 1, XRP(20000), aliceX), + onBehalfOf(carolX)); + envX.close(); + envX.require(balance(bobX, bobXBalance - drops(baseFee))); + envX.require(balance(carolX, carolXBalance - XRP(2))); + envX.require(balance(Account::master, doorXBalance - XRP(20000))); + envX.require(balance(rewardX, rewardXBalance + XRP(2))); + envX.require(balance(aliceX, aliceXBalance + XRP(20000))); + bobXBalance = envX.balance(bobX, XRP); + carolXBalance = envX.balance(carolX, XRP); + doorXBalance = envX.balance(Account::master, XRP); + rewardXBalance = envX.balance(rewardX, XRP); + aliceXBalance = envX.balance(aliceX, XRP); + BEAST_EXPECT( + !envX.le(keylet::xChainClaimID(STXChainBridge(jvBridge), 1))); + } + + env.require(balance(door, doorBalance)); + env.require(balance(alice, aliceBalance)); + env.require(balance(bob, bobBalance)); + envX.require(balance(Account::master, doorXBalance)); + envX.require(balance(attesterX, attesterXBalance)); + envX.require(balance(signerX, signerXBalance)); + envX.require(balance(rewardX, rewardXBalance)); + envX.require(balance(aliceX, aliceXBalance)); + envX.require(balance(bobX, bobXBalance)); + envX.require(balance(carolX, carolXBalance)); + } + + void + testPaymentChannel(FeatureBitset features) + { + testcase("test PaymentChannel transactions"); + using namespace jtx; + + auto signClaimAuth = [&](PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt) { + Serializer msg; + serializePayChanAuthorization(msg, channel, authAmt.xrp()); + return sign(pk, sk, msg.slice()); + }; + + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(account_permission::accountPermissionSet( + alice, + carol, + {"PaymentChannelCreate", + "PaymentChannelFund", + "PaymentChannelClaim"})); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, carol) == 0); + + auto const settleDelay = std::chrono::seconds(100); + auto const chan = channel(alice, bob, env.seq(alice)); + + // carol creates channel on behalf of alice + // since carol will send the transaction on behalf of alice, public + // key is alice's key + auto const pkAlice = alice.pk(); + env(create(carol, bob, XRP(1000), settleDelay, pkAlice), + onBehalfOf(alice)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + BEAST_EXPECT(ownerCount(env, carol) == 0); + BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); + + { + // carol fund channel on behalf of alice + auto const preAlice = env.balance(alice); + auto const preCarol = env.balance(carol); + env(fund(carol, chan, XRP(1000)), onBehalfOf(alice)); + auto const feeDrops = env.current()->fees().base; + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000)); + BEAST_EXPECT(env.balance(carol) == preCarol - feeDrops); + BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(2000)); + } + + env(account_permission::accountPermissionSet( + bob, + carol, + {"PaymentChannelCreate", + "PaymentChannelFund", + "PaymentChannelClaim"})); + + { + // carol claim on behalf of bob + auto preBob = env.balance(bob); + auto preCarol = env.balance(carol); + auto const delta = XRP(500); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP(100); + auto const sig = + signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(carol, chan, reqBal, authAmt, Slice(sig), alice.pk()), + onBehalfOf(bob)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(bob) == preBob + delta); + BEAST_EXPECT(env.balance(carol) == preCarol - feeDrops); + } + } + } + + void + testPayment(FeatureBitset features) + { + testcase("test payment"); + using namespace jtx; + + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + + XRPAmount const baseFee{env.current()->fees().base}; + + // use different initial amout to distinguish the source balance + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(30000), carol); + env.close(); + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + auto carolBalance = env.balance(carol, XRP); + + env(account_permission::accountPermissionSet(alice, bob, {"Payment"})); + env.close(); + env.require(balance(alice, aliceBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + + // bob pay 50 XRP to carol on behalf of alice + env(pay(bob, carol, XRP(50)), onBehalfOf(alice)); + env.close(); + env.require(balance(alice, aliceBalance - XRP(50))); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(carol, carolBalance + XRP(50))); + aliceBalance = env.balance(alice, XRP); + bobBalance = env.balance(bob, XRP); + carolBalance = env.balance(carol, XRP); + + // bob pay 50 XRP to bob self on behalf of alice + env(pay(bob, bob, XRP(50)), onBehalfOf(alice)); + env.close(); + env.require(balance(alice, aliceBalance - XRP(50))); + env.require(balance(bob, bobBalance + XRP(50) - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + bobBalance = env.balance(bob, XRP); + + // bob pay 50 XRP to alice self on behalf of alice + env(pay(bob, alice, XRP(50)), onBehalfOf(alice), ter(temREDUNDANT)); + env.close(); + + // final balance check + env.require(balance(alice, aliceBalance)); + env.require(balance(bob, bobBalance)); + env.require(balance(carol, carolBalance)); + } + + void + testPaymentGranular(FeatureBitset features) + { + testcase("test payment granular"); + using namespace jtx; + + // test PaymentMint and PaymentBurn + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account gw{"gateway"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(40000), gw); + env.trust(USD(200), alice); + env.close(); + + XRPAmount const baseFee{env.current()->fees().base}; + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + auto gwBalance = env.balance(gw, XRP); + + // gw gives bob burn permission + env(account_permission::accountPermissionSet( + gw, bob, {"PaymentBurn"})); + env.close(); + env.require(balance(gw, gwBalance - drops(baseFee))); + gwBalance = env.balance(gw, XRP); + // bob can not mint on behalf of gw because he only has burn + // permission + env(pay(bob, alice, USD(50)), + onBehalfOf(gw), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // gw gives bob mint permission, alice gives bob burn permission + env(account_permission::accountPermissionSet( + gw, bob, {"PaymentMint"})); + env(account_permission::accountPermissionSet( + alice, bob, {"PaymentBurn"})); + env.close(); + env.require(balance(alice, aliceBalance - drops(baseFee))); + env.require(balance(gw, gwBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + gwBalance = env.balance(gw, XRP); + + // can not send XRP + env(pay(bob, alice, XRP(50)), + onBehalfOf(gw), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // mint 50 USD + env(pay(bob, alice, USD(50)), onBehalfOf(gw)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, alice["USD"](-50))); + env.require(balance(alice, USD(50))); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + bobBalance = env.balance(bob, XRP); + + // burn 30 USD + env(pay(bob, gw, USD(30)), onBehalfOf(alice)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, alice["USD"](-20))); + env.require(balance(alice, USD(20))); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + bobBalance = env.balance(bob, XRP); + + // final balance check + env.require(balance(alice, aliceBalance)); + env.require(balance(bob, bobBalance)); + env.require(balance(gw, gwBalance)); + } + + // test PaymentMint won't affect Payment transaction level delegation. + { + Env env(*this, features); + Account alice{"alice"}; + Account bob{"bob"}; + Account gw{"gateway"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(40000), gw); + env.trust(USD(200), alice); + env.close(); + + XRPAmount const baseFee{env.current()->fees().base}; + + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + auto gwBalance = env.balance(gw, XRP); + + // gw gives bob PaymentBurn permission + env(account_permission::accountPermissionSet( + gw, bob, {"PaymentBurn"})); + env.close(); + env.require(balance(gw, gwBalance - drops(baseFee))); + gwBalance = env.balance(gw, XRP); + + // bob can not mint on behalf of gw because he only has burn + // permission + env(pay(bob, alice, USD(50)), + onBehalfOf(gw), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // gw gives bob Payment permission as well + env(account_permission::accountPermissionSet( + gw, bob, {"PaymentBurn", "Payment"})); + env.close(); + + // bob now can mint on behalf of gw + env(pay(bob, alice, USD(50)), onBehalfOf(gw)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, alice["USD"](-50))); + env.require(balance(alice, USD(50))); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + bobBalance = env.balance(bob, XRP); + } + } + + void + testOffer(FeatureBitset features) + { + testcase("test offer"); + using namespace jtx; + + Env env(*this, features); + + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(100), alice); + env.close(); + env(pay(gw, alice, USD(50))); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"OfferCreate", "OfferCancel"})); + env.close(); + + // add some distance for alice's sequence + for (int i = 0; i < 20; i++) + { + env(noop(alice)); + } + env.close(); + + // create offer + auto aliceSeq = env.seq(alice); + auto bobSeq = env.seq(bob); + auto const offer1Seq = aliceSeq; + env(offer(bob, XRP(500), USD(100)), onBehalfOf(alice)); + env.close(); + env.require(offers(alice, 1)); + BEAST_EXPECT(isOffer(env, alice, XRP(500), USD(100))); + aliceSeq++; + bobSeq++; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + // create offer while cancelling previous one + auto const offer2Seq = aliceSeq; + env(offer(bob, XRP(300), USD(100)), + json(jss::OfferSequence, offer1Seq), + onBehalfOf(alice)); + env.close(); + env.require(offers(alice, 1)); + BEAST_EXPECT( + isOffer(env, alice, XRP(300), USD(100)) && + !isOffer(env, alice, XRP(500), USD(100))); + aliceSeq++; + bobSeq++; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + // cancel offer + env(offer_cancel(bob, offer2Seq), onBehalfOf(alice)); + env.close(); + env.require(offers(alice, 0)); + BEAST_EXPECT(!isOffer(env, alice, XRP(300), USD(100))); + aliceSeq++; + bobSeq++; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + } + + void + testTicket(FeatureBitset features) + { + testcase("test ticket"); + using namespace jtx; + + Env env(*this, features); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"TicketCreate"})); + env.close(); + env.require(owners(alice, 1), tickets(alice, 0)); + env.require(owners(bob, 0), tickets(bob, 0)); + + // add some distance for alice's sequence + for (int i = 0; i < 20; i++) + { + env(noop(alice)); + } + env.close(); + + auto aliceSeq = env.seq(alice); + auto bobSeq = env.seq(bob); + + // create ticket + env(ticket::create(bob, 1), onBehalfOf(alice)); + env.close(); + auto aliceTicket1 = aliceSeq + 1; + aliceSeq += 2; + bobSeq++; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(alice, 2), tickets(alice, 1)); + env.require(owners(bob, 0), tickets(bob, 0)); + + // use ticket to create tickets + env(ticket::create(bob, 3), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceTicket1)); + env.close(); + auto aliceTicket2 = aliceSeq; + auto aliceTicket3 = aliceSeq + 1; + auto aliceTicket4 = aliceSeq + 2; + aliceSeq += 3; + bobSeq++; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(alice, 4), tickets(alice, 3)); + env.require(owners(bob, 0), tickets(bob, 0)); + + // use tickets + env(noop(alice), ticket::use(aliceTicket2)); + env(noop(alice), ticket::use(aliceTicket3)); + env(noop(alice), ticket::use(aliceTicket4)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(alice, 1), tickets(alice, 0)); + env.require(owners(bob, 0), tickets(bob, 0)); + + // create ticket for delegated account + env(ticket::create(bob, 2)); + env.close(); + auto bobTicket1 = bobSeq + 1; + auto bobTicket2 = bobSeq + 2; + bobSeq += 3; + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(bob, 2), tickets(bob, 2)); + + // create ticket with delegated ticket + env(ticket::create(bob, 1), ticket::use(bobTicket1), onBehalfOf(alice)); + env.close(); + aliceTicket1 = aliceSeq + 1; + aliceSeq += 2; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(alice, 2), tickets(alice, 1)); + env.require(owners(bob, 1), tickets(bob, 1)); + + // use ticket to create tickets with delegated ticket + env(ticket::create(bob, 3), + ticket::use(bobTicket2), + onBehalfOf(alice), + delegateSequence(0), + delegateTicketSequence(aliceTicket1)); + env.close(); + aliceTicket2 = aliceSeq; + aliceTicket3 = aliceSeq + 1; + aliceTicket4 = aliceSeq + 2; + aliceSeq += 3; + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(alice, 4), tickets(alice, 3)); + env.require(owners(bob, 0), tickets(bob, 0)); + + // use tickets + env(noop(alice), ticket::use(aliceTicket2)); + env(noop(alice), ticket::use(aliceTicket3)); + env(noop(alice), ticket::use(aliceTicket4)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(owners(alice, 1), tickets(alice, 0)); + env.require(owners(bob, 0), tickets(bob, 0)); + } + + void + testTrustSetGranular(FeatureBitset features) + { + testcase("test TrustSet granular permissions"); + using namespace jtx; + + // test TrustlineUnfreeze, TrustlineFreeze and TrustlineAuthorize + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + env(fset(alice, asfRequireAuth)); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"TrustlineUnfreeze"})); + env.close(); + // bob can not create trustline on behalf of alice because he only + // has unfreeze permission + env(trust(bob, gw["USD"](50), 0), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env.close(); + + // alice creates trustline by herself + env(trust(alice, gw["USD"](50), 0)); + env.close(); + + // unsupported flags + env(trust(bob, gw["USD"](50), tfSetNoRipple), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env(trust(bob, gw["USD"](50), tfClearNoRipple), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env.close(); + + // supported flags with wrong permission + env(trust(bob, gw["USD"](50), tfSetfAuth), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env(trust(bob, gw["USD"](50), tfSetFreeze), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env.close(); + env(account_permission::accountPermissionSet( + alice, bob, {"TrustlineAuthorize"})); + env.close(); + env(trust(bob, gw["USD"](50), tfClearFreeze), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env.close(); + + // supported flags with correct permission + env(trust(bob, gw["USD"](50), tfSetfAuth), onBehalfOf(alice)); + env.close(); + env(account_permission::accountPermissionSet( + alice, bob, {"TrustlineAuthorize", "TrustlineFreeze"})); + env.close(); + env(trust(bob, gw["USD"](50), tfSetFreeze), onBehalfOf(alice)); + env.close(); + env(account_permission::accountPermissionSet( + alice, bob, {"TrustlineAuthorize", "TrustlineUnfreeze"})); + env.close(); + env(trust(bob, gw["USD"](50), tfClearFreeze), onBehalfOf(alice)); + env.close(); + // but bob can not freeze trustline because he no longer has freeze + // permission + env(trust(bob, gw["USD"](50), tfSetFreeze), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + + // cannot update LimitAmout with granular permission, both high and + // low account + env(trust(gw, alice["USD"](50), 0)); + env(account_permission::accountPermissionSet( + gw, bob, {"TrustlineUnfreeze"})); + env.close(); + env(trust(bob, gw["USD"](100)), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env(trust(bob, alice["USD"](100)), + onBehalfOf(gw), + ter(tecNO_PERMISSION)); + } + + // test mix of transaction level delegation and granular delegation + { + Env env(*this, features); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + env(fset(alice, asfRequireAuth)); + env.close(); + + env(account_permission::accountPermissionSet( + alice, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer"})); + env.close(); + env(trust(bob, gw["USD"](50), 0), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + + // add TrustSet permission and some unrelated permission + env(account_permission::accountPermissionSet( + alice, + bob, + {"TrustlineUnfreeze", + "NFTokenCreateOffer", + "TrustSet", + "AccountTransferRateSet"})); + env.close(); + env(trust(bob, gw["USD"](50), 0), onBehalfOf(alice)); + env.close(); + + // since bob has TrustSet permission, he does not need + // TrustlineFreeze granular permission to freeze the trustline + env(trust(bob, gw["USD"](50), tfSetFreeze), onBehalfOf(alice)); + env(trust(bob, gw["USD"](50), tfClearFreeze), onBehalfOf(alice)); + env(trust(bob, gw["USD"](50), tfSetNoRipple), onBehalfOf(alice)); + env(trust(bob, gw["USD"](50), tfClearNoRipple), onBehalfOf(alice)); + env(trust(bob, gw["USD"](50), tfSetfAuth), onBehalfOf(alice)); + } + } + + void + testAccountSetGranular(FeatureBitset features) + { + testcase("test AccountSet granular permissions"); + using namespace jtx; + + // test AccountDomainSet, AccountEmailHashSet, + // AccountMessageKeySet,AccountTransferRateSet, and AccountTickSizeSet + // granular permissions + { + Env env(*this, features); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // alice gives bob some random permission + env(account_permission::accountPermissionSet( + alice, bob, {"TrustlineUnfreeze"})); + env.close(); + + // bob does not have permission to set domain + // on behalf of alice + std::string const domain = "example.com"; + auto jt = noop(bob); + jt[sfDomain.fieldName] = strHex(domain); + jt[sfOnBehalfOf.fieldName] = alice.human(); + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountDomainSet to bob + env(account_permission::accountPermissionSet( + alice, bob, {"AccountDomainSet"})); + env.close(); + + // bob set account domain on behalf of alice + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // bob can reset domain + jt[sfDomain.fieldName] = ""; + env(jt); + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain)); + + // bob tries to update domain and set email hash, + // but he does not have permission to set email hash + jt[sfDomain.fieldName] = strHex(domain); + std::string const mh("5F31A79367DC3137FADA860C05742EE6"); + jt[sfEmailHash.fieldName] = mh; + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountEmailHashSet to bob + env(account_permission::accountPermissionSet( + alice, bob, {"AccountDomainSet", "AccountEmailHashSet"})); + env.close(); + env(jt); + BEAST_EXPECT(to_string((*env.le(alice))[sfEmailHash]) == mh); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // bob does not have permission to set message key for alice + auto const rkp = randomKeyPair(KeyType::ed25519); + jt[sfMessageKey.fieldName] = strHex(rkp.first.slice()); + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountMessageKeySet to bob + env(account_permission::accountPermissionSet( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + // bob can set message key for alice + env(jt); + BEAST_EXPECT( + strHex((*env.le(alice))[sfMessageKey]) == + strHex(rkp.first.slice())); + jt[sfMessageKey.fieldName] = ""; + env(jt); + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfMessageKey)); + + // bob does not have permission to set transfer rate for alice + env(rate(bob, 2.0), onBehalfOf(alice), ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountTransferRateSet to bob + env(account_permission::accountPermissionSet( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet", + "AccountTransferRateSet"})); + env.close(); + env(rate(bob, 2.0), onBehalfOf(alice)); + BEAST_EXPECT((*env.le(alice))[sfTransferRate] == 2000000000); + + // bob does not have permission to set ticksize for alice + jt[sfTickSize.fieldName] = 8; + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountTickSizeSet to bob + env(account_permission::accountPermissionSet( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet", + "AccountTransferRateSet", + "AccountTickSizeSet"})); + env.close(); + env(jt); + BEAST_EXPECT((*env.le(alice))[sfTickSize] == 8); + + // can not set asfRequireAuth flag for alice + // get tecOWNERS because alice owns account permission object + env(fset(bob, asfRequireAuth), onBehalfOf(alice), ter(tecOWNERS)); + + // reset account permission will delete the account permission + // object + env(account_permission::accountPermissionSet(alice, bob, {})); + // bib still does not have permission to set asfRequireAuth for + // alice + env(fset(bob, asfRequireAuth), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + // alice can set for herself + env(fset(alice, asfRequireAuth)); + env.require(flags(alice, asfRequireAuth)); + env.close(); + + // can not update tick size because bob no longer has permission + jt[sfTickSize.fieldName] = 7; + env(jt, ter(tecNO_PERMISSION)); + + // bob does not have permission to set wallet locater for alice + std::string const locator = + "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF" + "05"; + auto jt2 = noop(bob); + jt2[sfDomain.fieldName] = strHex(domain); + jt2[sfOnBehalfOf.fieldName] = alice.human(); + jt2[sfWalletLocator.fieldName] = locator; + env(jt2, ter(tecNO_PERMISSION)); + } + + // can not set AccountSet flags on behalf of other account + { + Env env(*this, features); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto testSetClearFlag = [&](std::uint32_t flag) { + // bob can not set flag on behalf of alice + env(fset(bob, flag), onBehalfOf(alice), ter(tecNO_PERMISSION)); + // alice set by herself + env(fset(alice, flag)); + env.close(); + env.require(flags(alice, flag)); + // bob can not clear on behalf of alice + env(fclear(bob, flag), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + }; + + // testSetClearFlag(asfNoFreeze); + testSetClearFlag(asfRequireAuth); + testSetClearFlag(asfAllowTrustLineClawback); + + // alice gives some granular permissions to bob + env(account_permission::accountPermissionSet( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + testSetClearFlag(asfDefaultRipple); + testSetClearFlag(asfDepositAuth); + testSetClearFlag(asfDisallowIncomingCheck); + testSetClearFlag(asfDisallowIncomingNFTokenOffer); + testSetClearFlag(asfDisallowIncomingPayChan); + testSetClearFlag(asfDisallowIncomingTrustline); + testSetClearFlag(asfDisallowXRP); + testSetClearFlag(asfRequireDest); + testSetClearFlag(asfGlobalFreeze); + + // bob can not set asfAccountTxnID on behalf of alice + env(fset(bob, asfAccountTxnID), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + env(fset(alice, asfAccountTxnID)); + env.close(); + BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID)); + env(fclear(bob, asfAccountTxnID), + onBehalfOf(alice), + ter(tecNO_PERMISSION)); + + // bob can not set asfAuthorizedNFTokenMinter on behalf of alice + Json::Value jt = fset(bob, asfAuthorizedNFTokenMinter); + jt[sfOnBehalfOf.fieldName] = alice.human(); + jt[sfNFTokenMinter.fieldName] = bob.human(); + env(jt, ter(tecNO_PERMISSION)); + + // bob gives alice some permissions + env(account_permission::accountPermissionSet( + bob, + alice, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + // since we can not set asfNoFreeze if asfAllowTrustLineClawback is + // set, which can not be clear either. Test alice set asfNoFreeze on + // behalf of bob. + env(fset(alice, asfNoFreeze), + onBehalfOf(bob), + ter(tecNO_PERMISSION)); + env(fset(bob, asfNoFreeze)); + env.close(); + env.require(flags(bob, asfNoFreeze)); + // alice can not clear on behalf of bob + env(fclear(alice, asfNoFreeze), + onBehalfOf(bob), + ter(tecNO_PERMISSION)); + + // bob can not set asfDisableMaster on behalf of alice + Account const bobKey{"bobKey", KeyType::secp256k1}; + env(regkey(bob, bobKey)); + env.close(); + env(fset(bob, asfDisableMaster), + onBehalfOf(alice), + sig(bob), + ter(tecNO_PERMISSION)); + } + } + + void + testPath(FeatureBitset features) + { + testcase("test paths"); + using namespace jtx; + Env env( + *this, + envconfig([](std::unique_ptr cfg) { + cfg->PATH_SEARCH_OLD = 7; + cfg->PATH_SEARCH = 7; + cfg->PATH_SEARCH_MAX = 10; + return cfg; + }), + features); + + auto const gw = Account("gateway"); + auto const USD = gw["USD"]; + auto const gw2 = Account("gateway2"); + auto const gw2_USD = gw2["USD"]; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol, gw, gw2); + env.trust(USD(600), alice); + env.trust(gw2_USD(800), alice); + env.trust(USD(700), bob); + env.trust(gw2_USD(900), bob); + env.close(); + + env(account_permission::accountPermissionSet( + alice, carol, {"Payment"})); + env.close(); + + env(pay(gw, alice, USD(70))); + env(pay(gw2, alice, gw2_USD(70))); + env(pay(carol, bob, bob["USD"](140)), + paths(alice["USD"], alice.human()), + onBehalfOf(alice)); + env.close(); + env.require(balance(alice, USD(0))); + env.require(balance(alice, gw2_USD(0))); + env.require(balance(bob, USD(70))); + env.require(balance(bob, gw2_USD(70))); + env.require(balance(gw, alice["USD"](0))); + env.require(balance(gw, bob["USD"](-70))); + env.require(balance(gw2, alice["USD"](0))); + env.require(balance(gw2, bob["USD"](-70))); + } + + void + run() override + { + FeatureBitset const all{jtx::supported_amendments()}; + testFeatureDisabled(all - featureAccountPermission); + testInvalidRequest(all); + testAccountDelete(all); + testReserve(all); + testAccountPermissionSet(all); + testDelegateSequenceAndTicket(all); + testAMM(all); + testCheck(all); + testClawback(all); + testCredentials(all); + testDepositPreauth(all); + testDID(all); + testEscrow(all); + testMPToken(all); + testNFToken(all); + testOffer(all); + testOracle(all); + testPath(all); + testPayment(all); + testPaymentChannel(all); + testTicket(all); + testTrustSet(all); + testXChain(all); + testPaymentGranular(all); + testTrustSetGranular(all); + testAccountSetGranular(all); + testMPTokenIssuanceSetGranular(all); + } +}; +BEAST_DEFINE_TESTSUITE(AccountPermission, app, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 2c4f44ce79f..2e242951409 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -23,65 +23,6 @@ namespace ripple { namespace test { -namespace jtx { - -/** Set Expiration on a JTx. */ -class expiration -{ -private: - std::uint32_t const expry_; - -public: - explicit expiration(NetClock::time_point const& expiry) - : expry_{expiry.time_since_epoch().count()} - { - } - - void - operator()(Env&, JTx& jt) const - { - jt[sfExpiration.jsonName] = expry_; - } -}; - -/** Set SourceTag on a JTx. */ -class source_tag -{ -private: - std::uint32_t const tag_; - -public: - explicit source_tag(std::uint32_t tag) : tag_{tag} - { - } - - void - operator()(Env&, JTx& jt) const - { - jt[sfSourceTag.jsonName] = tag_; - } -}; - -/** Set DestinationTag on a JTx. */ -class dest_tag -{ -private: - std::uint32_t const tag_; - -public: - explicit dest_tag(std::uint32_t tag) : tag_{tag} - { - } - - void - operator()(Env&, JTx& jt) const - { - jt[sfDestinationTag.jsonName] = tag_; - } -}; - -} // namespace jtx -} // namespace test class Check_test : public beast::unit_test::suite { @@ -93,21 +34,6 @@ class Check_test : public beast::unit_test::suite return keylet::check(account, uSequence).key; } - // Helper function that returns the Checks on an account. - static std::vector> - checksOnAccount(test::jtx::Env& env, test::jtx::Account account) - { - std::vector> result; - forEachItem( - *env.current(), - account, - [&result](std::shared_ptr const& sle) { - if (sle && sle->getType() == ltCHECK) - result.push_back(sle); - }); - return result; - } - // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling @@ -212,8 +138,9 @@ class Check_test : public beast::unit_test::suite std::uint32_t const fromOwnerCount{ownerCount(env, from)}; std::uint32_t const toOwnerCount{ownerCount(env, to)}; - std::size_t const fromCkCount{checksOnAccount(env, from).size()}; - std::size_t const toCkCount{checksOnAccount(env, to).size()}; + std::size_t const fromCkCount{ + check::checksOnAccount(env, from).size()}; + std::size_t const toCkCount{check::checksOnAccount(env, to).size()}; env(check::create(from, to, XRP(2000))); env.close(); @@ -221,8 +148,10 @@ class Check_test : public beast::unit_test::suite env(check::create(from, to, USD(50))); env.close(); - BEAST_EXPECT(checksOnAccount(env, from).size() == fromCkCount + 2); - BEAST_EXPECT(checksOnAccount(env, to).size() == toCkCount + 2); + BEAST_EXPECT( + check::checksOnAccount(env, from).size() == fromCkCount + 2); + BEAST_EXPECT( + check::checksOnAccount(env, to).size() == toCkCount + 2); env.require(owners(from, fromOwnerCount + 2)); env.require( @@ -238,26 +167,28 @@ class Check_test : public beast::unit_test::suite // the expiration, they are just plopped into the ledger. So I'm // not looking at interactions. using namespace std::chrono_literals; - std::size_t const aliceCount{checksOnAccount(env, alice).size()}; - std::size_t const bobCount{checksOnAccount(env, bob).size()}; - env(check::create(alice, bob, USD(50)), expiration(env.now() + 1s)); + std::size_t const aliceCount{check::checksOnAccount(env, alice).size()}; + std::size_t const bobCount{check::checksOnAccount(env, bob).size()}; + env(check::create(alice, bob, USD(50)), + check::expiration(env.now() + 1s)); env.close(); - env(check::create(alice, bob, USD(50)), source_tag(2)); + env(check::create(alice, bob, USD(50)), check::source_tag(2)); env.close(); - env(check::create(alice, bob, USD(50)), dest_tag(3)); + env(check::create(alice, bob, USD(50)), check::dest_tag(3)); env.close(); env(check::create(alice, bob, USD(50)), invoice_id(uint256{4})); env.close(); env(check::create(alice, bob, USD(50)), - expiration(env.now() + 1s), - source_tag(12), - dest_tag(13), + check::expiration(env.now() + 1s), + check::source_tag(12), + check::dest_tag(13), invoice_id(uint256{4})); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == aliceCount + 5); - BEAST_EXPECT(checksOnAccount(env, bob).size() == bobCount + 5); + BEAST_EXPECT( + check::checksOnAccount(env, alice).size() == aliceCount + 5); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == bobCount + 5); // Use a regular key and also multisign to create a check. Account const alie{"alie", KeyType::ed25519}; @@ -272,8 +203,9 @@ class Check_test : public beast::unit_test::suite // alice uses her regular key to create a check. env(check::create(alice, bob, USD(50)), sig(alie)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == aliceCount + 6); - BEAST_EXPECT(checksOnAccount(env, bob).size() == bobCount + 6); + BEAST_EXPECT( + check::checksOnAccount(env, alice).size() == aliceCount + 6); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == bobCount + 6); // alice uses multisigning to create a check. XRPAmount const baseFeeDrops{env.current()->fees().base}; @@ -281,8 +213,9 @@ class Check_test : public beast::unit_test::suite msig(bogie, demon), fee(3 * baseFeeDrops)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == aliceCount + 7); - BEAST_EXPECT(checksOnAccount(env, bob).size() == bobCount + 7); + BEAST_EXPECT( + check::checksOnAccount(env, alice).size() == aliceCount + 7); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == bobCount + 7); } void @@ -325,8 +258,9 @@ class Check_test : public beast::unit_test::suite std::uint32_t const fromOwnerCount{ownerCount(env, from)}; std::uint32_t const toOwnerCount{ownerCount(env, to)}; - std::size_t const fromCkCount{checksOnAccount(env, from).size()}; - std::size_t const toCkCount{checksOnAccount(env, to).size()}; + std::size_t const fromCkCount{ + check::checksOnAccount(env, from).size()}; + std::size_t const toCkCount{check::checksOnAccount(env, to).size()}; env(check::create(from, to, XRP(2000)), ter(expected)); env.close(); @@ -337,8 +271,10 @@ class Check_test : public beast::unit_test::suite if (expected == tesSUCCESS) { BEAST_EXPECT( - checksOnAccount(env, from).size() == fromCkCount + 2); - BEAST_EXPECT(checksOnAccount(env, to).size() == toCkCount + 2); + check::checksOnAccount(env, from).size() == + fromCkCount + 2); + BEAST_EXPECT( + check::checksOnAccount(env, to).size() == toCkCount + 2); env.require(owners(from, fromOwnerCount + 2)); env.require( @@ -346,8 +282,9 @@ class Check_test : public beast::unit_test::suite return; } - BEAST_EXPECT(checksOnAccount(env, from).size() == fromCkCount); - BEAST_EXPECT(checksOnAccount(env, to).size() == toCkCount); + BEAST_EXPECT( + check::checksOnAccount(env, from).size() == fromCkCount); + BEAST_EXPECT(check::checksOnAccount(env, to).size() == toCkCount); env.require(owners(from, fromOwnerCount)); env.require(owners(to, to == from ? fromOwnerCount : toOwnerCount)); @@ -440,7 +377,7 @@ class Check_test : public beast::unit_test::suite // Bad expiration. env(check::create(alice, bob, USD(50)), - expiration(NetClock::time_point{}), + check::expiration(NetClock::time_point{}), ter(temBAD_EXPIRATION)); env.close(); @@ -456,7 +393,7 @@ class Check_test : public beast::unit_test::suite env(check::create(alice, bob, USD(50)), ter(tecDST_TAG_NEEDED)); env.close(); - env(check::create(alice, bob, USD(50)), dest_tag(11)); + env(check::create(alice, bob, USD(50)), check::dest_tag(11)); env.close(); env(fclear(bob, asfRequireDest)); @@ -539,12 +476,13 @@ class Check_test : public beast::unit_test::suite // Expired expiration. env(check::create(alice, bob, USD(50)), - expiration(env.now()), + check::expiration(env.now()), ter(tecEXPIRED)); env.close(); using namespace std::chrono_literals; - env(check::create(alice, bob, USD(50)), expiration(env.now() + 1s)); + env(check::create(alice, bob, USD(50)), + check::expiration(env.now() + 1s)); env.close(); // Insufficient reserve. @@ -586,8 +524,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, startBalance - drops(baseFeeDrops))); env.require(balance(bob, startBalance)); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 0); @@ -597,8 +535,8 @@ class Check_test : public beast::unit_test::suite balance(alice, startBalance - XRP(10) - drops(baseFeeDrops))); env.require( balance(bob, startBalance + XRP(10) - drops(baseFeeDrops))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 0); BEAST_EXPECT(ownerCount(env, bob) == 0); @@ -635,8 +573,8 @@ class Check_test : public beast::unit_test::suite env.require(balance(alice, reserve)); env.require(balance( bob, startBalance + checkAmount - drops(baseFeeDrops * 3))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 0); BEAST_EXPECT(ownerCount(env, bob) == 0); @@ -667,8 +605,8 @@ class Check_test : public beast::unit_test::suite env.require(balance(alice, reserve)); env.require(balance( bob, startBalance + checkAmount - drops(baseFeeDrops * 2 + 1))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 0); BEAST_EXPECT(ownerCount(env, bob) == 0); @@ -715,6 +653,7 @@ class Check_test : public beast::unit_test::suite // alice gets almost enough funds. bob tries and fails again. env(trust(alice, USD(20))); env.close(); + env(pay(gw, alice, USD(9.5))); env.close(); env(check::cash(bob, chkId1, USD(10)), ter(tecPATH_PARTIAL)); @@ -757,8 +696,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(10))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -773,8 +712,8 @@ class Check_test : public beast::unit_test::suite uint256 const chkId2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, USD(7))); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); // bob cashes the check for less than the face amount. That works, // consumes the check, and bob receives as much as he asked for. @@ -782,8 +721,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(2))); env.require(balance(bob, USD(8))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -794,16 +733,16 @@ class Check_test : public beast::unit_test::suite uint256 const chkId4{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, USD(2))); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 2); // bob cashes the second check for the face amount. env(check::cash(bob, chkId4, USD(2))); env.close(); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(10))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -813,8 +752,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(10))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -852,8 +791,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(10))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } @@ -894,8 +833,8 @@ class Check_test : public beast::unit_test::suite verifyDeliveredAmount(env, USD(8)); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(8))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 3); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 3); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 3); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 3); BEAST_EXPECT(ownerCount(env, alice) == 4); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -909,8 +848,8 @@ class Check_test : public beast::unit_test::suite verifyDeliveredAmount(env, USD(7)); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(8))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 2); BEAST_EXPECT(ownerCount(env, alice) == 3); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -924,8 +863,8 @@ class Check_test : public beast::unit_test::suite verifyDeliveredAmount(env, USD(6)); env.require(balance(alice, USD(2))); env.require(balance(bob, USD(6))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == 1); @@ -935,8 +874,8 @@ class Check_test : public beast::unit_test::suite verifyDeliveredAmount(env, USD(2)); env.require(balance(alice, USD(0))); env.require(balance(bob, USD(8))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } @@ -999,8 +938,8 @@ class Check_test : public beast::unit_test::suite env.require(balance(alice, USD(8) - bobGot)); env.require(balance(bob, bobGot)); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } @@ -1052,8 +991,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(7))); env.require(balance(bob, USD(1))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == 2); BEAST_EXPECT(ownerCount(env, bob) == signersCount + 1); @@ -1065,8 +1004,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(5))); env.require(balance(bob, USD(3))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == signersCount + 1); } @@ -1125,8 +1064,8 @@ class Check_test : public beast::unit_test::suite verifyDeliveredAmount(env, USD(100)); env.require(balance(alice, USD(1000 - 125))); env.require(balance(bob, USD(0 + 100))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 1); // Adjust gw's rate... env(rate(gw, 1.2)); @@ -1138,8 +1077,8 @@ class Check_test : public beast::unit_test::suite env.close(); env.require(balance(alice, USD(1000 - 125 - 60))); env.require(balance(bob, USD(0 + 100 + 50))); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); - BEAST_EXPECT(checksOnAccount(env, bob).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, bob).size() == 0); } void @@ -1412,7 +1351,8 @@ class Check_test : public beast::unit_test::suite using namespace std::chrono_literals; uint256 const chkIdExp{getCheckIndex(alice, env.seq(alice))}; - env(check::create(alice, bob, XRP(10)), expiration(env.now() + 1s)); + env(check::create(alice, bob, XRP(10)), + check::expiration(env.now() + 1s)); env.close(); uint256 const chkIdFroz1{getCheckIndex(alice, env.seq(alice))}; @@ -1436,7 +1376,7 @@ class Check_test : public beast::unit_test::suite env.close(); uint256 const chkIdHasDest2{getCheckIndex(alice, env.seq(alice))}; - env(check::create(alice, bob, USD(2)), dest_tag(7)); + env(check::create(alice, bob, USD(2)), check::dest_tag(7)); env.close(); // Same set of failing cases for both IOU and XRP check cashing. @@ -1689,30 +1629,33 @@ class Check_test : public beast::unit_test::suite using namespace std::chrono_literals; uint256 const chkIdNotExp1{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10)), - expiration(env.now() + 600s)); + check::expiration(env.now() + 600s)); env.close(); uint256 const chkIdNotExp2{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, USD(10)), - expiration(env.now() + 600s)); + check::expiration(env.now() + 600s)); env.close(); uint256 const chkIdNotExp3{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10)), - expiration(env.now() + 600s)); + check::expiration(env.now() + 600s)); env.close(); // Three checks that expire in one second. uint256 const chkIdExp1{getCheckIndex(alice, env.seq(alice))}; - env(check::create(alice, bob, USD(10)), expiration(env.now() + 1s)); + env(check::create(alice, bob, USD(10)), + check::expiration(env.now() + 1s)); env.close(); uint256 const chkIdExp2{getCheckIndex(alice, env.seq(alice))}; - env(check::create(alice, bob, XRP(10)), expiration(env.now() + 1s)); + env(check::create(alice, bob, XRP(10)), + check::expiration(env.now() + 1s)); env.close(); uint256 const chkIdExp3{getCheckIndex(alice, env.seq(alice))}; - env(check::create(alice, bob, USD(10)), expiration(env.now() + 1s)); + env(check::create(alice, bob, USD(10)), + check::expiration(env.now() + 1s)); env.close(); // Two checks to cancel using a regular key and using multisigning. @@ -1723,55 +1666,55 @@ class Check_test : public beast::unit_test::suite uint256 const chkIdMSig{getCheckIndex(alice, env.seq(alice))}; env(check::create(alice, bob, XRP(10))); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 11); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 11); BEAST_EXPECT(ownerCount(env, alice) == 11); // Creator, destination, and an outsider cancel the checks. env(check::cancel(alice, chkId1)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 10); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 10); BEAST_EXPECT(ownerCount(env, alice) == 10); env(check::cancel(bob, chkId2)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 9); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 9); BEAST_EXPECT(ownerCount(env, alice) == 9); env(check::cancel(zoe, chkId3), ter(tecNO_PERMISSION)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 9); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 9); BEAST_EXPECT(ownerCount(env, alice) == 9); // Creator, destination, and an outsider cancel unexpired checks. env(check::cancel(alice, chkIdNotExp1)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 8); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 8); BEAST_EXPECT(ownerCount(env, alice) == 8); env(check::cancel(bob, chkIdNotExp2)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 7); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 7); BEAST_EXPECT(ownerCount(env, alice) == 7); env(check::cancel(zoe, chkIdNotExp3), ter(tecNO_PERMISSION)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 7); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 7); BEAST_EXPECT(ownerCount(env, alice) == 7); // Creator, destination, and an outsider cancel expired checks. env(check::cancel(alice, chkIdExp1)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 6); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 6); BEAST_EXPECT(ownerCount(env, alice) == 6); env(check::cancel(bob, chkIdExp2)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 5); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 5); BEAST_EXPECT(ownerCount(env, alice) == 5); env(check::cancel(zoe, chkIdExp3)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 4); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 4); BEAST_EXPECT(ownerCount(env, alice) == 4); // Use a regular key and also multisign to cancel checks. @@ -1792,7 +1735,7 @@ class Check_test : public beast::unit_test::suite // alice uses her regular key to cancel a check. env(check::cancel(alice, chkIdReg), sig(alie)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 3); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 3); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 3); // alice uses multisigning to cancel a check. @@ -1801,18 +1744,18 @@ class Check_test : public beast::unit_test::suite msig(bogie, demon), fee(3 * baseFeeDrops)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 2); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 2); // Creator and destination cancel the remaining unexpired checks. env(check::cancel(alice, chkId3), sig(alice)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 1); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 1); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 1); env(check::cancel(bob, chkIdNotExp3)); env.close(); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); BEAST_EXPECT(ownerCount(env, alice) == signersCount + 0); } } @@ -1961,7 +1904,7 @@ class Check_test : public beast::unit_test::suite // Alice used four tickets but created four checks. env.require(owners(alice, 10)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 4); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 4); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(owners(bob, 10)); @@ -1974,7 +1917,7 @@ class Check_test : public beast::unit_test::suite env.require(owners(alice, 8)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 2); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(owners(bob, 8)); @@ -1987,7 +1930,7 @@ class Check_test : public beast::unit_test::suite env.require(owners(alice, 6)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); - BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(check::checksOnAccount(env, alice).size() == 0); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(balance(alice, USD(700))); env.require(balance(alice, drops(699'999'940))); @@ -2717,4 +2660,5 @@ class Check_test : public beast::unit_test::suite BEAST_DEFINE_TESTSUITE(Check, tx, ripple); +} // namespace test } // namespace ripple diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 481850562fd..45a421d81fe 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -32,26 +32,6 @@ namespace ripple { namespace test { - -static inline bool -checkVL( - std::shared_ptr const& sle, - SField const& field, - std::string const& expected) -{ - return strHex(expected) == strHex(sle->getFieldVL(field)); -} - -static inline Keylet -credentialKeylet( - test::jtx::Account const& subject, - test::jtx::Account const& issuer, - std::string_view credType) -{ - return keylet::credential( - subject.id(), issuer.id(), Slice(credType.data(), credType.size())); -} - struct Credentials_test : public beast::unit_test::suite { void @@ -71,7 +51,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Create for subject."); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); env.fund(XRP(5000), subject, issuer, other); env.close(); @@ -91,8 +72,9 @@ struct Credentials_test : public beast::unit_test::suite BEAST_EXPECT(!sleCred->getFieldU32(sfFlags)); BEAST_EXPECT(ownerCount(env, issuer) == 1); BEAST_EXPECT(!ownerCount(env, subject)); - BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); - BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + BEAST_EXPECT( + credentials::checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(credentials::checkVL(sleCred, sfURI, uri)); auto const jle = credentials::ledgerEntry(env, subject, issuer, credType); BEAST_EXPECT( @@ -124,8 +106,9 @@ struct Credentials_test : public beast::unit_test::suite BEAST_EXPECT(sleCred->getAccountID(sfIssuer) == issuer.id()); BEAST_EXPECT(!ownerCount(env, issuer)); BEAST_EXPECT(ownerCount(env, subject) == 1); - BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); - BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + BEAST_EXPECT( + credentials::checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(credentials::checkVL(sleCred, sfURI, uri)); BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted); } @@ -149,7 +132,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Create for themself."); - auto const credKey = credentialKeylet(issuer, issuer, credType); + auto const credKey = + credentials::credentialKeylet(issuer, issuer, credType); env(credentials::create(issuer, issuer, credType), credentials::uri(uri)); @@ -167,8 +151,9 @@ struct Credentials_test : public beast::unit_test::suite sleCred->getFieldU64(sfIssuerNode) == sleCred->getFieldU64(sfSubjectNode)); BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); - BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + BEAST_EXPECT( + credentials::checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(credentials::checkVL(sleCred, sfURI, uri)); auto const jle = credentials::ledgerEntry(env, issuer, issuer, credType); BEAST_EXPECT( @@ -223,7 +208,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete issuer before accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); @@ -259,7 +245,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete issuer after accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); env(credentials::accept(subject, issuer, credType)); @@ -297,7 +284,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete subject before accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); @@ -333,7 +321,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete subject after accept"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); env(credentials::create(subject, issuer, credType)); env.close(); env(credentials::accept(subject, issuer, credType)); @@ -371,7 +360,8 @@ struct Credentials_test : public beast::unit_test::suite { testcase("Delete by other"); - auto const credKey = credentialKeylet(subject, issuer, credType); + auto const credKey = + credentials::credentialKeylet(subject, issuer, credType); auto jv = credentials::create(subject, issuer, credType); uint32_t const t = env.current() ->info() @@ -416,7 +406,7 @@ struct Credentials_test : public beast::unit_test::suite env.close(); { auto const credKey = - credentialKeylet(subject, issuer, credType); + credentials::credentialKeylet(subject, issuer, credType); BEAST_EXPECT(!env.le(credKey)); BEAST_EXPECT(!ownerCount(env, subject)); BEAST_EXPECT(!ownerCount(env, issuer)); @@ -438,7 +428,7 @@ struct Credentials_test : public beast::unit_test::suite env.close(); { auto const credKey = - credentialKeylet(subject, issuer, credType); + credentials::credentialKeylet(subject, issuer, credType); BEAST_EXPECT(!env.le(credKey)); BEAST_EXPECT(!ownerCount(env, subject)); BEAST_EXPECT(!ownerCount(env, issuer)); diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp index 3f9cce1d33e..47447aece57 100644 --- a/src/test/app/DID_test.cpp +++ b/src/test/app/DID_test.cpp @@ -30,14 +30,6 @@ namespace ripple { namespace test { -bool -checkVL(Slice const& result, std::string expected) -{ - Serializer s; - s.addRaw(result); - return s.getString() == expected; -} - struct DID_test : public beast::unit_test::suite { void @@ -287,7 +279,7 @@ struct DID_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); BEAST_EXPECT(sleDID); - BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfURI], initialURI)); BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); } @@ -297,7 +289,7 @@ struct DID_test : public beast::unit_test::suite env(did::set(alice), did::uri(""), ter(tecEMPTY_DID)); BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); - BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfURI], initialURI)); BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); } @@ -308,8 +300,9 @@ struct DID_test : public beast::unit_test::suite env(did::set(alice), did::document(initialDocument)); BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); - BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); - BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT( + did::checkVL((*sleDID)[sfDIDDocument], initialDocument)); BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); } @@ -319,9 +312,10 @@ struct DID_test : public beast::unit_test::suite env(did::set(alice), did::data(initialData)); BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); - BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI)); - BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); - BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfURI], initialURI)); + BEAST_EXPECT( + did::checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfData], initialData)); } // Remove URI @@ -330,8 +324,9 @@ struct DID_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); - BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); - BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData)); + BEAST_EXPECT( + did::checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfData], initialData)); } // Remove Data @@ -340,7 +335,8 @@ struct DID_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); - BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument)); + BEAST_EXPECT( + did::checkVL((*sleDID)[sfDIDDocument], initialDocument)); BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); } @@ -350,7 +346,7 @@ struct DID_test : public beast::unit_test::suite env(did::set(alice), did::uri(secondURI), did::document("")); BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); - BEAST_EXPECT(checkVL((*sleDID)[sfURI], secondURI)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfURI], secondURI)); BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); } @@ -362,7 +358,8 @@ struct DID_test : public beast::unit_test::suite BEAST_EXPECT(ownerCount(env, alice) == 1); auto const sleDID = env.le(keylet::did(alice.id())); BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); - BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], secondDocument)); + BEAST_EXPECT( + did::checkVL((*sleDID)[sfDIDDocument], secondDocument)); BEAST_EXPECT(!sleDID->isFieldPresent(sfData)); } @@ -374,7 +371,7 @@ struct DID_test : public beast::unit_test::suite auto const sleDID = env.le(keylet::did(alice.id())); BEAST_EXPECT(!sleDID->isFieldPresent(sfURI)); BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument)); - BEAST_EXPECT(checkVL((*sleDID)[sfData], secondData)); + BEAST_EXPECT(did::checkVL((*sleDID)[sfData], secondData)); } // Delete DID diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 463c606dc61..9bb74ed9151 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -383,25 +383,6 @@ struct DepositAuth_test : public beast::unit_test::suite } }; -static Json::Value -ledgerEntryDepositPreauth( - jtx::Env& env, - jtx::Account const& acc, - std::vector const& auth) -{ - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = acc.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr(jvParams[jss::deposit_preauth][jss::authorized_credentials]); - for (auto const& o : auth) - { - arr.append(o.toLEJson()); - } - return env.rpc("json", "ledger_entry", to_string(jvParams)); -} - struct DepositPreauth_test : public beast::unit_test::suite { void @@ -878,8 +859,8 @@ struct DepositPreauth_test : public beast::unit_test::suite env(deposit::authCredentials(bob, {{issuer, credType}})); env.close(); - auto const jDP = - ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); + auto const jDP = deposit::ledgerEntryDepositPreauth( + env, bob, {{issuer, credType}}); BEAST_EXPECT( jDP.isObject() && jDP.isMember(jss::result) && !jDP[jss::result].isMember(jss::error) && @@ -1140,8 +1121,8 @@ struct DepositPreauth_test : public beast::unit_test::suite env(deposit::authCredentials(bob, {{issuer, credType}})); env.close(); - auto const jDP = - ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); + auto const jDP = deposit::ledgerEntryDepositPreauth( + env, bob, {{issuer, credType}}); BEAST_EXPECT( jDP.isObject() && jDP.isMember(jss::result) && !jDP[jss::result].isMember(jss::error) && @@ -1174,8 +1155,8 @@ struct DepositPreauth_test : public beast::unit_test::suite { env(deposit::unauthCredentials(bob, {{issuer, credType}})); env.close(); - auto const jDP = - ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); + auto const jDP = deposit::ledgerEntryDepositPreauth( + env, bob, {{issuer, credType}}); BEAST_EXPECT( jDP.isObject() && jDP.isMember(jss::result) && jDP[jss::result].isMember(jss::error) && @@ -1465,7 +1446,7 @@ struct DepositPreauth_test : public beast::unit_test::suite env.close(); auto const dp = - ledgerEntryDepositPreauth(env, stock, credentials); + deposit::ledgerEntryDepositPreauth(env, stock, credentials); auto const& authCred( dp[jss::result][jss::node]["AuthorizeCredentials"]); BEAST_EXPECT( diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index a56f0a45674..f3765b8de4a 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -811,6 +811,7 @@ class NFTokenBurnBaseUtil_test : public beast::unit_test::suite tesSUCCESS, env.current()->fees().base, tapNONE, + std::unordered_set{}, jlog}; // Verify that the last page is present and contains one NFT. @@ -855,6 +856,7 @@ class NFTokenBurnBaseUtil_test : public beast::unit_test::suite tesSUCCESS, env.current()->fees().base, tapNONE, + std::unordered_set{}, jlog}; // Verify that the middle page is present. diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index bc1cbba69c0..c3859dd9421 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -59,15 +59,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) { diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 52039f74aea..ae043345407 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -66,6 +66,7 @@ struct CreateArg std::optional seq = std::nullopt; std::optional ms = std::nullopt; std::optional err = std::nullopt; + std::optional onBehalfOf = std::nullopt; bool close = true; }; @@ -79,6 +80,7 @@ struct DepositArg std::optional flags = std::nullopt; std::optional> assets = std::nullopt; std::optional seq = std::nullopt; + std::optional onBehalfOf = std::nullopt; std::optional tfee = std::nullopt; std::optional err = std::nullopt; }; @@ -94,6 +96,7 @@ struct WithdrawArg std::optional> assets = std::nullopt; std::optional seq = std::nullopt; std::optional err = std::nullopt; + std::optional onBehalfOf = std::nullopt; }; struct VoteArg @@ -104,6 +107,7 @@ struct VoteArg std::optional seq = std::nullopt; std::optional> assets = std::nullopt; std::optional err = std::nullopt; + std::optional onBehalfOf = std::nullopt; }; struct BidArg @@ -114,6 +118,7 @@ struct BidArg std::vector authAccounts = {}; std::optional flags = std::nullopt; std::optional> assets = std::nullopt; + std::optional onBehalfOf = std::nullopt; }; /** Convenience class to test AMM functionality. @@ -151,6 +156,7 @@ class AMM std::optional seq = std::nullopt, std::optional ms = std::nullopt, std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt, bool close = true); AMM(Env& env, Account const& account, @@ -159,6 +165,12 @@ class AMM ter const& ter, bool log = false, bool close = true); + AMM(Env& env, + Account const& account, + STAmount const& asset1, + STAmount const& asset2, + Account const& onBehalfOf, + ter const& ter); AMM(Env& env, Account const& account, STAmount const& asset1, @@ -241,7 +253,8 @@ class AMM std::optional const& asset2InAmount = std::nullopt, std::optional const& maxEP = std::nullopt, std::optional const& flags = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); IOUAmount deposit( @@ -254,7 +267,8 @@ class AMM std::optional> const& assets, std::optional const& seq, std::optional const& tfee = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); IOUAmount deposit(DepositArg const& arg); @@ -287,7 +301,8 @@ class AMM STAmount const& asset1Out, std::optional const& asset2Out = std::nullopt, std::optional const& maxEP = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); IOUAmount withdraw( @@ -299,7 +314,8 @@ class AMM std::optional const& flags, std::optional> const& assets, std::optional const& seq, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); IOUAmount withdraw(WithdrawArg const& arg); @@ -311,7 +327,8 @@ class AMM std::optional const& flags = std::nullopt, std::optional const& seq = std::nullopt, std::optional> const& assets = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); void vote(VoteArg const& arg); @@ -364,7 +381,8 @@ class AMM void ammDelete( AccountID const& deleter, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); void setClose(bool close) @@ -389,7 +407,8 @@ class AMM std::uint32_t tfee = 0, std::optional const& flags = std::nullopt, std::optional const& seq = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); IOUAmount deposit( @@ -397,7 +416,8 @@ class AMM Json::Value& jv, std::optional> const& assets = std::nullopt, std::optional const& seq = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); IOUAmount withdraw( @@ -405,7 +425,8 @@ class AMM Json::Value& jv, std::optional const& seq, std::optional> const& assets = std::nullopt, - std::optional const& ter = std::nullopt); + std::optional const& ter = std::nullopt, + std::optional const& onBehalfOf = std::nullopt); void log(bool log) @@ -445,7 +466,8 @@ ammClawback( Account const& holder, Issue const& asset, Issue const& asset2, - std::optional const& amount); + std::optional const& amount, + std::optional const& onBehalfOf = std::nullopt); } // namespace amm } // namespace jtx diff --git a/src/test/jtx/AccountPermission.h b/src/test/jtx/AccountPermission.h new file mode 100644 index 00000000000..672b8feb930 --- /dev/null +++ b/src/test/jtx/AccountPermission.h @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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. +*/ +//============================================================================== + +#pragma once + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace account_permission { + +Json::Value +accountPermissionSet( + jtx::Account const& account, + jtx::Account const& authorize, + std::list const& permissions); + +Json::Value +ledgerEntry( + jtx::Env& env, + jtx::Account const& account, + jtx::Account const& authorize); + +} // namespace account_permission +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 81ba1b406a0..4615bd61ea8 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -50,6 +50,7 @@ struct JTx rpcException = std::nullopt; bool fill_fee = true; bool fill_seq = true; + bool fill_delegating_seq = true; bool fill_sig = true; bool fill_netid = true; std::shared_ptr stx; diff --git a/src/test/jtx/Oracle.h b/src/test/jtx/Oracle.h index 32ec0b2e859..5b674c20e24 100644 --- a/src/test/jtx/Oracle.h +++ b/src/test/jtx/Oracle.h @@ -73,6 +73,8 @@ struct CreateArg int fee = 10; std::optional err = std::nullopt; bool close = false; + std::optional onBehalfOf = std::nullopt; + std::optional sender = std::nullopt; }; // Typical defaults for Update @@ -90,6 +92,8 @@ struct UpdateArg std::optional seq = std::nullopt; int fee = 10; std::optional err = std::nullopt; + std::optional onBehalfOf = std::nullopt; + std::optional sender = std::nullopt; }; struct RemoveArg @@ -101,6 +105,8 @@ struct RemoveArg std::optional seq = std::nullopt; int fee = 10; std::optional const& err = std::nullopt; + std::optional onBehalfOf = std::nullopt; + std::optional sender = std::nullopt; }; // Simulate testStartTime as 10'000s from Ripple epoch time to make diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index d81551aa840..7b7702dc228 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -343,6 +343,71 @@ struct fulfillment } }; +struct onBehalfOf +{ +private: + jtx::Account onBehalfOf_; + +public: + explicit onBehalfOf(jtx::Account const& u) : onBehalfOf_(u) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[sfOnBehalfOf.jsonName] = onBehalfOf_.human(); + } +}; + +struct delegateSequence +{ +private: + bool manual_ = true; + std::optional num_; + +public: + explicit delegateSequence(autofill_t) : manual_(false) + { + } + + explicit delegateSequence(none_t) + { + } + + explicit delegateSequence(std::uint32_t num) : num_(num) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + if (!manual_) + return; + jtx.fill_delegating_seq = false; + if (num_) + jtx[jss::DelegateSequence] = *num_; + } +}; + +struct delegateTicketSequence +{ +private: + std::uint32_t ticketSeq_; + +public: + explicit delegateTicketSequence(std::uint32_t ticketSeq) + : ticketSeq_(ticketSeq) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[jss::DelegateTicketSequence] = ticketSeq_; + } +}; + /* Payment Channel */ /******************************************************************************/ @@ -401,6 +466,9 @@ channel(Account const& account, Account const& dst, std::uint32_t 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); diff --git a/src/test/jtx/check.h b/src/test/jtx/check.h index 6bad6b9db5e..d9585a41852 100644 --- a/src/test/jtx/check.h +++ b/src/test/jtx/check.h @@ -31,9 +31,68 @@ namespace jtx { /** Check operations. */ namespace check { +/** Set Expiration on a JTx. */ +class expiration +{ +private: + std::uint32_t const expry_; + +public: + explicit expiration(NetClock::time_point const& expiry) + : expry_{expiry.time_since_epoch().count()} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfExpiration.jsonName] = expry_; + } +}; + +/** Set SourceTag on a JTx. */ +class source_tag +{ +private: + std::uint32_t const tag_; + +public: + explicit source_tag(std::uint32_t tag) : tag_{tag} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfSourceTag.jsonName] = tag_; + } +}; + +/** Set DestinationTag on a JTx. */ +class dest_tag +{ +private: + std::uint32_t const tag_; + +public: + explicit dest_tag(std::uint32_t tag) : tag_{tag} + { + } + + void + operator()(Env&, JTx& jt) const + { + jt[sfDestinationTag.jsonName] = tag_; + } +}; + /** Cash a check requiring that a specific amount be delivered. */ Json::Value -cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount); +cash( + jtx::Account const& dest, + uint256 const& checkId, + STAmount const& amount, + std::optional const& onBehalfOf = std::nullopt); /** Type used to specify DeliverMin for cashing a check. */ struct DeliverMin @@ -49,11 +108,18 @@ Json::Value cash( jtx::Account const& dest, uint256 const& checkId, - DeliverMin const& atLeast); + DeliverMin const& atLeast, + std::optional const& onBehalfOf = std::nullopt); /** Cancel a check. */ Json::Value -cancel(jtx::Account const& dest, uint256 const& checkId); +cancel( + jtx::Account const& dest, + uint256 const& checkId, + std::optional const& onBehalfOf = std::nullopt); + +std::vector> +checksOnAccount(test::jtx::Env& env, test::jtx::Account account); } // namespace check diff --git a/src/test/jtx/credentials.h b/src/test/jtx/credentials.h index 2f5c63dccb8..4f17dad70d6 100644 --- a/src/test/jtx/credentials.h +++ b/src/test/jtx/credentials.h @@ -67,6 +67,18 @@ class ids } }; +bool +checkVL( + std::shared_ptr const& sle, + SField const& field, + std::string const& expected); + +Keylet +credentialKeylet( + test::jtx::Account const& subject, + test::jtx::Account const& issuer, + std::string_view credType); + Json::Value create( jtx::Account const& subject, diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h index 9bd73d383dd..48b79c62548 100644 --- a/src/test/jtx/deposit.h +++ b/src/test/jtx/deposit.h @@ -32,11 +32,17 @@ namespace deposit { /** Preauthorize for deposit. Invoke as deposit::auth. */ Json::Value -auth(Account const& account, Account const& auth); +auth( + Account const& account, + Account const& auth, + std::optional const& onBehalfOf = std::nullopt); /** Remove preauthorization for deposit. Invoke as deposit::unauth. */ Json::Value -unauth(Account const& account, Account const& unauth); +unauth( + Account const& account, + Account const& unauth, + std::optional const& onBehalfOf = std::nullopt); struct AuthorizeCredentials { @@ -76,6 +82,12 @@ unauthCredentials( jtx::Account const& account, std::vector const& auth); +Json::Value +ledgerEntryDepositPreauth( + jtx::Env& env, + jtx::Account const& acc, + std::vector const& auth); + } // namespace deposit } // namespace jtx diff --git a/src/test/jtx/did.h b/src/test/jtx/did.h index 0cffb60e527..6d3b1874a4d 100644 --- a/src/test/jtx/did.h +++ b/src/test/jtx/did.h @@ -92,8 +92,11 @@ class data }; Json::Value -del(jtx::Account const& account); +del(jtx::Account const& account, + std::optional const& onBehalfOf = std::nullopt); +bool +checkVL(Slice const& result, std::string expected); } // namespace did } // namespace jtx diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index c8887cdee4b..02c1059da78 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -83,6 +83,18 @@ class flags_helper case asfAllowTrustLineClawback: mask_ |= lsfAllowTrustLineClawback; break; + case asfDisallowIncomingCheck: + mask_ |= lsfDisallowIncomingCheck; + break; + case asfDisallowIncomingNFTokenOffer: + mask_ |= lsfDisallowIncomingNFTokenOffer; + break; + case asfDisallowIncomingPayChan: + mask_ |= lsfDisallowIncomingPayChan; + break; + case asfDisallowIncomingTrustline: + mask_ |= lsfDisallowIncomingTrustline; + break; default: Throw("unknown flag"); } diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 089d3508d70..e03f4019756 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -58,6 +58,7 @@ AMM::AMM( std::optional seq, std::optional ms, std::optional const& ter, + std::optional const& onBehalfOf, bool close) : env_(env) , creatorAccount_(account) @@ -72,7 +73,7 @@ AMM::AMM( , bidMax_() , msig_(ms) , fee_(fee) - , ammAccount_(create(tfee, flags, seq, ter)) + , ammAccount_(create(tfee, flags, seq, ter, onBehalfOf)) , lptIssue_(ripple::ammLPTIssue( asset1_.issue().currency, asset2_.issue().currency, @@ -99,10 +100,34 @@ AMM::AMM( std::nullopt, std::nullopt, ter, + std::nullopt, close) { } +AMM::AMM( + Env& env, + Account const& account, + STAmount const& asset1, + STAmount const& asset2, + Account const& onBehalfOf, + ter const& ter) + : AMM(env, + account, + asset1, + asset2, + false, + 0, + 0, + std::nullopt, + std::nullopt, + std::nullopt, + ter, + onBehalfOf, + false) +{ +} + AMM::AMM( Env& env, Account const& account, @@ -120,6 +145,7 @@ AMM::AMM( arg.seq, arg.ms, arg.err, + arg.onBehalfOf, arg.close) { } @@ -129,7 +155,8 @@ AMM::create( std::uint32_t tfee, std::optional const& flags, std::optional const& seq, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { Json::Value jv; jv[jss::Account] = creatorAccount_.human(); @@ -137,6 +164,8 @@ AMM::create( jv[jss::Amount2] = asset2_.getJson(JsonOptions::none); jv[jss::TradingFee] = tfee; jv[jss::TransactionType] = jss::AMMCreate; + if (onBehalfOf) + jv[jss::OnBehalfOf] = onBehalfOf->human(); if (flags) jv[jss::Flags] = *flags; if (fee_ != 0) @@ -393,17 +422,23 @@ AMM::deposit( Json::Value& jv, std::optional> const& assets, std::optional const& seq, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { auto const& acct = account ? *account : creatorAccount_; - auto const lpTokens = getLPTokensBalance(acct); + auto const lpTokens = + onBehalfOf ? getLPTokensBalance(*onBehalfOf) : getLPTokensBalance(acct); jv[jss::Account] = acct.human(); setTokens(jv, assets); jv[jss::TransactionType] = jss::AMMDeposit; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); + if (onBehalfOf) + jv[jss::OnBehalfOf] = onBehalfOf->human(); submit(jv, seq, ter); - return getLPTokensBalance(acct) - lpTokens; + auto const curLPTokens = + onBehalfOf ? getLPTokensBalance(*onBehalfOf) : getLPTokensBalance(acct); + return curLPTokens - lpTokens; } IOUAmount @@ -424,7 +459,8 @@ AMM::deposit( std::nullopt, std::nullopt, std::nullopt, - ter); + ter, + std::nullopt); } IOUAmount @@ -434,7 +470,8 @@ AMM::deposit( std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { assert(!(asset2In && maxEP)); return deposit( @@ -447,7 +484,8 @@ AMM::deposit( std::nullopt, std::nullopt, std::nullopt, - ter); + ter, + onBehalfOf); } IOUAmount @@ -461,7 +499,8 @@ AMM::deposit( std::optional> const& assets, std::optional const& seq, std::optional const& tfee, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { Json::Value jv; if (tokens) @@ -494,7 +533,7 @@ AMM::deposit( jvflags |= tfSingleAsset; } jv[jss::Flags] = jvflags; - return deposit(account, jv, assets, seq, ter); + return deposit(account, jv, assets, seq, ter, onBehalfOf); } IOUAmount @@ -510,7 +549,8 @@ AMM::deposit(DepositArg const& arg) arg.assets, arg.seq, arg.tfee, - arg.err); + arg.err, + arg.onBehalfOf); } IOUAmount @@ -519,17 +559,21 @@ AMM::withdraw( Json::Value& jv, std::optional const& seq, std::optional> const& assets, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { auto const& acct = account ? *account : creatorAccount_; - auto const lpTokens = getLPTokensBalance(acct); + auto const lpTokens = + onBehalfOf ? getLPTokensBalance(*onBehalfOf) : getLPTokensBalance(acct); jv[jss::Account] = acct.human(); setTokens(jv, assets); jv[jss::TransactionType] = jss::AMMWithdraw; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); submit(jv, seq, ter); - return lpTokens - getLPTokensBalance(acct); + auto const curLPTokens = + onBehalfOf ? getLPTokensBalance(*onBehalfOf) : getLPTokensBalance(acct); + return lpTokens - curLPTokens; } IOUAmount @@ -558,7 +602,8 @@ AMM::withdraw( STAmount const& asset1Out, std::optional const& asset2Out, std::optional const& maxEP, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { assert(!(asset2Out && maxEP)); return withdraw( @@ -570,7 +615,8 @@ AMM::withdraw( std::nullopt, std::nullopt, std::nullopt, - ter); + ter, + onBehalfOf); } IOUAmount @@ -583,7 +629,8 @@ AMM::withdraw( std::optional const& flags, std::optional> const& assets, std::optional const& seq, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { Json::Value jv; if (tokens) @@ -613,6 +660,8 @@ AMM::withdraw( else if (asset1Out) jvflags |= tfSingleAsset; } + if (onBehalfOf) + jv[jss::OnBehalfOf] = onBehalfOf->human(); jv[jss::Flags] = jvflags; return withdraw(account, jv, seq, assets, ter); } @@ -629,7 +678,8 @@ AMM::withdraw(WithdrawArg const& arg) arg.flags, arg.assets, arg.seq, - arg.err); + arg.err, + arg.onBehalfOf); } void @@ -639,7 +689,8 @@ AMM::vote( std::optional const& flags, std::optional const& seq, std::optional> const& assets, - std::optional const& ter) + std::optional const& ter, + std::optional const& onBehalfOf) { Json::Value jv; jv[jss::Account] = account ? account->human() : creatorAccount_.human(); @@ -650,13 +701,22 @@ AMM::vote( jv[jss::Flags] = *flags; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); + if (onBehalfOf) + jv[jss::OnBehalfOf] = onBehalfOf->human(); submit(jv, seq, ter); } void AMM::vote(VoteArg const& arg) { - return vote(arg.account, arg.tfee, arg.flags, arg.seq, arg.assets, arg.err); + return vote( + arg.account, + arg.tfee, + arg.flags, + arg.seq, + arg.assets, + arg.err, + arg.onBehalfOf); } Json::Value @@ -720,6 +780,8 @@ AMM::bid(BidArg const& arg) jv[jss::TransactionType] = jss::AMMBid; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); + if (arg.onBehalfOf) + jv[jss::OnBehalfOf] = arg.onBehalfOf->human(); return jv; } @@ -788,7 +850,10 @@ AMM::expectAuctionSlot(auto&& cb) const } void -AMM::ammDelete(AccountID const& deleter, std::optional const& ter) +AMM::ammDelete( + AccountID const& deleter, + std::optional const& ter, + std::optional const& onBehalfOf) { Json::Value jv; jv[jss::Account] = to_string(deleter); @@ -796,6 +861,8 @@ AMM::ammDelete(AccountID const& deleter, std::optional const& ter) jv[jss::TransactionType] = jss::AMMDelete; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); + if (onBehalfOf) + jv[jss::OnBehalfOf] = onBehalfOf->human(); submit(jv, std::nullopt, ter); } @@ -830,7 +897,8 @@ ammClawback( Account const& holder, Issue const& asset, Issue const& asset2, - std::optional const& amount) + std::optional const& amount, + std::optional const& onBehalfOf) { Json::Value jv; jv[jss::TransactionType] = jss::AMMClawback; @@ -840,6 +908,8 @@ ammClawback( jv[jss::Asset2] = to_json(asset2); if (amount) jv[jss::Amount] = amount->getJson(JsonOptions::none); + if (onBehalfOf) + jv[jss::OnBehalfOf] = onBehalfOf->human(); return jv; } diff --git a/src/test/jtx/impl/AccountPermission.cpp b/src/test/jtx/impl/AccountPermission.cpp new file mode 100644 index 00000000000..62e5786dacd --- /dev/null +++ b/src/test/jtx/impl/AccountPermission.cpp @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 + +namespace ripple { +namespace test { +namespace jtx { + +namespace account_permission { + +Json::Value +accountPermissionSet( + jtx::Account const& account, + jtx::Account const& authorize, + std::list const& permissions) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::AccountPermissionSet; + jv[jss::Account] = account.human(); + jv[jss::Authorize] = authorize.human(); + Json::Value permissionsJson(Json::arrayValue); + for (auto const& permission : permissions) + { + Json::Value permissionValue; + permissionValue[jss::PermissionValue] = permission; + Json::Value permissionObj; + permissionObj[jss::Permission] = permissionValue; + permissionsJson.append(permissionObj); + } + + jv[jss::Permissions] = permissionsJson; + + return jv; +} + +Json::Value +ledgerEntry( + jtx::Env& env, + jtx::Account const& account, + jtx::Account const& authorize) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::account_permission][jss::account] = account.human(); + jvParams[jss::account_permission][jss::authorize] = authorize.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +} // namespace account_permission +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 43286ab7824..6e3d94df1f0 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -492,6 +492,8 @@ Env::autofill(JTx& jt) jtx::fill_fee(jv, *current()); if (jt.fill_seq) jtx::fill_seq(jv, *current()); + if (jt.fill_delegating_seq) + jtx::fill_delegating_seq(jv, *current()); if (jt.fill_netid) { diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp index df9483cbaae..72cc05e1f03 100644 --- a/src/test/jtx/impl/Oracle.cpp +++ b/src/test/jtx/impl/Oracle.cpp @@ -65,6 +65,11 @@ Oracle::remove(RemoveArg const& arg) jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops()); if (arg.flags != 0) jv[jss::Flags] = arg.flags; + if (arg.onBehalfOf && arg.sender) + { + jv[jss::Account] = arg.sender->human(); + jv[jss::OnBehalfOf] = arg.onBehalfOf->human(); + } submit(jv, arg.msig, arg.seq, arg.err); } @@ -262,6 +267,12 @@ Oracle::set(UpdateArg const& arg) dataSeries.append(priceData); } jv[jss::PriceDataSeries] = dataSeries; + if (arg.onBehalfOf && arg.sender) + { + owner_ = *arg.onBehalfOf; + jv[jss::Account] = arg.sender->human(); + jv[jss::OnBehalfOf] = arg.onBehalfOf->human(); + } submit(jv, arg.msig, arg.seq, arg.err); } @@ -280,7 +291,9 @@ Oracle::set(CreateArg const& arg) .msig = arg.msig, .seq = arg.seq, .fee = arg.fee, - .err = arg.err}); + .err = arg.err, + .onBehalfOf = arg.onBehalfOf, + .sender = arg.sender}); } Json::Value diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 3bf69729ab0..3f41987c369 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -339,6 +339,15 @@ channelBalance(ReadView const& view, uint256 const& chan) 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) { diff --git a/src/test/jtx/impl/check.cpp b/src/test/jtx/impl/check.cpp index 21af6c9cc3f..ac6b7b0b173 100644 --- a/src/test/jtx/impl/check.cpp +++ b/src/test/jtx/impl/check.cpp @@ -29,7 +29,11 @@ namespace check { // Cash a check requiring that a specific amount be delivered. Json::Value -cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount) +cash( + jtx::Account const& dest, + uint256 const& checkId, + STAmount const& amount, + std::optional const& onBehalfOf) { Json::Value jv; jv[sfAccount.jsonName] = dest.human(); @@ -37,6 +41,8 @@ cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount) jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; jv[sfFlags.jsonName] = tfUniversal; + if (onBehalfOf) + jv[sfOnBehalfOf.jsonName] = onBehalfOf->human(); return jv; } @@ -45,7 +51,8 @@ Json::Value cash( jtx::Account const& dest, uint256 const& checkId, - DeliverMin const& atLeast) + DeliverMin const& atLeast, + std::optional const& onBehalfOf) { Json::Value jv; jv[sfAccount.jsonName] = dest.human(); @@ -53,21 +60,43 @@ cash( jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; jv[sfFlags.jsonName] = tfUniversal; + if (onBehalfOf) + jv[sfOnBehalfOf.jsonName] = onBehalfOf->human(); return jv; } // Cancel a check. Json::Value -cancel(jtx::Account const& dest, uint256 const& checkId) +cancel( + jtx::Account const& dest, + uint256 const& checkId, + std::optional const& onBehalfOf) { Json::Value jv; jv[sfAccount.jsonName] = dest.human(); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCancel; jv[sfFlags.jsonName] = tfUniversal; + if (onBehalfOf) + jv[sfOnBehalfOf.jsonName] = onBehalfOf->human(); return jv; } +// Helper function that returns the Checks on an account. +std::vector> +checksOnAccount(test::jtx::Env& env, test::jtx::Account account) +{ + std::vector> result; + forEachItem( + *env.current(), + account, + [&result](std::shared_ptr const& sle) { + if (sle && sle->getType() == ltCHECK) + result.push_back(sle); + }); + return result; +} + } // namespace check } // namespace jtx diff --git a/src/test/jtx/impl/credentials.cpp b/src/test/jtx/impl/credentials.cpp index bc7ccf93cd4..1bc81ce0f93 100644 --- a/src/test/jtx/impl/credentials.cpp +++ b/src/test/jtx/impl/credentials.cpp @@ -26,6 +26,24 @@ namespace test { namespace jtx { namespace credentials { +bool +checkVL( + std::shared_ptr const& sle, + SField const& field, + std::string const& expected) +{ + return strHex(expected) == strHex(sle->getFieldVL(field)); +} + +Keylet +credentialKeylet( + test::jtx::Account const& subject, + test::jtx::Account const& issuer, + std::string_view credType) +{ + return keylet::credential( + subject.id(), issuer.id(), Slice(credType.data(), credType.size())); +} Json::Value create( diff --git a/src/test/jtx/impl/deposit.cpp b/src/test/jtx/impl/deposit.cpp index d91607c9906..07436b74248 100644 --- a/src/test/jtx/impl/deposit.cpp +++ b/src/test/jtx/impl/deposit.cpp @@ -28,23 +28,33 @@ namespace deposit { // Add DepositPreauth. Json::Value -auth(jtx::Account const& account, jtx::Account const& auth) +auth( + jtx::Account const& account, + jtx::Account const& auth, + std::optional const& onBehalfOf) { Json::Value jv; jv[sfAccount.jsonName] = account.human(); jv[sfAuthorize.jsonName] = auth.human(); jv[sfTransactionType.jsonName] = jss::DepositPreauth; + if (onBehalfOf) + jv[sfOnBehalfOf.jsonName] = onBehalfOf->human(); return jv; } // Remove DepositPreauth. Json::Value -unauth(jtx::Account const& account, jtx::Account const& unauth) +unauth( + jtx::Account const& account, + jtx::Account const& unauth, + std::optional const& onBehalfOf) { Json::Value jv; jv[sfAccount.jsonName] = account.human(); jv[sfUnauthorize.jsonName] = unauth.human(); jv[sfTransactionType.jsonName] = jss::DepositPreauth; + if (onBehalfOf) + jv[sfOnBehalfOf.jsonName] = onBehalfOf->human(); return jv; } @@ -88,6 +98,25 @@ unauthCredentials( return jv; } +Json::Value +ledgerEntryDepositPreauth( + jtx::Env& env, + jtx::Account const& acc, + std::vector const& auth) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = acc.human(); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + Json::arrayValue; + auto& arr(jvParams[jss::deposit_preauth][jss::authorized_credentials]); + for (auto const& o : auth) + { + arr.append(o.toLEJson()); + } + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + } // namespace deposit } // namespace jtx diff --git a/src/test/jtx/impl/did.cpp b/src/test/jtx/impl/did.cpp index a9a6e974ef4..1f9d3cf99b2 100644 --- a/src/test/jtx/impl/did.cpp +++ b/src/test/jtx/impl/did.cpp @@ -50,15 +50,24 @@ setValid(jtx::Account const& account) } Json::Value -del(jtx::Account const& account) +del(jtx::Account const& account, std::optional const& onBehalfOf) { Json::Value jv; jv[jss::TransactionType] = jss::DIDDelete; jv[jss::Account] = to_string(account.id()); jv[jss::Flags] = tfUniversal; + if (onBehalfOf) + jv[sfOnBehalfOf.jsonName] = onBehalfOf->human(); return jv; } +bool +checkVL(Slice const& result, std::string expected) +{ + Serializer s; + s.addRaw(result); + return s.getString() == expected; +} } // namespace did } // namespace jtx diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index ead6a47c25e..2a3d8c7c9e8 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -56,15 +56,17 @@ MPTTester::makeHolders(std::vector const& holders) return accounts; } -MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) +MPTTester::MPTTester(Env& env, Account const& account, MPTInit const& arg) : env_(env) - , issuer_(issuer) + , issuer_(account) + , sender_(account) , holders_(makeHolders(arg.holders)) , close_(arg.close) { if (arg.fund) { env_.fund(arg.xrp, issuer_); + // env_.fund(arg.xrp, sender_); for (auto it : holders_) env_.fund(arg.xrpHolders, it.second); } @@ -82,6 +84,20 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) } } +// when the MPT is issued on behalf of another account +MPTTester::MPTTester( + Env& env, + Account const& sender, + Account const& issuer, + MPTInit const& arg) + : env_(env) + , issuer_(issuer) + , sender_(sender) + , holders_(makeHolders(arg.holders)) + , close_(arg.close) +{ +} + void MPTTester::create(const MPTCreate& arg) { @@ -99,6 +115,14 @@ MPTTester::create(const MPTCreate& arg) jv[sfMPTokenMetadata] = strHex(*arg.metadata); if (arg.maxAmt) jv[sfMaximumAmount] = std::to_string(*arg.maxAmt); + if (arg.onBehalfOf) + { + if (*arg.onBehalfOf != issuer_) + Throw( + "create has to be sent on behalf of issuer"); + jv[sfAccount] = sender_.human(); + jv[sfOnBehalfOf] = arg.onBehalfOf->human(); + } if (submit(arg, jv) != tesSUCCESS) { // Verify issuance doesn't exist @@ -128,6 +152,8 @@ MPTTester::destroy(MPTDestroy const& arg) Throw("MPT has not been created"); jv[sfMPTokenIssuanceID] = to_string(*id_); } + if (arg.onBehalfOf) + jv[sfOnBehalfOf] = arg.onBehalfOf->human(); jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; submit(arg, jv); } @@ -160,10 +186,14 @@ MPTTester::authorize(MPTAuthorize const& arg) } if (arg.holder) jv[sfHolder] = arg.holder->human(); + if (arg.onBehalfOf) + jv[sfOnBehalfOf] = arg.onBehalfOf->human(); + auto const account = + arg.onBehalfOf ? arg.onBehalfOf : (arg.account ? arg.account : issuer_); if (auto const result = submit(arg, jv); result == tesSUCCESS) { // Issuer authorizes - if (!arg.account || *arg.account == issuer_) + if (!account || *account == issuer_) { auto const flags = getFlags(arg.holder); // issuer un-authorizes the holder @@ -177,29 +207,28 @@ MPTTester::authorize(MPTAuthorize const& arg) // Holder authorizes else if (arg.flags.value_or(0) != tfMPTUnauthorize) { - auto const flags = getFlags(arg.account); + auto const flags = getFlags(account); // holder creates a token - env_.require(mptflags(*this, flags, arg.account)); - env_.require(mptbalance(*this, *arg.account, 0)); + env_.require(mptflags(*this, flags, account)); + env_.require(mptbalance(*this, *account, 0)); } else { // Verify that the MPToken doesn't exist. forObject( [&](SLEP const& sle) { return env_.test.BEAST_EXPECT(!sle); }, - arg.account); + account); } } else if ( - arg.account && *arg.account != issuer_ && + account && *account != issuer_ && arg.flags.value_or(0) != tfMPTUnauthorize && id_) { if (result == tecDUPLICATE) { // Verify that MPToken already exists env_.require(requireAny([&]() -> bool { - return env_.le(keylet::mptoken(*id_, arg.account->id())) != - nullptr; + return env_.le(keylet::mptoken(*id_, account->id())) != nullptr; })); } else @@ -207,8 +236,7 @@ MPTTester::authorize(MPTAuthorize const& arg) // Verify MPToken doesn't exist if holder failed authorizing(unless // it already exists) env_.require(requireAny([&]() -> bool { - return env_.le(keylet::mptoken(*id_, arg.account->id())) == - nullptr; + return env_.le(keylet::mptoken(*id_, account->id())) == nullptr; })); } } @@ -233,6 +261,8 @@ MPTTester::set(MPTSet const& arg) } if (arg.holder) jv[sfHolder] = arg.holder->human(); + if (arg.onBehalfOf) + jv[sfOnBehalfOf] = arg.onBehalfOf->human(); if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) { auto require = [&](std::optional const& holder, diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 393e36e9d61..4bb70e226ae 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -29,7 +29,13 @@ void paths::operator()(Env& env, JTx& jt) const { auto& jv = jt.jv; - auto const from = env.lookup(jv[jss::Account].asString()); + std::string fromAccount; + if (from_.empty()) + fromAccount = jv[jss::Account].asString(); + else + fromAccount = from_; + + auto const from = env.lookup(fromAccount); auto const to = env.lookup(jv[jss::Destination].asString()); auto const amount = amountFromJson(sfAmount, jv[jss::Amount]); Pathfinder pf( diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index 49f473fcd82..3cb7c0113e3 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -242,6 +243,26 @@ modify(jtx::Account const& account, uint256 const& nftokenID) return jv; } +// returns a randomly generated string which fits +// the constraints of a URI. Empty strings may be returned. +// In the empty string case do not add the URI to the nft. +std::string +randURI() +{ + std::string ret; + + // About 20% of the returned strings should be empty + if (rand_int(4) == 0) + return ret; + + std::size_t const strLen = rand_int(256); + ret.reserve(strLen); + for (std::size_t i = 0; i < strLen; ++i) + ret.push_back(rand_byte()); + + return ret; +} + } // namespace token } // namespace jtx } // namespace test diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index c10fb918540..2c66a12688e 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -75,6 +75,20 @@ fill_seq(Json::Value& jv, ReadView const& view) jv[jss::Sequence] = ar->getFieldU32(sfSequence); } +void +fill_delegating_seq(Json::Value& jv, ReadView const& view) +{ + if (jv.isMember(jss::DelegateSequence) || !jv.isMember(jss::OnBehalfOf)) + return; + auto const account = parseBase58(jv[jss::OnBehalfOf].asString()); + if (!account) + Throw("unexpected invalid OnBehalfOf Account"); + auto const ar = view.read(keylet::account(*account)); + if (!ar) + Throw("unexpected missing OnBehalfOf account root"); + jv[jss::DelegateSequence] = ar->getFieldU32(sfSequence); +} + Json::Value cmdToJSONRPC( std::vector const& args, diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 12b9d74d27c..38be20287da 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -104,6 +104,7 @@ struct MPTCreate std::optional holderCount = std::nullopt; bool fund = true; std::optional flags = {0}; + std::optional onBehalfOf = std::nullopt; std::optional err = std::nullopt; }; @@ -114,6 +115,7 @@ struct MPTDestroy std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; + std::optional onBehalfOf = std::nullopt; std::optional err = std::nullopt; }; @@ -125,6 +127,7 @@ struct MPTAuthorize std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; + std::optional onBehalfOf = std::nullopt; std::optional err = std::nullopt; }; @@ -136,6 +139,7 @@ struct MPTSet std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; + std::optional onBehalfOf = std::nullopt; std::optional err = std::nullopt; }; @@ -143,6 +147,7 @@ class MPTTester { Env& env_; Account const& issuer_; + Account const& sender_; std::unordered_map const holders_; std::optional id_; bool close_; @@ -150,6 +155,12 @@ class MPTTester public: MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {}); + MPTTester( + Env& env, + Account const& issuer, + Account const& sender, + MPTInit const& constr = {}); + void create(MPTCreate const& arg = MPTCreate{}); @@ -231,6 +242,7 @@ class MPTTester auto const err = env_.ter(); if (close_) env_.close(); + // auto const account = arg.onBehalfOf ? *arg.onBehalfOf : issuer_; if (arg.ownerCount) env_.require(owners(issuer_, *arg.ownerCount)); if (arg.holderCount) diff --git a/src/test/jtx/paths.h b/src/test/jtx/paths.h index cc9caf3af72..b53e56e366b 100644 --- a/src/test/jtx/paths.h +++ b/src/test/jtx/paths.h @@ -33,6 +33,7 @@ class paths { private: Issue in_; + std::string from_; int depth_; unsigned int limit_; @@ -42,6 +43,15 @@ class paths { } + paths( + Issue const& in, + std::string from, + int depth = 7, + unsigned int limit = 4) + : in_(in), from_(from), depth_(depth), limit_(limit) + { + } + void operator()(Env&, JTx& jt) const; }; diff --git a/src/test/jtx/token.h b/src/test/jtx/token.h index f22a1a01dae..b82c44f47c1 100644 --- a/src/test/jtx/token.h +++ b/src/test/jtx/token.h @@ -241,6 +241,9 @@ clearMinter(jtx::Account const& account); Json::Value modify(jtx::Account const& account, uint256 const& nftokenID); +std::string +randURI(); + } // namespace token } // namespace jtx diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index 6d34452cdf8..93d9bdd0376 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -62,6 +62,10 @@ fill_fee(Json::Value& jv, ReadView const& view); void fill_seq(Json::Value& jv, ReadView const& view); +/** Set the delegating sequence number automatically. */ +void +fill_delegating_seq(Json::Value& jv, ReadView const& view); + /** Given a rippled unit test rpc command, return the corresponding JSON. */ Json::Value cmdToJSONRPC( diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index ecf1c8e3979..3dbd5367fd7 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -98,6 +98,7 @@ class Invariants_test : public beast::unit_test::suite tesSUCCESS, env.current()->fees().base, tapNONE, + std::unordered_set{}, jlog}; BEAST_EXPECT(precheck(A1, A2, ac)); diff --git a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp index 6bdb602fd83..fcee2508d49 100644 --- a/src/xrpld/app/ledger/AcceptedLedgerTx.cpp +++ b/src/xrpld/app/ledger/AcceptedLedgerTx.cpp @@ -58,7 +58,7 @@ AcceptedLedgerTx::AcceptedLedgerTx( if (mTxn->getTxnType() == ttOFFER_CREATE) { - auto const& account = mTxn->getAccountID(sfAccount); + auto const& account = mTxn->getEffectiveAccountID(); auto const amount = mTxn->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the owner balance diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 3f6869df1d8..3977f5a6028 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -206,7 +206,7 @@ fillJsonTx( if ((fill.options & LedgerFill::ownerFunds) && txn->getTxnType() == ttOFFER_CREATE) { - auto const account = txn->getAccountID(sfAccount); + auto const& account = txn->getEffectiveAccountID(); auto const amount = txn->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the diff --git a/src/xrpld/app/ledger/detail/LocalTxs.cpp b/src/xrpld/app/ledger/detail/LocalTxs.cpp index a6eb7721a3e..57739b50076 100644 --- a/src/xrpld/app/ledger/detail/LocalTxs.cpp +++ b/src/xrpld/app/ledger/detail/LocalTxs.cpp @@ -62,7 +62,7 @@ class LocalTx : m_txn(txn) , m_expire(index + holdLedgers) , m_id(txn->getTransactionID()) - , m_account(txn->getAccountID(sfAccount)) + , m_account(txn->getEffectiveAccountID()) , m_seqProxy(txn->getSeqProxy()) { if (txn->isFieldPresent(sfLastLedgerSequence)) diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp index a18cd40336b..09d41dc4ec2 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -288,7 +288,7 @@ verifyDepositPreauth( bool const credentialsPresent = ctx.tx.isFieldPresent(sfCredentialIDs); if (credentialsPresent && - credentials::removeExpired(ctx.view(), ctx.tx, ctx.journal)) + credentials::removeExpired(ctx.view(), ctx.tx.getSTTx(), ctx.journal)) return tecEXPIRED; if (sleDst && (sleDst->getFlags() & lsfDepositAuth)) diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 3800b359efa..5a624e82b10 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -3041,7 +3041,9 @@ NetworkOPsImp::transJson( if (transaction->getTxnType() == ttOFFER_CREATE) { - auto const account = transaction->getAccountID(sfAccount); + auto const& account = transaction->isFieldPresent(sfOnBehalfOf) + ? transaction->getAccountID(sfOnBehalfOf) + : transaction->getAccountID(sfAccount); auto const amount = transaction->getFieldAmount(sfTakerGets); // If the offer create is not self funded then add the owner balance diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index a0721e031ef..a93ea40e940 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -308,8 +308,8 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j) << " rules or flags have changed. Flags from " << pfresult->flags << " to " << flags; - pfresult.emplace( - preflight(app, view.rules(), pfresult->tx, flags, pfresult->j)); + pfresult.emplace(preflight( + app, view.rules(), pfresult->tx.getSTTx(), flags, pfresult->j)); } auto pcresult = preclaim(*pfresult, app, view); diff --git a/src/xrpld/app/tx/apply.h b/src/xrpld/app/tx/apply.h index 38325252c99..e66fe378003 100644 --- a/src/xrpld/app/tx/apply.h +++ b/src/xrpld/app/tx/apply.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/tx/applySteps.h b/src/xrpld/app/tx/applySteps.h index c1eb17c9920..d2b091e1793 100644 --- a/src/xrpld/app/tx/applySteps.h +++ b/src/xrpld/app/tx/applySteps.h @@ -22,6 +22,8 @@ #include #include +#include +#include namespace ripple { @@ -163,7 +165,7 @@ struct PreflightResult { public: /// From the input - the transaction - STTx const& tx; + STTxDelegated const tx; /// From the input - the rules Rules const rules; /// Consequences of the transaction @@ -172,7 +174,6 @@ struct PreflightResult ApplyFlags const flags; /// From the input - the journal beast::Journal const j; - /// Intermediate transaction result NotTEC const ter; @@ -208,7 +209,7 @@ struct PreclaimResult /// From the input - the ledger view ReadView const& view; /// From the input - the transaction - STTx const& tx; + STTxDelegated const tx; /// From the input - the flags ApplyFlags const flags; /// From the input - the journal @@ -216,18 +217,25 @@ struct PreclaimResult /// Intermediate transaction result TER const ter; + /// granular permissions enabled for the transaction. If empty and + /// isDelegated=true, then the entire transaction is authorized. + std::unordered_set permissions; /// Success flag - whether the transaction is likely to /// claim a fee bool const likelyToClaimFee; /// Constructor template - PreclaimResult(Context const& ctx_, TER ter_) + PreclaimResult( + Context const& ctx_, + TER ter_, + std::unordered_set permissions_) : view(ctx_.view) , tx(ctx_.tx) , flags(ctx_.flags) , j(ctx_.j) , ter(ter_) + , permissions(std::move(permissions_)) , likelyToClaimFee(ter == tesSUCCESS || isTecClaimHardFail(ter, flags)) { } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 23e8529cfc9..b661910013d 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -250,8 +250,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) if (auto const ter = checkAmount(amount2, amount2Balance)) return ter; - auto const lpTokens = - ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j); + auto const lpTokens = ammLPHolds(ctx.view, *ammSle, accountID, ctx.j); auto const lpTokensWithdraw = tokensWithdraw(lpTokens, ctx.tx[~sfLPTokenIn], ctx.tx.getFlags()); @@ -476,7 +475,7 @@ AMMWithdraw::withdraw( lpTokensWithdraw, tfee, FreezeHandling::fhZERO_IF_FROZEN, - isWithdrawAll(ctx_.tx), + isWithdrawAll(ctx_.tx.getSTTx()), mPriorBalance, j_); return {ter, newLPTokenBalance}; @@ -713,7 +712,7 @@ AMMWithdraw::equalWithdrawTokens( lpTokensWithdraw, tfee, FreezeHandling::fhZERO_IF_FROZEN, - isWithdrawAll(ctx_.tx), + isWithdrawAll(ctx_.tx.getSTTx()), mPriorBalance, ctx_.journal); return {ter, newLPTokenBalance}; diff --git a/src/xrpld/app/tx/detail/AccountPermissionSet.cpp b/src/xrpld/app/tx/detail/AccountPermissionSet.cpp new file mode 100644 index 00000000000..197e59cd4bc --- /dev/null +++ b/src/xrpld/app/tx/detail/AccountPermissionSet.cpp @@ -0,0 +1,155 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 + +namespace ripple { + +constexpr std::size_t permissionMaxSize = 10; + +NotTEC +AccountPermissionSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureAccountPermission)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + + auto const& permissions = ctx.tx.getFieldArray(sfPermissions); + if (permissions.size() > permissionMaxSize) + return temARRAY_TOO_LARGE; + + // can not authorize self + if (ctx.tx[sfAccount] == ctx.tx[sfAuthorize]) + return temMALFORMED; + + std::unordered_set permissionSet; + + for (auto const& permission : permissions) + { + auto const permissionValue = permission[sfPermissionValue]; + + if (permissionSet.find(permissionValue) != permissionSet.end()) + return temMALFORMED; + + permissionSet.insert(permissionValue); + } + + return preflight2(ctx); +} + +TER +AccountPermissionSet::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.exists(keylet::account(ctx.tx[sfAccount]))) + return terNO_ACCOUNT; // LCOV_EXCL_LINE + + if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))) + return terNO_ACCOUNT; + + return tesSUCCESS; +} + +TER +AccountPermissionSet::doApply() +{ + auto const sleOwner = ctx_.view().peek(keylet::account(account_)); + if (!sleOwner) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const& authAccount = ctx_.tx[sfAuthorize]; + auto const accountPermissionKey = + keylet::accountPermission(account_, authAccount); + + auto sle = ctx_.view().peek(accountPermissionKey); + if (sle) + { + auto const& permissions = ctx_.tx.getFieldArray(sfPermissions); + if (permissions.empty()) + // if permissions array is empty, delete the ledger object. + return deleteAccountPermission(view(), sle, account_, j_); + + sle->setFieldArray(sfPermissions, permissions); + ctx_.view().update(sle); + return tesSUCCESS; + } + + STAmount const reserve{ctx_.view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + + sle = std::make_shared(accountPermissionKey); + sle->setAccountID(sfAccount, account_); + sle->setAccountID(sfAuthorize, authAccount); + auto const& permissions = ctx_.tx.getFieldArray(sfPermissions); + sle->setFieldArray(sfPermissions, permissions); + auto const page = ctx_.view().dirInsert( + keylet::ownerDir(account_), + accountPermissionKey, + describeOwnerDir(account_)); + + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + + (*sle)[sfOwnerNode] = *page; + ctx_.view().insert(sle); + adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal); + return tesSUCCESS; +} + +TER +AccountPermissionSet::deleteAccountPermission( + ApplyView& view, + std::shared_ptr const& sle, + AccountID const& account, + beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove( + keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete AccountPermission from owner."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + auto const sleOwner = view.peek(keylet::account(account)); + if (!sleOwner) + return tecINTERNAL; // LCOV_EXCL_LINE + + adjustOwnerCount(view, sleOwner, -1, j); + + view.erase(sle); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/AccountPermissionSet.h b/src/xrpld/app/tx/detail/AccountPermissionSet.h new file mode 100644 index 00000000000..d6096dc794d --- /dev/null +++ b/src/xrpld/app/tx/detail/AccountPermissionSet.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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_TX_ACCOUNTPERMISSIONSET_H_INCLUDED +#define RIPPLE_TX_ACCOUNTPERMISSIONSET_H_INCLUDED + +#include + +namespace ripple { + +class AccountPermissionSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit AccountPermissionSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + // Interface used by DeleteAccount + static TER + deleteAccountPermission( + ApplyView& view, + std::shared_ptr const& sle, + AccountID const& account, + beast::Journal j); +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 5a37f74b2f0..3b05ce01b25 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -35,11 +35,13 @@ ApplyContext::ApplyContext( TER preclaimResult_, XRPAmount baseFee_, ApplyFlags flags, + std::unordered_set const permissions, beast::Journal journal_) : app(app_) - , tx(tx_) + , tx(STTxDelegated(tx_, tx_.isFieldPresent(sfOnBehalfOf))) , preclaimResult(preclaimResult_) , baseFee(baseFee_) + , permissions(std::move(permissions)) , journal(journal_) , base_(base) , flags_(flags) @@ -56,7 +58,7 @@ ApplyContext::discard() std::optional ApplyContext::apply(TER ter) { - return view_->apply(base_, tx, ter, flags_ & tapDRY_RUN, journal); + return view_->apply(base_, tx.getSTTx(), ter, flags_ & tapDRY_RUN, journal); } std::size_t diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 1ddcff55065..b385330284d 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -24,7 +24,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -42,12 +43,14 @@ class ApplyContext TER preclaimResult, XRPAmount baseFee, ApplyFlags flags, + std::unordered_set const permissions, beast::Journal = beast::Journal{beast::Journal::getNullSink()}); Application& app; - STTx const& tx; + STTxDelegated const tx; TER const preclaimResult; XRPAmount const baseFee; + std::unordered_set permissions; beast::Journal const journal; ApplyView& diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index f6e5f6f3e3f..0a2e5bc33f2 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -138,7 +138,7 @@ CashCheck::preclaim(PreclaimContext const& ctx) STAmount const value{[](STTx const& tx) { auto const optAmount = tx[~sfAmount]; return optAmount ? *optAmount : tx[sfDeliverMin]; - }(ctx.tx)}; + }(ctx.tx.getSTTx())}; STAmount const sendMax = sleCheck->at(sfSendMax); Currency const currency{value.getCurrency()}; diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index 45834742c8e..286f7efe067 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -359,9 +359,9 @@ Change::applyFee() }; if (view().rules().enabled(featureXRPFees)) { - set(feeObject, ctx_.tx, sfBaseFeeDrops); - set(feeObject, ctx_.tx, sfReserveBaseDrops); - set(feeObject, ctx_.tx, sfReserveIncrementDrops); + set(feeObject, ctx_.tx.getSTTx(), sfBaseFeeDrops); + set(feeObject, ctx_.tx.getSTTx(), sfReserveBaseDrops); + set(feeObject, ctx_.tx.getSTTx(), sfReserveIncrementDrops); // Ensure the old fields are removed feeObject->makeFieldAbsent(sfBaseFee); feeObject->makeFieldAbsent(sfReferenceFeeUnits); @@ -370,10 +370,10 @@ Change::applyFee() } else { - set(feeObject, ctx_.tx, sfBaseFee); - set(feeObject, ctx_.tx, sfReferenceFeeUnits); - set(feeObject, ctx_.tx, sfReserveBase); - set(feeObject, ctx_.tx, sfReserveIncrement); + set(feeObject, ctx_.tx.getSTTx(), sfBaseFee); + set(feeObject, ctx_.tx.getSTTx(), sfReferenceFeeUnits); + set(feeObject, ctx_.tx.getSTTx(), sfReserveBase); + set(feeObject, ctx_.tx.getSTTx(), sfReserveIncrement); } view().update(feeObject); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 3a278eed738..589ad7f4a7b 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -184,7 +184,7 @@ CreateCheck::doApply() // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. - std::uint32_t const seq = ctx_.tx.getSeqProxy().value(); + std::uint32_t const seq = ctx_.tx.getEffectiveSeq(); Keylet const checkKeylet = keylet::check(account_, seq); auto sleCheck = std::make_shared(checkKeylet); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index f1b66468840..10ea0179418 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -36,7 +36,8 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) return amount.native() ? amount.xrp() : beast::zero; }; - return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; + return TxConsequences{ + ctx.tx.getSTTx(), calculateMaxXRPSpend(ctx.tx.getSTTx())}; } NotTEC @@ -969,7 +970,7 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) // Note that we we use the value from the sequence or ticket as the // offer sequence. For more explanation see comments in SeqProxy.h. - auto const offerSequence = ctx_.tx.getSeqProxy().value(); + auto const offerSequence = ctx_.tx.getEffectiveSeq(); // This is the original rate of the offer, and is the rate at which // it will be placed, even if crossing offers change the amounts that diff --git a/src/xrpld/app/tx/detail/CreateTicket.cpp b/src/xrpld/app/tx/detail/CreateTicket.cpp index b04f4af1d30..58fcdef87c4 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.cpp +++ b/src/xrpld/app/tx/detail/CreateTicket.cpp @@ -31,7 +31,7 @@ TxConsequences CreateTicket::makeTxConsequences(PreflightContext const& ctx) { // Create TxConsequences identifying the number of sequences consumed. - return TxConsequences{ctx.tx, ctx.tx[sfTicketCount]}; + return TxConsequences{ctx.tx.getSTTx(), ctx.tx[sfTicketCount]}; } NotTEC @@ -110,7 +110,9 @@ CreateTicket::doApply() // Sanity check that the transaction machinery really did already // increment the account root Sequence. - if (std::uint32_t const txSeq = ctx_.tx[sfSequence]; + if (std::uint32_t const txSeq = ctx_.tx.isDelegated() + ? ctx_.tx[sfDelegateSequence] + : ctx_.tx[sfSequence]; txSeq != 0 && txSeq != (firstTicketSeq - 1)) return tefINTERNAL; diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 18ddf01a8d7..75cdadee4d9 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -180,6 +181,19 @@ removeCredentialFromLedger( return credentials::deleteSLE(view, sleDel, j); } +TER +removeAccountPermissionFromLedger( + Application& app, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return AccountPermissionSet::deleteAccountPermission( + view, sleDel, account, j); +} + // Return nullptr if the LedgerEntryType represents an obligation that can't // be deleted. Otherwise return the pointer to the function that can delete // the non-obligation @@ -202,6 +216,8 @@ nonObligationDeleter(LedgerEntryType t) return removeDIDFromLedger; case ltORACLE: return removeOracleFromLedger; + case ltACCOUNT_PERMISSION: + return removeAccountPermissionFromLedger; case ltCREDENTIAL: return removeCredentialFromLedger; default: diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 48b9867d3a0..9baee1c3d03 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -94,7 +94,7 @@ after(NetClock::time_point now, std::uint32_t mark) TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx.getSTTx(), ctx.tx[sfAmount].xrp()}; } NotTEC @@ -244,7 +244,7 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. Keylet const escrowKeylet = - keylet::escrow(account, ctx_.tx.getSeqProxy().value()); + keylet::escrow(account, ctx_.tx.getEffectiveSeq()); auto const slep = std::make_shared(escrowKeylet); (*slep)[sfAmount] = ctx_.tx[sfAmount]; (*slep)[sfAccount] = account; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index d39492c1085..aa8e1cd118e 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -45,7 +45,7 @@ TransactionFeeCheck::visitEntry( bool TransactionFeeCheck::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const, XRPAmount const fee, ReadView const&, @@ -139,7 +139,7 @@ XRPNotCreated::visitEntry( bool XRPNotCreated::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const, XRPAmount const fee, ReadView const&, @@ -200,7 +200,7 @@ XRPBalanceChecks::visitEntry( bool XRPBalanceChecks::finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -244,7 +244,7 @@ NoBadOffers::visitEntry( bool NoBadOffers::finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -289,7 +289,7 @@ NoZeroEscrow::visitEntry( bool NoZeroEscrow::finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -318,7 +318,7 @@ AccountRootsNotDeleted::visitEntry( bool AccountRootsNotDeleted::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const&, @@ -373,7 +373,7 @@ AccountRootsDeletedClean::visitEntry( bool AccountRootsDeletedClean::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const& view, @@ -463,6 +463,7 @@ LedgerEntryTypesMatch::visitEntry( switch (after->getType()) { case ltACCOUNT_ROOT: + case ltACCOUNT_PERMISSION: case ltDIR_NODE: case ltRIPPLE_STATE: case ltTICKET: @@ -498,7 +499,7 @@ LedgerEntryTypesMatch::visitEntry( bool LedgerEntryTypesMatch::finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -541,7 +542,7 @@ NoXRPTrustLines::visitEntry( bool NoXRPTrustLines::finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -578,7 +579,7 @@ NoDeepFreezeTrustLinesWithoutFreeze::visitEntry( bool NoDeepFreezeTrustLinesWithoutFreeze::finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -626,7 +627,7 @@ TransfersNotFrozen::visitEntry( bool TransfersNotFrozen::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const ter, XRPAmount const fee, ReadView const& view, @@ -784,7 +785,7 @@ bool TransfersNotFrozen::validateIssuerChanges( std::shared_ptr const& issuer, IssuerChanges const& changes, - STTx const& tx, + STTxDelegated const& tx, beast::Journal const& j, bool enforce) { @@ -827,7 +828,7 @@ bool TransfersNotFrozen::validateFrozenState( BalanceChange const& change, bool high, - STTx const& tx, + STTxDelegated const& tx, beast::Journal const& j, bool enforce, bool globalFreeze) @@ -887,7 +888,7 @@ ValidNewAccountRoot::visitEntry( bool ValidNewAccountRoot::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const& view, @@ -1038,7 +1039,7 @@ ValidNFTokenPage::visitEntry( bool ValidNFTokenPage::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const& view, @@ -1114,7 +1115,7 @@ NFTokenCountTracking::visitEntry( bool NFTokenCountTracking::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const& view, @@ -1215,7 +1216,7 @@ ValidClawback::visitEntry( bool ValidClawback::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const& view, @@ -1303,7 +1304,7 @@ ValidMPTIssuance::visitEntry( bool ValidMPTIssuance::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const _fee, ReadView const& _view, @@ -1494,7 +1495,7 @@ ValidPermissionedDomain::visitEntry( bool ValidPermissionedDomain::finalize( - STTx const& tx, + STTxDelegated const& tx, TER const result, XRPAmount const, ReadView const& view, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index cb06b0fb054..87105e408ea 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include @@ -74,7 +74,7 @@ class InvariantChecker_PROTOTYPE */ bool finalize( - STTx const& tx, + STTxDelegated const& tx, TER const tec, XRPAmount const fee, ReadView const& view, @@ -99,7 +99,7 @@ class TransactionFeeCheck bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -127,7 +127,7 @@ class XRPNotCreated bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -155,7 +155,7 @@ class AccountRootsNotDeleted bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -185,7 +185,7 @@ class AccountRootsDeletedClean bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -212,7 +212,7 @@ class XRPBalanceChecks bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -237,7 +237,7 @@ class LedgerEntryTypesMatch bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -263,7 +263,7 @@ class NoXRPTrustLines bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -290,7 +290,7 @@ class NoDeepFreezeTrustLinesWithoutFreeze bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -331,7 +331,7 @@ class TransfersNotFrozen bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -364,7 +364,7 @@ class TransfersNotFrozen validateIssuerChanges( std::shared_ptr const& issuer, IssuerChanges const& changes, - STTx const& tx, + STTxDelegated const& tx, beast::Journal const& j, bool enforce); @@ -372,7 +372,7 @@ class TransfersNotFrozen validateFrozenState( BalanceChange const& change, bool high, - STTx const& tx, + STTxDelegated const& tx, beast::Journal const& j, bool enforce, bool globalFreeze); @@ -398,7 +398,7 @@ class NoBadOffers bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -422,7 +422,7 @@ class NoZeroEscrow bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -448,7 +448,7 @@ class ValidNewAccountRoot bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -485,7 +485,7 @@ class ValidNFTokenPage bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -521,7 +521,7 @@ class NFTokenCountTracking bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -550,7 +550,7 @@ class ValidClawback bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -574,7 +574,7 @@ class ValidMPTIssuance bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, @@ -609,7 +609,7 @@ class ValidPermissionedDomain bool finalize( - STTx const&, + STTxDelegated const&, TER const, XRPAmount const, ReadView const&, diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 1297a918e1d..9b5948d6a10 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -131,7 +131,7 @@ MPTokenIssuanceCreate::doApply() ctx_.journal, {.priorBalance = mPriorBalance, .account = account_, - .sequence = tx.getSeqProxy().value(), + .sequence = tx.getEffectiveSeq(), .flags = tx.getFlags(), .maxAmount = tx[~sfMaximumAmount], .assetScale = tx[~sfAssetScale], diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 4e395c30be6..bbded6c388e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -102,9 +102,21 @@ MPTokenIssuanceSet::doApply() std::uint32_t const flagsIn = sle->getFieldU32(sfFlags); std::uint32_t flagsOut = flagsIn; - if (txFlags & tfMPTLock) + bool lock = txFlags & tfMPTLock; + bool unlock = txFlags & tfMPTUnlock; + + if (ctx_.tx.isDelegated() && !ctx_.permissions.empty()) + { + // if permissions is not empty, granular delegation is happening. + if (lock && !ctx_.permissions.contains(MPTokenIssuanceLock)) + return tecNO_PERMISSION; + if (unlock && !ctx_.permissions.contains(MPTokenIssuanceUnlock)) + return tecNO_PERMISSION; + } + + if (lock) flagsOut |= lsfMPTLocked; - else if (txFlags & tfMPTUnlock) + else if (unlock) flagsOut &= ~lsfMPTLocked; if (flagsIn != flagsOut) diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 43178d31b4a..fbac2e99372 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -98,7 +98,7 @@ NFTokenCreateOffer::doApply() ctx_.tx[sfAmount], ctx_.tx[~sfDestination], ctx_.tx[~sfExpiration], - ctx_.tx.getSeqProxy(), + ctx_.tx.getEffectiveSeq(), ctx_.tx[sfNFTokenID], mPriorBalance, j_, diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 76e561cfc3f..357c59cd35b 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -334,7 +334,7 @@ NFTokenMint::doApply() ctx_.tx[sfAmount], ctx_.tx[~sfDestination], ctx_.tx[~sfExpiration], - ctx_.tx.getSeqProxy(), + ctx_.tx.getEffectiveSeq(), nftokenID, mPriorBalance, j_); diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 04eb53ae764..778989a67d7 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -1013,7 +1013,7 @@ tokenOfferCreateApply( STAmount const& amount, std::optional const& dest, std::optional const& expiration, - SeqProxy seqProxy, + std::uint32_t seq, uint256 const& nftokenID, XRPAmount const& priorBalance, beast::Journal j, @@ -1024,7 +1024,7 @@ tokenOfferCreateApply( priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1)) return tecINSUFFICIENT_RESERVE; - auto const offerID = keylet::nftoffer(acctID, seqProxy.value()); + auto const offerID = keylet::nftoffer(acctID, seq); // Create the offer: { diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.h b/src/xrpld/app/tx/detail/NFTokenUtils.h index f5232630eef..873a917a28a 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.h +++ b/src/xrpld/app/tx/detail/NFTokenUtils.h @@ -146,7 +146,7 @@ tokenOfferCreateApply( STAmount const& amount, std::optional const& dest, std::optional const& expiration, - SeqProxy seqProxy, + std::uint32_t seq, uint256 const& nftokenID, XRPAmount const& priorBalance, beast::Journal j, diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index aa248075d56..b74c689a7e7 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -168,7 +168,7 @@ closeChannel( TxConsequences PayChanCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx.getSTTx(), ctx.tx[sfAmount].xrp()}; } NotTEC @@ -259,7 +259,7 @@ PayChanCreate::doApply() // Note that we 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.getSeqProxy().value()); + keylet::payChan(account, dst, ctx_.tx.getEffectiveSeq()); auto const slep = std::make_shared(payChanKeylet); // Funds held in this channel @@ -310,7 +310,7 @@ PayChanCreate::doApply() TxConsequences PayChanFund::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ctx.tx.getSTTx(), ctx.tx[sfAmount].xrp()}; } NotTEC diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 1ed3bacbbd8..01b7fb01d03 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -42,7 +42,8 @@ Payment::makeTxConsequences(PreflightContext const& ctx) return maxAmount.native() ? maxAmount.xrp() : beast::zero; }; - return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; + return TxConsequences{ + ctx.tx.getSTTx(), calculateMaxXRPSpend(ctx.tx.getSTTx())}; } STAmount @@ -341,6 +342,30 @@ Payment::doApply() AccountID const dstAccountID(ctx_.tx.getAccountID(sfDestination)); STAmount const dstAmount(ctx_.tx.getFieldAmount(sfAmount)); + + // if the transaction is transaction-level delegated, ctx_.permissions + // was already cleaned up. + if (ctx_.tx.isDelegated() && !ctx_.permissions.empty()) + { + // If permissions is not empty, granular delegation is happening. + // Currently we only support PaymentMint and PaymentBurn granular + // permission. PaymentMint means the sender is the issuer. PaymentBurn + // means the destination is the issuer. + bool authorized = false; + auto const amountIssue = dstAmount.issue(); + if (isXRP(amountIssue)) + return tecNO_PERMISSION; + if (amountIssue.account == account_ && + ctx_.permissions.contains(PaymentMint)) + authorized = true; + if (amountIssue.account == dstAccountID && + ctx_.permissions.contains(PaymentBurn)) + authorized = true; + + if (!authorized) + return tecNO_PERMISSION; + } + bool const mptDirect = dstAmount.holds(); STAmount const maxSourceAmount = getMaxSourceAmount(account_, dstAmount, sendMax); diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index c0e115c2497..365a6a84b1e 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -52,7 +52,8 @@ SetAccount::makeTxConsequences(PreflightContext const& ctx) return TxConsequences::normal; }; - return TxConsequences{ctx.tx, getTxConsequencesCategory(ctx.tx)}; + return TxConsequences{ + ctx.tx.getSTTx(), getTxConsequencesCategory(ctx.tx.getSTTx())}; } NotTEC @@ -262,12 +263,28 @@ SetAccount::doApply() std::uint32_t const uFlagsIn = sle->getFieldU32(sfFlags); std::uint32_t uFlagsOut = uFlagsIn; - STTx const& tx{ctx_.tx}; + STTx const& tx{ctx_.tx.getSTTx()}; std::uint32_t const uSetFlag{tx.getFieldU32(sfSetFlag)}; std::uint32_t const uClearFlag{tx.getFieldU32(sfClearFlag)}; // legacy AccountSet flags std::uint32_t const uTxFlags{tx.getFlags()}; + + bool granularDelegated = false; + // AccountSet can not be delegated on the transaction level. + if (ctx_.tx.isDelegated() && !ctx_.permissions.empty()) + { + // if permissions is not empty, granular delegation is happening. + granularDelegated = true; + + // We don't support any flag based granular permission under + // AccountSet transaction. If any delegated account is trying to + // update the flag onbehalf of another account, it is not + // authorized. + if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags != 0) + return tecNO_PERMISSION; + } + bool const bSetRequireDest{ (uTxFlags & tfRequireDestTag) || (uSetFlag == asfRequireDest)}; bool const bClearRequireDest{ @@ -450,6 +467,11 @@ SetAccount::doApply() // if (tx.isFieldPresent(sfEmailHash)) { + if (granularDelegated && + ctx_.permissions.find(AccountEmailHashSet) == + ctx_.permissions.end()) + return tecNO_PERMISSION; + uint128 const uHash = tx.getFieldH128(sfEmailHash); if (!uHash) @@ -469,6 +491,9 @@ SetAccount::doApply() // if (tx.isFieldPresent(sfWalletLocator)) { + if (granularDelegated) + return tecNO_PERMISSION; + uint256 const uHash = tx.getFieldH256(sfWalletLocator); if (!uHash) @@ -488,6 +513,11 @@ SetAccount::doApply() // if (tx.isFieldPresent(sfMessageKey)) { + if (granularDelegated && + ctx_.permissions.find(AccountMessageKeySet) == + ctx_.permissions.end()) + return tecNO_PERMISSION; + Blob const messageKey = tx.getFieldVL(sfMessageKey); if (messageKey.empty()) @@ -507,6 +537,10 @@ SetAccount::doApply() // if (tx.isFieldPresent(sfDomain)) { + if (granularDelegated && + ctx_.permissions.find(AccountDomainSet) == ctx_.permissions.end()) + return tecNO_PERMISSION; + Blob const domain = tx.getFieldVL(sfDomain); if (domain.empty()) @@ -526,6 +560,11 @@ SetAccount::doApply() // if (tx.isFieldPresent(sfTransferRate)) { + if (granularDelegated && + ctx_.permissions.find(AccountTransferRateSet) == + ctx_.permissions.end()) + return tecNO_PERMISSION; + std::uint32_t uRate = tx.getFieldU32(sfTransferRate); if (uRate == 0 || uRate == QUALITY_ONE) @@ -545,6 +584,10 @@ SetAccount::doApply() // if (tx.isFieldPresent(sfTickSize)) { + if (granularDelegated && + ctx_.permissions.find(AccountTickSizeSet) == ctx_.permissions.end()) + return tecNO_PERMISSION; + auto uTickSize = tx[sfTickSize]; if ((uTickSize == 0) || (uTickSize == Quality::maxTickSize)) { diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index 173107e02ae..b67cfc16e9d 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -90,7 +90,7 @@ SetSignerList::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - auto const result = determineOperation(ctx.tx, ctx.flags, ctx.j); + auto const result = determineOperation(ctx.tx.getSTTx(), ctx.flags, ctx.j); if (std::get<0>(result) != tesSUCCESS) return std::get<0>(result); @@ -145,7 +145,7 @@ void SetSignerList::preCompute() { // Get the quorum and operation info. - auto result = determineOperation(ctx_.tx, view().flags(), j_); + auto result = determineOperation(ctx_.tx.getSTTx(), view().flags(), j_); XRPL_ASSERT( std::get<0>(result) == tesSUCCESS, "ripple::SetSignerList::preCompute : result is tesSUCCESS"); diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index b1e0494ba46..4c0d6b9e3e9 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -343,6 +343,23 @@ SetTrust::doApply() bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze); bool const bClearDeepFreeze = (uTxFlags & tfClearDeepFreeze); + bool granularDelegated = false; + if (ctx_.tx.isDelegated() && !ctx_.permissions.empty()) + { + granularDelegated = true; + // If granular permission is delegated under the TrustSet transaction. + // Currently we only support TrustlineAuthorize, TrustlineFreeze and + // TrustlineUnfreeze granular permission. + if (bSetNoRipple || bClearNoRipple || bQualityIn || bQualityOut) + return tecNO_PERMISSION; + if (bSetAuth && !ctx_.permissions.contains(TrustlineAuthorize)) + return tecNO_PERMISSION; + if (bSetFreeze && !ctx_.permissions.contains(TrustlineFreeze)) + return tecNO_PERMISSION; + if (bClearFreeze && !ctx_.permissions.contains(TrustlineUnfreeze)) + return tecNO_PERMISSION; + } + auto viewJ = ctx_.app.journal("View"); // Trust lines to self are impossible but because of the old bug there @@ -400,6 +417,18 @@ SetTrust::doApply() // // Limits // + if (granularDelegated) + { + // Currently we only support TrustlineAuthorize, TrustlineFreeze and + // TrustlineUnfreeze granular permission. So updating the + // LimitAmount is not allowed unless the delegated account has full + // transaction level permission. + auto const curLimit = bHigh + ? sleRippleState->getFieldAmount(sfHighLimit) + : sleRippleState->getFieldAmount(sfLowLimit); + if (curLimit != saLimitAllow) + return tecNO_PERMISSION; + } sleRippleState->setFieldAmount( !bHigh ? sfLowLimit : sfHighLimit, saLimitAllow); @@ -639,6 +668,13 @@ SetTrust::doApply() } else { + if (granularDelegated) + // currently we only allow TrustlineAuthorize, TrustlineFreeze and + // TrustlineUnfreeze granular permission delegation, a delegated + // account can not create a new trust line if it is not fully + // delegated with the whole TrustSet transaction based permission. + return tecNO_PERMISSION; + // Zero balance in currency. STAmount saBalance(Issue{currency, noAccount()}); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 9d3e9e39460..56b50f592d1 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -41,7 +41,7 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx) { - if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) + if (!isPseudoTx(ctx.tx.getSTTx()) || ctx.tx.isFieldPresent(sfNetworkID)) { uint32_t nodeNID = ctx.app.config().NETWORK_ID; std::optional txNID = ctx.tx[~sfNetworkID]; @@ -81,6 +81,10 @@ preflight0(PreflightContext const& ctx) NotTEC preflight1(PreflightContext const& ctx) { + if (!ctx.rules.enabled(featureAccountPermission) && + ctx.tx.isFieldPresent(sfOnBehalfOf)) + return temDISABLED; + // This is inappropriate in preflight0, because only Change transactions // skip this function, and those do not allow an sfTicketSequence field. if (ctx.tx.isFieldPresent(sfTicketSequence) && @@ -93,7 +97,7 @@ preflight1(PreflightContext const& ctx) if (!isTesSuccess(ret)) return ret; - auto const id = ctx.tx.getAccountID(sfAccount); + auto const id = ctx.tx.getSenderAccount(); if (id == beast::zero) { JLOG(ctx.j.warn()) << "preflight1: bad account id"; @@ -162,7 +166,7 @@ preflight2(PreflightContext const& ctx) } auto const sigValid = checkValidity( - ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); + ctx.app.getHashRouter(), ctx.tx.getSTTx(), ctx.rules, ctx.app.config()); if (sigValid.first == Validity::SigBad) { JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; @@ -179,7 +183,11 @@ PreflightContext::PreflightContext( Rules const& rules_, ApplyFlags flags_, beast::Journal j_) - : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) + : app(app_) + , tx(STTxDelegated(tx_, tx_.isFieldPresent(sfOnBehalfOf))) + , rules(rules_) + , flags(flags_) + , j(j_) { } @@ -208,6 +216,52 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) return baseFee + (signerCount * baseFee); } +TER +Transactor::checkPermissions( + ReadView const& view, + STTx const& tx, + std::unordered_set& permissions) +{ + if (!tx.isFieldPresent(sfOnBehalfOf) || + !view.exists(keylet::account(tx[sfOnBehalfOf]))) + return terNO_ACCOUNT; + + auto const accountPermissionKey = + keylet::accountPermission(tx[sfOnBehalfOf], tx[sfAccount]); + auto const sle = view.read(accountPermissionKey); + if (!sle) + return tecNO_PERMISSION; + + auto const permissionArray = sle->getFieldArray(sfPermissions); + auto const transactionType = tx.getTxnType(); + + for (auto const& permission : permissionArray) + { + auto const permissionValue = permission[sfPermissionValue]; + if (permissionValue == transactionType + 1) + { + // if the transaction permission is authorized, do not need to check + // granular permission. + permissions.clear(); + return tesSUCCESS; + } + + auto const gpType = + static_cast(permissionValue); + auto const& type = Permission::getInstance().getGranularTxType(gpType); + if (type && *type == transactionType) + permissions.insert(gpType); + } + + if (permissions.empty()) + return tecNO_PERMISSION; + + // When the code reaches here, the transaction permission is not authorized. + // But one or more of its granular permission under this transaction type is + // authorized. And the granular types are stored in permissions. + return tesSUCCESS; +} + XRPAmount Transactor::minimumFee( Application& app, @@ -246,7 +300,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (feePaid == beast::zero) return tesSUCCESS; - auto const id = ctx.tx.getAccountID(sfAccount); + auto const id = ctx.tx.getSenderAccount(); auto const sle = ctx.view.read(keylet::account(id)); if (!sle) return terNO_ACCOUNT; @@ -276,15 +330,20 @@ Transactor::payFee() { auto const feePaid = ctx_.tx[sfFee].xrp(); - auto const sle = view().peek(keylet::account(account_)); + // whether the transaction is being delegated to another account or not, + // the sender account will pay the fee. + auto const sle = view().peek(keylet::account(ctx_.tx.getSenderAccount())); if (!sle) return tefINTERNAL; // Deduct the fee, so it's not available during the transaction. // Will only write the account back if the transaction succeeds. - mSourceBalance -= feePaid; - sle->setFieldAmount(sfBalance, mSourceBalance); + sle->setFieldAmount(sfBalance, STAmount{(*sle)[sfBalance]}.xrp() - feePaid); + if (!ctx_.tx.isDelegated()) + { + mSourceBalance -= feePaid; + } // VFALCO Should we call view().rawDestroyXRP() here as well? @@ -312,6 +371,8 @@ Transactor::checkSeqProxy( SeqProxy const t_seqProx = tx.getSeqProxy(); SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]); + // check delegate seq + if (t_seqProx.isSeq()) { if (tx.isFieldPresent(sfTicketSequence) && @@ -363,10 +424,83 @@ Transactor::checkSeqProxy( return tesSUCCESS; } +NotTEC +Transactor::checkDelegateSeqProxy( + ReadView const& view, + STTx const& tx, + beast::Journal j) +{ + if (!tx.isFieldPresent(sfDelegateSequence)) + { + JLOG(j.trace()) << "applyTransaction: has no DelegateTicketSequence"; + return temBAD_SEQUENCE; + } + + auto const id = tx.getAccountID(sfOnBehalfOf); + auto const sle = view.read(keylet::account(id)); + if (!sle) + { + JLOG(j.trace()) << "applyTransaction: delay: delegating source account " + "does not exist " + << toBase58(id); + return terNO_ACCOUNT; + } + + SeqProxy const t_seqProx = tx.getDelegateSeqProxy(); + SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]); + + if (t_seqProx.isSeq()) + { + if (tx.isFieldPresent(sfDelegateTicketSequence) && + view.rules().enabled(featureTicketBatch)) + { + JLOG(j.trace()) + << "applyTransaction: has both a DelegateTicketSequence " + "and a non-zero DelegateSequence number"; + return temSEQ_AND_TICKET; + } + if (t_seqProx != a_seq) + { + if (a_seq < t_seqProx) + { + JLOG(j.trace()) << "applyTransaction: has future delegating " + "sequence number " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return terPRE_SEQ; + } + // It's an already-used sequence number. + JLOG(j.trace()) + << "applyTransaction: has past delegating sequence number " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return tefPAST_SEQ; + } + } + else if (t_seqProx.isTicket()) + { + if (a_seq.value() <= t_seqProx.value()) + { + JLOG(j.trace()) << "applyTransaction: has future ticket id " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return terPRE_TICKET; + } + + // Transaction can never succeed if the Ticket is not in the ledger. + if (!view.exists(keylet::ticket(id, t_seqProx))) + { + JLOG(j.trace()) + << "applyTransaction: ticket already used or never created " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return tefNO_TICKET; + } + } + + return tesSUCCESS; +} + NotTEC Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) { - auto const id = ctx.tx.getAccountID(sfAccount); + auto const id = ctx.tx.getSenderAccount(); auto const sle = ctx.view.read(keylet::account(id)); @@ -394,11 +528,14 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) } TER -Transactor::consumeSeqProxy(SLE::pointer const& sleAccount) +Transactor::consumeSeqProxy( + AccountID const& account, + SLE::pointer const& sleAccount, + SeqProxy const& seqProx) { XRPL_ASSERT( sleAccount, "ripple::Transactor::consumeSeqProxy : non-null account"); - SeqProxy const seqProx = ctx_.tx.getSeqProxy(); + if (seqProx.isSeq()) { // Note that if this transaction is a TicketCreate, then @@ -407,8 +544,7 @@ Transactor::consumeSeqProxy(SLE::pointer const& sleAccount) sleAccount->setFieldU32(sfSequence, seqProx.value() + 1); return tesSUCCESS; } - return ticketDelete( - view(), account_, getTicketIndex(account_, seqProx), j_); + return ticketDelete(view(), account, getTicketIndex(account, seqProx), j_); } // Remove a single Ticket from the ledger. @@ -481,20 +617,23 @@ Transactor::apply() // If the transactor requires a valid account and the transaction doesn't // list one, preflight will have already a flagged a failure. - auto const sle = view().peek(keylet::account(account_)); + auto const sleEffective = view().peek(keylet::account(account_)); + auto const sender = ctx_.tx.getSenderAccount(); + auto const sle = view().peek(keylet::account(sender)); // sle must exist except for transactions // that allow zero account. + XRPL_ASSERT( - sle != nullptr || account_ == beast::zero, + sle != nullptr || sender == beast::zero, "ripple::Transactor::apply : non-null SLE or zero account"); if (sle) { - mPriorBalance = STAmount{(*sle)[sfBalance]}.xrp(); + mPriorBalance = STAmount{(*sleEffective)[sfBalance]}.xrp(); mSourceBalance = mPriorBalance; - TER result = consumeSeqProxy(sle); + TER result = consumeSeqProxy(sender, sle, ctx_.tx.getSeqProxy()); if (result != tesSUCCESS) return result; @@ -506,6 +645,18 @@ Transactor::apply() sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID()); view().update(sle); + + if (ctx_.tx.isDelegated()) + { + auto const accountDelegating = ctx_.tx.getAccountID(sfAccount); + auto const sleDelegating = + view().peek(keylet::account(accountDelegating)); + result = consumeSeqProxy( + accountDelegating, + sleDelegating, + ctx_.tx.getDelegateSeqProxy()); + view().update(sleDelegating); + } } return doApply(); @@ -542,7 +693,7 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } // Look up the account. - auto const idAccount = ctx.tx.getAccountID(sfAccount); + auto const idAccount = ctx.tx.getSenderAccount(); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; @@ -612,7 +763,7 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) NotTEC Transactor::checkMultiSign(PreclaimContext const& ctx) { - auto const id = ctx.tx.getAccountID(sfAccount); + auto const id = ctx.tx.getSenderAccount(); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = ctx.view.read(keylet::signers(id)); @@ -863,8 +1014,8 @@ Transactor::reset(XRPAmount fee) { ctx_.discard(); - auto const txnAcct = - view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount))); + auto sender = ctx_.tx.getSenderAccount(); + auto const txnAcct = view().peek(keylet::account(sender)); if (!txnAcct) // The account should never be missing from the ledger. But if it // is missing then we can't very well charge it a fee, can we? @@ -889,13 +1040,28 @@ Transactor::reset(XRPAmount fee) // then the ledger is corrupted. Rather than make things worse we // reject the transaction. txnAcct->setFieldAmount(sfBalance, balance - fee); - TER const ter{consumeSeqProxy(txnAcct)}; + TER const ter{consumeSeqProxy(sender, txnAcct, ctx_.tx.getSeqProxy())}; XRPL_ASSERT( isTesSuccess(ter), "ripple::Transactor::reset : result is tesSUCCESS"); if (isTesSuccess(ter)) view().update(txnAcct); + if (ctx_.tx.isDelegated()) + { + auto const accountDelegating = ctx_.tx.getAccountID(sfAccount); + auto const sleDelegating = + view().peek(keylet::account(accountDelegating)); + TER const terDelegate{consumeSeqProxy( + accountDelegating, sleDelegating, ctx_.tx.getDelegateSeqProxy())}; + XRPL_ASSERT( + isTesSuccess(terDelegate), + "ripple::Transactor::reset delegate seq : result is tesSUCCESS"); + + if (isTesSuccess(terDelegate)) + view().update(sleDelegating); + } + return {ter, fee}; } @@ -926,7 +1092,7 @@ Transactor::operator()() SerialIter sit(ser.slice()); STTx s2(sit); - if (!s2.isEquivalent(ctx_.tx)) + if (!s2.isEquivalent(ctx_.tx.getSTTx())) { JLOG(j_.fatal()) << "Transaction serdes mismatch"; JLOG(j_.info()) << to_string(ctx_.tx.getJson(JsonOptions::none)); diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index a4ba3bfed25..8cc54a6382b 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace ripple { @@ -32,7 +33,7 @@ struct PreflightContext { public: Application& app; - STTx const& tx; + STTxDelegated const tx; Rules const rules; ApplyFlags flags; beast::Journal const j; @@ -55,7 +56,7 @@ struct PreclaimContext Application& app; ReadView const& view; TER preflightResult; - STTx const& tx; + STTxDelegated const tx; ApplyFlags flags; beast::Journal const j; @@ -69,7 +70,7 @@ struct PreclaimContext : app(app_) , view(view_) , preflightResult(preflightResult_) - , tx(tx_) + , tx(STTxDelegated(tx_, tx_.isFieldPresent(sfOnBehalfOf))) , flags(flags_) , j(j_) { @@ -128,6 +129,12 @@ class Transactor static NotTEC checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j); + static NotTEC + checkDelegateSeqProxy( + ReadView const& view, + STTx const& tx, + beast::Journal j); + static NotTEC checkPriorTxAndLastLedger(PreclaimContext const& ctx); @@ -141,6 +148,12 @@ class Transactor static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + static TER + checkPermissions( + ReadView const& view, + STTx const& tx, + std::unordered_set& permissions); + static TER preclaim(PreclaimContext const& ctx) { @@ -191,7 +204,10 @@ class Transactor reset(XRPAmount fee); TER - consumeSeqProxy(SLE::pointer const& sleAccount); + consumeSeqProxy( + AccountID const& account, + SLE::pointer const& sleAccount, + SeqProxy const& seqProx); TER payFee(); static NotTEC diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 0c6be61040c..3457eeffdca 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -1223,7 +1223,7 @@ attestationPreflight(PreflightContext const& ctx) if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; - auto const att = toClaim(ctx.tx); + auto const att = toClaim(ctx.tx.getSTTx()); if (!att) return temMALFORMED; @@ -1247,7 +1247,7 @@ template TER attestationPreclaim(PreclaimContext const& ctx) { - auto const att = toClaim(ctx.tx); + auto const att = toClaim(ctx.tx.getSTTx()); if (!att) return tecINTERNAL; // checked in preflight @@ -1277,7 +1277,7 @@ template TER attestationDoApply(ApplyContext& ctx) { - auto const att = toClaim(ctx.tx); + auto const att = toClaim(ctx.tx.getSTTx()); if (!att) // Should already be checked in preflight return tecINTERNAL; @@ -1901,7 +1901,7 @@ XChainCommit::makeTxConsequences(PreflightContext const& ctx) return XRPAmount{beast::zero}; }(); - return TxConsequences{ctx.tx, maxSpend}; + return TxConsequences{ctx.tx.getSTTx(), maxSpend}; } NotTEC diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 95cc3521a91..74a2f3426a3 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -120,7 +121,7 @@ requires(T::ConsequencesFactory == Transactor::Normal) TxConsequences consequences_helper(PreflightContext const& ctx) { - return TxConsequences(ctx.tx); + return TxConsequences(ctx.tx.getSTTx()); }; // For Transactor::Blocker @@ -129,7 +130,7 @@ requires(T::ConsequencesFactory == Transactor::Blocker) TxConsequences consequences_helper(PreflightContext const& ctx) { - return TxConsequences(ctx.tx, TxConsequences::blocker); + return TxConsequences(ctx.tx.getSTTx(), TxConsequences::blocker); }; // For Transactor::Custom @@ -165,7 +166,7 @@ invoke_preflight(PreflightContext const& ctx) } } -static TER +static std::pair> invoke_preclaim(PreclaimContext const& ctx) { try @@ -177,31 +178,53 @@ invoke_preclaim(PreclaimContext const& ctx) // doesn't list one, preflight will have already a flagged a // failure. auto const id = ctx.tx.getAccountID(sfAccount); + std::unordered_set permissions; if (id != beast::zero) { - TER result = T::checkSeqProxy(ctx.view, ctx.tx, ctx.j); + TER result = + T::checkSeqProxy(ctx.view, ctx.tx.getSTTx(), ctx.j); if (result != tesSUCCESS) - return result; + return std::make_pair(result, permissions); + + if (ctx.tx.isDelegated()) + { + result = T::checkDelegateSeqProxy( + ctx.view, ctx.tx.getSTTx(), ctx.j); + if (result != tesSUCCESS) + return std::make_pair(result, permissions); + } result = T::checkPriorTxAndLastLedger(ctx); if (result != tesSUCCESS) - return result; + return std::make_pair(result, permissions); - result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx)); + result = T::checkFee( + ctx, calculateBaseFee(ctx.view, ctx.tx.getSTTx())); if (result != tesSUCCESS) - return result; + return std::make_pair(result, permissions); result = T::checkSign(ctx); if (result != tesSUCCESS) - return result; + return std::make_pair(result, permissions); + + if (ctx.tx.isDelegated()) + { + // if this is a delegated transaction, check if the account + // has authorization. + result = T::checkPermissions( + ctx.view, ctx.tx.getSTTx(), permissions); + + if (result != tesSUCCESS) + return std::make_pair(result, permissions); + } } - return T::preclaim(ctx); + return std::make_pair(T::preclaim(ctx), permissions); }); } catch (UnknownTxnType const& e) @@ -210,7 +233,7 @@ invoke_preclaim(PreclaimContext const& ctx) JLOG(ctx.j.fatal()) << "Unknown transaction type in preclaim: " << e.txnType; UNREACHABLE("ripple::invoke_preclaim : unknown transaction type"); - return temUNKNOWN; + return {temUNKNOWN, std::unordered_set{}}; } } @@ -324,14 +347,14 @@ preclaim( auto secondFlight = preflight( app, view.rules(), - preflightResult.tx, + preflightResult.tx.getSTTx(), preflightResult.flags, preflightResult.j); ctx.emplace( app, view, secondFlight.ter, - secondFlight.tx, + secondFlight.tx.getSTTx(), secondFlight.flags, secondFlight.j); } @@ -341,20 +364,26 @@ preclaim( app, view, preflightResult.ter, - preflightResult.tx, + preflightResult.tx.getSTTx(), preflightResult.flags, preflightResult.j); } try { if (ctx->preflightResult != tesSUCCESS) - return {*ctx, ctx->preflightResult}; - return {*ctx, invoke_preclaim(*ctx)}; + return { + *ctx, + ctx->preflightResult, + std::unordered_set{}}; + + auto const& res = invoke_preclaim(*ctx); + return {*ctx, res.first, res.second}; } catch (std::exception const& e) { JLOG(ctx->j.fatal()) << "apply: " << e.what(); - return {*ctx, tefEXCEPTION}; + return { + *ctx, tefEXCEPTION, std::unordered_set{}}; } } @@ -386,10 +415,11 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view) ApplyContext ctx( app, view, - preclaimResult.tx, + preclaimResult.tx.getSTTx(), preclaimResult.ter, - calculateBaseFee(view, preclaimResult.tx), + calculateBaseFee(view, preclaimResult.tx.getSTTx()), preclaimResult.flags, + preclaimResult.permissions, preclaimResult.j); return invoke_apply(ctx); } diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index ef194bd398c..99863e47722 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -46,7 +46,9 @@ canHaveMPTokenIssuanceID( TxMeta const& transactionMeta); std::optional -getIDFromCreatedIssuance(TxMeta const& transactionMeta); +getIDFromCreatedIssuance( + TxMeta const& transactionMeta, + AccountID const& sender); void insertMPTokenIssuanceID( diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index 721be652622..8cc0907729e 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -48,7 +48,7 @@ canHaveMPTokenIssuanceID( } std::optional -getIDFromCreatedIssuance(TxMeta const& transactionMeta) +getIDFromCreatedIssuance(TxMeta const& transactionMeta, AccountID const& sender) { for (STObject const& node : transactionMeta.getNodes()) { @@ -58,8 +58,7 @@ getIDFromCreatedIssuance(TxMeta const& transactionMeta) auto const& mptNode = node.peekAtField(sfNewFields).downcast(); - return makeMptID( - mptNode.getFieldU32(sfSequence), mptNode.getAccountID(sfIssuer)); + return makeMptID(mptNode.getFieldU32(sfSequence), sender); } return std::nullopt; @@ -74,7 +73,9 @@ insertMPTokenIssuanceID( if (!canHaveMPTokenIssuanceID(transaction, transactionMeta)) return; - std::optional result = getIDFromCreatedIssuance(transactionMeta); + auto const sender = transaction->getAccountID(sfAccount); + std::optional result = + getIDFromCreatedIssuance(transactionMeta, sender); if (result) response[jss::mpt_issuance_id] = to_string(result.value()); } diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 7298ee290d8..bbb7666cc7a 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -67,6 +67,43 @@ parseAuthorizeCredentials(Json::Value const& jv) return arr; } +static std::optional +parseAccountPermission(Json::Value const& params, Json::Value& jvResult) +{ + // LCOV_EXCL_START + if (!params.isObject()) + { + uint256 uNodeIndex; + if (!uNodeIndex.parseHex(params.asString())) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return uNodeIndex; + } + // LCOV_EXCL_STOP + if (!params.isMember(jss::account) || !params.isMember(jss::authorize)) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + auto const account = + parseBase58(params[jss::account].asString()); + if (!account) + { + jvResult[jss::error] = "malformedAccount"; + return std::nullopt; + } + auto const authorize = + parseBase58(params[jss::authorize].asString()); + if (!authorize) + { + jvResult[jss::error] = "malformedAuthorize"; + return std::nullopt; + } + return keylet::accountPermission(*account, *authorize).key; +} + static std::optional parseIndex(Json::Value const& params, Json::Value& jvResult) { @@ -873,6 +910,7 @@ doLedgerEntry(RPC::JsonContext& context) {jss::index, parseIndex, ltANY}, {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, // TODO: add amendments + {jss::account_permission, parseAccountPermission, ltACCOUNT_PERMISSION}, {jss::amm, parseAMM, ltAMM}, {jss::bridge, parseBridge, ltBRIDGE}, {jss::check, parseCheck, ltCHECK},