mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-24 12:35:50 +00:00
Compare commits
24 Commits
nd-use-git
...
cronjob
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf3b38fad | ||
|
|
86a9ea999f | ||
|
|
0755fb186a | ||
|
|
526154b97a | ||
|
|
052135a800 | ||
|
|
793249d031 | ||
|
|
c1e011a16a | ||
|
|
caa486f19c | ||
|
|
106abfa9e0 | ||
|
|
88828bbf63 | ||
|
|
cf3db6eb42 | ||
|
|
9b1f3ebdd7 | ||
|
|
e64692fc8b | ||
|
|
ac1bf88596 | ||
|
|
0d7dd0597d | ||
|
|
3dddb907c2 | ||
|
|
9d6ea9ac60 | ||
|
|
e9a414cff2 | ||
|
|
fe1b424bea | ||
|
|
b1c366761f | ||
|
|
82af6d9eee | ||
|
|
20e6e62660 | ||
|
|
1d6066127c | ||
|
|
d3d5c757fe |
@@ -457,6 +457,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CreateCheck.cpp
|
||||
src/ripple/app/tx/impl/CreateOffer.cpp
|
||||
src/ripple/app/tx/impl/CreateTicket.cpp
|
||||
src/ripple/app/tx/impl/Cron.cpp
|
||||
src/ripple/app/tx/impl/DeleteAccount.cpp
|
||||
src/ripple/app/tx/impl/DepositPreauth.cpp
|
||||
src/ripple/app/tx/impl/Escrow.cpp
|
||||
@@ -474,6 +475,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/Payment.cpp
|
||||
src/ripple/app/tx/impl/Remit.cpp
|
||||
src/ripple/app/tx/impl/SetAccount.cpp
|
||||
src/ripple/app/tx/impl/SetCron.cpp
|
||||
src/ripple/app/tx/impl/SetHook.cpp
|
||||
src/ripple/app/tx/impl/SetRemarks.cpp
|
||||
src/ripple/app/tx/impl/SetRegularKey.cpp
|
||||
@@ -734,6 +736,7 @@ if (tests)
|
||||
src/test/app/BaseFee_test.cpp
|
||||
src/test/app/Check_test.cpp
|
||||
src/test/app/ClaimReward_test.cpp
|
||||
src/test/app/Cron_test.cpp
|
||||
src/test/app/Clawback_test.cpp
|
||||
src/test/app/CrossingLimits_test.cpp
|
||||
src/test/app/DeliverMin_test.cpp
|
||||
@@ -898,6 +901,7 @@ if (tests)
|
||||
src/test/jtx/impl/amount.cpp
|
||||
src/test/jtx/impl/balance.cpp
|
||||
src/test/jtx/impl/check.cpp
|
||||
src/test/jtx/impl/cron.cpp
|
||||
src/test/jtx/impl/delivermin.cpp
|
||||
src/test/jtx/impl/deposit.cpp
|
||||
src/test/jtx/impl/envconfig.cpp
|
||||
|
||||
@@ -75,6 +75,11 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
|
||||
switch (tt)
|
||||
{
|
||||
case ttCRON: {
|
||||
ADD_TSH(tx.getAccountID(sfOwner), tshWEAK);
|
||||
break;
|
||||
}
|
||||
|
||||
case ttREMIT: {
|
||||
if (destAcc)
|
||||
ADD_TSH(*destAcc, tshSTRONG);
|
||||
|
||||
@@ -1477,6 +1477,64 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject cron transactions, if any
|
||||
if (view.rules().enabled(featureCron))
|
||||
{
|
||||
uint32_t currentTime =
|
||||
view.parentCloseTime().time_since_epoch().count();
|
||||
uint256 klStart = keylet::cron(0, AccountID(beast::zero)).key;
|
||||
uint256 const klEnd =
|
||||
keylet::cron(currentTime + 1, AccountID(beast::zero)).key;
|
||||
|
||||
std::set<AccountID> cronAccs;
|
||||
|
||||
auto counter = 0;
|
||||
// include max 128 cron txns in the ledger
|
||||
while (++counter < 129 && klStart < klEnd)
|
||||
{
|
||||
std::optional<uint256 const> next = view.succ(klStart, klEnd);
|
||||
if (!next.has_value())
|
||||
break;
|
||||
|
||||
Keylet kl{ltANY, *next};
|
||||
|
||||
if (view.exists(kl))
|
||||
{
|
||||
auto sle = view.read(kl);
|
||||
if (safe_cast<LedgerEntryType>(
|
||||
sle->getFieldU16(sfLedgerEntryType)) == ltCRON)
|
||||
{
|
||||
// valid cron object, add it to the list
|
||||
cronAccs.emplace(sle->getAccountID(sfOwner));
|
||||
}
|
||||
}
|
||||
|
||||
klStart = *next;
|
||||
}
|
||||
|
||||
auto const seq = view.info().seq;
|
||||
|
||||
// insert Cron pseudos for each of the accs we need to ping
|
||||
for (AccountID const& id : cronAccs)
|
||||
{
|
||||
STTx cronTx(ttCRON, [=](auto& obj) {
|
||||
obj[sfAccount] = AccountID();
|
||||
obj[sfLedgerSequence] = seq;
|
||||
obj[sfOwner] = id;
|
||||
});
|
||||
|
||||
uint256 txID = cronTx.getTransactionID();
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
cronTx.add(*s);
|
||||
|
||||
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
app.getHashRouter().setFlags(txID, SF_EMITTED);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
ledgerChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject emitted transactions if any
|
||||
if (view.rules().enabled(featureHooks))
|
||||
do
|
||||
|
||||
188
src/ripple/app/tx/impl/Cron.cpp
Normal file
188
src/ripple/app/tx/impl/Cron.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
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 <ripple/app/tx/impl/Cron.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <ripple/protocol/Quality.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
Cron::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Cron::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureCron))
|
||||
return temDISABLED;
|
||||
|
||||
auto const ret = preflight0(ctx);
|
||||
if (!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto account = ctx.tx.getAccountID(sfAccount);
|
||||
if (account != beast::zero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: Bad source id";
|
||||
return temBAD_SRC_ACCOUNT;
|
||||
}
|
||||
|
||||
// No point in going any further if the transaction fee is malformed.
|
||||
auto const fee = ctx.tx.getFieldAmount(sfFee);
|
||||
if (!fee.native() || fee != beast::zero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: invalid fee";
|
||||
return temBAD_FEE;
|
||||
}
|
||||
|
||||
if (!ctx.tx.getSigningPubKey().empty() || !ctx.tx.getSignature().empty() ||
|
||||
ctx.tx.isFieldPresent(sfSigners))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: Bad signature";
|
||||
return temBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
if (ctx.tx.getFieldU32(sfSequence) != 0 ||
|
||||
ctx.tx.isFieldPresent(sfPreviousTxnID))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Cron: Bad sequence";
|
||||
return temBAD_SEQUENCE;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Cron::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureCron))
|
||||
return temDISABLED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Cron::doApply()
|
||||
{
|
||||
auto& view = ctx_.view();
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
AccountID const& id = tx.getAccountID(sfOwner);
|
||||
|
||||
auto sle = view.peek(keylet::account(id));
|
||||
if (!sle)
|
||||
{
|
||||
// should never happen... but might in a race condition with acc delete
|
||||
JLOG(j_.warn()) << "Cron: sfOwner account missing. " << id;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (!sle->isFieldPresent(sfCron))
|
||||
{
|
||||
JLOG(j_.warn()) << "Cron: sfCron missing from account " << id;
|
||||
return tefINTERNAL;
|
||||
}
|
||||
|
||||
uint256 ptr = sle->getFieldH256(sfCron);
|
||||
Keylet klOld{ltCRON, ptr};
|
||||
auto sleCron = view.peek(klOld);
|
||||
if (!sleCron)
|
||||
{
|
||||
JLOG(j_.warn()) << "Cron: Cron object missing for account " << id;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
uint32_t delay = sleCron->getFieldU32(sfDelaySeconds);
|
||||
uint32_t recur = sleCron->getFieldU32(sfRepeatCount);
|
||||
|
||||
uint32_t currentTime = view.parentCloseTime().time_since_epoch().count();
|
||||
|
||||
// do all this sanity checking before we modify the ledger...
|
||||
uint32_t afterTime = currentTime + delay;
|
||||
if (afterTime < currentTime)
|
||||
return tefINTERNAL;
|
||||
|
||||
// in all circumstances the Cron object is deleted...
|
||||
// if there are further crons to do then a new one is created at the next
|
||||
// time point
|
||||
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(id), (*sleCron)[sfOwnerNode], klOld, false))
|
||||
return tefBAD_LEDGER;
|
||||
|
||||
view.erase(sleCron);
|
||||
|
||||
if (recur == 0)
|
||||
{
|
||||
// already at last execution, stop here
|
||||
adjustOwnerCount(view, sle, -1, j_);
|
||||
sle->makeFieldAbsent(sfCron);
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// more executions remain, so create a new object
|
||||
|
||||
Keylet klCron = keylet::cron(afterTime, id);
|
||||
|
||||
// insert into owner dir, we don't need to check reserve because we've just
|
||||
// deleted an object
|
||||
auto const page =
|
||||
view.dirInsert(keylet::ownerDir(id), klCron, describeOwnerDir(id));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
|
||||
sleCron = std::make_shared<SLE>(klCron);
|
||||
|
||||
sleCron->setFieldU64(sfOwnerNode, *page);
|
||||
sleCron->setFieldU32(sfDelaySeconds, delay);
|
||||
sleCron->setFieldU32(sfRepeatCount, recur - 1);
|
||||
sleCron->setAccountID(sfOwner, id);
|
||||
|
||||
sle->setFieldH256(sfCron, klCron.key);
|
||||
|
||||
view.insert(sleCron);
|
||||
view.update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
Cron::preCompute()
|
||||
{
|
||||
assert(account_ == beast::zero);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
Cron::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
return XRPAmount{0};
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
60
src/ripple/app/tx/impl/Cron.h
Normal file
60
src/ripple/app/tx/impl/Cron.h
Normal file
@@ -0,0 +1,60 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
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_CRON_H_INCLUDED
|
||||
#define RIPPLE_TX_CRON_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Cron : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const&);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
preCompute() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -491,6 +491,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltNFTOKEN_PAGE:
|
||||
case ltNFTOKEN_OFFER:
|
||||
case ltURI_TOKEN:
|
||||
case ltCRON:
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
break;
|
||||
|
||||
317
src/ripple/app/tx/impl/SetCron.cpp
Normal file
317
src/ripple/app/tx/impl/SetCron.cpp
Normal file
@@ -0,0 +1,317 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
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 <ripple/app/tx/impl/SetCron.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
SetCron::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetCron::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureCron))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto& tx = ctx.tx;
|
||||
auto& j = ctx.j;
|
||||
|
||||
if (tx.getFlags() & tfCronSetMask)
|
||||
{
|
||||
JLOG(j.warn()) << "SetCron: Invalid flags set.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// StartAfter(s) DelaySeconds (D), RepeatCount (R)
|
||||
|
||||
// SDR - Set Cron with After, Delay and Repeat
|
||||
// SD- - Invalid, if repeat count isn't included then only start or delay
|
||||
// S-R - Invalid
|
||||
// S-- - Set Cron with After for a once off execution
|
||||
|
||||
// -DR - Set Cron with Delay and Repeat
|
||||
// -D- - Set Cron (once off) with Delay only (repat implicitly 0)
|
||||
// --R - Invalid
|
||||
// --- - Clear any existing cron (succeeds even if there isn't one) / with
|
||||
// tfCronUnset flag set
|
||||
|
||||
bool const hasStart = tx.isFieldPresent(sfStartAfter);
|
||||
bool const hasDelay = tx.isFieldPresent(sfDelaySeconds);
|
||||
bool const hasRepeat = tx.isFieldPresent(sfRepeatCount);
|
||||
|
||||
// unset is a special case, handle first
|
||||
if (tx.isFlag(tfCronUnset))
|
||||
{
|
||||
if (hasDelay || hasRepeat || hasStart)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: tfCronUnset flag cannot be used with "
|
||||
"DelaySeconds or RepeatCount.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
if (hasStart)
|
||||
{
|
||||
if (hasRepeat && hasDelay)
|
||||
{
|
||||
// valid, this is a fully specified cron
|
||||
// fall through to validate other fields
|
||||
}
|
||||
else if (!hasRepeat && !hasDelay)
|
||||
{
|
||||
// valid this is a once off cron
|
||||
// no other fields to validate, done
|
||||
return preflight2(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// invalid, must specify both or neither repeat and delay count with
|
||||
// startafter
|
||||
JLOG(j.debug()) << "SetCron: StartAfter can only be used with "
|
||||
"either both or neither of "
|
||||
"DelaySeconds and RepeatCount.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDelay)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: DelaySeconds or StartAfter must be "
|
||||
"specified to create a cron.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// check delay is not too high
|
||||
auto delay = tx.getFieldU32(sfDelaySeconds);
|
||||
if (delay > 31536000UL /* 365 days in seconds */)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: DelaySeconds was too high. (max 365 "
|
||||
"days in seconds).";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// check repeat is not too high
|
||||
if (hasRepeat)
|
||||
{
|
||||
auto recur = tx.getFieldU32(sfRepeatCount);
|
||||
if (recur > 256)
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "SetCron: RepeatCount too high. Limit is 256. Issue "
|
||||
"new SetCron to increase.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SetCron::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfStartAfter))
|
||||
{
|
||||
uint32_t currentTime =
|
||||
ctx.view.parentCloseTime().time_since_epoch().count();
|
||||
uint32_t afterTime = ctx.tx.getFieldU32(sfStartAfter);
|
||||
|
||||
if (afterTime <= currentTime)
|
||||
{
|
||||
// we'll pass this as though they meant execute asap, similar to a
|
||||
// delay of 0
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
uint32_t waitSeconds = afterTime - currentTime;
|
||||
|
||||
if (waitSeconds > afterTime)
|
||||
return tefINTERNAL;
|
||||
|
||||
if (waitSeconds >> 31536000UL /* 365 days in seconds */)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "SetCron: DelaySeconds was too high. (max 365 "
|
||||
"days in seconds).";
|
||||
return tecSTART_AFTER_TOO_HIGH;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SetCron::doApply()
|
||||
{
|
||||
auto& view = ctx_.view();
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
bool const isDelete = tx.isFlag(tfCronUnset);
|
||||
|
||||
// delay can be zero, in which case the cron will usually execute next
|
||||
// ledger.
|
||||
uint32_t delay{0};
|
||||
uint32_t recur{0};
|
||||
uint32_t after{0};
|
||||
|
||||
if (!isDelete)
|
||||
{
|
||||
if (tx.isFieldPresent(sfDelaySeconds))
|
||||
delay = tx.getFieldU32(sfDelaySeconds);
|
||||
if (tx.isFieldPresent(sfRepeatCount))
|
||||
recur = tx.getFieldU32(sfRepeatCount);
|
||||
}
|
||||
|
||||
uint32_t currentTime = view.parentCloseTime().time_since_epoch().count();
|
||||
|
||||
// do all this sanity checking before we modify the ledger...
|
||||
// even for a delete operation this will fall through without incident
|
||||
|
||||
uint32_t afterTime = tx.isFieldPresent(sfStartAfter)
|
||||
? tx.getFieldU32(sfStartAfter)
|
||||
: currentTime + delay;
|
||||
|
||||
if (afterTime < currentTime)
|
||||
return tefINTERNAL;
|
||||
|
||||
AccountID const& id = tx.getAccountID(sfAccount);
|
||||
auto sle = view.peek(keylet::account(id));
|
||||
if (!sle)
|
||||
return tefINTERNAL;
|
||||
|
||||
// in all cases whatsoever, this transaction will delete an existing
|
||||
// old cron object and return the owner reserve to the owner.
|
||||
|
||||
if (sle->isFieldPresent(sfCron))
|
||||
{
|
||||
Keylet klOld{ltCRON, sle->getFieldH256(sfCron)};
|
||||
|
||||
auto sleCron = view.peek(klOld);
|
||||
if (!sleCron)
|
||||
{
|
||||
JLOG(j_.warn()) << "SetCron: Cron object didn't exist.";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
if (safe_cast<LedgerEntryType>(
|
||||
sleCron->getFieldU16(sfLedgerEntryType)) != ltCRON)
|
||||
{
|
||||
JLOG(j_.warn()) << "SetCron: sfCron pointed to non-cron object!!";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(id), (*sleCron)[sfOwnerNode], klOld, false))
|
||||
{
|
||||
JLOG(j_.warn()) << "SetCron: Ownerdir bad. " << id;
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
view.erase(sleCron);
|
||||
adjustOwnerCount(view, sle, -1, j_);
|
||||
sle->makeFieldAbsent(sfCron);
|
||||
}
|
||||
|
||||
// if the operation is a delete (no delay or recur specified then stop
|
||||
// here.)
|
||||
if (isDelete)
|
||||
{
|
||||
view.update(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// execution to here means we're creating a new Cron object and adding it to
|
||||
// the user's owner dir
|
||||
|
||||
Keylet klCron = keylet::cron(afterTime, id);
|
||||
|
||||
std::shared_ptr<SLE> sleCron = std::make_shared<SLE>(klCron);
|
||||
|
||||
STAmount const reserve{
|
||||
view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)};
|
||||
|
||||
STAmount const afterFee =
|
||||
mPriorBalance - ctx_.tx.getFieldAmount(sfFee).xrp();
|
||||
|
||||
if (afterFee > mPriorBalance || afterFee < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
// add to owner dir
|
||||
auto const page =
|
||||
view.dirInsert(keylet::ownerDir(id), klCron, describeOwnerDir(id));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
|
||||
sleCron->setFieldU64(sfOwnerNode, *page);
|
||||
|
||||
adjustOwnerCount(view, sle, 1, j_);
|
||||
|
||||
// set the fields
|
||||
sleCron->setFieldU32(sfDelaySeconds, delay);
|
||||
sleCron->setFieldU32(sfRepeatCount, recur);
|
||||
sleCron->setAccountID(sfOwner, id);
|
||||
|
||||
sle->setFieldH256(sfCron, klCron.key);
|
||||
|
||||
view.update(sle);
|
||||
|
||||
view.insert(sleCron);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
SetCron::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const baseFee = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
auto const hasRepeat = tx.isFieldPresent(sfRepeatCount);
|
||||
|
||||
if (tx.isFlag(tfCronUnset))
|
||||
// delete cron
|
||||
return baseFee;
|
||||
|
||||
// factor a cost based on the total number of txns expected
|
||||
// for RepeatCount of 0 we have this txn (SetCron) and the
|
||||
// single Cron txn (2). For a RepeatCount of 1 we have this txn,
|
||||
// the first time the cron executes, and the second time (3).
|
||||
uint32_t const additionalExpectedExecutions =
|
||||
hasRepeat ? tx.getFieldU32(sfRepeatCount) + 1 : 1;
|
||||
auto const additionalFee = baseFee * additionalExpectedExecutions;
|
||||
|
||||
if (baseFee + additionalFee < baseFee)
|
||||
return baseFee;
|
||||
|
||||
return baseFee + additionalFee;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
56
src/ripple/app/tx/impl/SetCron.h
Normal file
56
src/ripple/app/tx/impl/SetCron.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
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_SETCRON_H_INCLUDED
|
||||
#define RIPPLE_TX_SETCRON_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class SetCron : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit SetCron(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const&);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <ripple/app/tx/impl/CreateCheck.h>
|
||||
#include <ripple/app/tx/impl/CreateOffer.h>
|
||||
#include <ripple/app/tx/impl/CreateTicket.h>
|
||||
#include <ripple/app/tx/impl/Cron.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/Escrow.h>
|
||||
@@ -43,6 +44,7 @@
|
||||
#include <ripple/app/tx/impl/Payment.h>
|
||||
#include <ripple/app/tx/impl/Remit.h>
|
||||
#include <ripple/app/tx/impl/SetAccount.h>
|
||||
#include <ripple/app/tx/impl/SetCron.h>
|
||||
#include <ripple/app/tx/impl/SetHook.h>
|
||||
#include <ripple/app/tx/impl/SetRegularKey.h>
|
||||
#include <ripple/app/tx/impl/SetRemarks.h>
|
||||
@@ -181,6 +183,10 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return invoke_preflight_helper<URIToken>(ctx);
|
||||
case ttCRON_SET:
|
||||
return invoke_preflight_helper<SetCron>(ctx);
|
||||
case ttCRON:
|
||||
return invoke_preflight_helper<Cron>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
|
||||
@@ -306,6 +312,10 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return invoke_preclaim<URIToken>(ctx);
|
||||
case ttCRON_SET:
|
||||
return invoke_preclaim<SetCron>(ctx);
|
||||
case ttCRON:
|
||||
return invoke_preclaim<Cron>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return temUNKNOWN;
|
||||
@@ -393,6 +403,10 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return URIToken::calculateBaseFee(view, tx);
|
||||
case ttCRON_SET:
|
||||
return SetCron::calculateBaseFee(view, tx);
|
||||
case ttCRON:
|
||||
return Cron::calculateBaseFee(view, tx);
|
||||
default:
|
||||
return XRPAmount{0};
|
||||
}
|
||||
@@ -586,6 +600,14 @@ invoke_apply(ApplyContext& ctx)
|
||||
URIToken p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCRON_SET: {
|
||||
SetCron p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCRON: {
|
||||
Cron p(ctx);
|
||||
return p();
|
||||
}
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, false};
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 86;
|
||||
static constexpr std::size_t numFeatures = 87;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -373,6 +373,7 @@ extern uint256 const fixProvisionalDoubleThreading;
|
||||
extern uint256 const featureClawback;
|
||||
extern uint256 const featureDeepFreeze;
|
||||
extern uint256 const featureIOUIssuerWeakTSH;
|
||||
extern uint256 const featureCron;
|
||||
extern uint256 const fixInvalidTxFlags;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -297,6 +297,9 @@ import_vlseq(PublicKey const& key) noexcept;
|
||||
Keylet
|
||||
uritoken(AccountID const& issuer, Blob const& uri);
|
||||
|
||||
Keylet
|
||||
cron(uint32_t timestamp, AccountID const& id);
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -58,6 +58,12 @@ enum LedgerEntryType : std::uint16_t
|
||||
*/
|
||||
ltACCOUNT_ROOT = 0x0061,
|
||||
|
||||
/** A ledger object representing a scheduled cron execution on an account.
|
||||
|
||||
\sa keylet::cron
|
||||
*/
|
||||
ltCRON = 0x0041,
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
|
||||
\sa keylet::page, keylet::quality, keylet::book, keylet::next and
|
||||
|
||||
@@ -410,6 +410,9 @@ extern SF_UINT32 const sfRewardLgrLast;
|
||||
extern SF_UINT32 const sfFirstNFTokenSequence;
|
||||
extern SF_UINT32 const sfImportSequence;
|
||||
extern SF_UINT32 const sfXahauActivationLgrSeq;
|
||||
extern SF_UINT32 const sfDelaySeconds;
|
||||
extern SF_UINT32 const sfRepeatCount;
|
||||
extern SF_UINT32 const sfStartAfter;
|
||||
|
||||
// 64-bit integers (common)
|
||||
extern SF_UINT64 const sfIndexNext;
|
||||
@@ -486,6 +489,7 @@ extern SF_UINT256 const sfURITokenID;
|
||||
extern SF_UINT256 const sfGovernanceFlags;
|
||||
extern SF_UINT256 const sfGovernanceMarks;
|
||||
extern SF_UINT256 const sfEmittedTxnID;
|
||||
extern SF_UINT256 const sfCron;
|
||||
|
||||
// currency amount (common)
|
||||
extern SF_AMOUNT const sfAmount;
|
||||
|
||||
@@ -343,6 +343,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecINSUF_RESERVE_SELLER = 187,
|
||||
tecIMMUTABLE = 188,
|
||||
tecTOO_MANY_REMARKS = 189,
|
||||
tecSTART_AFTER_TOO_HIGH = 190,
|
||||
tecLAST_POSSIBLE_ENTRY = 255,
|
||||
};
|
||||
|
||||
|
||||
@@ -200,6 +200,12 @@ constexpr std::uint32_t const tfImmutable = 1;
|
||||
// Clawback flags:
|
||||
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
|
||||
|
||||
// CronSet Flags:
|
||||
enum CronSetFlags : uint32_t {
|
||||
tfCronUnset = 0x00000001,
|
||||
};
|
||||
constexpr std::uint32_t const tfCronSetMask = ~(tfUniversal | tfCronUnset);
|
||||
|
||||
// clang-format on
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -149,6 +149,12 @@ enum TxType : std::uint16_t
|
||||
ttURITOKEN_CREATE_SELL_OFFER = 48,
|
||||
ttURITOKEN_CANCEL_SELL_OFFER = 49,
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
ttCRON = 92,
|
||||
|
||||
/* Sechedule an alarm for later */
|
||||
ttCRON_SET = 93,
|
||||
|
||||
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
|
||||
ttREMARKS_SET = 94,
|
||||
|
||||
|
||||
@@ -479,6 +479,7 @@ REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
|
||||
@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
URI_TOKEN = 'U',
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
CRON = 'A',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -443,6 +444,51 @@ uritoken(AccountID const& issuer, Blob const& uri)
|
||||
LedgerNameSpace::URI_TOKEN, issuer, Slice{uri.data(), uri.size()})};
|
||||
}
|
||||
|
||||
// Constructs an ordered CRON keylet (32 bytes):
|
||||
// [8-byte namespace][4-byte timestamp (big-endian, seconds)][20-byte
|
||||
// AccountID]
|
||||
//
|
||||
// Properties
|
||||
// - Namespacing: first 8 bytes are the most-significant bytes of
|
||||
// indexHash(LedgerNameSpace::CRON).
|
||||
// - Uniqueness (per ts,acct): exactly ONE cron per (timestamp, AccountID).
|
||||
// Insert is upsert (last-write-wins).
|
||||
// This is fine because we only ever allow one cron per account at a time—if
|
||||
// the same (ts,acct) is written, it’s simply an update to that single entry
|
||||
// (idempotent; no duplicate leaves).
|
||||
// - Iteration order: chronological by timestamp (BE), then by raw AccountID
|
||||
// bytes.
|
||||
// NOTE: raw AccountID ordering may bias priority; consider hashing AccountID
|
||||
// for uniform per-timestamp spread.
|
||||
// - Expected accidental prefix collisions (foreign objects sharing the 8-byte
|
||||
// namespace): n / 2^64,
|
||||
// assuming uniform high-64-bit distribution of other objects.
|
||||
// Examples: 100M → ~5.4e-12, 1B → ~5.4e-11, 10B → ~5.4e-10, 100B → ~5.4e-9
|
||||
// (negligible).
|
||||
Keylet
|
||||
cron(uint32_t timestamp, AccountID const& id)
|
||||
{
|
||||
static const uint256 ns = indexHash(LedgerNameSpace::CRON);
|
||||
|
||||
uint8_t h[32];
|
||||
|
||||
// first 8 bytes are the namespacing
|
||||
std::memcpy(h, ns.data(), 8);
|
||||
|
||||
// next 4 bytes are the timestamp in BE
|
||||
h[8] = static_cast<uint8_t>((timestamp >> 24) & 0xFFU);
|
||||
h[9] = static_cast<uint8_t>((timestamp >> 16) & 0xFFU);
|
||||
h[10] = static_cast<uint8_t>((timestamp >> 8) & 0xFFU);
|
||||
h[11] = static_cast<uint8_t>((timestamp >> 0) & 0xFFU);
|
||||
|
||||
const uint256 accHash = indexHash(LedgerNameSpace::CRON, timestamp, id);
|
||||
|
||||
// final 20 bytes are account ID
|
||||
std::memcpy(h + 12, accHash.cdata(), 20);
|
||||
|
||||
return {ltCRON, uint256::fromVoid(h)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -68,6 +68,7 @@ LedgerFormats::LedgerFormats()
|
||||
{sfGovernanceMarks, soeOPTIONAL},
|
||||
{sfAccountIndex, soeOPTIONAL},
|
||||
{sfTouchCount, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
@@ -366,6 +367,16 @@ LedgerFormats::LedgerFormats()
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Cron,
|
||||
ltCRON,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfDelaySeconds, soeREQUIRED},
|
||||
{sfRepeatCount, soeREQUIRED},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,9 @@ CONSTRUCT_TYPED_SFIELD(sfLockCount, "LockCount", UINT32,
|
||||
|
||||
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
|
||||
|
||||
CONSTRUCT_TYPED_SFIELD(sfStartAfter, "StartAfter", UINT32, 93);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRepeatCount, "RepeatCount", UINT32, 94);
|
||||
CONSTRUCT_TYPED_SFIELD(sfDelaySeconds, "DelaySeconds", UINT32, 95);
|
||||
CONSTRUCT_TYPED_SFIELD(sfXahauActivationLgrSeq, "XahauActivationLgrSeq",UINT32, 96);
|
||||
CONSTRUCT_TYPED_SFIELD(sfImportSequence, "ImportSequence", UINT32, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRewardTime, "RewardTime", UINT32, 98);
|
||||
@@ -239,6 +242,7 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceFlags, "GovernanceFlags", UINT256,
|
||||
CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256, 98);
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
|
||||
CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95);
|
||||
|
||||
// currency amount (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);
|
||||
|
||||
@@ -615,7 +615,7 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -94,6 +94,7 @@ transResults()
|
||||
MAKE_ERROR(tecINSUF_RESERVE_SELLER, "The seller of an object has insufficient reserves, and thus cannot complete the sale."),
|
||||
MAKE_ERROR(tecIMMUTABLE, "The remark is marked immutable on the object, and therefore cannot be updated."),
|
||||
MAKE_ERROR(tecTOO_MANY_REMARKS, "The number of remarks on the object would exceed the limit of 32."),
|
||||
MAKE_ERROR(tecSTART_AFTER_TOO_HIGH, "The proposed StartAfter time is greater than one year away."),
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."),
|
||||
|
||||
@@ -472,6 +472,23 @@ TxFormats::TxFormats()
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Cron,
|
||||
ttCRON,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::CronSet,
|
||||
ttCRON_SET,
|
||||
{
|
||||
{sfDelaySeconds, soeOPTIONAL},
|
||||
{sfRepeatCount, soeOPTIONAL},
|
||||
{sfStartAfter, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -51,14 +51,16 @@ JSS(Amendments); // ledger type.
|
||||
JSS(Amount); // in: TransactionSign; field.
|
||||
JSS(Authorize); // field
|
||||
JSS(Blob);
|
||||
JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
JSS(Cron);
|
||||
JSS(CronSet);
|
||||
JSS(DeliverMin); // in: TransactionSign
|
||||
JSS(DepositPreauth); // transaction and ledger type.
|
||||
JSS(Destination); // in: TransactionSign; field.
|
||||
|
||||
@@ -423,6 +423,7 @@ private:
|
||||
addFlagsToJson<NFTokenMintFlags>(ret, "NFTokenMint");
|
||||
addFlagsToJson<NFTokenCreateOfferFlags>(ret, "NFTokenCreateOffer");
|
||||
addFlagsToJson<ClaimRewardFlags>(ret, "ClaimReward");
|
||||
addFlagsToJson<CronSetFlags>(ret, "CronSet");
|
||||
struct FlagData
|
||||
{
|
||||
std::string name;
|
||||
|
||||
461
src/test/app/Cron_test.cpp
Normal file
461
src/test/app/Cron_test.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
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 <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
struct Cron_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("enabled");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
// setup env
|
||||
auto const alice = Account("alice");
|
||||
auto const issuer = Account("issuer");
|
||||
|
||||
for (bool const withCron : {false, true})
|
||||
{
|
||||
auto const amend = withCron ? features : features - featureCron;
|
||||
Env env{*this, amend};
|
||||
|
||||
env.fund(XRP(1000), alice, issuer);
|
||||
env.close();
|
||||
|
||||
auto const expectResult =
|
||||
withCron ? ter(tesSUCCESS) : ter(temDISABLED);
|
||||
|
||||
auto tx = cron::set(alice);
|
||||
// CLAIM
|
||||
env(cron::set(alice), cron::delay(100), fee(XRP(1)), expectResult);
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFee(FeatureBitset features)
|
||||
{
|
||||
testcase("fee");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// create with RepeatCount
|
||||
auto expected = baseFee * 2 + baseFee * 256;
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(expected),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// create with no RepeatCount
|
||||
expected = baseFee * 2;
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
fee(expected),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// delete
|
||||
expected = baseFee;
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronUnset),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronUnset),
|
||||
fee(expected),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidPreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("invalid preflight");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
|
||||
test::jtx::Env env{
|
||||
*this, network::makeNetworkConfig(21337), features | featureCron};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preflight
|
||||
|
||||
// temINVALID_FLAG
|
||||
{
|
||||
env(cron::set(alice), txflags(tfClose), ter(temINVALID_FLAG));
|
||||
env(cron::set(alice),
|
||||
txflags(tfUniversalMask),
|
||||
ter(temINVALID_FLAG));
|
||||
}
|
||||
|
||||
// temMALFORMED
|
||||
{
|
||||
// Invalid both DelaySeconds and RepeatCount are not specified
|
||||
env(cron::set(alice), ter(temMALFORMED));
|
||||
|
||||
// Invalid DelaySeconds and RepeatCount combination
|
||||
// (only RepeatCount specified)
|
||||
env(cron::set(alice), cron::repeat(256), ter(temMALFORMED));
|
||||
|
||||
// Invalid DelaySeconds
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60 + 1),
|
||||
cron::repeat(256),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid RepeatCount
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60),
|
||||
cron::repeat(257),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid tfCronUnset flag
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60),
|
||||
txflags(tfCronUnset),
|
||||
ter(temMALFORMED));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidPreclaim(FeatureBitset features)
|
||||
{
|
||||
testcase("invalid preclaim");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
auto const alice = Account("alice");
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
// there is no check in preclaim
|
||||
BEAST_EXPECT(true);
|
||||
}
|
||||
|
||||
void
|
||||
testDoApply(FeatureBitset features)
|
||||
{
|
||||
testcase("doApply");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
auto const alice = Account("alice");
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto const aliceOwnerCount = ownerCount(env, alice);
|
||||
|
||||
// create cron
|
||||
env(cron::set(alice),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// increment owner count
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount + 1);
|
||||
|
||||
auto const accSle = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(accSle);
|
||||
BEAST_EXPECT(accSle->isFieldPresent(sfCron));
|
||||
|
||||
auto const cronKey = keylet::child(accSle->getFieldH256(sfCron));
|
||||
auto const cronSle = env.le(cronKey);
|
||||
BEAST_EXPECT(cronSle);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfDelaySeconds) == 356 * 24 * 60 * 60);
|
||||
BEAST_EXPECT(cronSle->getFieldU32(sfRepeatCount) == 256);
|
||||
|
||||
// update cron
|
||||
env(cron::set(alice),
|
||||
cron::delay(100),
|
||||
cron::repeat(10),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// owner count does not change
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount + 1);
|
||||
|
||||
auto const accSle2 = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(accSle2);
|
||||
BEAST_EXPECT(accSle2->isFieldPresent(sfCron));
|
||||
|
||||
// old cron sle is deleted
|
||||
BEAST_EXPECT(!env.le(cronKey));
|
||||
|
||||
auto const cronKey2 = keylet::child(accSle2->getFieldH256(sfCron));
|
||||
auto const cronSle2 = env.le(cronKey2);
|
||||
BEAST_EXPECT(cronSle2);
|
||||
BEAST_EXPECT(cronSle2->getFieldU32(sfDelaySeconds) == 100);
|
||||
BEAST_EXPECT(cronSle2->getFieldU32(sfRepeatCount) == 10);
|
||||
|
||||
// delete cron
|
||||
env(cron::set(alice),
|
||||
fee(XRP(1)),
|
||||
txflags(tfCronUnset),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// owner count decremented
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
|
||||
|
||||
auto const accSle3 = env.le(keylet::account(alice.id()));
|
||||
BEAST_EXPECT(accSle3);
|
||||
BEAST_EXPECT(!accSle3->isFieldPresent(sfCron));
|
||||
|
||||
// old cron sle is deleted
|
||||
BEAST_EXPECT(!env.le(cronKey2));
|
||||
|
||||
// delete cron without object will succeed
|
||||
env(cron::set(alice),
|
||||
fee(XRP(1)),
|
||||
txflags(tfCronUnset),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testCronExecution(FeatureBitset features)
|
||||
{
|
||||
testcase("cron execution");
|
||||
using namespace jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
auto const alice = Account("alice");
|
||||
|
||||
{
|
||||
// test ttCron execution and repeatCount
|
||||
Env env{*this, features | featureCron};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
auto baseTime = env.timeKeeper().now().time_since_epoch().count();
|
||||
|
||||
auto repeatCount = 10;
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::delay(100),
|
||||
cron::repeat(repeatCount),
|
||||
fee(XRP(1)));
|
||||
env.close(10s);
|
||||
|
||||
auto lastCronKeylet =
|
||||
keylet::child(env.le(alice)->getFieldH256(sfCron));
|
||||
|
||||
while (repeatCount >= 0)
|
||||
{
|
||||
// close ledger until 100 seconds has passed
|
||||
while (env.timeKeeper().now().time_since_epoch().count() -
|
||||
baseTime <
|
||||
100)
|
||||
{
|
||||
env.close(10s);
|
||||
auto txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 0);
|
||||
}
|
||||
|
||||
// close after 100 seconds passed
|
||||
env.close();
|
||||
|
||||
auto txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 1);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
// check pseudo txn format
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
BEAST_EXPECT(tx.getAccountID(sfAccount) == AccountID());
|
||||
BEAST_EXPECT(tx.getAccountID(sfOwner) == alice.id());
|
||||
BEAST_EXPECT(
|
||||
tx.getFieldU32(sfLedgerSequence) ==
|
||||
env.closed()->info().seq);
|
||||
BEAST_EXPECT(tx.getFieldAmount(sfFee) == XRP(0));
|
||||
BEAST_EXPECT(tx.getFieldVL(sfSigningPubKey).size() == 0);
|
||||
|
||||
// check old Cron object is deleted
|
||||
BEAST_EXPECT(!env.le(lastCronKeylet));
|
||||
|
||||
if (repeatCount > 0)
|
||||
{
|
||||
// check new Cron object
|
||||
auto const cronKeylet =
|
||||
keylet::child(env.le(alice)->getFieldH256(sfCron));
|
||||
auto const cronSle = env.le(cronKeylet);
|
||||
BEAST_EXPECT(cronSle);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfDelaySeconds) == 100);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfRepeatCount) ==
|
||||
--repeatCount);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getAccountID(sfOwner) == alice.id());
|
||||
|
||||
// set new base time
|
||||
baseTime =
|
||||
env.timeKeeper().now().time_since_epoch().count();
|
||||
lastCronKeylet = cronKeylet;
|
||||
}
|
||||
else
|
||||
{
|
||||
// after all executions, the cron object should be
|
||||
// deleted
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfCron));
|
||||
BEAST_EXPECT(!env.le(lastCronKeylet));
|
||||
--repeatCount; // decrement for break double loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// test ttCron limit in a ledger
|
||||
Env env{*this, features | featureCron};
|
||||
std::vector<Account> accounts;
|
||||
accounts.reserve(300);
|
||||
for (int i = 0; i < 300; ++i)
|
||||
{
|
||||
auto const& account = accounts.emplace_back(
|
||||
Account("account_" + std::to_string(i)));
|
||||
accounts.emplace_back(account);
|
||||
env.fund(XRP(10000), account);
|
||||
}
|
||||
env.close();
|
||||
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
env(cron::set(account), cron::delay(0), fee(XRP(1)));
|
||||
}
|
||||
env.close();
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 128);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
}
|
||||
}
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 128);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
}
|
||||
}
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 44);
|
||||
for (auto it = txns.begin(); it != txns.end(); ++it)
|
||||
{
|
||||
auto const& tx = *it->first;
|
||||
BEAST_EXPECT(tx.getTxnType() == ttCRON);
|
||||
}
|
||||
}
|
||||
|
||||
// proceed ledger
|
||||
env.close();
|
||||
{
|
||||
auto const txns = env.closed()->txs;
|
||||
auto size = std::distance(txns.begin(), txns.end());
|
||||
BEAST_EXPECT(size == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testEnabled(features);
|
||||
testFee(features);
|
||||
testInvalidPreflight(features);
|
||||
testInvalidPreclaim(features);
|
||||
testDoApply(features);
|
||||
|
||||
testCronExecution(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testWithFeats(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Cron, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/PayChan.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
@@ -748,10 +749,22 @@ private:
|
||||
jtx::Env& env,
|
||||
int const& expected,
|
||||
uint64_t const& lineno)
|
||||
{
|
||||
auto const hashStr =
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString();
|
||||
uint256 const txHash = uint256::fromVoid(strUnHex(hashStr)->data());
|
||||
testTSHStrongWeak(env, txHash, expected, lineno);
|
||||
}
|
||||
|
||||
void
|
||||
testTSHStrongWeak(
|
||||
jtx::Env& env,
|
||||
uint256 const& txHash,
|
||||
int const& expected,
|
||||
uint64_t const& lineno)
|
||||
{
|
||||
Json::Value params;
|
||||
params[jss::transaction] =
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash];
|
||||
params[jss::transaction] = strHex(txHash);
|
||||
auto const jrr = env.rpc("json", "tx", to_string(params));
|
||||
auto const meta = jrr[jss::result][jss::meta];
|
||||
validateTSHStrongWeak(meta, expected, lineno);
|
||||
@@ -6251,6 +6264,103 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// CronSet
|
||||
// | otxn | tsh | cset |
|
||||
// | A | A | S |
|
||||
void
|
||||
testCronSetTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("cron set tsh");
|
||||
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// otxn: account
|
||||
// tsh account
|
||||
// w/s: strong
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const account = Account("alice");
|
||||
env.fund(XRP(1000), account);
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, account);
|
||||
|
||||
// set tsh hook
|
||||
setTSHHook(env, account, testStrong);
|
||||
|
||||
// cron set
|
||||
env(cron::set(account),
|
||||
cron::delay(100),
|
||||
cron::repeat(1),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
testTSHStrongWeak(env, tshSTRONG, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
// | otxn | tsh | cron |
|
||||
// | - | O | W |
|
||||
void
|
||||
testCronTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("cron tsh");
|
||||
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// otxn: -
|
||||
// tsh owner
|
||||
// w/s: weak
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const account = Account("alice");
|
||||
env.fund(XRP(1000), account);
|
||||
env.close();
|
||||
|
||||
// cron set
|
||||
env(cron::set(account),
|
||||
cron::delay(100),
|
||||
cron::repeat(1),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, account);
|
||||
|
||||
// set tsh hook
|
||||
setTSHHook(env, account, testStrong);
|
||||
|
||||
// proceed ledger
|
||||
env.close(100s);
|
||||
|
||||
// close ledger
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
auto const expected = testStrong ? tshNONE : tshWEAK;
|
||||
auto const txs = env.closed()->txs;
|
||||
BEAST_EXPECT(std::distance(txs.begin(), txs.end()) == 1);
|
||||
auto const tx = txs.begin()->first;
|
||||
BEAST_EXPECT(tx->getTxnType() == ttCRON);
|
||||
testTSHStrongWeak(env, tx->getTransactionID(), expected, __LINE__);
|
||||
}
|
||||
}
|
||||
void
|
||||
testEmissionOrdering(FeatureBitset features)
|
||||
{
|
||||
@@ -6398,6 +6508,8 @@ private:
|
||||
testURITokenCancelSellOfferTSH(features);
|
||||
testURITokenCreateSellOfferTSH(features);
|
||||
testRemitTSH(features);
|
||||
testCronSetTSH(features);
|
||||
testCronTSH(features);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h>
|
||||
#include <test/jtx/check.h>
|
||||
#include <test/jtx/cron.h>
|
||||
#include <test/jtx/delivermin.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
|
||||
74
src/test/jtx/cron.h
Normal file
74
src/test/jtx/cron.h
Normal file
@@ -0,0 +1,74 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
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_CRON_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_CRON_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/** Cron operations. */
|
||||
namespace cron {
|
||||
|
||||
/** Set a cron. */
|
||||
Json::Value
|
||||
set(jtx::Account const& account);
|
||||
|
||||
/** Sets the optional DelaySeconds on a JTx. */
|
||||
class delay
|
||||
{
|
||||
private:
|
||||
uint32_t delay_;
|
||||
|
||||
public:
|
||||
explicit delay(uint32_t delay) : delay_(delay)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
/** Sets the optional RepeatCount on a JTx. */
|
||||
class repeat
|
||||
{
|
||||
private:
|
||||
uint32_t repeat_;
|
||||
|
||||
public:
|
||||
explicit repeat(uint32_t repeat) : repeat_(repeat)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
} // namespace cron
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TEST_JTX_CRON_H_INCLUDED
|
||||
56
src/test/jtx/impl/cron.cpp
Normal file
56
src/test/jtx/impl/cron.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
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 <ripple/protocol/jss.h>
|
||||
#include <test/jtx/cron.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
namespace cron {
|
||||
|
||||
// Set a cron.
|
||||
Json::Value
|
||||
set(jtx::Account const& account)
|
||||
{
|
||||
using namespace jtx;
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::CronSet;
|
||||
jv[jss::Account] = account.human();
|
||||
return jv;
|
||||
}
|
||||
|
||||
void
|
||||
delay::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfDelaySeconds.jsonName] = delay_;
|
||||
}
|
||||
|
||||
void
|
||||
repeat::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfRepeatCount.jsonName] = repeat_;
|
||||
}
|
||||
|
||||
} // namespace cron
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
Reference in New Issue
Block a user