Compare commits

...

1 Commits

Author SHA1 Message Date
Denis Angell
dc8a689a20 feature-subscriptions 2025-09-10 11:03:22 +02:00
22 changed files with 4715 additions and 1 deletions

View File

@@ -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:

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View 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

View 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

View 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

View File

@@ -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 &&

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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>

View File

@@ -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);