mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-04 11:15:56 +00:00
Compare commits
1 Commits
415a412d42
...
dangell7/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc8a689a20 |
@@ -349,6 +349,19 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
|
||||
|
||||
Keylet
|
||||
permissionedDomain(uint256 const& domainID) noexcept;
|
||||
|
||||
Keylet
|
||||
subscription(
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
std::uint32_t const& seq) noexcept;
|
||||
|
||||
inline Keylet
|
||||
subscription(uint256 const& key) noexcept
|
||||
{
|
||||
return {ltSUBSCRIPTION, key};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -62,7 +62,6 @@ enum LedgerEntryType : std::uint16_t
|
||||
|
||||
#undef LEDGER_ENTRY
|
||||
#pragma pop_macro("LEDGER_ENTRY")
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
/** A special type, matching any ledger entry type.
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(Subscription, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -504,5 +504,25 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
|
||||
}))
|
||||
|
||||
/** A ledger object representing a subscription.
|
||||
|
||||
\sa keylet::mptoken
|
||||
*/
|
||||
LEDGER_ENTRY(ltSUBSCRIPTION, 0x0085, Subscription, subscription, ({
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
{sfSequence, soeREQUIRED},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfDestination, soeREQUIRED},
|
||||
{sfDestinationTag, soeOPTIONAL},
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfBalance, soeREQUIRED},
|
||||
{sfFrequency, soeREQUIRED},
|
||||
{sfNextClaimTime, soeREQUIRED},
|
||||
{sfExpiration, soeOPTIONAL},
|
||||
{sfDestinationNode, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -114,6 +114,9 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
|
||||
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
||||
TYPED_SFIELD(sfFrequency, UINT32, 53)
|
||||
TYPED_SFIELD(sfStartTime, UINT32, 54)
|
||||
TYPED_SFIELD(sfNextClaimTime, UINT32, 55)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -197,6 +200,7 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
|
||||
TYPED_SFIELD(sfDomainID, UINT256, 34)
|
||||
TYPED_SFIELD(sfVaultID, UINT256, 35)
|
||||
TYPED_SFIELD(sfParentBatchID, UINT256, 36)
|
||||
TYPED_SFIELD(sfSubscriptionID, UINT256, 37)
|
||||
|
||||
// number (common)
|
||||
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
||||
|
||||
@@ -526,6 +526,28 @@ TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({
|
||||
{sfBatchSigners, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type batches together transactions. */
|
||||
TRANSACTION(ttSUBSCRIPTION_SET, 72, SubscriptionSet, Delegation::delegatable, ({
|
||||
{sfDestination, soeOPTIONAL},
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||
{sfFrequency, soeOPTIONAL},
|
||||
{sfStartTime, soeOPTIONAL},
|
||||
{sfExpiration, soeOPTIONAL},
|
||||
{sfDestinationTag, soeOPTIONAL},
|
||||
{sfSubscriptionID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type batches together transactions. */
|
||||
TRANSACTION(ttSUBSCRIPTION_CANCEL, 73, SubscriptionCancel, Delegation::delegatable, ({
|
||||
{sfSubscriptionID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This transaction type batches together transactions. */
|
||||
TRANSACTION(ttSUBSCRIPTION_CLAIM, 74, SubscriptionClaim, Delegation::delegatable, ({
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||
{sfSubscriptionID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -99,6 +99,7 @@ JSS(Signer); // field.
|
||||
JSS(Signers); // field.
|
||||
JSS(SigningPubKey); // field.
|
||||
JSS(Subject); // in: Credential transactions
|
||||
JSS(SubscriptionID); // in: Subscription transactions
|
||||
JSS(TakerGets); // field.
|
||||
JSS(TakerPays); // field.
|
||||
JSS(TradingFee); // in/out: AMM trading fee
|
||||
@@ -283,6 +284,7 @@ JSS(fee_mult_max); // in: TransactionSign
|
||||
JSS(fee_ref); // out: NetworkOPs, DEPRECATED
|
||||
JSS(fetch_pack); // out: NetworkOPs
|
||||
JSS(FIELDS); // out: RPC server_definitions
|
||||
JSS(Frequency); // in: Subscription transactions
|
||||
// matches definitions.json format
|
||||
JSS(first); // out: rpc/Version
|
||||
JSS(finished);
|
||||
|
||||
@@ -96,6 +96,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
PERMISSIONED_DOMAIN = 'm',
|
||||
DELEGATE = 'E',
|
||||
VAULT = 'V',
|
||||
SUBSCRIPTION = 'U',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -580,6 +581,17 @@ permissionedDomain(uint256 const& domainID) noexcept
|
||||
return {ltPERMISSIONED_DOMAIN, domainID};
|
||||
}
|
||||
|
||||
Keylet
|
||||
subscription(
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
std::uint32_t const& seq) noexcept
|
||||
{
|
||||
return {
|
||||
ltSUBSCRIPTION,
|
||||
indexHash(LedgerNameSpace::SUBSCRIPTION, account, dest, seq)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
3124
src/test/app/Subscription_test.cpp
Normal file
3124
src/test/app/Subscription_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@
|
||||
#include <test/jtx/sendmax.h>
|
||||
#include <test/jtx/seq.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/subscription.h>
|
||||
#include <test/jtx/tag.h>
|
||||
#include <test/jtx/tags.h>
|
||||
#include <test/jtx/ter.h>
|
||||
|
||||
107
src/test/jtx/impl/subscription.cpp
Normal file
107
src/test/jtx/impl/subscription.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx/subscription.h>
|
||||
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/** Subscription operations. */
|
||||
namespace subscription {
|
||||
|
||||
void
|
||||
start_time::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfStartTime.jsonName] = value_.time_since_epoch().count();
|
||||
}
|
||||
|
||||
Json::Value
|
||||
create(
|
||||
jtx::Account const& account,
|
||||
jtx::Account const& destination,
|
||||
STAmount const& amount,
|
||||
NetClock::duration const& frequency,
|
||||
std::optional<NetClock::time_point> const& expiration)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SubscriptionSet;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::Destination] = to_string(destination.id());
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::Frequency] = frequency.count();
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
if (expiration)
|
||||
jv[sfExpiration.jsonName] = expiration->time_since_epoch().count();
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
update(
|
||||
jtx::Account const& account,
|
||||
uint256 const& subscriptionId,
|
||||
STAmount const& amount,
|
||||
std::optional<NetClock::time_point> const& expiration)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SubscriptionSet;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::SubscriptionID] = to_string(subscriptionId);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
if (expiration)
|
||||
jv[sfExpiration.jsonName] = expiration->time_since_epoch().count();
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
cancel(jtx::Account const& account, uint256 const& subscriptionId)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SubscriptionCancel;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::SubscriptionID] = to_string(subscriptionId);
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
claim(
|
||||
jtx::Account const& account,
|
||||
uint256 const& subscriptionId,
|
||||
STAmount const& amount)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SubscriptionClaim;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::SubscriptionID] = to_string(subscriptionId);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace subscription
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
79
src/test/jtx/subscription.h
Normal file
79
src/test/jtx/subscription.h
Normal file
@@ -0,0 +1,79 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2019 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TEST_JTX_SUBSCRIPTION_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_SUBSCRIPTION_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/** Subscription operations. */
|
||||
namespace subscription {
|
||||
|
||||
Json::Value
|
||||
create(
|
||||
jtx::Account const& account,
|
||||
jtx::Account const& destination,
|
||||
STAmount const& amount,
|
||||
NetClock::duration const& frequency,
|
||||
std::optional<NetClock::time_point> const& expiration = std::nullopt);
|
||||
|
||||
Json::Value
|
||||
update(
|
||||
jtx::Account const& account,
|
||||
uint256 const& subscriptionId,
|
||||
STAmount const& amount,
|
||||
std::optional<NetClock::time_point> const& expiration = std::nullopt);
|
||||
|
||||
Json::Value
|
||||
cancel(jtx::Account const& account, uint256 const& subscriptionId);
|
||||
|
||||
Json::Value
|
||||
claim(
|
||||
jtx::Account const& account,
|
||||
uint256 const& subscriptionId,
|
||||
STAmount const& amount);
|
||||
|
||||
/** Set the "StartTime" time tag on a JTx */
|
||||
class start_time
|
||||
{
|
||||
private:
|
||||
NetClock::time_point value_;
|
||||
|
||||
public:
|
||||
explicit start_time(NetClock::time_point const& value) : value_(value)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
} // namespace subscription
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
284
src/xrpld/app/misc/SubscriptionHelpers.h
Normal file
284
src/xrpld/app/misc/SubscriptionHelpers.h
Normal file
@@ -0,0 +1,284 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_SUBSCRIPTIONHELPERS_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_SUBSCRIPTIONHELPERS_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/paths/Flow.h>
|
||||
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
|
||||
#include <xrpld/ledger/ApplyView.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
template <ValidIssueType T>
|
||||
static TER
|
||||
canTransferTokenHelper(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal const& j);
|
||||
|
||||
template <>
|
||||
TER
|
||||
canTransferTokenHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
AccountID issuer = amount.getIssuer();
|
||||
if (issuer == account)
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "canTransferTokenHelper: Issuer is the same as the account.";
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// If the issuer does not exist, return tecNO_ISSUER
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Issuer does not exist.";
|
||||
return tecNO_ISSUER;
|
||||
}
|
||||
|
||||
// If the account does not have a trustline to the issuer, return tecNO_LINE
|
||||
auto const sleRippleState =
|
||||
view.read(keylet::line(account, issuer, amount.getCurrency()));
|
||||
if (!sleRippleState)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Trust line does not exist.";
|
||||
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)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Invalid trust line state.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// If balance is negative, issuer must have lower address than account
|
||||
if (balance < beast::zero && issuer > account)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Invalid trust line state.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is authorized
|
||||
if (auto const ter = requireAuth(view, amount.issue(), account);
|
||||
ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Account is not authorized";
|
||||
return ter;
|
||||
}
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, amount.issue(), dest);
|
||||
ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "canTransferTokenHelper: Destination is not authorized.";
|
||||
return ter;
|
||||
}
|
||||
|
||||
// If the issuer has frozen the account, return tecFROZEN
|
||||
if (isFrozen(view, account, amount.issue()) ||
|
||||
isDeepFrozen(
|
||||
view, account, amount.issue().currency, amount.issue().account))
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Account is frozen.";
|
||||
return tecFROZEN;
|
||||
}
|
||||
|
||||
// If the issuer has frozen the destination, return tecFROZEN
|
||||
if (isFrozen(view, dest, amount.issue()) ||
|
||||
isDeepFrozen(
|
||||
view, dest, amount.issue().currency, amount.issue().account))
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Destination is frozen.";
|
||||
return tecFROZEN;
|
||||
}
|
||||
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view, account, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
|
||||
|
||||
// If the balance is less than or equal to 0, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::zero)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
|
||||
"than or equal to 0.";
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
|
||||
"than the 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
|
||||
canTransferTokenHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
AccountID issuer = amount.getIssuer();
|
||||
if (issuer == account)
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "canTransferTokenHelper: Issuer is the same as the account.";
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey =
|
||||
keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "canTransferTokenHelper: MPT issuance does not exist.";
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
|
||||
// If the issuer is not the same as the issuer of the mpt, return
|
||||
// tecNO_PERMISSION
|
||||
if (sleIssuance->getAccountID(sfIssuer) != issuer)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Issuer is not the same as "
|
||||
"the issuer of the MPT.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "canTransferTokenHelper: Account does not have the MPT.";
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter =
|
||||
requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Account is not authorized.";
|
||||
return ter;
|
||||
}
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is
|
||||
// authorized
|
||||
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth);
|
||||
ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(j.trace())
|
||||
<< "canTransferTokenHelper: Destination is not authorized.";
|
||||
return ter;
|
||||
}
|
||||
|
||||
// If the issuer has locked the account, return tecLOCKED
|
||||
if (isFrozen(view, account, mptIssue))
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Account is locked.";
|
||||
return tecLOCKED;
|
||||
}
|
||||
|
||||
// If the issuer has locked the destination, return tecLOCKED
|
||||
if (isFrozen(view, dest, mptIssue))
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Destination is locked.";
|
||||
return tecLOCKED;
|
||||
}
|
||||
|
||||
// If the mpt cannot be transferred, return tecNO_AUTH
|
||||
if (auto const ter = canTransfer(view, mptIssue, account, dest);
|
||||
ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: MPT cannot be transferred.";
|
||||
return ter;
|
||||
}
|
||||
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<MPTIssue>(),
|
||||
fhIGNORE_FREEZE,
|
||||
ahIGNORE_AUTH,
|
||||
j);
|
||||
|
||||
// If the balance is less than or equal to 0, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::zero)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
|
||||
"than or equal to 0.";
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
{
|
||||
JLOG(j.trace()) << "canTransferTokenHelper: Spendable amount is less "
|
||||
"than the 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;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -543,6 +543,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltCREDENTIAL:
|
||||
case ltPERMISSIONED_DOMAIN:
|
||||
case ltVAULT:
|
||||
case ltSUBSCRIPTION:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
@@ -1511,6 +1512,9 @@ ValidMPTIssuance::finalize(
|
||||
if (tx.getTxnType() == ttESCROW_FINISH)
|
||||
return true;
|
||||
|
||||
if (tx.getTxnType() == ttSUBSCRIPTION_CLAIM)
|
||||
return true;
|
||||
|
||||
if ((tx.getTxnType() == ttVAULT_CLAWBACK ||
|
||||
tx.getTxnType() == ttVAULT_WITHDRAW) &&
|
||||
mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
|
||||
|
||||
106
src/xrpld/app/tx/detail/SubscriptionCancel.cpp
Normal file
106
src/xrpld/app/tx/detail/SubscriptionCancel.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/paths/Flow.h>
|
||||
#include <xrpld/app/tx/detail/SubscriptionCancel.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
NotTEC
|
||||
SubscriptionCancel::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureSubscription))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SubscriptionCancel::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const sleSub = ctx.view.read(
|
||||
keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
|
||||
if (!sleSub)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "SubscriptionCancel: Subscription does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SubscriptionCancel::doApply()
|
||||
{
|
||||
Sandbox sb(&ctx_.view());
|
||||
|
||||
auto const sleSub =
|
||||
sb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
|
||||
if (!sleSub)
|
||||
{
|
||||
JLOG(ctx_.journal.debug())
|
||||
<< "SubscriptionCancel: Subscription does not exist.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
AccountID const account{sleSub->getAccountID(sfAccount)};
|
||||
AccountID const dstAcct{sleSub->getAccountID(sfDestination)};
|
||||
auto viewJ = ctx_.app.journal("View");
|
||||
|
||||
std::uint64_t const ownerPage{(*sleSub)[sfOwnerNode]};
|
||||
if (!sb.dirRemove(
|
||||
keylet::ownerDir(account), ownerPage, sleSub->key(), true))
|
||||
{
|
||||
JLOG(j_.fatal()) << "Unable to delete subscription from source.";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
std::uint64_t const destPage{(*sleSub)[sfDestinationNode]};
|
||||
if (!sb.dirRemove(keylet::ownerDir(dstAcct), destPage, sleSub->key(), true))
|
||||
{
|
||||
JLOG(j_.fatal()) << "Unable to delete subscription from destination.";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
auto const sleSrc = sb.peek(keylet::account(account));
|
||||
sb.erase(sleSub);
|
||||
|
||||
adjustOwnerCount(sb, sleSrc, -1, viewJ);
|
||||
|
||||
sb.apply(ctx_.rawView());
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/SubscriptionCancel.h
Normal file
48
src/xrpld/app/tx/detail/SubscriptionCancel.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SUBSCRIPTIONCANCEL_H_INCLUDED
|
||||
#define RIPPLE_TX_SUBSCRIPTIONCANCEL_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class SubscriptionCancel : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit SubscriptionCancel(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TX_SUBSCRIPTIONCANCEL_H_INCLUDED
|
||||
426
src/xrpld/app/tx/detail/SubscriptionClaim.cpp
Normal file
426
src/xrpld/app/tx/detail/SubscriptionClaim.cpp
Normal file
@@ -0,0 +1,426 @@
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/misc/SubscriptionHelpers.h>
|
||||
#include <xrpld/app/paths/Flow.h>
|
||||
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
|
||||
#include <xrpld/app/tx/detail/SubscriptionClaim.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
NotTEC
|
||||
SubscriptionClaim::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureSubscription))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SubscriptionClaim::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const sleSub = ctx.view.read(
|
||||
keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
|
||||
if (!sleSub)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionClaim: Subscription does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
// Only claim a subscription with this account as the destination.
|
||||
AccountID const dest = sleSub->getAccountID(sfDestination);
|
||||
if (ctx.tx[sfAccount] != dest)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SubscriptionClaim: Cashing a subscription with "
|
||||
"wrong Destination.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
AccountID const account = sleSub->getAccountID(sfAccount);
|
||||
if (account == dest)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SubscriptionClaim: Malformed transaction: "
|
||||
"Cashing subscription to self.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
{
|
||||
auto const sleSrc = ctx.view.read(keylet::account(account));
|
||||
auto const sleDst = ctx.view.read(keylet::account(dest));
|
||||
if (!sleSrc || !sleDst)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionClaim: source or destination not in ledger";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
|
||||
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
|
||||
if (amount.asset() != sleAmount.asset())
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SubscriptionClaim: Subscription claim does "
|
||||
"not match subscription currency.";
|
||||
return tecWRONG_ASSET;
|
||||
}
|
||||
|
||||
if (amount > sleAmount)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SubscriptionClaim: Claim amount exceeds "
|
||||
"subscription amount.";
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
// Time/period context
|
||||
std::uint32_t const currentTime =
|
||||
ctx.view.info().parentCloseTime.time_since_epoch().count();
|
||||
std::uint32_t const nextClaimTime =
|
||||
sleSub->getFieldU32(sfNextClaimTime);
|
||||
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
|
||||
|
||||
// Determine effective available balance:
|
||||
// - If we have crossed into a later period AND the previous period had
|
||||
// a partial
|
||||
// balance remaining (carryover not allowed), then the effective
|
||||
// period rolls forward once and its balance resets to sleAmount.
|
||||
// - Otherwise we operate on the period at nextClaimTime with its stored
|
||||
// balance.
|
||||
STAmount balance = sleSub->getFieldAmount(sfBalance);
|
||||
bool const arrears = currentTime >= nextClaimTime + frequency;
|
||||
if (arrears && balance != sleAmount)
|
||||
{
|
||||
// We will effectively operate on (nextClaimTime + frequency) with a
|
||||
// full balance.
|
||||
balance = sleAmount;
|
||||
}
|
||||
|
||||
if (amount > balance)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionClaim: Claim amount exceeds remaining "
|
||||
"balance for this period.";
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
if (isXRP(amount))
|
||||
{
|
||||
if (xrpLiquid(ctx.view, account, 0, ctx.j) < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return canTransferTokenHelper<T>(
|
||||
ctx.view, account, dest, amount, ctx.j);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
// Must be at or past the start of the effective period.
|
||||
if (!hasExpired(ctx.view, sleSub->getFieldU32(sfNextClaimTime)))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SubscriptionClaim: The subscription has not "
|
||||
"reached the next claim time.";
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
static TER
|
||||
doTransferTokenHelper(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& sleDest,
|
||||
STAmount const& xrpBalance,
|
||||
STAmount const& amount,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
TER
|
||||
doTransferTokenHelper<Issue>(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> 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;
|
||||
|
||||
// Review Note: We could remove this and just say to use batch to auth the
|
||||
// token first
|
||||
if (!view.exists(trustLineKey) && createAsset && issuer != receiver)
|
||||
{
|
||||
// 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())
|
||||
<< "doTransferTokenHelper: 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))
|
||||
{
|
||||
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to create trust line: " << transToken(ter);
|
||||
return ter;
|
||||
}
|
||||
// clang-format on
|
||||
|
||||
view.update(sleDest);
|
||||
}
|
||||
|
||||
if (!view.exists(trustLineKey) && issuer != receiver)
|
||||
return tecNO_LINE;
|
||||
|
||||
auto const ter = accountSend(
|
||||
view, sender, receiver, amount, journal, WaiveTransferFee::No);
|
||||
if (ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(journal.trace()) << "doTransferTokenHelper: Failed to send token: "
|
||||
<< transToken(ter);
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
doTransferTokenHelper<MPTIssue>(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& sleDest,
|
||||
STAmount const& xrpBalance,
|
||||
STAmount const& amount,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
auto const mptID = amount.get<MPTIssue>().getMptID();
|
||||
auto const issuanceKey = keylet::mptIssuance(mptID);
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset)
|
||||
{
|
||||
if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
|
||||
xrpBalance < view.fees().accountReserve(ownerCount + 1))
|
||||
{
|
||||
JLOG(journal.trace())
|
||||
<< "doTransferTokenHelper: MPT does not exist. "
|
||||
"Insufficent reserve to create MPT.";
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
if (auto const ter =
|
||||
MPTokenAuthorize::createMPToken(view, mptID, receiver, 0);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
JLOG(journal.trace())
|
||||
<< "doTransferTokenHelper: Failed to create MPT: "
|
||||
<< transToken(ter);
|
||||
return ter;
|
||||
}
|
||||
|
||||
// Update owner count.
|
||||
adjustOwnerCount(view, sleDest, 1, journal);
|
||||
}
|
||||
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)))
|
||||
{
|
||||
JLOG(journal.trace()) << "doTransferTokenHelper: MPT does not exist.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
auto const ter = accountSend(
|
||||
view, sender, receiver, amount, journal, WaiveTransferFee::No);
|
||||
if (ter != tesSUCCESS)
|
||||
{
|
||||
JLOG(journal.trace())
|
||||
<< "doTransferTokenHelper: Failed to send MPT: " << transToken(ter);
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SubscriptionClaim::doApply()
|
||||
{
|
||||
PaymentSandbox psb(&ctx_.view());
|
||||
auto viewJ = ctx_.app.journal("View");
|
||||
|
||||
auto sleSub =
|
||||
psb.peek(keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
|
||||
if (!sleSub)
|
||||
{
|
||||
JLOG(j_.trace()) << "SubscriptionClaim: Subscription does not exist.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
AccountID const account = sleSub->getAccountID(sfAccount);
|
||||
if (!psb.exists(keylet::account(account)))
|
||||
{
|
||||
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
AccountID const dest = sleSub->getAccountID(sfDestination);
|
||||
if (!psb.exists(keylet::account(dest)))
|
||||
{
|
||||
JLOG(j_.trace()) << "SubscriptionClaim: Account does not exist.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
if (dest != ctx_.tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(j_.trace()) << "SubscriptionClaim: Account is not the "
|
||||
"destination of the subscription.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
STAmount const sleAmount = sleSub->getFieldAmount(sfAmount);
|
||||
STAmount const deliverAmount = ctx_.tx.getFieldAmount(sfAmount);
|
||||
|
||||
// Pull current period info
|
||||
std::uint32_t const currentTime =
|
||||
psb.info().parentCloseTime.time_since_epoch().count();
|
||||
std::uint32_t nextClaimTime = sleSub->getFieldU32(sfNextClaimTime);
|
||||
std::uint32_t const frequency = sleSub->getFieldU32(sfFrequency);
|
||||
|
||||
STAmount availableBalance = sleSub->getFieldAmount(sfBalance);
|
||||
bool const arrears = currentTime >= nextClaimTime + frequency;
|
||||
|
||||
// If we crossed into a later period and the previous period was partially
|
||||
// used, forfeit the leftover and roll forward exactly one period; reset the
|
||||
// balance.
|
||||
if (arrears && availableBalance != sleAmount)
|
||||
{
|
||||
nextClaimTime += frequency;
|
||||
availableBalance = sleAmount;
|
||||
|
||||
// Reflect the rollover immediately in the SLE so subsequent logic is
|
||||
// consistent.
|
||||
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
|
||||
sleSub->setFieldAmount(sfBalance, availableBalance);
|
||||
}
|
||||
|
||||
// Enforce available balance for the effective period.
|
||||
if (deliverAmount > availableBalance)
|
||||
{
|
||||
JLOG(j_.trace()) << "SubscriptionClaim: Claim amount exceeds remaining "
|
||||
<< "balance for this period.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
// Perform the transfer
|
||||
if (isXRP(deliverAmount))
|
||||
{
|
||||
if (TER const ter{
|
||||
transferXRP(psb, account, dest, deliverAmount, viewJ)};
|
||||
ter != tesSUCCESS)
|
||||
{
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return doTransferTokenHelper<T>(
|
||||
psb,
|
||||
psb.peek(keylet::account(dest)),
|
||||
mPriorBalance,
|
||||
deliverAmount,
|
||||
deliverAmount.getIssuer(),
|
||||
account,
|
||||
dest,
|
||||
true, // create asset
|
||||
viewJ);
|
||||
},
|
||||
deliverAmount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Update balance and period pointer
|
||||
STAmount const newBalance = availableBalance - deliverAmount;
|
||||
|
||||
if (newBalance == sleAmount.zeroed())
|
||||
{
|
||||
// Full period claimed: advance exactly one period and reset next period
|
||||
// balance.
|
||||
nextClaimTime += frequency;
|
||||
sleSub->setFieldU32(sfNextClaimTime, nextClaimTime);
|
||||
sleSub->setFieldAmount(sfBalance, sleAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial claim within the same effective period.
|
||||
sleSub->setFieldAmount(sfBalance, newBalance);
|
||||
// Do not advance nextClaimTime; if we had a rollover-forfeit above,
|
||||
// we already moved nextClaimTime forward exactly once.
|
||||
}
|
||||
|
||||
psb.update(sleSub);
|
||||
|
||||
if (sleSub->isFieldPresent(sfExpiration) &&
|
||||
psb.info().parentCloseTime.time_since_epoch().count() >=
|
||||
sleSub->getFieldU32(sfExpiration))
|
||||
{
|
||||
psb.erase(sleSub);
|
||||
}
|
||||
|
||||
psb.apply(ctx_.rawView());
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/SubscriptionClaim.h
Normal file
48
src/xrpld/app/tx/detail/SubscriptionClaim.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SUBSCRIPTIONCLAIM_H_INCLUDED
|
||||
#define RIPPLE_TX_SUBSCRIPTIONCLAIM_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class SubscriptionClaim : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit SubscriptionClaim(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TX_SUBSCRIPTIONCLAIM_H_INCLUDED
|
||||
337
src/xrpld/app/tx/detail/SubscriptionSet.cpp
Normal file
337
src/xrpld/app/tx/detail/SubscriptionSet.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/misc/SubscriptionHelpers.h>
|
||||
#include <xrpld/app/paths/Flow.h>
|
||||
#include <xrpld/app/tx/detail/SubscriptionSet.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
template <ValidIssueType T>
|
||||
static NotTEC
|
||||
setPreflightHelper(PreflightContext const& ctx);
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
setPreflightHelper<Issue>(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
|
||||
setPreflightHelper<MPTIssue>(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
|
||||
SubscriptionSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureSubscription))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfSubscriptionID))
|
||||
{
|
||||
// update
|
||||
if (!ctx.tx.isFieldPresent(sfAmount))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Malformed transaction: SubscriptionID "
|
||||
"is present, but Amount is not.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfDestination) ||
|
||||
ctx.tx.isFieldPresent(sfFrequency) ||
|
||||
ctx.tx.isFieldPresent(sfStartTime))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Malformed transaction: SubscriptionID "
|
||||
"is present, but optional fields are also present.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// create
|
||||
if (!ctx.tx.isFieldPresent(sfDestination) ||
|
||||
!ctx.tx.isFieldPresent(sfAmount) ||
|
||||
!ctx.tx.isFieldPresent(sfFrequency))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Malformed transaction: SubscriptionID "
|
||||
"is not present, and required fields are not present.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (ctx.tx.getAccountID(sfDestination) ==
|
||||
ctx.tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Malformed transaction: Account "
|
||||
"is the same as the destination.";
|
||||
return temDST_IS_SRC;
|
||||
}
|
||||
}
|
||||
|
||||
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
|
||||
if (amount.native())
|
||||
{
|
||||
if (!isLegalNet(amount) || amount <= beast::zero)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Malformed transaction: bad amount: "
|
||||
<< amount.getFullText();
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return setPreflightHelper<T>(ctx);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SubscriptionSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
STAmount const amount = ctx.tx.getFieldAmount(sfAmount);
|
||||
AccountID const account = ctx.tx.getAccountID(sfAccount);
|
||||
AccountID const dest = ctx.tx.getAccountID(sfDestination);
|
||||
if (ctx.tx.isFieldPresent(sfSubscriptionID))
|
||||
{
|
||||
// update
|
||||
auto sle = ctx.view.read(
|
||||
keylet::subscription(ctx.tx.getFieldH256(sfSubscriptionID)));
|
||||
if (!sle)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Subscription does not exist.";
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
if (sle->getAccountID(sfAccount) != ctx.tx.getAccountID(sfAccount))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SubscriptionSet: Account is not the "
|
||||
"owner of the subscription.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// create
|
||||
auto const sleDest =
|
||||
ctx.view.read(keylet::account(ctx.tx.getAccountID(sfDestination)));
|
||||
if (!sleDest)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: Destination account does not exist.";
|
||||
return tecNO_DST;
|
||||
}
|
||||
|
||||
auto const flags = sleDest->getFlags();
|
||||
if ((flags & lsfRequireDestTag) && !ctx.tx[~sfDestinationTag])
|
||||
return tecDST_TAG_NEEDED;
|
||||
|
||||
if (ctx.tx.getFieldU32(sfFrequency) <= 0)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "SubscriptionSet: The frequency is less than or equal to 0.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return canTransferTokenHelper<T>(
|
||||
ctx.view, account, dest, amount, ctx.j);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SubscriptionSet::doApply()
|
||||
{
|
||||
Sandbox sb(&ctx_.view());
|
||||
|
||||
AccountID const account = ctx_.tx.getAccountID(sfAccount);
|
||||
auto const sleAccount = sb.peek(keylet::account(account));
|
||||
if (!sleAccount)
|
||||
{
|
||||
JLOG(ctx_.journal.trace())
|
||||
<< "SubscriptionSet: Account does not exist.";
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfSubscriptionID))
|
||||
{
|
||||
// update
|
||||
auto sle = sb.peek(
|
||||
keylet::subscription(ctx_.tx.getFieldH256(sfSubscriptionID)));
|
||||
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
|
||||
if (ctx_.tx.isFieldPresent(sfExpiration))
|
||||
{
|
||||
auto const currentTime =
|
||||
sb.info().parentCloseTime.time_since_epoch().count();
|
||||
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
|
||||
|
||||
if (expiration < currentTime)
|
||||
{
|
||||
JLOG(ctx_.journal.trace())
|
||||
<< "SubscriptionSet: The expiration time is in the past.";
|
||||
return temBAD_EXPIRATION;
|
||||
}
|
||||
|
||||
sle->setFieldU32(sfExpiration, ctx_.tx.getFieldU32(sfExpiration));
|
||||
}
|
||||
|
||||
sb.update(sle);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const currentTime =
|
||||
sb.info().parentCloseTime.time_since_epoch().count();
|
||||
auto startTime = currentTime;
|
||||
auto nextClaimTime = currentTime;
|
||||
|
||||
// create
|
||||
{
|
||||
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
|
||||
auto const reserve =
|
||||
sb.fees().accountReserve((*sleAccount)[sfOwnerCount] + 1);
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
AccountID const dest = ctx_.tx.getAccountID(sfDestination);
|
||||
Keylet const subKeylet =
|
||||
keylet::subscription(account, dest, ctx_.tx.getSeqProxy().value());
|
||||
auto sle = std::make_shared<SLE>(subKeylet);
|
||||
sle->setAccountID(sfAccount, account);
|
||||
sle->setAccountID(sfDestination, dest);
|
||||
if (ctx_.tx.isFieldPresent(sfDestinationTag))
|
||||
sle->setFieldU32(
|
||||
sfDestinationTag, ctx_.tx.getFieldU32(sfDestinationTag));
|
||||
sle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount));
|
||||
sle->setFieldAmount(sfBalance, ctx_.tx.getFieldAmount(sfAmount));
|
||||
sle->setFieldU32(sfFrequency, ctx_.tx.getFieldU32(sfFrequency));
|
||||
if (ctx_.tx.isFieldPresent(sfStartTime))
|
||||
{
|
||||
startTime = ctx_.tx.getFieldU32(sfStartTime);
|
||||
nextClaimTime = startTime;
|
||||
if (startTime < currentTime)
|
||||
{
|
||||
JLOG(ctx_.journal.trace())
|
||||
<< "SubscriptionSet: The start time is in the past.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
sle->setFieldU32(sfNextClaimTime, nextClaimTime);
|
||||
if (ctx_.tx.isFieldPresent(sfExpiration))
|
||||
{
|
||||
auto const expiration = ctx_.tx.getFieldU32(sfExpiration);
|
||||
|
||||
if (expiration < currentTime)
|
||||
{
|
||||
JLOG(ctx_.journal.trace())
|
||||
<< "SubscriptionSet: The expiration time is in the past.";
|
||||
return temBAD_EXPIRATION;
|
||||
}
|
||||
|
||||
if (expiration < nextClaimTime)
|
||||
{
|
||||
JLOG(ctx_.journal.trace())
|
||||
<< "SubscriptionSet: The expiration time is "
|
||||
"less than the next claim time.";
|
||||
return temBAD_EXPIRATION;
|
||||
}
|
||||
sle->setFieldU32(sfExpiration, expiration);
|
||||
}
|
||||
|
||||
{
|
||||
auto page = sb.dirInsert(
|
||||
keylet::ownerDir(account),
|
||||
subKeylet,
|
||||
describeOwnerDir(account));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
(*sle)[sfOwnerNode] = *page;
|
||||
}
|
||||
|
||||
{
|
||||
auto page = sb.dirInsert(
|
||||
keylet::ownerDir(dest), subKeylet, describeOwnerDir(dest));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
(*sle)[sfDestinationNode] = *page;
|
||||
}
|
||||
|
||||
adjustOwnerCount(sb, sleAccount, 1, ctx_.journal);
|
||||
sb.insert(sle);
|
||||
}
|
||||
sb.apply(ctx_.rawView());
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/SubscriptionSet.h
Normal file
48
src/xrpld/app/tx/detail/SubscriptionSet.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SUBSCRIPTIONSET_H_INCLUDED
|
||||
#define RIPPLE_TX_SUBSCRIPTIONSET_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class SubscriptionSet : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit SubscriptionSet(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TX_SUBSCRIPTIONSET_H_INCLUDED
|
||||
@@ -62,6 +62,9 @@
|
||||
#include <xrpld/app/tx/detail/SetRegularKey.h>
|
||||
#include <xrpld/app/tx/detail/SetSignerList.h>
|
||||
#include <xrpld/app/tx/detail/SetTrust.h>
|
||||
#include <xrpld/app/tx/detail/SubscriptionCancel.h>
|
||||
#include <xrpld/app/tx/detail/SubscriptionClaim.h>
|
||||
#include <xrpld/app/tx/detail/SubscriptionSet.h>
|
||||
#include <xrpld/app/tx/detail/VaultClawback.h>
|
||||
#include <xrpld/app/tx/detail/VaultCreate.h>
|
||||
#include <xrpld/app/tx/detail/VaultDelete.h>
|
||||
|
||||
@@ -681,6 +681,32 @@ parseXChainOwnedCreateAccountClaimID(
|
||||
return keylet.key;
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseSubscription(Json::Value const& params, Json::StaticString const fieldName)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
return parseObjectID(params, fieldName);
|
||||
}
|
||||
|
||||
auto const account = LedgerEntryHelpers::requiredAccountID(
|
||||
params, jss::account, "malformedAccount");
|
||||
if (!account)
|
||||
return Unexpected(account.error());
|
||||
|
||||
auto const destination = LedgerEntryHelpers::requiredAccountID(
|
||||
params, jss::destination, "malformedDestination");
|
||||
if (!destination)
|
||||
return Unexpected(destination.error());
|
||||
|
||||
auto const seq = LedgerEntryHelpers::requiredUInt32(
|
||||
params, jss::seq, "malformedRequest");
|
||||
if (!seq)
|
||||
return Unexpected(seq.error());
|
||||
|
||||
return keylet::subscription(*account, *destination, *seq).key;
|
||||
}
|
||||
|
||||
using FunctionType = Expected<uint256, Json::Value> (*)(
|
||||
Json::Value const&,
|
||||
Json::StaticString const);
|
||||
|
||||
Reference in New Issue
Block a user