mirror of
				https://github.com/XRPLF/rippled.git
				synced 2025-11-04 11:15:56 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			aaa2210b69
			...
			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