mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +00:00
Compare commits
1 Commits
ripple/wam
...
dangell7/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc8a689a20 |
@@ -349,6 +349,19 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
|
|||||||
|
|
||||||
Keylet
|
Keylet
|
||||||
permissionedDomain(uint256 const& domainID) noexcept;
|
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
|
} // namespace keylet
|
||||||
|
|
||||||
// Everything below is deprecated and should be removed in favor of keylets:
|
// Everything below is deprecated and should be removed in favor of keylets:
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ enum LedgerEntryType : std::uint16_t
|
|||||||
|
|
||||||
#undef LEDGER_ENTRY
|
#undef LEDGER_ENTRY
|
||||||
#pragma pop_macro("LEDGER_ENTRY")
|
#pragma pop_macro("LEDGER_ENTRY")
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
/** A special type, matching any ledger entry type.
|
/** 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`
|
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||||
// in include/xrpl/protocol/Feature.h.
|
// in include/xrpl/protocol/Feature.h.
|
||||||
|
|
||||||
|
XRPL_FEATURE(Subscription, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)
|
XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
|
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (AMMClawbackRounding, Supported::yes, 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)
|
// 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 EXPAND
|
||||||
#undef LEDGER_ENTRY_DUPLICATE
|
#undef LEDGER_ENTRY_DUPLICATE
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48)
|
|||||||
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
||||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||||
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
||||||
|
TYPED_SFIELD(sfFrequency, UINT32, 53)
|
||||||
|
TYPED_SFIELD(sfStartTime, UINT32, 54)
|
||||||
|
TYPED_SFIELD(sfNextClaimTime, UINT32, 55)
|
||||||
|
|
||||||
// 64-bit integers (common)
|
// 64-bit integers (common)
|
||||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||||
@@ -197,6 +200,7 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
|
|||||||
TYPED_SFIELD(sfDomainID, UINT256, 34)
|
TYPED_SFIELD(sfDomainID, UINT256, 34)
|
||||||
TYPED_SFIELD(sfVaultID, UINT256, 35)
|
TYPED_SFIELD(sfVaultID, UINT256, 35)
|
||||||
TYPED_SFIELD(sfParentBatchID, UINT256, 36)
|
TYPED_SFIELD(sfParentBatchID, UINT256, 36)
|
||||||
|
TYPED_SFIELD(sfSubscriptionID, UINT256, 37)
|
||||||
|
|
||||||
// number (common)
|
// number (common)
|
||||||
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
TYPED_SFIELD(sfNumber, NUMBER, 1)
|
||||||
|
|||||||
@@ -526,6 +526,28 @@ TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({
|
|||||||
{sfBatchSigners, soeOPTIONAL},
|
{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.
|
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||||
|
|
||||||
For details, see: https://xrpl.org/amendments.html
|
For details, see: https://xrpl.org/amendments.html
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ JSS(Signer); // field.
|
|||||||
JSS(Signers); // field.
|
JSS(Signers); // field.
|
||||||
JSS(SigningPubKey); // field.
|
JSS(SigningPubKey); // field.
|
||||||
JSS(Subject); // in: Credential transactions
|
JSS(Subject); // in: Credential transactions
|
||||||
|
JSS(SubscriptionID); // in: Subscription transactions
|
||||||
JSS(TakerGets); // field.
|
JSS(TakerGets); // field.
|
||||||
JSS(TakerPays); // field.
|
JSS(TakerPays); // field.
|
||||||
JSS(TradingFee); // in/out: AMM trading fee
|
JSS(TradingFee); // in/out: AMM trading fee
|
||||||
@@ -283,6 +284,7 @@ JSS(fee_mult_max); // in: TransactionSign
|
|||||||
JSS(fee_ref); // out: NetworkOPs, DEPRECATED
|
JSS(fee_ref); // out: NetworkOPs, DEPRECATED
|
||||||
JSS(fetch_pack); // out: NetworkOPs
|
JSS(fetch_pack); // out: NetworkOPs
|
||||||
JSS(FIELDS); // out: RPC server_definitions
|
JSS(FIELDS); // out: RPC server_definitions
|
||||||
|
JSS(Frequency); // in: Subscription transactions
|
||||||
// matches definitions.json format
|
// matches definitions.json format
|
||||||
JSS(first); // out: rpc/Version
|
JSS(first); // out: rpc/Version
|
||||||
JSS(finished);
|
JSS(finished);
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
|||||||
PERMISSIONED_DOMAIN = 'm',
|
PERMISSIONED_DOMAIN = 'm',
|
||||||
DELEGATE = 'E',
|
DELEGATE = 'E',
|
||||||
VAULT = 'V',
|
VAULT = 'V',
|
||||||
|
SUBSCRIPTION = 'U',
|
||||||
|
|
||||||
// No longer used or supported. Left here to reserve the space
|
// No longer used or supported. Left here to reserve the space
|
||||||
// to avoid accidental reuse.
|
// to avoid accidental reuse.
|
||||||
@@ -580,6 +581,17 @@ permissionedDomain(uint256 const& domainID) noexcept
|
|||||||
return {ltPERMISSIONED_DOMAIN, domainID};
|
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 keylet
|
||||||
|
|
||||||
} // namespace ripple
|
} // 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/sendmax.h>
|
||||||
#include <test/jtx/seq.h>
|
#include <test/jtx/seq.h>
|
||||||
#include <test/jtx/sig.h>
|
#include <test/jtx/sig.h>
|
||||||
|
#include <test/jtx/subscription.h>
|
||||||
#include <test/jtx/tag.h>
|
#include <test/jtx/tag.h>
|
||||||
#include <test/jtx/tags.h>
|
#include <test/jtx/tags.h>
|
||||||
#include <test/jtx/ter.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 ltCREDENTIAL:
|
||||||
case ltPERMISSIONED_DOMAIN:
|
case ltPERMISSIONED_DOMAIN:
|
||||||
case ltVAULT:
|
case ltVAULT:
|
||||||
|
case ltSUBSCRIPTION:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
invalidTypeAdded_ = true;
|
invalidTypeAdded_ = true;
|
||||||
@@ -1511,6 +1512,9 @@ ValidMPTIssuance::finalize(
|
|||||||
if (tx.getTxnType() == ttESCROW_FINISH)
|
if (tx.getTxnType() == ttESCROW_FINISH)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (tx.getTxnType() == ttSUBSCRIPTION_CLAIM)
|
||||||
|
return true;
|
||||||
|
|
||||||
if ((tx.getTxnType() == ttVAULT_CLAWBACK ||
|
if ((tx.getTxnType() == ttVAULT_CLAWBACK ||
|
||||||
tx.getTxnType() == ttVAULT_WITHDRAW) &&
|
tx.getTxnType() == ttVAULT_WITHDRAW) &&
|
||||||
mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
|
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/SetRegularKey.h>
|
||||||
#include <xrpld/app/tx/detail/SetSignerList.h>
|
#include <xrpld/app/tx/detail/SetSignerList.h>
|
||||||
#include <xrpld/app/tx/detail/SetTrust.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/VaultClawback.h>
|
||||||
#include <xrpld/app/tx/detail/VaultCreate.h>
|
#include <xrpld/app/tx/detail/VaultCreate.h>
|
||||||
#include <xrpld/app/tx/detail/VaultDelete.h>
|
#include <xrpld/app/tx/detail/VaultDelete.h>
|
||||||
|
|||||||
@@ -681,6 +681,32 @@ parseXChainOwnedCreateAccountClaimID(
|
|||||||
return keylet.key;
|
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> (*)(
|
using FunctionType = Expected<uint256, Json::Value> (*)(
|
||||||
Json::Value const&,
|
Json::Value const&,
|
||||||
Json::StaticString const);
|
Json::StaticString const);
|
||||||
|
|||||||
Reference in New Issue
Block a user