diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 01f5b50fb6..c9d4376082 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -42,7 +42,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, /** This transaction type creates an escrow object. */ #if TRANSACTION_INCLUDE -# include +# include #endif TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegable, @@ -58,6 +58,9 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, })) /** This transaction type completes an existing escrow. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegable, uint256{}, @@ -94,7 +97,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, /** This transaction type cancels an existing escrow. */ #if TRANSACTION_INCLUDE -# include +# include #endif TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegable, @@ -180,7 +183,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, /** This transaction type creates a new unidirectional XRP payment channel. */ #if TRANSACTION_INCLUDE -# include +# include #endif TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegable, @@ -196,6 +199,9 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, })) /** This transaction type funds an existing unidirectional XRP payment channel. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegable, uint256{}, @@ -207,6 +213,9 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, })) /** This transaction type submits a claim against an existing unidirectional payment channel. */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegable, uint256{}, @@ -617,7 +626,7 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, /** This transaction type creates or updates a DID */ #if TRANSACTION_INCLUDE -# include +# include #endif TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegable, @@ -630,6 +639,9 @@ TRANSACTION(ttDID_SET, 49, DIDSet, })) /** This transaction type deletes a DID */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegable, featureDID, @@ -739,7 +751,7 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, /** This transaction type create an Credential instance */ #if TRANSACTION_INCLUDE -# include +# include #endif TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegable, @@ -753,6 +765,9 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, })) /** This transaction type accept an Credential object */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegable, featureCredentials, @@ -763,6 +778,9 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, })) /** This transaction type delete an Credential object */ +#if TRANSACTION_INCLUDE +# include +#endif TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegable, featureCredentials, diff --git a/include/xrpl/tx/transactors/credentials/CredentialAccept.h b/include/xrpl/tx/transactors/credentials/CredentialAccept.h new file mode 100644 index 0000000000..1895e54589 --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialAccept.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class CredentialAccept : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/credentials/CredentialCreate.h b/include/xrpl/tx/transactors/credentials/CredentialCreate.h new file mode 100644 index 0000000000..500b034a9c --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialCreate.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class CredentialCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/credentials/CredentialDelete.h b/include/xrpl/tx/transactors/credentials/CredentialDelete.h new file mode 100644 index 0000000000..26305232ce --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialDelete.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class CredentialDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/credentials/Credentials.h b/include/xrpl/tx/transactors/credentials/Credentials.h deleted file mode 100644 index 8c96f40526..0000000000 --- a/include/xrpl/tx/transactors/credentials/Credentials.h +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include - -namespace xrpl { - -class CredentialCreate : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit CredentialCreate(ApplyContext& ctx) : Transactor(ctx) - { - } - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -class CredentialDelete : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit CredentialDelete(ApplyContext& ctx) : Transactor(ctx) - { - } - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -class CredentialAccept : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit CredentialAccept(ApplyContext& ctx) : Transactor(ctx) - { - } - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -} // namespace xrpl diff --git a/include/xrpl/tx/transactors/did/DID.h b/include/xrpl/tx/transactors/did/DIDDelete.h similarity index 61% rename from include/xrpl/tx/transactors/did/DID.h rename to include/xrpl/tx/transactors/did/DIDDelete.h index 8ab80bc694..037a57bb02 100644 --- a/include/xrpl/tx/transactors/did/DID.h +++ b/include/xrpl/tx/transactors/did/DIDDelete.h @@ -4,24 +4,6 @@ namespace xrpl { -class DIDSet : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit DIDSet(ApplyContext& ctx) : Transactor(ctx) - { - } - - static NotTEC - preflight(PreflightContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - class DIDDelete : public Transactor { public: diff --git a/include/xrpl/tx/transactors/did/DIDSet.h b/include/xrpl/tx/transactors/did/DIDSet.h new file mode 100644 index 0000000000..fe1c476b47 --- /dev/null +++ b/include/xrpl/tx/transactors/did/DIDSet.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace xrpl { + +class DIDSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit DIDSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/escrow/Escrow.h b/include/xrpl/tx/transactors/escrow/Escrow.h deleted file mode 100644 index 0fd627e5a9..0000000000 --- a/include/xrpl/tx/transactors/escrow/Escrow.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include - -namespace xrpl { - -class EscrowCreate : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; - - explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx) - { - } - - static TxConsequences - makeTxConsequences(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -class EscrowFinish : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static NotTEC - preflightSigValidated(PreflightContext const& ctx); - - static XRPAmount - calculateBaseFee(ReadView const& view, STTx const& tx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -class EscrowCancel : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx) - { - } - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -} // namespace xrpl diff --git a/include/xrpl/tx/transactors/escrow/EscrowCancel.h b/include/xrpl/tx/transactors/escrow/EscrowCancel.h new file mode 100644 index 0000000000..c8b97203f0 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowCancel.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace xrpl { + +class EscrowCancel : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/escrow/EscrowCreate.h b/include/xrpl/tx/transactors/escrow/EscrowCreate.h new file mode 100644 index 0000000000..221ffee070 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowCreate.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace xrpl { + +class EscrowCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/escrow/EscrowFinish.h b/include/xrpl/tx/transactors/escrow/EscrowFinish.h new file mode 100644 index 0000000000..0917c35d93 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowFinish.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace xrpl { + +class EscrowFinish : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/payment_channel/PayChan.h b/include/xrpl/tx/transactors/payment_channel/PayChan.h deleted file mode 100644 index cc23f029f2..0000000000 --- a/include/xrpl/tx/transactors/payment_channel/PayChan.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include - -namespace xrpl { - -class PayChanCreate : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; - - explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx) - { - } - - static TxConsequences - makeTxConsequences(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -using PaymentChannelCreate = PayChanCreate; - -//------------------------------------------------------------------------------ - -class PayChanFund : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; - - explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx) - { - } - - static TxConsequences - makeTxConsequences(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - TER - doApply() override; -}; - -using PaymentChannelFund = PayChanFund; - -//------------------------------------------------------------------------------ - -class PayChanClaim : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -using PaymentChannelClaim = PayChanClaim; - -} // namespace xrpl diff --git a/include/xrpl/tx/transactors/payment_channel/PayChanClaim.h b/include/xrpl/tx/transactors/payment_channel/PayChanClaim.h new file mode 100644 index 0000000000..a2728a60e7 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PayChanClaim.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace xrpl { + +class PayChanClaim : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +using PaymentChannelClaim = PayChanClaim; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/payment_channel/PayChanCreate.h b/include/xrpl/tx/transactors/payment_channel/PayChanCreate.h new file mode 100644 index 0000000000..ff59fdbdd0 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PayChanCreate.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace xrpl { + +class PayChanCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +using PaymentChannelCreate = PayChanCreate; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/payment_channel/PayChanFund.h b/include/xrpl/tx/transactors/payment_channel/PayChanFund.h new file mode 100644 index 0000000000..ac0f9e0ce2 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PayChanFund.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace xrpl { + +class PayChanFund : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; +}; + +using PaymentChannelFund = PayChanFund; + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/account/DeleteAccount.cpp b/src/libxrpl/tx/transactors/account/DeleteAccount.cpp index 68c0d7f304..a3fb97fa0d 100644 --- a/src/libxrpl/tx/transactors/account/DeleteAccount.cpp +++ b/src/libxrpl/tx/transactors/account/DeleteAccount.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp new file mode 100644 index 0000000000..c3315873e8 --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -0,0 +1,113 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +using namespace credentials; + +std::uint32_t +CredentialAccept::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + +NotTEC +CredentialAccept::preflight(PreflightContext const& ctx) +{ + if (!ctx.tx[sfIssuer]) + { + JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + auto const credType = ctx.tx[sfCredentialType]; + if (credType.empty() || (credType.size() > maxCredentialTypeLength)) + { + JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +CredentialAccept::preclaim(PreclaimContext const& ctx) +{ + AccountID const subject = ctx.tx[sfAccount]; + AccountID const issuer = ctx.tx[sfIssuer]; + auto const credType(ctx.tx[sfCredentialType]); + + if (!ctx.view.exists(keylet::account(issuer))) + { + JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer); + return tecNO_ISSUER; + } + + auto const sleCred = ctx.view.read(keylet::credential(subject, issuer, credType)); + if (!sleCred) + { + JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " << to_string(issuer) + << ", " << credType; + return tecNO_ENTRY; + } + + if (sleCred->getFieldU32(sfFlags) & lsfAccepted) + { + JLOG(ctx.j.warn()) << "Credential already accepted: " << to_string(subject) << ", " + << to_string(issuer) << ", " << credType; + return tecDUPLICATE; + } + + return tesSUCCESS; +} + +TER +CredentialAccept::doApply() +{ + AccountID const issuer{ctx_.tx[sfIssuer]}; + + // Both exist as credential object exist itself (checked in preclaim) + auto const sleSubject = view().peek(keylet::account(account_)); + auto const sleIssuer = view().peek(keylet::account(issuer)); + + if (!sleSubject || !sleIssuer) + return tefINTERNAL; // LCOV_EXCL_LINE + + { + STAmount const reserve{ + view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)}; + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + auto const credType(ctx_.tx[sfCredentialType]); + Keylet const credentialKey = keylet::credential(account_, issuer, credType); + auto const sleCred = view().peek(credentialKey); // Checked in preclaim() + + if (checkExpired(sleCred, view().header().parentCloseTime)) + { + JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText(); + // delete expired credentials even if the transaction failed + auto const err = credentials::deleteSLE(view(), sleCred, j_); + return isTesSuccess(err) ? tecEXPIRED : err; + } + + sleCred->setFieldU32(sfFlags, lsfAccepted); + view().update(sleCred); + + adjustOwnerCount(view(), sleIssuer, -1, j_); + adjustOwnerCount(view(), sleSubject, 1, j_); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp new file mode 100644 index 0000000000..88b860e79e --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/* + Credentials + ====== + + A verifiable credentials (VC + https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C + specification (https://www.w3.org/TR/vc-data-model-2.0/), is a + secure and tamper-evident way to represent information about a subject, such + as an individual, organization, or even an IoT device. These credentials are + issued by a trusted entity and can be verified by third parties without + directly involving the issuer at all. +*/ + +using namespace credentials; + +std::uint32_t +CredentialCreate::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + +NotTEC +CredentialCreate::preflight(PreflightContext const& ctx) +{ + auto const& tx = ctx.tx; + auto& j = ctx.j; + + if (!tx[sfSubject]) + { + JLOG(j.trace()) << "Malformed transaction: Invalid Subject"; + return temMALFORMED; + } + + auto const uri = tx[~sfURI]; + if (uri && (uri->empty() || (uri->size() > maxCredentialURILength))) + { + JLOG(j.trace()) << "Malformed transaction: invalid size of URI."; + return temMALFORMED; + } + + auto const credType = tx[sfCredentialType]; + if (credType.empty() || (credType.size() > maxCredentialTypeLength)) + { + JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +CredentialCreate::preclaim(PreclaimContext const& ctx) +{ + auto const credType(ctx.tx[sfCredentialType]); + auto const subject = ctx.tx[sfSubject]; + + if (!ctx.view.exists(keylet::account(subject))) + { + JLOG(ctx.j.trace()) << "Subject doesn't exist."; + return tecNO_TARGET; + } + + if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType))) + { + JLOG(ctx.j.trace()) << "Credential already exists."; + return tecDUPLICATE; + } + + return tesSUCCESS; +} + +TER +CredentialCreate::doApply() +{ + auto const subject = ctx_.tx[sfSubject]; + auto const credType(ctx_.tx[sfCredentialType]); + Keylet const credentialKey = keylet::credential(subject, account_, credType); + + auto const sleCred = std::make_shared(credentialKey); + if (!sleCred) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const optExp = ctx_.tx[~sfExpiration]; + if (optExp) + { + std::uint32_t const closeTime = + ctx_.view().header().parentCloseTime.time_since_epoch().count(); + + if (closeTime > *optExp) + { + JLOG(j_.trace()) << "Malformed transaction: " + "Expiration time is in the past."; + return tecEXPIRED; + } + + sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration)); + } + + auto const sleIssuer = view().peek(keylet::account(account_)); + if (!sleIssuer) + return tefINTERNAL; // LCOV_EXCL_LINE + + { + STAmount const reserve{ + view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)}; + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + sleCred->setAccountID(sfSubject, subject); + sleCred->setAccountID(sfIssuer, account_); + sleCred->setFieldVL(sfCredentialType, credType); + + if (ctx_.tx.isFieldPresent(sfURI)) + sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI)); + + { + auto const page = + view().dirInsert(keylet::ownerDir(account_), credentialKey, describeOwnerDir(account_)); + JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key) + << ": " << (page ? "success" : "failure"); + if (!page) + return tecDIR_FULL; + sleCred->setFieldU64(sfIssuerNode, *page); + + adjustOwnerCount(view(), sleIssuer, 1, j_); + } + + if (subject == account_) + { + sleCred->setFieldU32(sfFlags, lsfAccepted); + } + else + { + auto const page = + view().dirInsert(keylet::ownerDir(subject), credentialKey, describeOwnerDir(subject)); + JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key) + << ": " << (page ? "success" : "failure"); + if (!page) + return tecDIR_FULL; + sleCred->setFieldU64(sfSubjectNode, *page); + view().update(view().peek(keylet::account(subject))); + } + + view().insert(sleCred); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp b/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp new file mode 100644 index 0000000000..d273135aa9 --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +using namespace credentials; + +std::uint32_t +CredentialDelete::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} + +NotTEC +CredentialDelete::preflight(PreflightContext const& ctx) +{ + auto const subject = ctx.tx[~sfSubject]; + auto const issuer = ctx.tx[~sfIssuer]; + + if (!subject && !issuer) + { + // Neither field is present, the transaction is malformed. + JLOG(ctx.j.trace()) << "Malformed transaction: " + "No Subject or Issuer fields."; + return temMALFORMED; + } + + // Make sure that the passed account is valid. + if ((subject && subject->isZero()) || (issuer && issuer->isZero())) + { + JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + auto const credType = ctx.tx[sfCredentialType]; + if (credType.empty() || (credType.size() > maxCredentialTypeLength)) + { + JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType."; + return temMALFORMED; + } + + return tesSUCCESS; +} + +TER +CredentialDelete::preclaim(PreclaimContext const& ctx) +{ + AccountID const account{ctx.tx[sfAccount]}; + auto const subject = ctx.tx[~sfSubject].value_or(account); + auto const issuer = ctx.tx[~sfIssuer].value_or(account); + auto const credType(ctx.tx[sfCredentialType]); + + if (!ctx.view.exists(keylet::credential(subject, issuer, credType))) + return tecNO_ENTRY; + + return tesSUCCESS; +} + +TER +CredentialDelete::doApply() +{ + auto const subject = ctx_.tx[~sfSubject].value_or(account_); + auto const issuer = ctx_.tx[~sfIssuer].value_or(account_); + + auto const credType(ctx_.tx[sfCredentialType]); + auto const sleCred = view().peek(keylet::credential(subject, issuer, credType)); + if (!sleCred) + return tefINTERNAL; // LCOV_EXCL_LINE + + if ((subject != account_) && (issuer != account_) && + !checkExpired(sleCred, ctx_.view().header().parentCloseTime)) + { + JLOG(j_.trace()) << "Can't delete non-expired credential."; + return tecNO_PERMISSION; + } + + return deleteSLE(view(), sleCred, j_); +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/credentials/Credentials.cpp b/src/libxrpl/tx/transactors/credentials/Credentials.cpp deleted file mode 100644 index 079b75a9fe..0000000000 --- a/src/libxrpl/tx/transactors/credentials/Credentials.cpp +++ /dev/null @@ -1,341 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace xrpl { - -/* - Credentials - ====== - - A verifiable credentials (VC - https://en.wikipedia.org/wiki/Verifiable_credentials), as defined by the W3C - specification (https://www.w3.org/TR/vc-data-model-2.0/), is a - secure and tamper-evident way to represent information about a subject, such - as an individual, organization, or even an IoT device. These credentials are - issued by a trusted entity and can be verified by third parties without - directly involving the issuer at all. -*/ - -using namespace credentials; - -// ------- CREATE -------------------------- - -std::uint32_t -CredentialCreate::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; -} - -NotTEC -CredentialCreate::preflight(PreflightContext const& ctx) -{ - auto const& tx = ctx.tx; - auto& j = ctx.j; - - if (!tx[sfSubject]) - { - JLOG(j.trace()) << "Malformed transaction: Invalid Subject"; - return temMALFORMED; - } - - auto const uri = tx[~sfURI]; - if (uri && (uri->empty() || (uri->size() > maxCredentialURILength))) - { - JLOG(j.trace()) << "Malformed transaction: invalid size of URI."; - return temMALFORMED; - } - - auto const credType = tx[sfCredentialType]; - if (credType.empty() || (credType.size() > maxCredentialTypeLength)) - { - JLOG(j.trace()) << "Malformed transaction: invalid size of CredentialType."; - return temMALFORMED; - } - - return tesSUCCESS; -} - -TER -CredentialCreate::preclaim(PreclaimContext const& ctx) -{ - auto const credType(ctx.tx[sfCredentialType]); - auto const subject = ctx.tx[sfSubject]; - - if (!ctx.view.exists(keylet::account(subject))) - { - JLOG(ctx.j.trace()) << "Subject doesn't exist."; - return tecNO_TARGET; - } - - if (ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType))) - { - JLOG(ctx.j.trace()) << "Credential already exists."; - return tecDUPLICATE; - } - - return tesSUCCESS; -} - -TER -CredentialCreate::doApply() -{ - auto const subject = ctx_.tx[sfSubject]; - auto const credType(ctx_.tx[sfCredentialType]); - Keylet const credentialKey = keylet::credential(subject, account_, credType); - - auto const sleCred = std::make_shared(credentialKey); - if (!sleCred) - return tefINTERNAL; // LCOV_EXCL_LINE - - auto const optExp = ctx_.tx[~sfExpiration]; - if (optExp) - { - std::uint32_t const closeTime = - ctx_.view().header().parentCloseTime.time_since_epoch().count(); - - if (closeTime > *optExp) - { - JLOG(j_.trace()) << "Malformed transaction: " - "Expiration time is in the past."; - return tecEXPIRED; - } - - sleCred->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration)); - } - - auto const sleIssuer = view().peek(keylet::account(account_)); - if (!sleIssuer) - return tefINTERNAL; // LCOV_EXCL_LINE - - { - STAmount const reserve{ - view().fees().accountReserve(sleIssuer->getFieldU32(sfOwnerCount) + 1)}; - if (mPriorBalance < reserve) - return tecINSUFFICIENT_RESERVE; - } - - sleCred->setAccountID(sfSubject, subject); - sleCred->setAccountID(sfIssuer, account_); - sleCred->setFieldVL(sfCredentialType, credType); - - if (ctx_.tx.isFieldPresent(sfURI)) - sleCred->setFieldVL(sfURI, ctx_.tx.getFieldVL(sfURI)); - - { - auto const page = - view().dirInsert(keylet::ownerDir(account_), credentialKey, describeOwnerDir(account_)); - JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key) - << ": " << (page ? "success" : "failure"); - if (!page) - return tecDIR_FULL; - sleCred->setFieldU64(sfIssuerNode, *page); - - adjustOwnerCount(view(), sleIssuer, 1, j_); - } - - if (subject == account_) - { - sleCred->setFieldU32(sfFlags, lsfAccepted); - } - else - { - auto const page = - view().dirInsert(keylet::ownerDir(subject), credentialKey, describeOwnerDir(subject)); - JLOG(j_.trace()) << "Adding Credential to owner directory " << to_string(credentialKey.key) - << ": " << (page ? "success" : "failure"); - if (!page) - return tecDIR_FULL; - sleCred->setFieldU64(sfSubjectNode, *page); - view().update(view().peek(keylet::account(subject))); - } - - view().insert(sleCred); - - return tesSUCCESS; -} - -// ------- DELETE -------------------------- - -std::uint32_t -CredentialDelete::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; -} - -NotTEC -CredentialDelete::preflight(PreflightContext const& ctx) -{ - auto const subject = ctx.tx[~sfSubject]; - auto const issuer = ctx.tx[~sfIssuer]; - - if (!subject && !issuer) - { - // Neither field is present, the transaction is malformed. - JLOG(ctx.j.trace()) << "Malformed transaction: " - "No Subject or Issuer fields."; - return temMALFORMED; - } - - // Make sure that the passed account is valid. - if ((subject && subject->isZero()) || (issuer && issuer->isZero())) - { - JLOG(ctx.j.trace()) << "Malformed transaction: Subject or Issuer " - "field zeroed."; - return temINVALID_ACCOUNT_ID; - } - - auto const credType = ctx.tx[sfCredentialType]; - if (credType.empty() || (credType.size() > maxCredentialTypeLength)) - { - JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType."; - return temMALFORMED; - } - - return tesSUCCESS; -} - -TER -CredentialDelete::preclaim(PreclaimContext const& ctx) -{ - AccountID const account{ctx.tx[sfAccount]}; - auto const subject = ctx.tx[~sfSubject].value_or(account); - auto const issuer = ctx.tx[~sfIssuer].value_or(account); - auto const credType(ctx.tx[sfCredentialType]); - - if (!ctx.view.exists(keylet::credential(subject, issuer, credType))) - return tecNO_ENTRY; - - return tesSUCCESS; -} - -TER -CredentialDelete::doApply() -{ - auto const subject = ctx_.tx[~sfSubject].value_or(account_); - auto const issuer = ctx_.tx[~sfIssuer].value_or(account_); - - auto const credType(ctx_.tx[sfCredentialType]); - auto const sleCred = view().peek(keylet::credential(subject, issuer, credType)); - if (!sleCred) - return tefINTERNAL; // LCOV_EXCL_LINE - - if ((subject != account_) && (issuer != account_) && - !checkExpired(sleCred, ctx_.view().header().parentCloseTime)) - { - JLOG(j_.trace()) << "Can't delete non-expired credential."; - return tecNO_PERMISSION; - } - - return deleteSLE(view(), sleCred, j_); -} - -// ------- APPLY -------------------------- - -std::uint32_t -CredentialAccept::getFlagsMask(PreflightContext const& ctx) -{ - // 0 means "Allow any flags" - return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; -} - -NotTEC -CredentialAccept::preflight(PreflightContext const& ctx) -{ - if (!ctx.tx[sfIssuer]) - { - JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed."; - return temINVALID_ACCOUNT_ID; - } - - auto const credType = ctx.tx[sfCredentialType]; - if (credType.empty() || (credType.size() > maxCredentialTypeLength)) - { - JLOG(ctx.j.trace()) << "Malformed transaction: invalid size of CredentialType."; - return temMALFORMED; - } - - return tesSUCCESS; -} - -TER -CredentialAccept::preclaim(PreclaimContext const& ctx) -{ - AccountID const subject = ctx.tx[sfAccount]; - AccountID const issuer = ctx.tx[sfIssuer]; - auto const credType(ctx.tx[sfCredentialType]); - - if (!ctx.view.exists(keylet::account(issuer))) - { - JLOG(ctx.j.warn()) << "No issuer: " << to_string(issuer); - return tecNO_ISSUER; - } - - auto const sleCred = ctx.view.read(keylet::credential(subject, issuer, credType)); - if (!sleCred) - { - JLOG(ctx.j.warn()) << "No credential: " << to_string(subject) << ", " << to_string(issuer) - << ", " << credType; - return tecNO_ENTRY; - } - - if (sleCred->getFieldU32(sfFlags) & lsfAccepted) - { - JLOG(ctx.j.warn()) << "Credential already accepted: " << to_string(subject) << ", " - << to_string(issuer) << ", " << credType; - return tecDUPLICATE; - } - - return tesSUCCESS; -} - -TER -CredentialAccept::doApply() -{ - AccountID const issuer{ctx_.tx[sfIssuer]}; - - // Both exist as credential object exist itself (checked in preclaim) - auto const sleSubject = view().peek(keylet::account(account_)); - auto const sleIssuer = view().peek(keylet::account(issuer)); - - if (!sleSubject || !sleIssuer) - return tefINTERNAL; // LCOV_EXCL_LINE - - { - STAmount const reserve{ - view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1)}; - if (mPriorBalance < reserve) - return tecINSUFFICIENT_RESERVE; - } - - auto const credType(ctx_.tx[sfCredentialType]); - Keylet const credentialKey = keylet::credential(account_, issuer, credType); - auto const sleCred = view().peek(credentialKey); // Checked in preclaim() - - if (checkExpired(sleCred, view().header().parentCloseTime)) - { - JLOG(j_.trace()) << "Credential is expired: " << sleCred->getText(); - // delete expired credentials even if the transaction failed - auto const err = credentials::deleteSLE(view(), sleCred, j_); - return isTesSuccess(err) ? tecEXPIRED : err; - } - - sleCred->setFieldU32(sfFlags, lsfAccepted); - view().update(sleCred); - - adjustOwnerCount(view(), sleIssuer, -1, j_); - adjustOwnerCount(view(), sleSubject, 1, j_); - - return tesSUCCESS; -} - -} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp b/src/libxrpl/tx/transactors/did/DIDDelete.cpp new file mode 100644 index 0000000000..02157d77d2 --- /dev/null +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +namespace xrpl { + +NotTEC +DIDDelete::preflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +TER +DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner) +{ + auto const sle = ctx.view().peek(sleKeylet); + if (!sle) + return tecNO_ENTRY; + + return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal); +} + +TER +DIDDelete::deleteSLE( + ApplyView& view, + std::shared_ptr sle, + AccountID const owner, + beast::Journal j) +{ + // Remove object from owner directory + if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete DID Token from owner."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + auto const sleOwner = view.peek(keylet::account(owner)); + if (!sleOwner) + return tecINTERNAL; // LCOV_EXCL_LINE + + adjustOwnerCount(view, sleOwner, -1, j); + view.update(sleOwner); + + // Remove object from ledger + view.erase(sle); + return tesSUCCESS; +} + +TER +DIDDelete::doApply() +{ + return deleteSLE(ctx_, keylet::did(account_), account_); +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/did/DID.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp similarity index 77% rename from src/libxrpl/tx/transactors/did/DID.cpp rename to src/libxrpl/tx/transactors/did/DIDSet.cpp index 080a031c79..f094e96bc8 100644 --- a/src/libxrpl/tx/transactors/did/DID.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include namespace xrpl { @@ -134,54 +134,4 @@ DIDSet::doApply() return addSLE(ctx_, sleDID, account_); } -NotTEC -DIDDelete::preflight(PreflightContext const& ctx) -{ - return tesSUCCESS; -} - -TER -DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner) -{ - auto const sle = ctx.view().peek(sleKeylet); - if (!sle) - return tecNO_ENTRY; - - return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal); -} - -TER -DIDDelete::deleteSLE( - ApplyView& view, - std::shared_ptr sle, - AccountID const owner, - beast::Journal j) -{ - // Remove object from owner directory - if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true)) - { - // LCOV_EXCL_START - JLOG(j.fatal()) << "Unable to delete DID Token from owner."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - auto const sleOwner = view.peek(keylet::account(owner)); - if (!sleOwner) - return tecINTERNAL; // LCOV_EXCL_LINE - - adjustOwnerCount(view, sleOwner, -1, j); - view.update(sleOwner); - - // Remove object from ledger - view.erase(sle); - return tesSUCCESS; -} - -TER -DIDDelete::doApply() -{ - return deleteSLE(ctx_, keylet::did(account_), account_); -} - } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/escrow/Escrow.cpp b/src/libxrpl/tx/transactors/escrow/Escrow.cpp deleted file mode 100644 index 3d7d408af7..0000000000 --- a/src/libxrpl/tx/transactors/escrow/Escrow.cpp +++ /dev/null @@ -1,1262 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl { - -// During an EscrowFinish, the transaction must specify both -// a condition and a fulfillment. We track whether that -// fulfillment matches and validates the condition. -constexpr HashRouterFlags SF_CF_INVALID = HashRouterFlags::PRIVATE5; -constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6; - -/* - Escrow - ====== - - Escrow is a feature of the XRP Ledger that allows you to send conditional - XRP payments. These conditional payments, called escrows, set aside XRP and - deliver it later when certain conditions are met. Conditions to successfully - finish an escrow include time-based unlocks and crypto-conditions. Escrows - can also be set to expire if not finished in time. - - The XRP set aside in an escrow is locked up. No one can use or destroy the - XRP until the escrow has been successfully finished or canceled. Before the - expiration time, only the intended receiver can get the XRP. After the - expiration time, the XRP can only be returned to the sender. - - For more details on escrow, including examples, diagrams and more please - visit https://xrpl.org/escrow.html - - For details on specific transactions, including fields and validation rules - please see: - - `EscrowCreate` - -------------- - See: https://xrpl.org/escrowcreate.html - - `EscrowFinish` - -------------- - See: https://xrpl.org/escrowfinish.html - - `EscrowCancel` - -------------- - See: https://xrpl.org/escrowcancel.html -*/ - -//------------------------------------------------------------------------------ - -TxConsequences -EscrowCreate::makeTxConsequences(PreflightContext const& ctx) -{ - auto const amount = ctx.tx[sfAmount]; - return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero}; -} - -template -static NotTEC -escrowCreatePreflightHelper(PreflightContext const& ctx); - -template <> -NotTEC -escrowCreatePreflightHelper(PreflightContext const& ctx) -{ - STAmount const amount = ctx.tx[sfAmount]; - if (amount.native() || amount <= beast::zero) - return temBAD_AMOUNT; - - if (badCurrency() == amount.getCurrency()) - return temBAD_CURRENCY; - - return tesSUCCESS; -} - -template <> -NotTEC -escrowCreatePreflightHelper(PreflightContext const& ctx) -{ - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; - - auto const amount = ctx.tx[sfAmount]; - if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero) - return temBAD_AMOUNT; - - return tesSUCCESS; -} - -NotTEC -EscrowCreate::preflight(PreflightContext const& ctx) -{ - STAmount const amount{ctx.tx[sfAmount]}; - if (!isXRP(amount)) - { - if (!ctx.rules.enabled(featureTokenEscrow)) - return temBAD_AMOUNT; - - if (auto const ret = std::visit( - [&](T const&) { return escrowCreatePreflightHelper(ctx); }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; - } - else - { - if (amount <= beast::zero) - return temBAD_AMOUNT; - } - - // We must specify at least one timeout value - if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter]) - return temBAD_EXPIRATION; - - // If both finish and cancel times are specified then the cancel time must - // be strictly after the finish time. - if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] && - ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) - return temBAD_EXPIRATION; - - // In the absence of a FinishAfter, the escrow can be finished - // immediately, which can be confusing. When creating an escrow, - // we want to ensure that either a FinishAfter time is explicitly - // specified or a completion condition is attached. - if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition]) - return temMALFORMED; - - if (auto const cb = ctx.tx[~sfCondition]) - { - using namespace xrpl::cryptoconditions; - - std::error_code ec; - - auto condition = Condition::deserialize(*cb, ec); - if (!condition) - { - JLOG(ctx.j.debug()) << "Malformed condition during escrow creation: " << ec.message(); - return temMALFORMED; - } - } - - return tesSUCCESS; -} - -template -static TER -escrowCreatePreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - AccountID const& dest, - STAmount const& amount); - -template <> -TER -escrowCreatePreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - AccountID const& dest, - STAmount const& amount) -{ - AccountID issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecNO_PERMISSION - if (issuer == account) - return tecNO_PERMISSION; - - // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION - auto const sleIssuer = ctx.view.read(keylet::account(issuer)); - if (!sleIssuer) - return tecNO_ISSUER; - if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) - return tecNO_PERMISSION; - - // If the account does not have a trustline to the issuer, return tecNO_LINE - auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, amount.getCurrency())); - if (!sleRippleState) - return tecNO_LINE; - - STAmount const balance = (*sleRippleState)[sfBalance]; - - // If balance is positive, issuer must have higher address than account - if (balance > beast::zero && issuer < account) - return tecNO_PERMISSION; // LCOV_EXCL_LINE - - // If balance is negative, issuer must have lower address than account - if (balance < beast::zero && issuer > account) - return tecNO_PERMISSION; // LCOV_EXCL_LINE - - // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS) - return ter; - - // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS) - return ter; - - // If the issuer has frozen the account, return tecFROZEN - if (isFrozen(ctx.view, account, amount.issue())) - return tecFROZEN; - - // If the issuer has frozen the destination, return tecFROZEN - if (isFrozen(ctx.view, dest, amount.issue())) - return tecFROZEN; - - STAmount const spendableAmount = - accountHolds(ctx.view, account, amount.getCurrency(), issuer, fhIGNORE_FREEZE, ctx.j); - - // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS - if (spendableAmount <= beast::zero) - return tecINSUFFICIENT_FUNDS; - - // If the spendable amount is less than the amount, return - // tecINSUFFICIENT_FUNDS - if (spendableAmount < amount) - return tecINSUFFICIENT_FUNDS; - - // If the amount is not addable to the balance, return tecPRECISION_LOSS - if (!canAdd(spendableAmount, amount)) - return tecPRECISION_LOSS; - - return tesSUCCESS; -} - -template <> -TER -escrowCreatePreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - AccountID const& dest, - STAmount const& amount) -{ - AccountID issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecNO_PERMISSION - if (issuer == account) - return tecNO_PERMISSION; - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION - if (!sleIssuance->isFlag(lsfMPTCanEscrow)) - return tecNO_PERMISSION; - - // If the issuer is not the same as the issuer of the mpt, return - // tecNO_PERMISSION - if (sleIssuance->getAccountID(sfIssuer) != issuer) - return tecNO_PERMISSION; // LCOV_EXCL_LINE - - // If the account does not have the mpt, return tecOBJECT_NOT_FOUND - if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the account is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - ter != tesSUCCESS) - return ter; - - // If the issuer has requireAuth set, check if the destination is - // authorized - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - ter != tesSUCCESS) - return ter; - - // If the issuer has frozen the account, return tecLOCKED - if (isFrozen(ctx.view, account, mptIssue)) - return tecLOCKED; - - // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) - return tecLOCKED; - - // If the mpt cannot be transferred, return tecNO_AUTH - if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); ter != tesSUCCESS) - return ter; - - STAmount const spendableAmount = accountHolds( - ctx.view, account, amount.get(), fhIGNORE_FREEZE, ahIGNORE_AUTH, ctx.j); - - // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS - if (spendableAmount <= beast::zero) - return tecINSUFFICIENT_FUNDS; - - // If the spendable amount is less than the amount, return - // tecINSUFFICIENT_FUNDS - if (spendableAmount < amount) - return tecINSUFFICIENT_FUNDS; - - return tesSUCCESS; -} - -TER -EscrowCreate::preclaim(PreclaimContext const& ctx) -{ - STAmount const amount{ctx.tx[sfAmount]}; - AccountID const account{ctx.tx[sfAccount]}; - AccountID const dest{ctx.tx[sfDestination]}; - - auto const sled = ctx.view.read(keylet::account(dest)); - if (!sled) - return tecNO_DST; - - // Pseudo-accounts cannot receive escrow. Note, this is not amendment-gated - // because all writes to pseudo-account discriminator fields **are** - // amendment gated, hence the behaviour of this check will always match the - // currently active amendments. - if (isPseudoAccount(sled)) - return tecNO_PERMISSION; - - if (!isXRP(amount)) - { - if (!ctx.view.rules().enabled(featureTokenEscrow)) - return temDISABLED; // LCOV_EXCL_LINE - - if (auto const ret = std::visit( - [&](T const&) { - return escrowCreatePreclaimHelper(ctx, account, dest, amount); - }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; - } - return tesSUCCESS; -} - -template -static TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal); - -template <> -TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal) -{ - // Defensive: Issuer cannot create an escrow - if (issuer == sender) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ter = rippleCredit( - view, sender, issuer, amount, amount.holds() ? false : true, journal); - if (ter != tesSUCCESS) - return ter; // LCOV_EXCL_LINE - return tesSUCCESS; -} - -template <> -TER -escrowLockApplyHelper( - ApplyView& view, - AccountID const& issuer, - AccountID const& sender, - STAmount const& amount, - beast::Journal journal) -{ - // Defensive: Issuer cannot create an escrow - if (issuer == sender) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const ter = rippleLockEscrowMPT(view, sender, amount, journal); - if (ter != tesSUCCESS) - return ter; // LCOV_EXCL_LINE - return tesSUCCESS; -} - -TER -EscrowCreate::doApply() -{ - auto const closeTime = ctx_.view().header().parentCloseTime; - - if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) - return tecNO_PERMISSION; - - if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter])) - return tecNO_PERMISSION; - - auto const sle = ctx_.view().peek(keylet::account(account_)); - if (!sle) - return tefINTERNAL; // LCOV_EXCL_LINE - - // Check reserve and funds availability - STAmount const amount{ctx_.tx[sfAmount]}; - - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - - if (mSourceBalance < reserve) - return tecINSUFFICIENT_RESERVE; - - // Check reserve and funds availability - if (isXRP(amount)) - { - if (mSourceBalance < reserve + STAmount(amount).xrp()) - return tecUNFUNDED; - } - - // Check destination account - { - auto const sled = ctx_.view().read(keylet::account(ctx_.tx[sfDestination])); - if (!sled) - return tecNO_DST; // LCOV_EXCL_LINE - if (((*sled)[sfFlags] & lsfRequireDestTag) && !ctx_.tx[~sfDestinationTag]) - return tecDST_TAG_NEEDED; - } - - // 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.getSeqValue()); - auto const slep = std::make_shared(escrowKeylet); - (*slep)[sfAmount] = amount; - (*slep)[sfAccount] = account_; - (*slep)[~sfCondition] = ctx_.tx[~sfCondition]; - (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; - (*slep)[sfDestination] = ctx_.tx[sfDestination]; - (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; - (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; - (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; - - if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) - { - (*slep)[sfSequence] = ctx_.tx.getSeqValue(); - } - - if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) - { - auto const xferRate = transferRate(ctx_.view(), amount); - if (xferRate != parityRate) - (*slep)[sfTransferRate] = xferRate.value; - } - - ctx_.view().insert(slep); - - // Add escrow to sender's owner directory - { - auto page = ctx_.view().dirInsert( - keylet::ownerDir(account_), escrowKeylet, describeOwnerDir(account_)); - if (!page) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*slep)[sfOwnerNode] = *page; - } - - // If it's not a self-send, add escrow to recipient's owner directory. - AccountID const dest = ctx_.tx[sfDestination]; - if (dest != account_) - { - auto page = - ctx_.view().dirInsert(keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest)); - if (!page) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*slep)[sfDestinationNode] = *page; - } - - // IOU escrow objects are added to the issuer's owner directory to help - // track the total locked balance. For MPT, this isn't necessary because the - // locked balance is already stored directly in the MPTokenIssuance object. - AccountID const issuer = amount.getIssuer(); - if (!isXRP(amount) && issuer != account_ && issuer != dest && !amount.holds()) - { - auto page = - ctx_.view().dirInsert(keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer)); - if (!page) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*slep)[sfIssuerNode] = *page; - } - - // Deduct owner's balance - if (isXRP(amount)) - (*sle)[sfBalance] = (*sle)[sfBalance] - amount; - else - { - if (auto const ret = std::visit( - [&](T const&) { - return escrowLockApplyHelper(ctx_.view(), issuer, account_, amount, j_); - }, - amount.asset().value()); - !isTesSuccess(ret)) - { - return ret; // LCOV_EXCL_LINE - } - } - - // increment owner count - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); - ctx_.view().update(sle); - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -static bool -checkCondition(Slice f, Slice c) -{ - using namespace xrpl::cryptoconditions; - - std::error_code ec; - - auto condition = Condition::deserialize(c, ec); - if (!condition) - return false; - - auto fulfillment = Fulfillment::deserialize(f, ec); - if (!fulfillment) - return false; - - return validate(*fulfillment, *condition); -} - -bool -EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) -{ - return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); -} - -NotTEC -EscrowFinish::preflight(PreflightContext const& ctx) -{ - auto const cb = ctx.tx[~sfCondition]; - auto const fb = ctx.tx[~sfFulfillment]; - - // If you specify a condition, then you must also specify - // a fulfillment. - if (static_cast(cb) != static_cast(fb)) - return temMALFORMED; - - return tesSUCCESS; -} - -NotTEC -EscrowFinish::preflightSigValidated(PreflightContext const& ctx) -{ - auto const cb = ctx.tx[~sfCondition]; - auto const fb = ctx.tx[~sfFulfillment]; - - if (cb && fb) - { - auto& router = ctx.registry.getHashRouter(); - - auto const id = ctx.tx.getTransactionID(); - auto const flags = router.getFlags(id); - - // If we haven't checked the condition, check it - // now. Whether it passes or not isn't important - // in preflight. - if (!any(flags & (SF_CF_INVALID | SF_CF_VALID))) - { - if (checkCondition(*fb, *cb)) - router.setFlags(id, SF_CF_VALID); - else - router.setFlags(id, SF_CF_INVALID); - } - } - - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) - return err; - - return tesSUCCESS; -} - -XRPAmount -EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) -{ - XRPAmount extraFee{0}; - - if (auto const fb = tx[~sfFulfillment]) - { - extraFee += view.fees().base * (32 + (fb->size() / 16)); - } - - return Transactor::calculateBaseFee(view, tx) + extraFee; -} - -template -static TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount); - -template <> -TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount) -{ - AccountID issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tesSUCCESS - if (issuer == dest) - return tesSUCCESS; - - // If the issuer has requireAuth set, check if the destination is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS) - return ter; - - // If the issuer has deep frozen the destination, return tecFROZEN - if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer())) - return tecFROZEN; - - return tesSUCCESS; -} - -template <> -TER -escrowFinishPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& dest, - STAmount const& amount) -{ - AccountID issuer = amount.getIssuer(); - // If the issuer is the same as the dest, return tesSUCCESS - if (issuer == dest) - return tesSUCCESS; - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the destination is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); - ter != tesSUCCESS) - return ter; - - // If the issuer has frozen the destination, return tecLOCKED - if (isFrozen(ctx.view, dest, mptIssue)) - return tecLOCKED; - - return tesSUCCESS; -} - -TER -EscrowFinish::preclaim(PreclaimContext const& ctx) -{ - if (ctx.view.rules().enabled(featureCredentials)) - { - if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); - !isTesSuccess(err)) - return err; - } - - if (ctx.view.rules().enabled(featureTokenEscrow)) - { - auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); - auto const slep = ctx.view.read(k); - if (!slep) - return tecNO_TARGET; - - AccountID const dest = (*slep)[sfDestination]; - STAmount const amount = (*slep)[sfAmount]; - - if (!isXRP(amount)) - { - if (auto const ret = std::visit( - [&](T const&) { - return escrowFinishPreclaimHelper(ctx, dest, amount); - }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; - } - } - return tesSUCCESS; -} - -template -static TER -escrowUnlockApplyHelper( - ApplyView& view, - Rate lockedRate, - std::shared_ptr const& sleDest, - STAmount const& xrpBalance, - STAmount const& amount, - AccountID const& issuer, - AccountID const& sender, - AccountID const& receiver, - bool createAsset, - beast::Journal journal); - -template <> -TER -escrowUnlockApplyHelper( - ApplyView& view, - Rate lockedRate, - std::shared_ptr const& sleDest, - STAmount const& xrpBalance, - STAmount const& amount, - AccountID const& issuer, - AccountID const& sender, - AccountID const& receiver, - bool createAsset, - beast::Journal journal) -{ - Keylet const trustLineKey = keylet::line(receiver, amount.issue()); - bool const recvLow = issuer > receiver; - bool const senderIssuer = issuer == sender; - bool const receiverIssuer = issuer == receiver; - bool const issuerHigh = issuer > receiver; - - if (senderIssuer) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (receiverIssuer) - return tesSUCCESS; - - if (!view.exists(trustLineKey) && createAsset && !receiverIssuer) - { - // Can the account cover the trust line's reserve? - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) - { - JLOG(journal.trace()) << "Trust line does not exist. " - "Insufficent reserve to create line."; - - return tecNO_LINE_INSUF_RESERVE; - } - - Currency const currency = amount.getCurrency(); - STAmount initialBalance(amount.issue()); - initialBalance.setIssuer(noAccount()); - - // clang-format off - if (TER const ter = trustCreate( - view, // payment sandbox - recvLow, // is dest low? - issuer, // source - receiver, // destination - trustLineKey.key, // ledger index - sleDest, // Account to add to - false, // authorize account - (sleDest->getFlags() & lsfDefaultRipple) == 0, - false, // freeze trust line - false, // deep freeze trust line - initialBalance, // zero initial balance - Issue(currency, receiver), // limit of zero - 0, // quality in - 0, // quality out - journal); // journal - !isTesSuccess(ter)) - { - return ter; // LCOV_EXCL_LINE - } - // clang-format on - - view.update(sleDest); - } - - if (!view.exists(trustLineKey) && !receiverIssuer) - return tecNO_LINE; - - auto const xferRate = transferRate(view, amount); - // update if issuer rate is less than locked rate - if (xferRate < lockedRate) - lockedRate = xferRate; - - // Transfer Rate only applies when: - // 1. Issuer is not involved in the transfer (senderIssuer or - // receiverIssuer) - // 2. The locked rate is different from the parity rate - - // NOTE: Transfer fee in escrow works a bit differently from a normal - // payment. In escrow, the fee is deducted from the locked/sending amount, - // whereas in a normal payment, the transfer fee is taken on top of the - // sending amount. - auto finalAmt = amount; - if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) - { - // compute transfer fee, if any - auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.issue(), true); - // compute balance to transfer - finalAmt = amount.value() - xferFee; - } - - // validate the line limit if the account submitting txn is not the receiver - // of the funds - if (!createAsset) - { - auto const sleRippleState = view.peek(trustLineKey); - if (!sleRippleState) - return tecINTERNAL; // LCOV_EXCL_LINE - - // if the issuer is the high, then we use the low limit - // otherwise we use the high limit - STAmount const lineLimit = - sleRippleState->getFieldAmount(issuerHigh ? sfLowLimit : sfHighLimit); - - STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance); - - // flip the sign of the line balance if the issuer is not high - if (!issuerHigh) - lineBalance.negate(); - - // add the final amount to the line balance - lineBalance += finalAmt; - - // if the transfer would exceed the line limit return tecLIMIT_EXCEEDED - if (lineLimit < lineBalance) - return tecLIMIT_EXCEEDED; - } - - // if destination is not the issuer then transfer funds - if (!receiverIssuer) - { - auto const ter = rippleCredit(view, issuer, receiver, finalAmt, true, journal); - if (ter != tesSUCCESS) - return ter; // LCOV_EXCL_LINE - } - return tesSUCCESS; -} - -template <> -TER -escrowUnlockApplyHelper( - ApplyView& view, - Rate lockedRate, - std::shared_ptr const& sleDest, - STAmount const& xrpBalance, - STAmount const& amount, - AccountID const& issuer, - AccountID const& sender, - AccountID const& receiver, - bool createAsset, - beast::Journal journal) -{ - bool const senderIssuer = issuer == sender; - bool const receiverIssuer = issuer == receiver; - - auto const mptID = amount.get().getMptID(); - auto const issuanceKey = keylet::mptIssuance(mptID); - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer) - { - if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; - xrpBalance < view.fees().accountReserve(ownerCount + 1)) - { - return tecINSUFFICIENT_RESERVE; - } - - if (auto const ter = MPTokenAuthorize::createMPToken(view, mptID, receiver, 0); - !isTesSuccess(ter)) - { - return ter; // LCOV_EXCL_LINE - } - - // update owner count. - adjustOwnerCount(view, sleDest, 1, journal); - } - - if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer) - return tecNO_PERMISSION; - - auto const xferRate = transferRate(view, amount); - // update if issuer rate is less than locked rate - if (xferRate < lockedRate) - lockedRate = xferRate; - - // Transfer Rate only applies when: - // 1. Issuer is not involved in the transfer (senderIssuer or - // receiverIssuer) - // 2. The locked rate is different from the parity rate - - // NOTE: Transfer fee in escrow works a bit differently from a normal - // payment. In escrow, the fee is deducted from the locked/sending amount, - // whereas in a normal payment, the transfer fee is taken on top of the - // sending amount. - auto finalAmt = amount; - if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) - { - // compute transfer fee, if any - auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true); - // compute balance to transfer - finalAmt = amount.value() - xferFee; - } - return rippleUnlockEscrowMPT( - view, - sender, - receiver, - finalAmt, - view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, - journal); -} - -TER -EscrowFinish::doApply() -{ - auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); - auto const slep = ctx_.view().peek(k); - if (!slep) - { - if (ctx_.view().rules().enabled(featureTokenEscrow)) - return tecINTERNAL; // LCOV_EXCL_LINE - - return tecNO_TARGET; - } - - // If a cancel time is present, a finish operation should only succeed prior - // to that time. - auto const now = ctx_.view().header().parentCloseTime; - - // Too soon: can't execute before the finish time - if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter])) - return tecNO_PERMISSION; - - // Too late: can't execute after the cancel time - if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) - return tecNO_PERMISSION; - - // Check cryptocondition fulfillment - { - auto const id = ctx_.tx.getTransactionID(); - auto flags = ctx_.registry.getHashRouter().getFlags(id); - - auto const cb = ctx_.tx[~sfCondition]; - - // It's unlikely that the results of the check will - // expire from the hash router, but if it happens, - // simply re-run the check. - if (cb && !any(flags & (SF_CF_INVALID | SF_CF_VALID))) - { - // LCOV_EXCL_START - auto const fb = ctx_.tx[~sfFulfillment]; - - if (!fb) - return tecINTERNAL; - - if (checkCondition(*fb, *cb)) - flags = SF_CF_VALID; - else - flags = SF_CF_INVALID; - - ctx_.registry.getHashRouter().setFlags(id, flags); - // LCOV_EXCL_STOP - } - - // If the check failed, then simply return an error - // and don't look at anything else. - if (any(flags & SF_CF_INVALID)) - return tecCRYPTOCONDITION_ERROR; - - // Check against condition in the ledger entry: - auto const cond = (*slep)[~sfCondition]; - - // If a condition wasn't specified during creation, - // one shouldn't be included now. - if (!cond && cb) - return tecCRYPTOCONDITION_ERROR; - - // If a condition was specified during creation of - // the suspended payment, the identical condition - // must be presented again. We don't check if the - // fulfillment matches the condition since we did - // that in preflight. - if (cond && (cond != cb)) - return tecCRYPTOCONDITION_ERROR; - } - - // NOTE: Escrow payments cannot be used to fund accounts. - AccountID const destID = (*slep)[sfDestination]; - auto const sled = ctx_.view().peek(keylet::account(destID)); - if (!sled) - return tecNO_DST; - - if (auto err = verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); - !isTesSuccess(err)) - return err; - - AccountID const account = (*slep)[sfAccount]; - - // Remove escrow from owner directory - { - auto const page = (*slep)[sfOwnerNode]; - if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true)) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - - // Remove escrow from recipient's owner directory, if present. - if (auto const optPage = (*slep)[~sfDestinationNode]) - { - if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true)) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - - STAmount const amount = slep->getFieldAmount(sfAmount); - // Transfer amount to destination - if (isXRP(amount)) - (*sled)[sfBalance] = (*sled)[sfBalance] + amount; - else - { - if (!ctx_.view().rules().enabled(featureTokenEscrow)) - return temDISABLED; // LCOV_EXCL_LINE - - Rate lockedRate = slep->isFieldPresent(sfTransferRate) - ? xrpl::Rate(slep->getFieldU32(sfTransferRate)) - : parityRate; - auto const issuer = amount.getIssuer(); - bool const createAsset = destID == account_; - if (auto const ret = std::visit( - [&](T const&) { - return escrowUnlockApplyHelper( - ctx_.view(), - lockedRate, - sled, - mPriorBalance, - amount, - issuer, - account, - destID, - createAsset, - j_); - }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; - - // Remove escrow from issuers owner directory, if present. - if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) - { - if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true)) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - } - - ctx_.view().update(sled); - - // Adjust source owner count - auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); - - // Remove escrow from ledger - ctx_.view().erase(slep); - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -NotTEC -EscrowCancel::preflight(PreflightContext const& ctx) -{ - return tesSUCCESS; -} - -template -static TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount); - -template <> -TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount) -{ - AccountID issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecINTERNAL - if (issuer == account) - return tecINTERNAL; // LCOV_EXCL_LINE - - // If the issuer has requireAuth set, check if the account is authorized - if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS) - return ter; - - return tesSUCCESS; -} - -template <> -TER -escrowCancelPreclaimHelper( - PreclaimContext const& ctx, - AccountID const& account, - STAmount const& amount) -{ - AccountID issuer = amount.getIssuer(); - // If the issuer is the same as the account, return tecINTERNAL - if (issuer == account) - return tecINTERNAL; // LCOV_EXCL_LINE - - // If the mpt does not exist, return tecOBJECT_NOT_FOUND - auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - // If the issuer has requireAuth set, check if the account is - // authorized - auto const& mptIssue = amount.get(); - if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); - ter != tesSUCCESS) - return ter; - - return tesSUCCESS; -} - -TER -EscrowCancel::preclaim(PreclaimContext const& ctx) -{ - if (ctx.view.rules().enabled(featureTokenEscrow)) - { - auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); - auto const slep = ctx.view.read(k); - if (!slep) - return tecNO_TARGET; - - AccountID const account = (*slep)[sfAccount]; - STAmount const amount = (*slep)[sfAmount]; - - if (!isXRP(amount)) - { - if (auto const ret = std::visit( - [&](T const&) { - return escrowCancelPreclaimHelper(ctx, account, amount); - }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; - } - } - return tesSUCCESS; -} - -TER -EscrowCancel::doApply() -{ - auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); - auto const slep = ctx_.view().peek(k); - if (!slep) - { - if (ctx_.view().rules().enabled(featureTokenEscrow)) - return tecINTERNAL; // LCOV_EXCL_LINE - - return tecNO_TARGET; - } - - auto const now = ctx_.view().header().parentCloseTime; - - // No cancel time specified: can't execute at all. - if (!(*slep)[~sfCancelAfter]) - return tecNO_PERMISSION; - - // Too soon: can't execute before the cancel time. - if (!after(now, (*slep)[sfCancelAfter])) - return tecNO_PERMISSION; - - AccountID const account = (*slep)[sfAccount]; - - // Remove escrow from owner directory - { - auto const page = (*slep)[sfOwnerNode]; - if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true)) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - - // Remove escrow from recipient's owner directory, if present. - if (auto const optPage = (*slep)[~sfDestinationNode]; optPage) - { - if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true)) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - - auto const sle = ctx_.view().peek(keylet::account(account)); - STAmount const amount = slep->getFieldAmount(sfAmount); - - // Transfer amount back to the owner - if (isXRP(amount)) - (*sle)[sfBalance] = (*sle)[sfBalance] + amount; - else - { - if (!ctx_.view().rules().enabled(featureTokenEscrow)) - return temDISABLED; // LCOV_EXCL_LINE - - auto const issuer = amount.getIssuer(); - bool const createAsset = account == account_; - if (auto const ret = std::visit( - [&](T const&) { - return escrowUnlockApplyHelper( - ctx_.view(), - parityRate, - slep, - mPriorBalance, - amount, - issuer, - account, // sender and receiver are the same - account, - createAsset, - j_); - }, - amount.asset().value()); - !isTesSuccess(ret)) - return ret; // LCOV_EXCL_LINE - - // Remove escrow from issuers owner directory, if present. - if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) - { - if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true)) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - } - - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); - - // Remove escrow from ledger - ctx_.view().erase(slep); - - return tesSUCCESS; -} - -} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp new file mode 100644 index 0000000000..c69e112f1a --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +NotTEC +EscrowCancel::preflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +template +static TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount); + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +TER +EscrowCancel::preclaim(PreclaimContext const& ctx) +{ + if (ctx.view.rules().enabled(featureTokenEscrow)) + { + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const account = (*slep)[sfAccount]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowCancelPreclaimHelper(ctx, account, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } + return tesSUCCESS; +} + +TER +EscrowCancel::doApply() +{ + auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const slep = ctx_.view().peek(k); + if (!slep) + { + if (ctx_.view().rules().enabled(featureTokenEscrow)) + return tecINTERNAL; // LCOV_EXCL_LINE + + return tecNO_TARGET; + } + + auto const now = ctx_.view().header().parentCloseTime; + + // No cancel time specified: can't execute at all. + if (!(*slep)[~sfCancelAfter]) + return tecNO_PERMISSION; + + // Too soon: can't execute before the cancel time. + if (!after(now, (*slep)[sfCancelAfter])) + return tecNO_PERMISSION; + + AccountID const account = (*slep)[sfAccount]; + + // Remove escrow from owner directory + { + auto const page = (*slep)[sfOwnerNode]; + if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + // Remove escrow from recipient's owner directory, if present. + if (auto const optPage = (*slep)[~sfDestinationNode]; optPage) + { + if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + auto const sle = ctx_.view().peek(keylet::account(account)); + STAmount const amount = slep->getFieldAmount(sfAmount); + + // Transfer amount back to the owner + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + auto const issuer = amount.getIssuer(); + bool const createAsset = account == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + parityRate, + slep, + mPriorBalance, + amount, + issuer, + account, // sender and receiver are the same + account, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + } + + adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); + ctx_.view().update(sle); + + // Remove escrow from ledger + ctx_.view().erase(slep); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp new file mode 100644 index 0000000000..d7cb588f8d --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -0,0 +1,498 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +/* + Escrow + ====== + + Escrow is a feature of the XRP Ledger that allows you to send conditional + XRP payments. These conditional payments, called escrows, set aside XRP and + deliver it later when certain conditions are met. Conditions to successfully + finish an escrow include time-based unlocks and crypto-conditions. Escrows + can also be set to expire if not finished in time. + + The XRP set aside in an escrow is locked up. No one can use or destroy the + XRP until the escrow has been successfully finished or canceled. Before the + expiration time, only the intended receiver can get the XRP. After the + expiration time, the XRP can only be returned to the sender. + + For more details on escrow, including examples, diagrams and more please + visit https://xrpl.org/escrow.html + + For details on specific transactions, including fields and validation rules + please see: + + `EscrowCreate` + -------------- + See: https://xrpl.org/escrowcreate.html + + `EscrowFinish` + -------------- + See: https://xrpl.org/escrowfinish.html + + `EscrowCancel` + -------------- + See: https://xrpl.org/escrowcancel.html +*/ + +//------------------------------------------------------------------------------ + +TxConsequences +EscrowCreate::makeTxConsequences(PreflightContext const& ctx) +{ + auto const amount = ctx.tx[sfAmount]; + return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero}; +} + +template +static NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (amount.native() || amount <= beast::zero) + return temBAD_AMOUNT; + + if (badCurrency() == amount.getCurrency()) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +NotTEC +EscrowCreate::preflight(PreflightContext const& ctx) +{ + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenEscrow)) + return temBAD_AMOUNT; + + if (auto const ret = std::visit( + [&](T const&) { return escrowCreatePreflightHelper(ctx); }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::zero) + return temBAD_AMOUNT; + } + + // We must specify at least one timeout value + if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter]) + return temBAD_EXPIRATION; + + // If both finish and cancel times are specified then the cancel time must + // be strictly after the finish time. + if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] && + ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) + return temBAD_EXPIRATION; + + // In the absence of a FinishAfter, the escrow can be finished + // immediately, which can be confusing. When creating an escrow, + // we want to ensure that either a FinishAfter time is explicitly + // specified or a completion condition is attached. + if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition]) + return temMALFORMED; + + if (auto const cb = ctx.tx[~sfCondition]) + { + using namespace xrpl::cryptoconditions; + + std::error_code ec; + + auto condition = Condition::deserialize(*cb, ec); + if (!condition) + { + JLOG(ctx.j.debug()) << "Malformed condition during escrow creation: " << ec.message(); + return temMALFORMED; + } + } + + return tesSUCCESS; +} + +template +static TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + return tecNO_PERMISSION; + + // If the account does not have a trustline to the issuer, return tecNO_LINE + auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, amount.getCurrency())); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than account + if (balance > beast::zero && issuer < account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than account + if (balance < beast::zero && issuer > account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(ctx.view, account, amount.issue())) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(ctx.view, dest, amount.issue())) + return tecFROZEN; + + STAmount const spendableAmount = + accountHolds(ctx.view, account, amount.getCurrency(), issuer, fhIGNORE_FREEZE, ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt, return + // tecNO_PERMISSION + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // If the account does not have the mpt, return tecOBJECT_NOT_FOUND + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the account is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is + // authorized + if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (isFrozen(ctx.view, account, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); ter != tesSUCCESS) + return ter; + + STAmount const spendableAmount = accountHolds( + ctx.view, account, amount.get(), fhIGNORE_FREEZE, ahIGNORE_AUTH, ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +TER +EscrowCreate::preclaim(PreclaimContext const& ctx) +{ + STAmount const amount{ctx.tx[sfAmount]}; + AccountID const account{ctx.tx[sfAccount]}; + AccountID const dest{ctx.tx[sfDestination]}; + + auto const sled = ctx.view.read(keylet::account(dest)); + if (!sled) + return tecNO_DST; + + // Pseudo-accounts cannot receive escrow. Note, this is not amendment-gated + // because all writes to pseudo-account discriminator fields **are** + // amendment gated, hence the behaviour of this check will always match the + // currently active amendments. + if (isPseudoAccount(sled)) + return tecNO_PERMISSION; + + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreclaimHelper(ctx, account, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; +} + +template +static TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = rippleCredit( + view, sender, issuer, amount, amount.holds() ? false : true, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + if (issuer == sender) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ter = rippleLockEscrowMPT(view, sender, amount, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +TER +EscrowCreate::doApply() +{ + auto const closeTime = ctx_.view().header().parentCloseTime; + + if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) + return tecNO_PERMISSION; + + if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter])) + return tecNO_PERMISSION; + + auto const sle = ctx_.view().peek(keylet::account(account_)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Check reserve and funds availability + STAmount const amount{ctx_.tx[sfAmount]}; + + auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + + if (mSourceBalance < reserve) + return tecINSUFFICIENT_RESERVE; + + // Check reserve and funds availability + if (isXRP(amount)) + { + if (mSourceBalance < reserve + STAmount(amount).xrp()) + return tecUNFUNDED; + } + + // Check destination account + { + auto const sled = ctx_.view().read(keylet::account(ctx_.tx[sfDestination])); + if (!sled) + return tecNO_DST; // LCOV_EXCL_LINE + if (((*sled)[sfFlags] & lsfRequireDestTag) && !ctx_.tx[~sfDestinationTag]) + return tecDST_TAG_NEEDED; + } + + // 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.getSeqValue()); + auto const slep = std::make_shared(escrowKeylet); + (*slep)[sfAmount] = amount; + (*slep)[sfAccount] = account_; + (*slep)[~sfCondition] = ctx_.tx[~sfCondition]; + (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; + (*slep)[sfDestination] = ctx_.tx[sfDestination]; + (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; + (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; + (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + + if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + (*slep)[sfSequence] = ctx_.tx.getSeqValue(); + } + + if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) + { + auto const xferRate = transferRate(ctx_.view(), amount); + if (xferRate != parityRate) + (*slep)[sfTransferRate] = xferRate.value; + } + + ctx_.view().insert(slep); + + // Add escrow to sender's owner directory + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(account_), escrowKeylet, describeOwnerDir(account_)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfOwnerNode] = *page; + } + + // If it's not a self-send, add escrow to recipient's owner directory. + AccountID const dest = ctx_.tx[sfDestination]; + if (dest != account_) + { + auto page = + ctx_.view().dirInsert(keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfDestinationNode] = *page; + } + + // IOU escrow objects are added to the issuer's owner directory to help + // track the total locked balance. For MPT, this isn't necessary because the + // locked balance is already stored directly in the MPTokenIssuance object. + AccountID const issuer = amount.getIssuer(); + if (!isXRP(amount) && issuer != account_ && issuer != dest && !amount.holds()) + { + auto page = + ctx_.view().dirInsert(keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfIssuerNode] = *page; + } + + // Deduct owner's balance + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockApplyHelper(ctx_.view(), issuer, account_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + { + return ret; // LCOV_EXCL_LINE + } + } + + // increment owner count + adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + ctx_.view().update(sle); + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp new file mode 100644 index 0000000000..885afb1918 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -0,0 +1,372 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +// During an EscrowFinish, the transaction must specify both +// a condition and a fulfillment. We track whether that +// fulfillment matches and validates the condition. +constexpr HashRouterFlags SF_CF_INVALID = HashRouterFlags::PRIVATE5; +constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6; + +//------------------------------------------------------------------------------ + +static bool +checkCondition(Slice f, Slice c) +{ + using namespace xrpl::cryptoconditions; + + std::error_code ec; + + auto condition = Condition::deserialize(c, ec); + if (!condition) + return false; + + auto fulfillment = Fulfillment::deserialize(f, ec); + if (!fulfillment) + return false; + + return validate(*fulfillment, *condition); +} + +bool +EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); +} + +NotTEC +EscrowFinish::preflight(PreflightContext const& ctx) +{ + auto const cb = ctx.tx[~sfCondition]; + auto const fb = ctx.tx[~sfFulfillment]; + + // If you specify a condition, then you must also specify + // a fulfillment. + if (static_cast(cb) != static_cast(fb)) + return temMALFORMED; + + return tesSUCCESS; +} + +NotTEC +EscrowFinish::preflightSigValidated(PreflightContext const& ctx) +{ + auto const cb = ctx.tx[~sfCondition]; + auto const fb = ctx.tx[~sfFulfillment]; + + if (cb && fb) + { + auto& router = ctx.registry.getHashRouter(); + + auto const id = ctx.tx.getTransactionID(); + auto const flags = router.getFlags(id); + + // If we haven't checked the condition, check it + // now. Whether it passes or not isn't important + // in preflight. + if (!any(flags & (SF_CF_INVALID | SF_CF_VALID))) + { + if (checkCondition(*fb, *cb)) + router.setFlags(id, SF_CF_VALID); + else + router.setFlags(id, SF_CF_INVALID); + } + } + + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + +XRPAmount +EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount extraFee{0}; + + if (auto const fb = tx[~sfFulfillment]) + { + extraFee += view.fees().base * (32 + (fb->size() / 16)); + } + + return Transactor::calculateBaseFee(view, tx) + extraFee; +} + +template +static TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); ter != tesSUCCESS) + return ter; + + // If the issuer has deep frozen the destination, return tecFROZEN + if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer())) + return tecFROZEN; + + return tesSUCCESS; +} + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the dest, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the issuer has requireAuth set, check if the destination is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + return tesSUCCESS; +} + +TER +EscrowFinish::preclaim(PreclaimContext const& ctx) +{ + if (ctx.view.rules().enabled(featureCredentials)) + { + if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); + !isTesSuccess(err)) + return err; + } + + if (ctx.view.rules().enabled(featureTokenEscrow)) + { + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowFinishPreclaimHelper(ctx, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + } + return tesSUCCESS; +} + +TER +EscrowFinish::doApply() +{ + auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const slep = ctx_.view().peek(k); + if (!slep) + { + if (ctx_.view().rules().enabled(featureTokenEscrow)) + return tecINTERNAL; // LCOV_EXCL_LINE + + return tecNO_TARGET; + } + + // If a cancel time is present, a finish operation should only succeed prior + // to that time. + auto const now = ctx_.view().header().parentCloseTime; + + // Too soon: can't execute before the finish time + if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter])) + return tecNO_PERMISSION; + + // Too late: can't execute after the cancel time + if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) + return tecNO_PERMISSION; + + // Check cryptocondition fulfillment + { + auto const id = ctx_.tx.getTransactionID(); + auto flags = ctx_.registry.getHashRouter().getFlags(id); + + auto const cb = ctx_.tx[~sfCondition]; + + // It's unlikely that the results of the check will + // expire from the hash router, but if it happens, + // simply re-run the check. + if (cb && !any(flags & (SF_CF_INVALID | SF_CF_VALID))) + { + // LCOV_EXCL_START + auto const fb = ctx_.tx[~sfFulfillment]; + + if (!fb) + return tecINTERNAL; + + if (checkCondition(*fb, *cb)) + flags = SF_CF_VALID; + else + flags = SF_CF_INVALID; + + ctx_.registry.getHashRouter().setFlags(id, flags); + // LCOV_EXCL_STOP + } + + // If the check failed, then simply return an error + // and don't look at anything else. + if (any(flags & SF_CF_INVALID)) + return tecCRYPTOCONDITION_ERROR; + + // Check against condition in the ledger entry: + auto const cond = (*slep)[~sfCondition]; + + // If a condition wasn't specified during creation, + // one shouldn't be included now. + if (!cond && cb) + return tecCRYPTOCONDITION_ERROR; + + // If a condition was specified during creation of + // the suspended payment, the identical condition + // must be presented again. We don't check if the + // fulfillment matches the condition since we did + // that in preflight. + if (cond && (cond != cb)) + return tecCRYPTOCONDITION_ERROR; + } + + // NOTE: Escrow payments cannot be used to fund accounts. + AccountID const destID = (*slep)[sfDestination]; + auto const sled = ctx_.view().peek(keylet::account(destID)); + if (!sled) + return tecNO_DST; + + if (auto err = verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, destID, sled, ctx_.journal); + !isTesSuccess(err)) + return err; + + AccountID const account = (*slep)[sfAccount]; + + // Remove escrow from owner directory + { + auto const page = (*slep)[sfOwnerNode]; + if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + // Remove escrow from recipient's owner directory, if present. + if (auto const optPage = (*slep)[~sfDestinationNode]) + { + if (!ctx_.view().dirRemove(keylet::ownerDir(destID), *optPage, k.key, true)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + STAmount const amount = slep->getFieldAmount(sfAmount); + // Transfer amount to destination + if (isXRP(amount)) + (*sled)[sfBalance] = (*sled)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + Rate lockedRate = slep->isFieldPresent(sfTransferRate) + ? xrpl::Rate(slep->getFieldU32(sfTransferRate)) + : parityRate; + auto const issuer = amount.getIssuer(); + bool const createAsset = destID == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + lockedRate, + sled, + mPriorBalance, + amount, + issuer, + account, + destID, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true)) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + } + + ctx_.view().update(sled); + + // Adjust source owner count + auto const sle = ctx_.view().peek(keylet::account(account)); + adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); + ctx_.view().update(sle); + + // Remove escrow from ledger + ctx_.view().erase(slep); + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h b/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h new file mode 100644 index 0000000000..ccbfff9657 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h @@ -0,0 +1,229 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +template +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal); + +template <> +inline TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + Keylet const trustLineKey = keylet::line(receiver, amount.issue()); + bool const recvLow = issuer > receiver; + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + bool const issuerHigh = issuer > receiver; + + if (senderIssuer) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (receiverIssuer) + return tesSUCCESS; + + if (!view.exists(trustLineKey) && createAsset && !receiverIssuer) + { + // Can the account cover the trust line's reserve? + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + JLOG(journal.trace()) << "Trust line does not exist. " + "Insufficient reserve to create line."; + + return tecNO_LINE_INSUF_RESERVE; + } + + Currency const currency = amount.getCurrency(); + STAmount initialBalance(amount.issue()); + initialBalance.setIssuer(noAccount()); + + // clang-format off + if (TER const ter = trustCreate( + view, // payment sandbox + recvLow, // is dest low? + issuer, // source + receiver, // destination + trustLineKey.key, // ledger index + sleDest, // Account to add to + false, // authorize account + (sleDest->getFlags() & lsfDefaultRipple) == 0, + false, // freeze trust line + false, // deep freeze trust line + initialBalance, // zero initial balance + Issue(currency, receiver), // limit of zero + 0, // quality in + 0, // quality out + journal); // journal + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + // clang-format on + + view.update(sleDest); + } + + if (!view.exists(trustLineKey) && !receiverIssuer) + return tecNO_LINE; + + auto const xferRate = transferRate(view, amount); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.issue(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + // validate the line limit if the account submitting txn is not the receiver + // of the funds + if (!createAsset) + { + auto const sleRippleState = view.peek(trustLineKey); + if (!sleRippleState) + return tecINTERNAL; // LCOV_EXCL_LINE + + // if the issuer is the high, then we use the low limit + // otherwise we use the high limit + STAmount const lineLimit = + sleRippleState->getFieldAmount(issuerHigh ? sfLowLimit : sfHighLimit); + + STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance); + + // flip the sign of the line balance if the issuer is not high + if (!issuerHigh) + lineBalance.negate(); + + // add the final amount to the line balance + lineBalance += finalAmt; + + // if the transfer would exceed the line limit return tecLIMIT_EXCEEDED + if (lineLimit < lineBalance) + return tecLIMIT_EXCEEDED; + } + + // if destination is not the issuer then transfer funds + if (!receiverIssuer) + { + auto const ter = rippleCredit(view, issuer, receiver, finalAmt, true, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + return tesSUCCESS; +} + +template <> +inline TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + + auto const mptID = amount.get().getMptID(); + auto const issuanceKey = keylet::mptIssuance(mptID); + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer) + { + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + return tecINSUFFICIENT_RESERVE; + } + + if (auto const ter = MPTokenAuthorize::createMPToken(view, mptID, receiver, 0); + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + + // update owner count. + adjustOwnerCount(view, sleDest, 1, journal); + } + + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && !receiverIssuer) + return tecNO_PERMISSION; + + auto const xferRate = transferRate(view, amount); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + return rippleUnlockEscrowMPT( + view, + sender, + receiver, + finalAmt, + view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt, + journal); +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PayChan.cpp b/src/libxrpl/tx/transactors/payment_channel/PayChan.cpp deleted file mode 100644 index f963153e87..0000000000 --- a/src/libxrpl/tx/transactors/payment_channel/PayChan.cpp +++ /dev/null @@ -1,542 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl { - -/* - PaymentChannel - - Payment channels permit off-ledger checkpoints of XRP payments flowing - in a single direction. A channel sequesters the owner's XRP in its own - ledger entry. The owner can authorize the recipient to claim up to a - given balance by giving the receiver a signed message (off-ledger). The - recipient can use this signed message to claim any unpaid balance while - the channel remains open. The owner can top off the line as needed. If - the channel has not paid out all its funds, the owner must wait out a - delay to close the channel to give the recipient a chance to supply any - claims. The recipient can close the channel at any time. Any transaction - that touches the channel after the expiration time will close the - channel. The total amount paid increases monotonically as newer claims - are issued. When the channel is closed any remaining balance is returned - to the owner. Channels are intended to permit intermittent off-ledger - settlement of ILP trust lines as balances get substantial. For - bidirectional channels, a payment channel can be used in each direction. - - PaymentChannelCreate - - Create a unidirectional channel. The parameters are: - Destination - The recipient at the end of the channel. - Amount - The amount of XRP to deposit in the channel immediately. - SettleDelay - The amount of time everyone but the recipient must wait for a - superior claim. - PublicKey - The key that will sign claims against the channel. - CancelAfter (optional) - Any channel transaction that touches this channel after the - `CancelAfter` time will close it. - DestinationTag (optional) - Destination tags allow the different accounts inside of a Hosted - Wallet to be mapped back onto the Ripple ledger. The destination tag - tells the server to which account in the Hosted Wallet the funds are - intended to go to. Required if the destination has lsfRequireDestTag - set. - SourceTag (optional) - Source tags allow the different accounts inside of a Hosted Wallet - to be mapped back onto the Ripple ledger. Source tags are similar to - destination tags but are for the channel owner to identify their own - transactions. - - PaymentChannelFund - - Add additional funds to the payment channel. Only the channel owner may - use this transaction. The parameters are: - Channel - The 256-bit ID of the channel. - Amount - The amount of XRP to add. - Expiration (optional) - Time the channel closes. The transaction will fail if the expiration - times does not satisfy the SettleDelay constraints. - - PaymentChannelClaim - - Place a claim against an existing channel. The parameters are: - Channel - The 256-bit ID of the channel. - Balance (optional) - The total amount of XRP delivered after this claim is processed - (optional, not needed if just closing). Amount (optional) The amount of XRP - the signature is for (not needed if equal to Balance or just closing the - line). Signature (optional) Authorization for the balance above, signed by - the owner (optional, not needed if closing or owner is performing the - transaction). The signature if for the following message: CLM\0 followed by - the 256-bit channel ID, and a 64-bit integer drops. PublicKey (optional) The - public key that made the signature (optional, required if a signature is - present) Flags tfClose Request that the channel be closed tfRenew Request - that the channel's expiration be reset. Only the owner may renew a channel. - -*/ - -//------------------------------------------------------------------------------ - -static TER -closeChannel( - std::shared_ptr const& slep, - ApplyView& view, - uint256 const& key, - beast::Journal j) -{ - AccountID const src = (*slep)[sfAccount]; - // Remove PayChan from owner directory - { - auto const page = (*slep)[sfOwnerNode]; - if (!view.dirRemove(keylet::ownerDir(src), page, key, true)) - { - // LCOV_EXCL_START - JLOG(j.fatal()) << "Could not remove paychan from src owner directory"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - - // Remove PayChan from recipient's owner directory, if present. - if (auto const page = (*slep)[~sfDestinationNode]) - { - auto const dst = (*slep)[sfDestination]; - if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true)) - { - // LCOV_EXCL_START - JLOG(j.fatal()) << "Could not remove paychan from dst owner directory"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - } - - // Transfer amount back to owner, decrement owner count - auto const sle = view.peek(keylet::account(src)); - if (!sle) - return tefINTERNAL; // LCOV_EXCL_LINE - - XRPL_ASSERT( - (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; - adjustOwnerCount(view, sle, -1, j); - view.update(sle); - - // Remove PayChan from ledger - view.erase(slep); - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -TxConsequences -PayChanCreate::makeTxConsequences(PreflightContext const& ctx) -{ - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; -} - -NotTEC -PayChanCreate::preflight(PreflightContext const& ctx) -{ - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) - return temBAD_AMOUNT; - - if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) - return temDST_IS_SRC; - - if (!publicKeyType(ctx.tx[sfPublicKey])) - return temMALFORMED; - - return tesSUCCESS; -} - -TER -PayChanCreate::preclaim(PreclaimContext const& ctx) -{ - auto const account = ctx.tx[sfAccount]; - auto const sle = ctx.view.read(keylet::account(account)); - if (!sle) - return terNO_ACCOUNT; - - // Check reserve and funds availability - { - auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx.tx[sfAmount]) - return tecUNFUNDED; - } - - auto const dst = ctx.tx[sfDestination]; - - { - // Check destination account - auto const sled = ctx.view.read(keylet::account(dst)); - if (!sled) - return tecNO_DST; - - auto const flags = sled->getFlags(); - - // Check if they have disallowed incoming payment channels - if (flags & lsfDisallowIncomingPayChan) - return tecNO_PERMISSION; - - if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag]) - return tecDST_TAG_NEEDED; - - // Pseudo-accounts cannot receive payment channels, other than native - // to their underlying ledger object - implemented in their respective - // transaction types. Note, this is not amendment-gated because all - // writes to pseudo-account discriminator fields **are** amendment - // gated, hence the behaviour of this check will always match the - // currently active amendments. - if (isPseudoAccount(sled)) - return tecNO_PERMISSION; - } - - return tesSUCCESS; -} - -TER -PayChanCreate::doApply() -{ - auto const account = ctx_.tx[sfAccount]; - auto const sle = ctx_.view().peek(keylet::account(account)); - if (!sle) - return tefINTERNAL; // LCOV_EXCL_LINE - - if (ctx_.view().rules().enabled(fixPayChanCancelAfter)) - { - auto const closeTime = ctx_.view().header().parentCloseTime; - if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) - return tecEXPIRED; - } - - auto const dst = ctx_.tx[sfDestination]; - - // Create PayChan in ledger. - // - // 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.getSeqValue()); - auto const slep = std::make_shared(payChanKeylet); - - // Funds held in this channel - (*slep)[sfAmount] = ctx_.tx[sfAmount]; - // Amount channel has already paid - (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed(); - (*slep)[sfAccount] = account; - (*slep)[sfDestination] = dst; - (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay]; - (*slep)[sfPublicKey] = ctx_.tx[sfPublicKey]; - (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; - (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; - (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; - if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) - { - (*slep)[sfSequence] = ctx_.tx.getSeqValue(); - } - - ctx_.view().insert(slep); - - // Add PayChan to owner directory - { - auto const page = ctx_.view().dirInsert( - keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account)); - if (!page) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*slep)[sfOwnerNode] = *page; - } - - // Add PayChan to the recipient's owner directory - { - auto const page = - ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst)); - if (!page) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*slep)[sfDestinationNode] = *page; - } - - // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); - ctx_.view().update(sle); - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -TxConsequences -PayChanFund::makeTxConsequences(PreflightContext const& ctx) -{ - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; -} - -NotTEC -PayChanFund::preflight(PreflightContext const& ctx) -{ - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) - return temBAD_AMOUNT; - - return tesSUCCESS; -} - -TER -PayChanFund::doApply() -{ - Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]); - auto const slep = ctx_.view().peek(k); - if (!slep) - return tecNO_ENTRY; - - AccountID const src = (*slep)[sfAccount]; - auto const txAccount = ctx_.tx[sfAccount]; - auto const expiration = (*slep)[~sfExpiration]; - - { - auto const cancelAfter = (*slep)[~sfCancelAfter]; - auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); - if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View")); - } - - if (src != txAccount) - // only the owner can add funds or extend - return tecNO_PERMISSION; - - if (auto extend = ctx_.tx[~sfExpiration]) - { - auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() + - (*slep)[sfSettleDelay]; - if (expiration && *expiration < minExpiration) - minExpiration = *expiration; - - if (*extend < minExpiration) - return temBAD_EXPIRATION; - (*slep)[~sfExpiration] = *extend; - ctx_.view().update(slep); - } - - auto const sle = ctx_.view().peek(keylet::account(txAccount)); - if (!sle) - return tefINTERNAL; // LCOV_EXCL_LINE - - { - // Check reserve and funds availability - auto const balance = (*sle)[sfBalance]; - auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + ctx_.tx[sfAmount]) - return tecUNFUNDED; - } - - // do not allow adding funds if dst does not exist - if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst))) - { - return tecNO_DST; - } - - (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; - ctx_.view().update(slep); - - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - ctx_.view().update(sle); - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -bool -PayChanClaim::checkExtraFeatures(PreflightContext const& ctx) -{ - return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); -} - -std::uint32_t -PayChanClaim::getFlagsMask(PreflightContext const&) -{ - return tfPaymentChannelClaimMask; -} - -NotTEC -PayChanClaim::preflight(PreflightContext const& ctx) -{ - auto const bal = ctx.tx[~sfBalance]; - if (bal && (!isXRP(*bal) || *bal <= beast::zero)) - return temBAD_AMOUNT; - - auto const amt = ctx.tx[~sfAmount]; - if (amt && (!isXRP(*amt) || *amt <= beast::zero)) - return temBAD_AMOUNT; - - if (bal && amt && *bal > *amt) - return temBAD_AMOUNT; - - { - auto const flags = ctx.tx.getFlags(); - - if ((flags & tfClose) && (flags & tfRenew)) - return temMALFORMED; - } - - if (auto const sig = ctx.tx[~sfSignature]) - { - if (!(ctx.tx[~sfPublicKey] && bal)) - return temMALFORMED; - - // Check the signature - // The signature isn't needed if txAccount == src, but if it's - // present, check it - - auto const reqBalance = bal->xrp(); - auto const authAmt = amt ? amt->xrp() : reqBalance; - - if (reqBalance > authAmt) - return temBAD_AMOUNT; - - Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]); - if (!publicKeyType(ctx.tx[sfPublicKey])) - return temMALFORMED; - - PublicKey const pk(ctx.tx[sfPublicKey]); - Serializer msg; - serializePayChanAuthorization(msg, k.key, authAmt); - if (!verify(pk, msg.slice(), *sig)) - return temBAD_SIGNATURE; - } - - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) - return err; - - return tesSUCCESS; -} - -TER -PayChanClaim::preclaim(PreclaimContext const& ctx) -{ - if (!ctx.view.rules().enabled(featureCredentials)) - return Transactor::preclaim(ctx); - - if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); - !isTesSuccess(err)) - return err; - - return tesSUCCESS; -} - -TER -PayChanClaim::doApply() -{ - Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]); - auto const slep = ctx_.view().peek(k); - if (!slep) - return tecNO_TARGET; - - AccountID const src = (*slep)[sfAccount]; - AccountID const dst = (*slep)[sfDestination]; - AccountID const txAccount = ctx_.tx[sfAccount]; - - auto const curExpiration = (*slep)[~sfExpiration]; - { - auto const cancelAfter = (*slep)[~sfCancelAfter]; - auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); - if ((cancelAfter && closeTime >= *cancelAfter) || - (curExpiration && closeTime >= *curExpiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View")); - } - - if (txAccount != src && txAccount != dst) - return tecNO_PERMISSION; - - if (ctx_.tx[~sfBalance]) - { - auto const chanBalance = slep->getFieldAmount(sfBalance).xrp(); - auto const chanFunds = slep->getFieldAmount(sfAmount).xrp(); - auto const reqBalance = ctx_.tx[sfBalance].xrp(); - - if (txAccount == dst && !ctx_.tx[~sfSignature]) - return temBAD_SIGNATURE; - - if (ctx_.tx[~sfSignature]) - { - PublicKey const pk((*slep)[sfPublicKey]); - if (ctx_.tx[sfPublicKey] != pk) - return temBAD_SIGNER; - } - - if (reqBalance > chanFunds) - return tecUNFUNDED_PAYMENT; - - if (reqBalance <= chanBalance) - // nothing requested - return tecUNFUNDED_PAYMENT; - - auto const sled = ctx_.view().peek(keylet::account(dst)); - if (!sled) - return tecNO_DST; - - if (auto err = - verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal); - !isTesSuccess(err)) - return err; - - (*slep)[sfBalance] = ctx_.tx[sfBalance]; - XRPAmount const reqDelta = reqBalance - chanBalance; - XRPL_ASSERT(reqDelta >= beast::zero, "xrpl::PayChanClaim::doApply : minimum balance delta"); - (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; - ctx_.view().update(sled); - ctx_.view().update(slep); - } - - if (ctx_.tx.getFlags() & tfRenew) - { - if (src != txAccount) - return tecNO_PERMISSION; - (*slep)[~sfExpiration] = std::nullopt; - ctx_.view().update(slep); - } - - if (ctx_.tx.getFlags() & tfClose) - { - // Channel will close immediately if dry or the receiver closes - if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View")); - - auto const settleExpiration = - ctx_.view().header().parentCloseTime.time_since_epoch().count() + - (*slep)[sfSettleDelay]; - - if (!curExpiration || *curExpiration > settleExpiration) - { - (*slep)[~sfExpiration] = settleExpiration; - ctx_.view().update(slep); - } - } - - return tesSUCCESS; -} - -} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PayChanClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PayChanClaim.cpp new file mode 100644 index 0000000000..d41e6f043c --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PayChanClaim.cpp @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +bool +PayChanClaim::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); +} + +std::uint32_t +PayChanClaim::getFlagsMask(PreflightContext const&) +{ + return tfPaymentChannelClaimMask; +} + +NotTEC +PayChanClaim::preflight(PreflightContext const& ctx) +{ + auto const bal = ctx.tx[~sfBalance]; + if (bal && (!isXRP(*bal) || *bal <= beast::zero)) + return temBAD_AMOUNT; + + auto const amt = ctx.tx[~sfAmount]; + if (amt && (!isXRP(*amt) || *amt <= beast::zero)) + return temBAD_AMOUNT; + + if (bal && amt && *bal > *amt) + return temBAD_AMOUNT; + + { + auto const flags = ctx.tx.getFlags(); + + if ((flags & tfClose) && (flags & tfRenew)) + return temMALFORMED; + } + + if (auto const sig = ctx.tx[~sfSignature]) + { + if (!(ctx.tx[~sfPublicKey] && bal)) + return temMALFORMED; + + // Check the signature + // The signature isn't needed if txAccount == src, but if it's + // present, check it + + auto const reqBalance = bal->xrp(); + auto const authAmt = amt ? amt->xrp() : reqBalance; + + if (reqBalance > authAmt) + return temBAD_AMOUNT; + + Keylet const k(ltPAYCHAN, ctx.tx[sfChannel]); + if (!publicKeyType(ctx.tx[sfPublicKey])) + return temMALFORMED; + + PublicKey const pk(ctx.tx[sfPublicKey]); + Serializer msg; + serializePayChanAuthorization(msg, k.key, authAmt); + if (!verify(pk, msg.slice(), *sig)) + return temBAD_SIGNATURE; + } + + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + +TER +PayChanClaim::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.rules().enabled(featureCredentials)) + return Transactor::preclaim(ctx); + + if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); + !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + +TER +PayChanClaim::doApply() +{ + Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]); + auto const slep = ctx_.view().peek(k); + if (!slep) + return tecNO_TARGET; + + AccountID const src = (*slep)[sfAccount]; + AccountID const dst = (*slep)[sfDestination]; + AccountID const txAccount = ctx_.tx[sfAccount]; + + auto const curExpiration = (*slep)[~sfExpiration]; + { + auto const cancelAfter = (*slep)[~sfCancelAfter]; + auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); + if ((cancelAfter && closeTime >= *cancelAfter) || + (curExpiration && closeTime >= *curExpiration)) + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View")); + } + + if (txAccount != src && txAccount != dst) + return tecNO_PERMISSION; + + if (ctx_.tx[~sfBalance]) + { + auto const chanBalance = slep->getFieldAmount(sfBalance).xrp(); + auto const chanFunds = slep->getFieldAmount(sfAmount).xrp(); + auto const reqBalance = ctx_.tx[sfBalance].xrp(); + + if (txAccount == dst && !ctx_.tx[~sfSignature]) + return temBAD_SIGNATURE; + + if (ctx_.tx[~sfSignature]) + { + PublicKey const pk((*slep)[sfPublicKey]); + if (ctx_.tx[sfPublicKey] != pk) + return temBAD_SIGNER; + } + + if (reqBalance > chanFunds) + return tecUNFUNDED_PAYMENT; + + if (reqBalance <= chanBalance) + // nothing requested + return tecUNFUNDED_PAYMENT; + + auto const sled = ctx_.view().peek(keylet::account(dst)); + if (!sled) + return tecNO_DST; + + if (auto err = + verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal); + !isTesSuccess(err)) + return err; + + (*slep)[sfBalance] = ctx_.tx[sfBalance]; + XRPAmount const reqDelta = reqBalance - chanBalance; + XRPL_ASSERT(reqDelta >= beast::zero, "xrpl::PayChanClaim::doApply : minimum balance delta"); + (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + ctx_.view().update(sled); + ctx_.view().update(slep); + } + + if (ctx_.tx.getFlags() & tfRenew) + { + if (src != txAccount) + return tecNO_PERMISSION; + (*slep)[~sfExpiration] = std::nullopt; + ctx_.view().update(slep); + } + + if (ctx_.tx.getFlags() & tfClose) + { + // Channel will close immediately if dry or the receiver closes + if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View")); + + auto const settleExpiration = + ctx_.view().header().parentCloseTime.time_since_epoch().count() + + (*slep)[sfSettleDelay]; + + if (!curExpiration || *curExpiration > settleExpiration) + { + (*slep)[~sfExpiration] = settleExpiration; + ctx_.view().update(slep); + } + } + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PayChanCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PayChanCreate.cpp new file mode 100644 index 0000000000..ccb0fa1a63 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PayChanCreate.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +/* + PaymentChannel + + Payment channels permit off-ledger checkpoints of XRP payments flowing + in a single direction. A channel sequesters the owner's XRP in its own + ledger entry. The owner can authorize the recipient to claim up to a + given balance by giving the receiver a signed message (off-ledger). The + recipient can use this signed message to claim any unpaid balance while + the channel remains open. The owner can top off the line as needed. If + the channel has not paid out all its funds, the owner must wait out a + delay to close the channel to give the recipient a chance to supply any + claims. The recipient can close the channel at any time. Any transaction + that touches the channel after the expiration time will close the + channel. The total amount paid increases monotonically as newer claims + are issued. When the channel is closed any remaining balance is returned + to the owner. Channels are intended to permit intermittent off-ledger + settlement of ILP trust lines as balances get substantial. For + bidirectional channels, a payment channel can be used in each direction. +*/ + +//------------------------------------------------------------------------------ + +TxConsequences +PayChanCreate::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; +} + +NotTEC +PayChanCreate::preflight(PreflightContext const& ctx) +{ + if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + return temBAD_AMOUNT; + + if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) + return temDST_IS_SRC; + + if (!publicKeyType(ctx.tx[sfPublicKey])) + return temMALFORMED; + + return tesSUCCESS; +} + +TER +PayChanCreate::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const sle = ctx.view.read(keylet::account(account)); + if (!sle) + return terNO_ACCOUNT; + + // Check reserve and funds availability + { + auto const balance = (*sle)[sfBalance]; + auto const reserve = ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + if (balance < reserve + ctx.tx[sfAmount]) + return tecUNFUNDED; + } + + auto const dst = ctx.tx[sfDestination]; + + { + // Check destination account + auto const sled = ctx.view.read(keylet::account(dst)); + if (!sled) + return tecNO_DST; + + auto const flags = sled->getFlags(); + + // Check if they have disallowed incoming payment channels + if (flags & lsfDisallowIncomingPayChan) + return tecNO_PERMISSION; + + if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag]) + return tecDST_TAG_NEEDED; + + // Pseudo-accounts cannot receive payment channels, other than native + // to their underlying ledger object - implemented in their respective + // transaction types. Note, this is not amendment-gated because all + // writes to pseudo-account discriminator fields **are** amendment + // gated, hence the behaviour of this check will always match the + // currently active amendments. + if (isPseudoAccount(sled)) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +PayChanCreate::doApply() +{ + auto const account = ctx_.tx[sfAccount]; + auto const sle = ctx_.view().peek(keylet::account(account)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (ctx_.view().rules().enabled(fixPayChanCancelAfter)) + { + auto const closeTime = ctx_.view().header().parentCloseTime; + if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) + return tecEXPIRED; + } + + auto const dst = ctx_.tx[sfDestination]; + + // Create PayChan in ledger. + // + // 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.getSeqValue()); + auto const slep = std::make_shared(payChanKeylet); + + // Funds held in this channel + (*slep)[sfAmount] = ctx_.tx[sfAmount]; + // Amount channel has already paid + (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed(); + (*slep)[sfAccount] = account; + (*slep)[sfDestination] = dst; + (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay]; + (*slep)[sfPublicKey] = ctx_.tx[sfPublicKey]; + (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; + (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; + (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) + { + (*slep)[sfSequence] = ctx_.tx.getSeqValue(); + } + + ctx_.view().insert(slep); + + // Add PayChan to owner directory + { + auto const page = ctx_.view().dirInsert( + keylet::ownerDir(account), payChanKeylet, describeOwnerDir(account)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfOwnerNode] = *page; + } + + // Add PayChan to the recipient's owner directory + { + auto const page = + ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfDestinationNode] = *page; + } + + // Deduct owner's balance, increment owner count + (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); + ctx_.view().update(sle); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PayChanFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PayChanFund.cpp new file mode 100644 index 0000000000..eb6fe1d2a9 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PayChanFund.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include + +#include + +namespace xrpl { + +TxConsequences +PayChanFund::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; +} + +NotTEC +PayChanFund::preflight(PreflightContext const& ctx) +{ + if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +PayChanFund::doApply() +{ + Keylet const k(ltPAYCHAN, ctx_.tx[sfChannel]); + auto const slep = ctx_.view().peek(k); + if (!slep) + return tecNO_ENTRY; + + AccountID const src = (*slep)[sfAccount]; + auto const txAccount = ctx_.tx[sfAccount]; + auto const expiration = (*slep)[~sfExpiration]; + + { + auto const cancelAfter = (*slep)[~sfCancelAfter]; + auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); + if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)) + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.journal("View")); + } + + if (src != txAccount) + // only the owner can add funds or extend + return tecNO_PERMISSION; + + if (auto extend = ctx_.tx[~sfExpiration]) + { + auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() + + (*slep)[sfSettleDelay]; + if (expiration && *expiration < minExpiration) + minExpiration = *expiration; + + if (*extend < minExpiration) + return temBAD_EXPIRATION; + (*slep)[~sfExpiration] = *extend; + ctx_.view().update(slep); + } + + auto const sle = ctx_.view().peek(keylet::account(txAccount)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + + { + // Check reserve and funds availability + auto const balance = (*sle)[sfBalance]; + auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + if (balance < reserve + ctx_.tx[sfAmount]) + return tecUNFUNDED; + } + + // do not allow adding funds if dst does not exist + if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst))) + { + return tecNO_DST; + } + + (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; + ctx_.view().update(slep); + + (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + ctx_.view().update(sle); + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PayChanHelpers.cpp b/src/libxrpl/tx/transactors/payment_channel/PayChanHelpers.cpp new file mode 100644 index 0000000000..b761bb3fc3 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PayChanHelpers.cpp @@ -0,0 +1,58 @@ +#include +#include +#include + +#include + +namespace xrpl { + +TER +closeChannel( + std::shared_ptr const& slep, + ApplyView& view, + uint256 const& key, + beast::Journal j) +{ + AccountID const src = (*slep)[sfAccount]; + // Remove PayChan from owner directory + { + auto const page = (*slep)[sfOwnerNode]; + if (!view.dirRemove(keylet::ownerDir(src), page, key, true)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Could not remove paychan from src owner directory"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + // Remove PayChan from recipient's owner directory, if present. + if (auto const page = (*slep)[~sfDestinationNode]) + { + auto const dst = (*slep)[sfDestination]; + if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Could not remove paychan from dst owner directory"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + } + + // Transfer amount back to owner, decrement owner count + auto const sle = view.peek(keylet::account(src)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE + + XRPL_ASSERT( + (*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount"); + (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; + adjustOwnerCount(view, sle, -1, j); + view.update(sle); + + // Remove PayChan from ledger + view.erase(slep); + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PayChanHelpers.h b/src/libxrpl/tx/transactors/payment_channel/PayChanHelpers.h new file mode 100644 index 0000000000..5d1a4c0aa1 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PayChanHelpers.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace xrpl { + +TER +closeChannel( + std::shared_ptr const& slep, + ApplyView& view, + uint256 const& key, + beast::Journal j); + +} // namespace xrpl