mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-08 21:02:21 +00:00
Outgoing/Incoming HookOn (#457)
This commit is contained in:
@@ -136,6 +136,8 @@
|
||||
#define sfEmittedTxnID ((5U << 16U) + 97U)
|
||||
#define sfHookCanEmit ((5U << 16U) + 96U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfAmount ((6U << 16U) + 1U)
|
||||
#define sfBalance ((6U << 16U) + 2U)
|
||||
#define sfLimitAmount ((6U << 16U) + 3U)
|
||||
|
||||
@@ -89,6 +89,12 @@ canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
|
||||
ripple::uint256
|
||||
getHookCanEmit(ripple::STObject const& hookObj, SLE::pointer const& hookDef);
|
||||
|
||||
ripple::uint256
|
||||
getHookOn(
|
||||
STObject const& obj,
|
||||
std::shared_ptr<SLE const> const& def,
|
||||
SField const& field);
|
||||
|
||||
struct HookResult;
|
||||
|
||||
HookResult
|
||||
|
||||
@@ -921,6 +921,23 @@ hook::getHookCanEmit(
|
||||
return hookCanEmit;
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
hook::getHookOn(
|
||||
STObject const& obj,
|
||||
std::shared_ptr<SLE const> const& def,
|
||||
SField const& field)
|
||||
{
|
||||
if (obj.isFieldPresent(field))
|
||||
return obj.getFieldH256(field);
|
||||
if (obj.isFieldPresent(sfHookOn))
|
||||
return obj.getFieldH256(sfHookOn);
|
||||
if (def->isFieldPresent(field))
|
||||
return def->getFieldH256(field);
|
||||
if (def->isFieldPresent(sfHookOn))
|
||||
return def->getFieldH256(sfHookOn);
|
||||
return uint256{0};
|
||||
};
|
||||
|
||||
// Update HookState ledger objects for the hook... only called after accept()
|
||||
// assumes the specified acc has already been checked for authoriation (hook
|
||||
// grants)
|
||||
|
||||
@@ -225,7 +225,9 @@ SetHook::inferOperation(STObject const& hookSetObj)
|
||||
!hasHash && !hasCode && !hookSetObj.isFieldPresent(sfHookGrants) &&
|
||||
!hookSetObj.isFieldPresent(sfHookNamespace) &&
|
||||
!hookSetObj.isFieldPresent(sfHookParameters) &&
|
||||
!hookSetObj.isFieldPresent(sfHookOn) &&
|
||||
!(hookSetObj.isFieldPresent(sfHookOn) ||
|
||||
(hookSetObj.isFieldPresent(sfHookOnOutgoing) &&
|
||||
hookSetObj.isFieldPresent(sfHookOnIncoming))) &&
|
||||
!hookSetObj.isFieldPresent(sfHookCanEmit) &&
|
||||
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
|
||||
!hookSetObj.isFieldPresent(sfFlags))
|
||||
@@ -261,6 +263,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
if (hookSetObj.isFieldPresent(sfHookGrants) ||
|
||||
hookSetObj.isFieldPresent(sfHookParameters) ||
|
||||
hookSetObj.isFieldPresent(sfHookOn) ||
|
||||
hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
|
||||
hookSetObj.isFieldPresent(sfHookOnIncoming) ||
|
||||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
|
||||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
|
||||
!hookSetObj.isFieldPresent(sfFlags) ||
|
||||
@@ -291,6 +295,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
if (hookSetObj.isFieldPresent(sfHookGrants) ||
|
||||
hookSetObj.isFieldPresent(sfHookParameters) ||
|
||||
hookSetObj.isFieldPresent(sfHookOn) ||
|
||||
hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
|
||||
hookSetObj.isFieldPresent(sfHookOnIncoming) ||
|
||||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
|
||||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
|
||||
hookSetObj.isFieldPresent(sfHookNamespace) ||
|
||||
@@ -448,12 +454,53 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
// validate sfHookOn
|
||||
if (!hookSetObj.isFieldPresent(sfHookOn))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
|
||||
<< HS_ACC()
|
||||
<< "]: Malformed transaction: SetHook must include "
|
||||
"sfHookOn when creating a new hook.";
|
||||
return false;
|
||||
if (!ctx.rules.enabled(featureHookOnV2))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
|
||||
<< HS_ACC()
|
||||
<< "]: Malformed transaction: SetHook must include "
|
||||
"sfHookOn before featureHookOnV2 is enabled.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
|
||||
!hookSetObj.isFieldPresent(sfHookOnIncoming))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
|
||||
<< HS_ACC()
|
||||
<< "]: Malformed transaction: SetHook must include "
|
||||
"sfHookOnOutgoing and sfHookOnIncoming "
|
||||
"when creating a new hook without sfHookOn.";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const outgoing = hookSetObj.getFieldH256(sfHookOnOutgoing);
|
||||
auto const incoming = hookSetObj.getFieldH256(sfHookOnIncoming);
|
||||
if (outgoing == incoming)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
|
||||
<< HS_ACC()
|
||||
<< "]: Malformed transaction: SetHook outgoing and "
|
||||
"incoming hookon must be different.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
|
||||
hookSetObj.isFieldPresent(sfHookOnIncoming))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
|
||||
<< HS_ACC()
|
||||
<< "]: Malformed transaction: SetHook must no"
|
||||
"include sfHookOnOutgoing and sfHookOnIncoming "
|
||||
"when creating a new hook with sfHookOn.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// validate sfHookCanEmit
|
||||
@@ -742,7 +789,8 @@ SetHook::preflight(PreflightContext const& ctx)
|
||||
|
||||
if (name != sfCreateCode && name != sfHookHash &&
|
||||
name != sfHookNamespace && name != sfHookParameters &&
|
||||
name != sfHookOn && name != sfHookGrants &&
|
||||
name != sfHookOn && name != sfHookOnOutgoing &&
|
||||
name != sfHookOnIncoming && name != sfHookGrants &&
|
||||
name != sfHookApiVersion && name != sfFlags &&
|
||||
name != sfHookCanEmit)
|
||||
{
|
||||
@@ -1264,10 +1312,14 @@ SetHook::setHook()
|
||||
std::optional<ripple::uint256> newNamespace;
|
||||
std::optional<ripple::Keylet> newDirKeylet;
|
||||
|
||||
std::optional<uint256> oldHookOn;
|
||||
std::optional<uint256> newHookOn;
|
||||
std::optional<uint256> defHookOn;
|
||||
|
||||
std::optional<uint256> newHookOnOutgoing;
|
||||
std::optional<uint256> newHookOnIncoming;
|
||||
std::optional<uint256> defHookOnOutgoing;
|
||||
std::optional<uint256> defHookOnIncoming;
|
||||
|
||||
std::optional<uint256> oldHookCanEmit;
|
||||
std::optional<uint256> newHookCanEmit;
|
||||
std::optional<uint256> defHookCanEmit;
|
||||
@@ -1325,13 +1377,18 @@ SetHook::setHook()
|
||||
|
||||
oldDirKeylet = keylet::hookStateDir(account_, *oldNamespace);
|
||||
oldDirSLE = view().peek(*oldDirKeylet);
|
||||
if (oldDefSLE)
|
||||
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookOn))
|
||||
defHookOn = oldDefSLE->getFieldH256(sfHookOn);
|
||||
|
||||
if (oldHook->get().isFieldPresent(sfHookOn))
|
||||
oldHookOn = oldHook->get().getFieldH256(sfHookOn);
|
||||
else if (defHookOn)
|
||||
oldHookOn = *defHookOn;
|
||||
if (oldDefSLE)
|
||||
{
|
||||
if (oldDefSLE->isFieldPresent(sfHookOnOutgoing))
|
||||
defHookOnOutgoing =
|
||||
oldDefSLE->getFieldH256(sfHookOnOutgoing);
|
||||
if (oldDefSLE->isFieldPresent(sfHookOnIncoming))
|
||||
defHookOnIncoming =
|
||||
oldDefSLE->getFieldH256(sfHookOnIncoming);
|
||||
}
|
||||
|
||||
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookCanEmit))
|
||||
defHookCanEmit = oldDefSLE->getFieldH256(sfHookCanEmit);
|
||||
@@ -1356,6 +1413,14 @@ SetHook::setHook()
|
||||
if (hookSetObj->get().isFieldPresent(sfHookOn))
|
||||
newHookOn = hookSetObj->get().getFieldH256(sfHookOn);
|
||||
|
||||
if (hookSetObj->get().isFieldPresent(sfHookOnOutgoing))
|
||||
newHookOnOutgoing =
|
||||
hookSetObj->get().getFieldH256(sfHookOnOutgoing);
|
||||
|
||||
if (hookSetObj->get().isFieldPresent(sfHookOnIncoming))
|
||||
newHookOnIncoming =
|
||||
hookSetObj->get().getFieldH256(sfHookOnIncoming);
|
||||
|
||||
if (hookSetObj->get().isFieldPresent(sfHookCanEmit))
|
||||
newHookCanEmit = hookSetObj->get().getFieldH256(sfHookCanEmit);
|
||||
|
||||
@@ -1469,6 +1534,14 @@ SetHook::setHook()
|
||||
if (oldHook->get().isFieldPresent(sfHookOn))
|
||||
newHook.setFieldH256(
|
||||
sfHookOn, oldHook->get().getFieldH256(sfHookOn));
|
||||
if (oldHook->get().isFieldPresent(sfHookOnOutgoing))
|
||||
newHook.setFieldH256(
|
||||
sfHookOnOutgoing,
|
||||
oldHook->get().getFieldH256(sfHookOnOutgoing));
|
||||
if (oldHook->get().isFieldPresent(sfHookOnIncoming))
|
||||
newHook.setFieldH256(
|
||||
sfHookOnIncoming,
|
||||
oldHook->get().getFieldH256(sfHookOnIncoming));
|
||||
if (oldHook->get().isFieldPresent(sfHookCanEmit))
|
||||
newHook.setFieldH256(
|
||||
sfHookCanEmit,
|
||||
@@ -1502,6 +1575,24 @@ SetHook::setHook()
|
||||
newHook.setFieldH256(sfHookOn, *newHookOn);
|
||||
}
|
||||
|
||||
if (newHookOnOutgoing)
|
||||
{
|
||||
if (*defHookOnOutgoing == *newHookOnOutgoing)
|
||||
{
|
||||
if (newHook.isFieldPresent(sfHookOnOutgoing))
|
||||
newHook.makeFieldAbsent(sfHookOnOutgoing);
|
||||
}
|
||||
}
|
||||
|
||||
if (newHookOnIncoming)
|
||||
{
|
||||
if (*defHookOnIncoming == *newHookOnIncoming)
|
||||
{
|
||||
if (newHook.isFieldPresent(sfHookOnIncoming))
|
||||
newHook.makeFieldAbsent(sfHookOnIncoming);
|
||||
}
|
||||
}
|
||||
|
||||
// set the hookcanemit field if it differs from definition
|
||||
if (newHookCanEmit)
|
||||
{
|
||||
@@ -1663,7 +1754,19 @@ SetHook::setHook()
|
||||
|
||||
auto newHookDef = std::make_shared<SLE>(keylet);
|
||||
newHookDef->setFieldH256(sfHookHash, *createHookHash);
|
||||
newHookDef->setFieldH256(sfHookOn, *newHookOn);
|
||||
|
||||
// only HookOn or (HookOnOutgoing and HookOnIncoming)
|
||||
if (!view().rules().enabled(featureHookOnV2) ||
|
||||
(!newHookOnOutgoing && !newHookOnIncoming))
|
||||
newHookDef->setFieldH256(sfHookOn, *newHookOn);
|
||||
else
|
||||
{
|
||||
newHookDef->setFieldH256(
|
||||
sfHookOnOutgoing, *newHookOnOutgoing);
|
||||
newHookDef->setFieldH256(
|
||||
sfHookOnIncoming, *newHookOnIncoming);
|
||||
}
|
||||
|
||||
if (newHookCanEmit)
|
||||
newHookDef->setFieldH256(
|
||||
sfHookCanEmit, *newHookCanEmit);
|
||||
@@ -1759,7 +1862,7 @@ SetHook::setHook()
|
||||
|
||||
// change which definition we're using to the new target
|
||||
defNamespace = newDefSLE->getFieldH256(sfHookNamespace);
|
||||
defHookOn = newDefSLE->getFieldH256(sfHookOn);
|
||||
|
||||
if (newDefSLE->isFieldPresent(sfHookCanEmit))
|
||||
defHookCanEmit = newDefSLE->getFieldH256(sfHookCanEmit);
|
||||
|
||||
@@ -1767,9 +1870,41 @@ SetHook::setHook()
|
||||
if (newNamespace && *defNamespace != *newNamespace)
|
||||
newHook.setFieldH256(sfHookNamespace, *newNamespace);
|
||||
|
||||
defHookOn = newDefSLE->getFieldH256(sfHookOn);
|
||||
defHookOnIncoming = newDefSLE->getFieldH256(sfHookOnIncoming);
|
||||
defHookOnOutgoing = newDefSLE->getFieldH256(sfHookOnOutgoing);
|
||||
|
||||
// set the hookon field if it differs from definition
|
||||
if (newHookOn && *defHookOn != *newHookOn)
|
||||
newHook.setFieldH256(sfHookOn, *newHookOn);
|
||||
if (newHookOn)
|
||||
{
|
||||
auto const diffFromDef = defHookOn != *newHookOn;
|
||||
auto const hasIncOutgDef =
|
||||
*defHookOnIncoming != *defHookOnOutgoing ||
|
||||
*newHookOn != *defHookOnIncoming;
|
||||
if (diffFromDef || hasIncOutgDef)
|
||||
{
|
||||
newHook.setFieldH256(sfHookOn, *newHookOn);
|
||||
}
|
||||
}
|
||||
|
||||
// set the incoming/outgoing hookon field if it differs from
|
||||
// definition
|
||||
if (newHookOnIncoming || newHookOnOutgoing)
|
||||
{
|
||||
auto const diffFromDef =
|
||||
*defHookOnIncoming != *newHookOnIncoming ||
|
||||
*defHookOnOutgoing != *newHookOnOutgoing;
|
||||
auto const hasHookOnDef =
|
||||
*newHookOnIncoming != *defHookOn ||
|
||||
*newHookOnOutgoing != *defHookOn;
|
||||
if (diffFromDef || hasHookOnDef)
|
||||
{
|
||||
newHook.setFieldH256(
|
||||
sfHookOnIncoming, *newHookOnIncoming);
|
||||
newHook.setFieldH256(
|
||||
sfHookOnOutgoing, *newHookOnOutgoing);
|
||||
}
|
||||
}
|
||||
|
||||
// set the hookcanemit field if it differs from definition
|
||||
if (newHookCanEmit &&
|
||||
@@ -1825,8 +1960,8 @@ SetHook::setHook()
|
||||
// sfHook: 1 reserve PER non-blank entry
|
||||
// sfParameters: 1 reserve PER entry
|
||||
// sfGrants are: 1 reserve PER entry
|
||||
// sfHookHash, sfHookNamespace, sfHookOn, sfHookCanEmit,
|
||||
// sfHookApiVersion, sfFlags: free
|
||||
// sfHookHash, sfHookNamespace, sfHookOn, sfHookOnOutgoing,
|
||||
// sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags: free
|
||||
|
||||
// sfHookDefinition is not reserved because it is an unowned object,
|
||||
// rather the uploader is billed via fee according to the following:
|
||||
@@ -1988,6 +2123,6 @@ SetHook::setHook()
|
||||
}
|
||||
|
||||
return nsDeleteResult;
|
||||
}
|
||||
} // namespace ripple
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -212,6 +212,7 @@ Transactor::calculateHookChainFee(
|
||||
ReadView const& view,
|
||||
STTx const& tx,
|
||||
Keylet const& hookKeylet,
|
||||
bool isOutgoing,
|
||||
bool collectCallsOnly)
|
||||
{
|
||||
std::shared_ptr<SLE const> hookSLE = view.read(hookKeylet);
|
||||
@@ -229,7 +230,7 @@ Transactor::calculateHookChainFee(
|
||||
|
||||
uint256 const& hash = hookObj.getFieldH256(sfHookHash);
|
||||
|
||||
std::shared_ptr<SLE const> hookDef =
|
||||
std::shared_ptr<SLE const> const& hookDef =
|
||||
view.read(keylet::hookDefinition(hash));
|
||||
|
||||
// this is an edge case that happens when a hook is deleted and executed
|
||||
@@ -240,18 +241,16 @@ Transactor::calculateHookChainFee(
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if the hook can fire
|
||||
uint256 hookOn =
|
||||
(hookObj.isFieldPresent(sfHookOn)
|
||||
? hookObj.getFieldH256(sfHookOn)
|
||||
: hookDef->getFieldH256(sfHookOn));
|
||||
|
||||
uint32_t flags = 0;
|
||||
if (hookObj.isFieldPresent(sfFlags))
|
||||
flags = hookObj.getFieldU32(sfFlags);
|
||||
else
|
||||
flags = hookDef->getFieldU32(sfFlags);
|
||||
|
||||
// check if the hook can fire
|
||||
uint256 hookOn = hook::getHookOn(
|
||||
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
|
||||
|
||||
if (hook::canHook(tx.getTxnType(), hookOn) &&
|
||||
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
|
||||
{
|
||||
@@ -335,7 +334,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
}
|
||||
else
|
||||
hookExecutionFee += calculateHookChainFee(
|
||||
view, tx, keylet::hook(tx.getAccountID(sfAccount)));
|
||||
view, tx, keylet::hook(tx.getAccountID(sfAccount)), true);
|
||||
|
||||
// find any additional stakeholders whose hooks will be executed and
|
||||
// charged to this transaction
|
||||
@@ -344,8 +343,8 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
|
||||
for (auto& [tshAcc, canRollback] : tsh)
|
||||
if (canRollback)
|
||||
hookExecutionFee +=
|
||||
calculateHookChainFee(view, tx, keylet::hook(tshAcc));
|
||||
hookExecutionFee += calculateHookChainFee(
|
||||
view, tx, keylet::hook(tshAcc), false);
|
||||
}
|
||||
|
||||
XRPAmount accumulator = baseFee;
|
||||
@@ -1188,6 +1187,7 @@ Transactor::executeHookChain(
|
||||
std::vector<hook::HookResult>& results,
|
||||
ripple::AccountID const& account,
|
||||
bool strong,
|
||||
bool isOutgoing,
|
||||
std::shared_ptr<STObject const> const& provisionalMeta)
|
||||
{
|
||||
std::set<uint256> hookSkips;
|
||||
@@ -1222,10 +1222,8 @@ Transactor::executeHookChain(
|
||||
}
|
||||
|
||||
// check if the hook can fire
|
||||
uint256 hookOn =
|
||||
(hookObj.isFieldPresent(sfHookOn)
|
||||
? hookObj.getFieldH256(sfHookOn)
|
||||
: hookDef->getFieldH256(sfHookOn));
|
||||
uint256 hookOn = hook::getHookOn(
|
||||
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
|
||||
|
||||
if (!hook::canHook(ctx_.tx.getTxnType(), hookOn))
|
||||
continue; // skip if it can't
|
||||
@@ -1244,8 +1242,8 @@ Transactor::executeHookChain(
|
||||
if (!strong && !(flags & hsfCOLLECT))
|
||||
continue;
|
||||
|
||||
// fetch the namespace either from the hook object of, if absent, the
|
||||
// hook def
|
||||
// fetch the namespace either from the hook object of, if absent,
|
||||
// the hook def
|
||||
uint256 const& ns =
|
||||
(hookObj.isFieldPresent(sfHookNamespace)
|
||||
? hookObj.getFieldH256(sfHookNamespace)
|
||||
@@ -1587,8 +1585,8 @@ Transactor::doTSH(
|
||||
continue;
|
||||
|
||||
// compute and deduct fees for the TSH if applicable
|
||||
XRPAmount tshFeeDrops =
|
||||
calculateHookChainFee(view, ctx_.tx, klTshHook, !canRollback);
|
||||
XRPAmount tshFeeDrops = calculateHookChainFee(
|
||||
view, ctx_.tx, klTshHook, false, !canRollback);
|
||||
|
||||
// no hooks to execute, skip tsh
|
||||
if (tshFeeDrops == 0)
|
||||
@@ -1646,7 +1644,13 @@ Transactor::doTSH(
|
||||
|
||||
// execution to here means we can run the TSH's hook chain
|
||||
TER tshResult = executeHookChain(
|
||||
tshHook, stateMap, results, tshAccountID, strong, provisionalMeta);
|
||||
tshHook,
|
||||
stateMap,
|
||||
results,
|
||||
tshAccountID,
|
||||
strong,
|
||||
false,
|
||||
provisionalMeta);
|
||||
|
||||
if (canRollback && (!isTesSuccess(tshResult)))
|
||||
return tshResult;
|
||||
@@ -1821,7 +1825,13 @@ Transactor::operator()()
|
||||
if (hooksOriginator && hooksOriginator->isFieldPresent(sfHooks) &&
|
||||
!ctx_.isEmittedTxn())
|
||||
result = executeHookChain(
|
||||
hooksOriginator, stateMap, hookResults, accountID, true, {});
|
||||
hooksOriginator,
|
||||
stateMap,
|
||||
hookResults,
|
||||
accountID,
|
||||
true,
|
||||
true,
|
||||
{});
|
||||
|
||||
if (isTesSuccess(result))
|
||||
{
|
||||
|
||||
@@ -179,6 +179,7 @@ public:
|
||||
ReadView const& view,
|
||||
STTx const& tx,
|
||||
Keylet const& hookKeylet,
|
||||
bool isOutgoing,
|
||||
bool collectCallsOnly = false);
|
||||
|
||||
protected:
|
||||
@@ -211,6 +212,7 @@ protected:
|
||||
std::vector<hook::HookResult>& results,
|
||||
ripple::AccountID const& account,
|
||||
bool strong,
|
||||
bool isOutgoing,
|
||||
std::shared_ptr<STObject const> const& provisionalMeta);
|
||||
|
||||
void
|
||||
|
||||
@@ -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 = 91;
|
||||
static constexpr std::size_t numFeatures = 92;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -378,6 +378,7 @@ extern uint256 const fixInvalidTxFlags;
|
||||
extern uint256 const featureExtendedHookState;
|
||||
extern uint256 const fixCronStacking;
|
||||
extern uint256 const fixHookAPI20251128;
|
||||
extern uint256 const featureHookOnV2;
|
||||
extern uint256 const featureHooksUpdate2;
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -465,6 +465,8 @@ extern SF_UINT256 const sfEmitParentTxnID;
|
||||
extern SF_UINT256 const sfEmitNonce;
|
||||
extern SF_UINT256 const sfEmitHookHash;
|
||||
extern SF_UINT256 const sfObjectID;
|
||||
extern SF_UINT256 const sfHookOnIncoming;
|
||||
extern SF_UINT256 const sfHookOnOutgoing;
|
||||
|
||||
// 256-bit (uncommon)
|
||||
extern SF_UINT256 const sfBookDirectory;
|
||||
|
||||
@@ -484,6 +484,7 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
|
||||
@@ -87,7 +87,9 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{{sfCreateCode, soeREQUIRED},
|
||||
{sfHookNamespace, soeREQUIRED},
|
||||
{sfHookParameters, soeREQUIRED},
|
||||
{sfHookOn, soeREQUIRED},
|
||||
{sfHookOn, soeOPTIONAL},
|
||||
{sfHookOnIncoming, soeOPTIONAL},
|
||||
{sfHookOnOutgoing, soeOPTIONAL},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookApiVersion, soeREQUIRED},
|
||||
{sfFlags, soeREQUIRED},
|
||||
@@ -101,6 +103,8 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfHookNamespace, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfHookOn, soeOPTIONAL},
|
||||
{sfHookOnIncoming, soeOPTIONAL},
|
||||
{sfHookOnOutgoing, soeOPTIONAL},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookApiVersion, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL}});
|
||||
|
||||
@@ -228,7 +228,9 @@ LedgerFormats::LedgerFormats()
|
||||
ltHOOK_DEFINITION,
|
||||
{
|
||||
{sfHookHash, soeREQUIRED},
|
||||
{sfHookOn, soeREQUIRED},
|
||||
{sfHookOn, soeOPTIONAL},
|
||||
{sfHookOnIncoming, soeOPTIONAL},
|
||||
{sfHookOnOutgoing, soeOPTIONAL},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookNamespace, soeREQUIRED},
|
||||
{sfHookParameters, soeREQUIRED},
|
||||
|
||||
@@ -244,6 +244,8 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256,
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
|
||||
CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookOnIncoming, "HookOnIncoming", UINT256, 94);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookOnOutgoing, "HookOnOutgoing", UINT256, 93);
|
||||
|
||||
// currency amount (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);
|
||||
|
||||
@@ -85,6 +85,8 @@ JSS(HookCanEmit); // field
|
||||
JSS(HookHash); // field
|
||||
JSS(HookNamespace); // field
|
||||
JSS(HookOn); // field
|
||||
JSS(HookOnIncoming); // field
|
||||
JSS(HookOnOutgoing); // field
|
||||
JSS(Hooks); // field
|
||||
JSS(HookGrants); // field
|
||||
JSS(HookParameters); // field
|
||||
|
||||
@@ -238,7 +238,12 @@ public:
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Env env{
|
||||
*this,
|
||||
envconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const gw = Account{"gateway"};
|
||||
@@ -710,14 +715,20 @@ public:
|
||||
env.close();
|
||||
}
|
||||
|
||||
// grants, parameters, hookon, hookcanemit, hookapiversion,
|
||||
// hooknamespace keys must be absent
|
||||
// grants, parameters, hookon, hookonincoming, hookonoutgoing,
|
||||
// hookcanemit, hookapiversion, hooknamespace keys must be absent
|
||||
for (auto const& [key, value] : JSSMap{
|
||||
{jss::HookGrants, Json::arrayValue},
|
||||
{jss::HookParameters, Json::arrayValue},
|
||||
{jss::HookOn,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookOnIncoming,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookOnOutgoing,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookCanEmit,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
@@ -733,7 +744,8 @@ public:
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
env(jv,
|
||||
M("Hook DELETE operation cannot include: grants, params, "
|
||||
"hookon, hookcanemit, apiversion, namespace"),
|
||||
"hookon, HookOnIncoming, HookOnOutgoing, hookcanemit, "
|
||||
"apiversion, namespace"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
@@ -894,6 +906,12 @@ public:
|
||||
{jss::HookOn,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookOnIncoming,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookOnOutgoing,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookCanEmit,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
@@ -910,7 +928,8 @@ public:
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
env(jv,
|
||||
M("Hook NSDELETE operation cannot include: grants, params, "
|
||||
"hookon, hookcanemit, apiversion"),
|
||||
"hookon, hookonincoming, hookonoutgoing, hookcanemit, "
|
||||
"apiversion"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
@@ -1294,6 +1313,367 @@ public:
|
||||
: preHookCount + 66);
|
||||
}
|
||||
|
||||
void
|
||||
testFeeRPC(jtx::Env& env, Json::Value tx, std::string expected)
|
||||
{
|
||||
auto const jtx = env.jt(tx);
|
||||
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
|
||||
// build tx_blob
|
||||
Json::Value params;
|
||||
params[jss::tx_blob] = strHex(jtx.stx->getSerializer().slice());
|
||||
|
||||
// fee request
|
||||
auto const jrr = env.rpc("json", "fee", to_string(params));
|
||||
// std::cout << "RESULT: " << jrr << "\n";
|
||||
|
||||
// verify base fee & open ledger fee
|
||||
auto const drops = jrr[jss::result][jss::drops];
|
||||
auto const baseFee = drops[jss::base_fee_no_hooks];
|
||||
BEAST_EXPECT(baseFee == to_string(feeDrops));
|
||||
auto const openLedgerFee = drops[jss::open_ledger_fee];
|
||||
BEAST_EXPECT(openLedgerFee == expected);
|
||||
|
||||
// verify hooks fee
|
||||
auto const hooksFee = jrr[jss::result][jss::fee_hooks_feeunits];
|
||||
BEAST_EXPECT(hooksFee == expected);
|
||||
}
|
||||
|
||||
void
|
||||
testHookOnV2(FeatureBitset features)
|
||||
{
|
||||
testcase("Test hook on v2");
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2);
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.fund(XRP(10000), bob);
|
||||
env.close();
|
||||
|
||||
auto const deleteHook = [&env](Account const& account) {
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::TransactionType] = jss::SetHook;
|
||||
jv[jss::Flags] = 0;
|
||||
jv[jss::Hooks] = Json::Value{Json::arrayValue};
|
||||
Json::Value iv;
|
||||
iv[jss::CreateCode] = "";
|
||||
iv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
|
||||
env(jv, M("hook DELETE"), HSFEE);
|
||||
env.close();
|
||||
};
|
||||
|
||||
// Disabled
|
||||
{
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"01";
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"02";
|
||||
// create
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Create: Disabled"),
|
||||
HSFEE,
|
||||
!hookOnV2 ? ter(temMALFORMED) : ter(tesSUCCESS));
|
||||
deleteHook(alice);
|
||||
|
||||
// install
|
||||
env(ripple::test::jtx::hook(bob, {{hso(accept_wasm)}}, 0),
|
||||
M("Install: Disabled prepare"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Install: Disabled"),
|
||||
HSFEE,
|
||||
!hookOnV2 ? ter(temMALFORMED) : ter(tesSUCCESS));
|
||||
env.close();
|
||||
deleteHook(alice);
|
||||
deleteHook(bob);
|
||||
|
||||
// update
|
||||
env(ripple::test::jtx::hook(alice, {{hso(accept_wasm)}}, 0),
|
||||
M("Update: Disabled prepare"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Update: Disabled"),
|
||||
HSFEE,
|
||||
!hookOnV2 ? ter(temMALFORMED) : ter(tesSUCCESS));
|
||||
env.close();
|
||||
deleteHook(alice);
|
||||
deleteHook(bob);
|
||||
}
|
||||
if (!hookOnV2)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
// Create
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
// Install
|
||||
env(ripple::test::jtx::hook(bob, {{hso(accept_wasm)}}, 0),
|
||||
M("Install: prepare"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
if (i == 2)
|
||||
{
|
||||
// Update
|
||||
env(ripple::test::jtx::hook(alice, {{hso(accept_wasm)}}, 0),
|
||||
M("Update: prepare"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
auto jv = hso(accept_wasm);
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv.removeMember(jss::HookOn);
|
||||
|
||||
for (auto const& key : {jss::HookOnIncoming, jss::HookOnOutgoing})
|
||||
{
|
||||
jv[key] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"00";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Only Incomig/Outgoing HookOn"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"00";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("One Incomig/Outgoing HookOn and HookOn"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
jv.removeMember(key);
|
||||
jv.removeMember(jss::HookOn);
|
||||
}
|
||||
// Incoming == Outgoing
|
||||
jv[jss::HookOnIncoming] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000123";
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000123";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Incoming == Outgoing"),
|
||||
ter(temMALFORMED));
|
||||
jv.removeMember(jss::HookOnIncoming);
|
||||
jv.removeMember(jss::HookOnOutgoing);
|
||||
|
||||
// HookOn and both Fields
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::HookOnIncoming] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000001";
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000002";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("HookOn and both Fields"),
|
||||
ter(temMALFORMED));
|
||||
deleteHook(alice);
|
||||
deleteHook(bob);
|
||||
}
|
||||
|
||||
// Execution
|
||||
for (int i = 1; i < 3; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
// HookOn from HookDefinition object
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
// HookOn from Hook Object (definition: incoming/outgoing)
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"00";
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"01";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
if (i == 2)
|
||||
{
|
||||
// HookOn from Hook Object (definition: HookOn)
|
||||
auto jv = hso(accept_wasm);
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffffbfff"
|
||||
"ff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbfff"
|
||||
"fe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto hookExecuted = [this, &env]() -> bool {
|
||||
auto meta = env.meta();
|
||||
BEAST_EXPECT(meta);
|
||||
return meta->isFieldPresent(sfHookExecutions);
|
||||
};
|
||||
|
||||
// Check Incoming high
|
||||
env(invoke::invoke(bob),
|
||||
invoke::dest(alice),
|
||||
M("Incoming high"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(hookExecuted());
|
||||
// Check Incoming low
|
||||
env(pay(bob, alice, XRP(1)), M("Incoming low"), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!hookExecuted());
|
||||
// Check Outgoing high
|
||||
env(pay(alice, bob, XRP(1)), M("Outgoing high"), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(hookExecuted());
|
||||
// Check Outgoing low
|
||||
env(invoke::invoke(alice), M("Outgoing high"), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!hookExecuted());
|
||||
deleteHook(alice);
|
||||
}
|
||||
|
||||
{
|
||||
// sfHookOn from Hook Object (definition: incoming/outgoing)
|
||||
{
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfff"
|
||||
"ff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bfff"
|
||||
"fe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto jv = hso(accept_wasm);
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"00";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto hookExecuted = [this, &env]() -> bool {
|
||||
auto meta = env.meta();
|
||||
BEAST_EXPECT(meta);
|
||||
return meta->isFieldPresent(sfHookExecutions);
|
||||
};
|
||||
|
||||
// Check Incoming high
|
||||
env(invoke::invoke(bob),
|
||||
invoke::dest(alice),
|
||||
M("Incoming high"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(hookExecuted());
|
||||
// Check Incoming low
|
||||
env(pay(bob, alice, XRP(1)), M("Incoming low"), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(hookExecuted());
|
||||
// Check Outgoing high
|
||||
env(pay(alice, bob, XRP(1)), M("Outgoing high"), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(hookExecuted());
|
||||
// Check Outgoing low
|
||||
env(invoke::invoke(alice), M("Outgoing high"), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(hookExecuted());
|
||||
deleteHook(alice);
|
||||
}
|
||||
|
||||
// Fee RPC
|
||||
{
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfff"
|
||||
"ff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bfff"
|
||||
"fe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
|
||||
{
|
||||
// incoming high
|
||||
auto tx = invoke::invoke(bob);
|
||||
tx[jss::Destination] = alice.human();
|
||||
std::string const feeResult = "19";
|
||||
testFeeRPC(env, tx, feeResult);
|
||||
}
|
||||
{
|
||||
// incoming low
|
||||
auto tx = pay(bob, alice, XRP(1));
|
||||
std::string const feeResult = "10";
|
||||
testFeeRPC(env, tx, feeResult);
|
||||
}
|
||||
{
|
||||
// outgoing high
|
||||
auto tx = pay(alice, bob, XRP(1));
|
||||
std::string const feeResult = "19";
|
||||
testFeeRPC(env, tx, feeResult);
|
||||
}
|
||||
{
|
||||
// outgoing low
|
||||
auto tx = invoke::invoke(alice);
|
||||
std::string const feeResult = "10";
|
||||
testFeeRPC(env, tx, feeResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFillCopy(FeatureBitset features)
|
||||
{
|
||||
@@ -13846,6 +14226,8 @@ public:
|
||||
testNSDeletePartial(features);
|
||||
testPageCap(features);
|
||||
|
||||
testHookOnV2(features);
|
||||
|
||||
testFillCopy(features);
|
||||
|
||||
testWasm(features);
|
||||
|
||||
Reference in New Issue
Block a user