Remarks amendment (#301)

Co-authored-by: Denis Angell <dangell@transia.co>
This commit is contained in:
RichardAH
2025-04-16 08:37:58 +10:00
committed by Richard Holland
parent 59e334c099
commit f9cd2e0d21
22 changed files with 1284 additions and 5 deletions

View File

@@ -456,6 +456,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/Remit.cpp
src/ripple/app/tx/impl/SetAccount.cpp
src/ripple/app/tx/impl/SetHook.cpp
src/ripple/app/tx/impl/SetRemarks.cpp
src/ripple/app/tx/impl/SetRegularKey.cpp
src/ripple/app/tx/impl/SetSignerList.cpp
src/ripple/app/tx/impl/SetTrust.cpp
@@ -752,7 +753,10 @@ if (tests)
src/test/app/Remit_test.cpp
src/test/app/SHAMapStore_test.cpp
src/test/app/SetAuth_test.cpp
src/test/app/SetHook_test.cpp
src/test/app/SetHookTSH_test.cpp
src/test/app/SetRegularKey_test.cpp
src/test/app/SetRemarks_test.cpp
src/test/app/SetTrust_test.cpp
src/test/app/Taker_test.cpp
src/test/app/TheoreticalQuality_test.cpp
@@ -765,8 +769,6 @@ if (tests)
src/test/app/ValidatorKeys_test.cpp
src/test/app/ValidatorList_test.cpp
src/test/app/ValidatorSite_test.cpp
src/test/app/SetHook_test.cpp
src/test/app/SetHookTSH_test.cpp
src/test/app/Wildcard_test.cpp
src/test/app/XahauGenesis_test.cpp
src/test/app/tx/apply_test.cpp
@@ -900,6 +902,7 @@ if (tests)
src/test/jtx/impl/rate.cpp
src/test/jtx/impl/regkey.cpp
src/test/jtx/impl/reward.cpp
src/test/jtx/impl/remarks.cpp
src/test/jtx/impl/remit.cpp
src/test/jtx/impl/sendmax.cpp
src/test/jtx/impl/seq.cpp

View File

@@ -17,8 +17,8 @@
*/
//==============================================================================
#ifndef RIPPLE_TX_SIMPLE_PAYMENT_H_INCLUDED
#define RIPPLE_TX_SIMPLE_PAYMENT_H_INCLUDED
#ifndef RIPPLE_TX_REMIT_H_INCLUDED
#define RIPPLE_TX_REMIT_H_INCLUDED
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>

View File

@@ -0,0 +1,450 @@
//------------------------------------------------------------------------------
/*
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/SetRemarks.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/st.h>
namespace ripple {
TxConsequences
SetRemarks::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetRemarks::validateRemarks(STArray const& remarks, beast::Journal const& j)
{
std::set<Blob> already_seen;
if (remarks.empty() || remarks.size() > 32)
{
JLOG(j.warn()) << "SetRemarks: Cannot set more than 32 remarks (or "
"fewer than 1) in a txn.";
return temMALFORMED;
}
for (auto const& remark : remarks)
{
if (remark.getFName() != sfRemark)
{
JLOG(j.warn()) << "SetRemarks: contained non-sfRemark field.";
return temMALFORMED;
}
// will be checked by template system, extra check for security
if (!remark.isFieldPresent(sfRemarkName))
return temMALFORMED;
Blob const& name = remark.getFieldVL(sfRemarkName);
if (name.size() == 0 || name.size() > 256)
{
JLOG(j.warn()) << "SetRemarks: RemarkName cannot be empty or "
"larger than 256 chars.";
return temMALFORMED;
}
if (already_seen.find(name) != already_seen.end())
{
JLOG(j.warn()) << "SetRemarks: duplicate RemarkName entry.";
return temMALFORMED;
}
already_seen.emplace(name);
uint32_t flags =
remark.isFieldPresent(sfFlags) ? remark.getFieldU32(sfFlags) : 0;
if (flags != 0 && flags != tfImmutable)
{
JLOG(j.warn())
<< "SetRemarks: Flags must be either tfImmutable or 0";
return temMALFORMED;
}
if (!remark.isFieldPresent(sfRemarkValue))
{
if (flags & tfImmutable)
{
JLOG(j.warn()) << "SetRemarks: A remark deletion cannot be "
"marked immutable.";
return temMALFORMED;
}
continue;
}
Blob const& val = remark.getFieldVL(sfRemarkValue);
if (val.size() == 0 || val.size() > 256)
{
JLOG(j.warn()) << "SetRemarks: RemarkValue cannot be empty or "
"larger than 256 chars.";
return temMALFORMED;
}
}
return tesSUCCESS;
}
NotTEC
SetRemarks::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureRemarks))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto& tx = ctx.tx;
auto& j = ctx.j;
if (tx.getFlags() & tfUniversalMask)
{
JLOG(j.warn()) << "SetRemarks: Invalid flags set.";
return temINVALID_FLAG;
}
auto const& remarks = tx.getFieldArray(sfRemarks);
if (NotTEC result = validateRemarks(remarks, j); !isTesSuccess(result))
return result;
return preflight2(ctx);
}
template <typename T>
inline std::optional<AccountID>
getRemarksIssuer(T const& sleO)
{
std::optional<AccountID> issuer;
// check if it's an allowable object type
uint16_t lt = sleO->getFieldU16(sfLedgerEntryType);
switch (lt)
{
case ltACCOUNT_ROOT:
case ltOFFER:
case ltESCROW:
case ltTICKET:
case ltPAYCHAN:
case ltCHECK:
case ltDEPOSIT_PREAUTH: {
issuer = sleO->getAccountID(sfAccount);
break;
}
case ltNFTOKEN_OFFER: {
issuer = sleO->getAccountID(sfOwner);
break;
}
case ltURI_TOKEN: {
issuer = sleO->getAccountID(sfIssuer);
break;
}
case ltRIPPLE_STATE: {
// remarks can only be attached to a trustline by the issuer
AccountID lowAcc = sleO->getFieldAmount(sfLowLimit).getIssuer();
AccountID highAcc = sleO->getFieldAmount(sfHighLimit).getIssuer();
STAmount bal = sleO->getFieldAmount(sfBalance);
if (bal < beast::zero)
{
// low account is issuer
issuer = lowAcc;
break;
}
if (bal > beast::zero)
{
// high acccount is issuer
issuer = highAcc;
break;
}
// if the balance is zero we'll look for the side in default state
// and assume this is the issuer
uint32_t flags = sleO->getFieldU32(sfFlags);
bool const highReserve = (flags & lsfHighReserve);
bool const lowReserve = (flags & lsfLowReserve);
if (!highReserve && !lowReserve)
{
// error state
// do nothing, fallthru.
}
else if (highReserve && lowReserve)
{
// in this edge case we don't know who is the issuer, because
// there isn't a clear issuer. do nothing, fallthru.
}
else
{
issuer = (highReserve ? lowAcc : highAcc);
break;
}
}
}
return issuer;
}
TER
SetRemarks::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureRemarks))
return temDISABLED;
auto const id = ctx.tx[sfAccount];
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
return terNO_ACCOUNT;
auto const objID = ctx.tx[sfObjectID];
auto const sleO = ctx.view.read(keylet::unchecked(objID));
if (!sleO)
return tecNO_TARGET;
std::optional<AccountID> issuer = getRemarksIssuer(sleO);
if (!issuer || *issuer != id)
return tecNO_PERMISSION;
// sanity check the remarks merge between txn and obj
auto const& remarksTxn = ctx.tx.getFieldArray(sfRemarks);
std::map<Blob, std::pair<Blob, bool>> keys;
if (sleO->isFieldPresent(sfRemarks))
{
auto const& remarksObj = sleO->getFieldArray(sfRemarks);
// map the remark name to its value and whether it's immutable
for (auto const& remark : remarksObj)
keys.emplace(std::make_pair(
remark.getFieldVL(sfRemarkName),
std::make_pair(
remark.getFieldVL(sfRemarkValue),
remark.isFieldPresent(sfFlags) &&
remark.getFieldU32(sfFlags) & tfImmutable)));
}
int64_t count = keys.size();
for (auto const& remark : remarksTxn)
{
std::optional<Blob> valTxn;
if (remark.isFieldPresent(sfRemarkValue))
valTxn = remark.getFieldVL(sfRemarkValue);
bool const isDeletion = !valTxn.has_value();
Blob name = remark.getFieldVL(sfRemarkName);
if (keys.find(name) == keys.end())
{
// new remark
if (isDeletion)
{
// this could have been an error but deleting something
// that doesn't exist is traditionally not an error in xrpl
continue;
}
++count;
continue;
}
auto const& [valObj, immutable] = keys[name];
// even if it's mutable, if we don't mutate it that's a noop so just
// pass it
if (valTxn.has_value() && *valTxn == valObj)
continue;
if (immutable)
{
JLOG(ctx.j.warn())
<< "SetRemarks: attempt to mutate an immutable remark.";
return tecIMMUTABLE;
}
if (isDeletion)
{
if (--count < 0)
{
JLOG(ctx.j.warn()) << "SetRemarks: insane remarks accounting.";
return tecCLAIM;
}
}
}
if (count > 32)
{
JLOG(ctx.j.warn())
<< "SetRemarks: an object may have at most 32 remarks.";
return tecTOO_MANY_REMARKS;
}
return tesSUCCESS;
}
TER
SetRemarks::doApply()
{
auto j = ctx_.journal;
Sandbox sb(&ctx_.view());
auto const sle = sb.read(keylet::account(account_));
if (!sle)
return terNO_ACCOUNT;
auto const objID = ctx_.tx[sfObjectID];
auto sleO = sb.peek(keylet::unchecked(objID));
if (!sleO)
return terNO_ACCOUNT;
std::optional<AccountID> issuer = getRemarksIssuer(sleO);
if (!issuer || *issuer != account_)
return tecNO_PERMISSION;
auto const& remarksTxn = ctx_.tx.getFieldArray(sfRemarks);
std::map<Blob, std::pair<Blob, bool>> remarksMap;
if (sleO->isFieldPresent(sfRemarks))
{
auto const& remarksObj = sleO->getFieldArray(sfRemarks);
for (auto const& remark : remarksObj)
{
uint32_t flags = remark.isFieldPresent(sfFlags)
? remark.getFieldU32(sfFlags)
: 0;
bool const immutable = (flags & tfImmutable) != 0;
remarksMap[remark.getFieldVL(sfRemarkName)] = {
remark.getFieldVL(sfRemarkValue),
remark.isFieldPresent(sfFlags) && immutable};
}
}
for (auto const& remark : remarksTxn)
{
std::optional<Blob> val;
if (remark.isFieldPresent(sfRemarkValue))
val = remark.getFieldVL(sfRemarkValue);
Blob name = remark.getFieldVL(sfRemarkName);
bool const isDeletion = !val.has_value();
uint32_t flags =
remark.isFieldPresent(sfFlags) ? remark.getFieldU32(sfFlags) : 0;
bool const setImmutable = (flags & tfImmutable) != 0;
if (isDeletion)
{
if (remarksMap.find(name) != remarksMap.end())
remarksMap.erase(name);
continue;
}
if (remarksMap.find(name) == remarksMap.end())
{
remarksMap[name] = std::make_pair(*val, setImmutable);
continue;
}
remarksMap[name].first = *val;
if (!remarksMap[name].second)
remarksMap[name].second = setImmutable;
}
// canonically order
std::vector<Blob> keys;
for (auto const& [k, _] : remarksMap)
keys.push_back(k);
std::sort(keys.begin(), keys.end());
STArray newRemarks{sfRemarks, static_cast<int>(keys.size())};
for (auto const& k : keys)
{
STObject remark{sfRemark};
remark.setFieldVL(sfRemarkName, k);
remark.setFieldVL(sfRemarkValue, remarksMap[k].first);
if (remarksMap[k].second)
remark.setFieldU32(sfFlags, lsfImmutable);
newRemarks.push_back(std::move(remark));
}
if (newRemarks.size() > 32)
return tecTOO_MANY_REMARKS;
if (newRemarks.empty() && sleO->isFieldPresent(sfRemarks))
sleO->makeFieldAbsent(sfRemarks);
else
sleO->setFieldArray(sfRemarks, std::move(newRemarks));
sb.update(sleO);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
XRPAmount
SetRemarks::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount remarkFee{0};
if (tx.isFieldPresent(sfRemarks))
{
int64_t remarkBytes = 0;
auto const& remarks = tx.getFieldArray(sfRemarks);
for (auto const& remark : remarks)
{
int64_t entryBytes = 0;
if (remark.isFieldPresent(sfRemarkName))
{
entryBytes += remark.getFieldVL(sfRemarkName).size();
}
if (remark.isFieldPresent(sfRemarkValue))
{
entryBytes += remark.getFieldVL(sfRemarkValue).size();
}
// overflow
if (remarkBytes + entryBytes < remarkBytes)
return INITIAL_XRP;
remarkBytes += entryBytes;
}
// one drop per byte
remarkFee = XRPAmount{remarkBytes};
}
auto fee = Transactor::calculateBaseFee(view, tx);
return fee + remarkFee;
}
} // namespace ripple

View 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_SETREMARKS_H_INCLUDED
#define RIPPLE_TX_SETREMARKS_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 SetRemarks : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit SetRemarks(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;
static NotTEC
validateRemarks(STArray const& remarks, beast::Journal const& j);
};
} // namespace ripple
#endif

View File

@@ -44,6 +44,7 @@
#include <ripple/app/tx/impl/SetAccount.h>
#include <ripple/app/tx/impl/SetHook.h>
#include <ripple/app/tx/impl/SetRegularKey.h>
#include <ripple/app/tx/impl/SetRemarks.h>
#include <ripple/app/tx/impl/SetSignerList.h>
#include <ripple/app/tx/impl/SetTrust.h>
#include <ripple/app/tx/impl/URIToken.h>
@@ -169,6 +170,8 @@ invoke_preflight(PreflightContext const& ctx)
return invoke_preflight_helper<Invoke>(ctx);
case ttREMIT:
return invoke_preflight_helper<Remit>(ctx);
case ttREMARKS_SET:
return invoke_preflight_helper<SetRemarks>(ctx);
case ttURITOKEN_MINT:
case ttURITOKEN_BURN:
case ttURITOKEN_BUY:
@@ -290,6 +293,8 @@ invoke_preclaim(PreclaimContext const& ctx)
return invoke_preclaim<Invoke>(ctx);
case ttREMIT:
return invoke_preclaim<Remit>(ctx);
case ttREMARKS_SET:
return invoke_preclaim<SetRemarks>(ctx);
case ttURITOKEN_MINT:
case ttURITOKEN_BURN:
case ttURITOKEN_BUY:
@@ -373,6 +378,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
return Invoke::calculateBaseFee(view, tx);
case ttREMIT:
return Remit::calculateBaseFee(view, tx);
case ttREMARKS_SET:
return SetRemarks::calculateBaseFee(view, tx);
case ttURITOKEN_MINT:
case ttURITOKEN_BURN:
case ttURITOKEN_BUY:
@@ -556,6 +563,10 @@ invoke_apply(ApplyContext& ctx)
Remit p(ctx);
return p();
}
case ttREMARKS_SET: {
SetRemarks p(ctx);
return p();
}
case ttURITOKEN_MINT:
case ttURITOKEN_BURN:
case ttURITOKEN_BUY:

View File

@@ -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 = 80;
static constexpr std::size_t numFeatures = 81;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -362,6 +362,7 @@ extern uint256 const fix240819;
extern uint256 const fixPageCap;
extern uint256 const fix240911;
extern uint256 const fixFloatDivide;
extern uint256 const featureRemarks;
extern uint256 const featureTouch;
extern uint256 const fixReduceImport;
extern uint256 const fixXahauV3;

View File

@@ -315,6 +315,9 @@ enum LedgerSpecificFlags {
// ltURI_TOKEN
lsfBurnable = 0x00000001, // True, issuer can burn the token
// remarks
lsfImmutable = 1,
};
//------------------------------------------------------------------------------

View File

@@ -460,6 +460,7 @@ extern SF_UINT256 const sfNFTokenID;
extern SF_UINT256 const sfEmitParentTxnID;
extern SF_UINT256 const sfEmitNonce;
extern SF_UINT256 const sfEmitHookHash;
extern SF_UINT256 const sfObjectID;
// 256-bit (uncommon)
extern SF_UINT256 const sfBookDirectory;
@@ -539,6 +540,8 @@ extern SF_VL const sfHookReturnString;
extern SF_VL const sfHookParameterName;
extern SF_VL const sfHookParameterValue;
extern SF_VL const sfBlob;
extern SF_VL const sfRemarkName;
extern SF_VL const sfRemarkValue;
// account
extern SF_ACCOUNT const sfAccount;
@@ -596,6 +599,7 @@ extern SField const sfImportVLKey;
extern SField const sfHookEmission;
extern SField const sfMintURIToken;
extern SField const sfAmountEntry;
extern SField const sfRemark;
// array of objects (common)
// ARRAY/1 is reserved for end of array
@@ -624,6 +628,7 @@ extern SField const sfActiveValidators;
extern SField const sfImportVLKeys;
extern SField const sfHookEmissions;
extern SField const sfAmounts;
extern SField const sfRemarks;
//------------------------------------------------------------------------------

View File

@@ -341,6 +341,8 @@ enum TECcodes : TERUnderlyingType {
tecXCHAIN_SELF_COMMIT = 185, // RESERVED - XCHAIN
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 186, // RESERVED - XCHAIN
tecINSUF_RESERVE_SELLER = 187,
tecIMMUTABLE = 188,
tecTOO_MANY_REMARKS = 189,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -191,6 +191,9 @@ enum ClaimRewardFlags : uint32_t {
};
constexpr std::uint32_t const tfClaimRewardMask = ~(tfUniversal | tfOptOut);
// Remarks flags:
constexpr std::uint32_t const tfImmutable = 1;
// clang-format on
} // namespace ripple

View File

@@ -146,6 +146,9 @@ enum TxType : std::uint16_t
ttURITOKEN_CREATE_SELL_OFFER = 48,
ttURITOKEN_CANCEL_SELL_OFFER = 49,
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
ttREMARKS_SET = 94,
/* A payment transactor that delivers only the exact amounts specified, creating accounts and TLs as needed
* that the sender pays for. */
ttREMIT = 95,

View File

@@ -468,6 +468,7 @@ REGISTER_FIX (fix240819, Supported::yes, VoteBehavior::De
REGISTER_FIX (fixPageCap, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixFloatDivide, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(Remarks, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(Touch, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultYes);

View File

@@ -159,6 +159,14 @@ InnerObjectFormats::InnerObjectFormats()
{sfDigest, soeOPTIONAL},
{sfFlags, soeOPTIONAL},
});
add(sfRemark.jsonName.c_str(),
sfRemark.getCode(),
{
{sfRemarkName, soeREQUIRED},
{sfRemarkValue, soeOPTIONAL},
{sfFlags, soeOPTIONAL},
});
}
InnerObjectFormats const&

View File

@@ -31,6 +31,7 @@ LedgerFormats::LedgerFormats()
{sfLedgerIndex, soeOPTIONAL},
{sfLedgerEntryType, soeREQUIRED},
{sfFlags, soeREQUIRED},
{sfRemarks, soeOPTIONAL},
};
add(jss::AccountRoot,

View File

@@ -211,6 +211,7 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenID, "NFTokenID", UINT256,
CONSTRUCT_TYPED_SFIELD(sfEmitParentTxnID, "EmitParentTxnID", UINT256, 11);
CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", UINT256, 12);
CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", UINT256, 13);
CONSTRUCT_TYPED_SFIELD(sfObjectID, "ObjectID", UINT256, 14);
// 256-bit (uncommon)
CONSTRUCT_TYPED_SFIELD(sfBookDirectory, "BookDirectory", UINT256, 16);
@@ -292,6 +293,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL,
CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24);
CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25);
CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26);
CONSTRUCT_TYPED_SFIELD(sfRemarkValue, "RemarkValue", VL, 98);
CONSTRUCT_TYPED_SFIELD(sfRemarkName, "RemarkName", VL, 99);
// account
CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1);
@@ -346,6 +349,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfHookExecution, "HookExecution", OBJECT,
CONSTRUCT_UNTYPED_SFIELD(sfHookDefinition, "HookDefinition", OBJECT, 22);
CONSTRUCT_UNTYPED_SFIELD(sfHookParameter, "HookParameter", OBJECT, 23);
CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, 24);
CONSTRUCT_UNTYPED_SFIELD(sfRemark, "Remark", OBJECT, 97);
CONSTRUCT_UNTYPED_SFIELD(sfGenesisMint, "GenesisMint", OBJECT, 96);
CONSTRUCT_UNTYPED_SFIELD(sfActiveValidator, "ActiveValidator", OBJECT, 95);
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, 94);
@@ -372,6 +376,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfDisabledValidators, "DisabledValidators", ARRAY,
CONSTRUCT_UNTYPED_SFIELD(sfHookExecutions, "HookExecutions", ARRAY, 18);
CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19);
CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20);
CONSTRUCT_UNTYPED_SFIELD(sfRemarks, "Remarks", ARRAY, 97);
CONSTRUCT_UNTYPED_SFIELD(sfGenesisMints, "GenesisMints", ARRAY, 96);
CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, 95);
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94);

View File

@@ -92,6 +92,8 @@ transResults()
MAKE_ERROR(tecREQUIRES_FLAG, "The transaction or part-thereof requires a flag that wasn't set."),
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
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(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."),

View File

@@ -456,6 +456,14 @@ TxFormats::TxFormats()
{sfTicketSequence, soeOPTIONAL},
},
commonFields);
add(jss::SetRemarks,
ttREMARKS_SET,
{
{sfObjectID, soeREQUIRED},
{sfRemarks, soeREQUIRED},
},
commonFields);
}
TxFormats const&

View File

@@ -122,6 +122,7 @@ JSS(Remit); // transaction type.
JSS(RippleState); // ledger type.
JSS(SLE_hit_rate); // out: GetCounts.
JSS(SetFee); // transaction type.
JSS(SetRemarks); // transaction type
JSS(UNLModify); // transaction type.
JSS(UNLReport); // transaction type.
JSS(SettleDelay); // in: TransactionSign

View File

@@ -0,0 +1,591 @@
//------------------------------------------------------------------------------
/*
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/core/ConfigSections.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/jss.h>
#include <sstream>
#include <test/jtx.h>
namespace ripple {
namespace test {
struct SetRemarks_test : public beast::unit_test::suite
{
// debugRemarks(env, keylet::account(alice).key);
void
debugRemarks(jtx::Env& env, uint256 const& id)
{
Json::Value params;
params[jss::index] = strHex(id);
auto const info = env.rpc("json", "ledger_entry", to_string(params));
std::cout << "INFO: " << info << "\n";
}
void
validateRemarks(
ReadView const& view,
uint256 const& id,
std::vector<jtx::remarks::remark> const& marks)
{
using namespace jtx;
auto const slep = view.read(keylet::unchecked(id));
if (slep && slep->isFieldPresent(sfRemarks))
{
auto const& remarksObj = slep->getFieldArray(sfRemarks);
BEAST_EXPECT(remarksObj.size() == marks.size());
for (int i = 0; i < marks.size(); ++i)
{
remarks::remark const expectedMark = marks[i];
STObject const remark = remarksObj[i];
Blob name = remark.getFieldVL(sfRemarkName);
// BEAST_EXPECT(expectedMark.name == name);
uint32_t flags = remark.isFieldPresent(sfFlags)
? remark.getFieldU32(sfFlags)
: 0;
BEAST_EXPECT(expectedMark.flags == flags);
std::optional<Blob> val;
if (remark.isFieldPresent(sfRemarkValue))
val = remark.getFieldVL(sfRemarkValue);
// BEAST_EXPECT(expectedMark.value == val);
}
}
}
void
testEnabled(FeatureBitset features)
{
testcase("enabled");
using namespace jtx;
// setup env
auto const alice = Account("alice");
auto const bob = Account("bob");
for (bool const withRemarks : {false, true})
{
// If the Remarks amendment is not enabled, you cannot add remarks
auto const amend =
withRemarks ? features : features - featureRemarks;
Env env{*this, amend};
env.fund(XRP(1000), alice, bob);
env.close();
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
};
auto const txResult =
withRemarks ? ter(tesSUCCESS) : ter(temDISABLED);
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
txResult);
env.close();
}
}
void
testPreflightInvalid(FeatureBitset features)
{
testcase("preflight invalid");
using namespace jtx;
// setup env
auto const alice = Account("alice");
auto const bob = Account("bob");
Env env{*this, features};
env.fund(XRP(1000), alice, bob);
env.close();
//----------------------------------------------------------------------
// preflight
// temDISABLED
// DA: testEnabled()
// temINVALID_FLAG: SetRemarks: Invalid flags set.
{
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
txflags(tfClose),
fee(XRP(1)),
ter(temINVALID_FLAG));
env.close();
}
// temMALFORMED: SetRemarks: Cannot set more than 32 remarks (or fewer
// than 1) in a txn.
{
std::vector<remarks::remark> marks;
for (int i = 0; i < 0; ++i)
{
marks.push_back({"CAFE", "DEADBEEF", 0});
}
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: Cannot set more than 32 remarks (or fewer
// than 1) in a txn.
{
std::vector<remarks::remark> marks;
for (int i = 0; i < 33; ++i)
{
marks.push_back({"CAFE", "DEADBEEF", 0});
}
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: contained non-sfRemark field.
{
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
};
Json::Value jv;
jv[jss::TransactionType] = jss::SetRemarks;
jv[jss::Account] = alice.human();
jv[sfObjectID.jsonName] = strHex(keylet::account(alice).key);
auto& ja = jv[sfRemarks.getJsonName()];
for (std::size_t i = 0; i < 1; ++i)
{
ja[i][sfGenesisMint.jsonName] = Json::Value{};
ja[i][sfGenesisMint.jsonName][jss::Amount] =
STAmount(1).getJson(JsonOptions::none);
ja[i][sfGenesisMint.jsonName][jss::Destination] = bob.human();
}
jv[sfRemarks.jsonName] = ja;
env(jv, fee(XRP(1)), ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: duplicate RemarkName entry.
{
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
{"CAFE", "DEADBEEF", 0},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: RemarkName cannot be empty or larger than
// 256 chars.
{
std::vector<remarks::remark> marks = {
{"", "DEADBEEF", 0},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: RemarkName cannot be empty or larger than
// 256 chars.
{
std::string const name((256 * 2) + 1, 'A');
std::vector<remarks::remark> marks = {
{name, "DEADBEEF", 0},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: Flags must be either tfImmutable or 0
{
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 2},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: A remark deletion cannot be marked
// immutable.
{
std::vector<remarks::remark> marks = {
{"CAFE", std::nullopt, 1},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: RemarkValue cannot be empty or larger than
// 256 chars.
{
std::vector<remarks::remark> marks = {
{"CAFE", "", 0},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
// temMALFORMED: SetRemarks: RemarkValue cannot be empty or larger than
// 256 chars.
{
std::string const value((256 * 2) + 1, 'A');
std::vector<remarks::remark> marks = {
{"CAFE", value, 0},
};
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(temMALFORMED));
env.close();
}
}
void
testPreclaimInvalid(FeatureBitset features)
{
testcase("preclaim invalid");
using namespace jtx;
// setup env
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.memoize(carol);
env.fund(XRP(1000), alice, bob);
env.close();
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
};
//----------------------------------------------------------------------
// preclaim
// temDISABLED
// DA: testEnabled()
// terNO_ACCOUNT - account doesnt exist
{
auto const carol = Account("carol");
env.memoize(carol);
auto tx =
remarks::setRemarks(carol, keylet::account(carol).key, marks);
tx[jss::Sequence] = 0;
env(tx, carol, fee(XRP(1)), ter(terNO_ACCOUNT));
env.close();
}
// tecNO_TARGET - object doesnt exist
{
env(remarks::setRemarks(alice, keylet::account(carol).key, marks),
fee(XRP(1)),
ter(tecNO_TARGET));
env.close();
}
// tecNO_PERMISSION: !issuer
{
env(deposit::auth(bob, alice));
env(remarks::setRemarks(
alice, keylet::depositPreauth(bob, alice).key, marks),
fee(XRP(1)),
ter(tecNO_PERMISSION));
env.close();
}
// tecNO_PERMISSION: issuer != _account
{
env(remarks::setRemarks(alice, keylet::account(bob).key, marks),
fee(XRP(1)),
ter(tecNO_PERMISSION));
env.close();
}
// tecIMMUTABLE: SetRemarks: attempt to mutate an immutable remark.
{
// alice creates immutable remark
std::vector<remarks::remark> immutableMarks = {
{"CAFF", "DEAD", tfImmutable},
};
env(remarks::setRemarks(
alice, keylet::account(alice).key, immutableMarks),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// alice cannot update immutable remark
std::vector<remarks::remark> badMarks = {
{"CAFF", "DEADBEEF", 0},
};
env(remarks::setRemarks(
alice, keylet::account(alice).key, badMarks),
fee(XRP(1)),
ter(tecIMMUTABLE));
env.close();
}
// tecCLAIM: SetRemarks: insane remarks accounting.
{} // tecTOO_MANY_REMARKS: SetRemarks: an object may have at most 32
// remarks.
{
std::vector<remarks::remark> _marks;
unsigned int hexValue = 0xEFAC;
for (int i = 0; i < 31; ++i)
{
std::stringstream ss;
ss << std::hex << std::uppercase << hexValue;
_marks.push_back({ss.str(), "DEADBEEF", 0});
hexValue++;
}
env(remarks::setRemarks(alice, keylet::account(alice).key, _marks),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
fee(XRP(1)),
ter(tecTOO_MANY_REMARKS));
env.close();
}
}
void
testDoApplyInvalid(FeatureBitset features)
{
testcase("doApply invalid");
using namespace jtx;
//----------------------------------------------------------------------
// doApply
// terNO_ACCOUNT
// tecNO_TARGET
// tecNO_PERMISSION
// tecTOO_MANY_REMARKS
}
void
testDelete(FeatureBitset features)
{
testcase("delete");
using namespace jtx;
// setup env
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(1000), alice, bob);
env.close();
auto const id = keylet::account(alice).key;
// Set Remarks
{
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
};
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// Delete Remarks
{
std::vector<remarks::remark> marks = {
{"CAFE", std::nullopt, 0},
};
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, {});
}
}
void
testLedgerObjects(FeatureBitset features)
{
testcase("ledger objects");
using namespace jtx;
// setup env
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const USD = gw["USD"];
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(10000), alice);
env.trust(USD(10000), bob);
env.close();
env(pay(gw, alice, USD(1000)));
env(pay(gw, bob, USD(1000)));
env.close();
std::vector<remarks::remark> marks = {
{"CAFE", "DEADBEEF", 0},
};
// ltACCOUNT_ROOT
{
auto const id = keylet::account(alice).key;
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltOFFER
{
auto const id = keylet::offer(alice, env.seq(alice)).key;
env(offer(alice, XRP(10), USD(10)), fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltESCROW
{
using namespace std::literals::chrono_literals;
auto const id = keylet::escrow(alice, env.seq(alice)).key;
env(escrow::create(alice, bob, XRP(10)),
escrow::finish_time(env.now() + 1s),
fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltTICKET
{
auto const id = keylet::ticket(alice, env.seq(alice) + 1).key;
env(ticket::create(alice, 10), fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltPAYCHAN
{
using namespace std::literals::chrono_literals;
auto const id = keylet::payChan(alice, bob, env.seq(alice)).key;
auto const pk = alice.pk();
auto const settleDelay = 100s;
env(paychan::create(alice, bob, XRP(10), settleDelay, pk),
fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltCHECK
{
auto const id = keylet::check(alice, env.seq(alice)).key;
env(check::create(alice, bob, XRP(10)), fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltDEPOSIT_PREAUTH
{
env(fset(bob, asfDepositAuth));
auto const id = keylet::depositPreauth(alice, bob).key;
env(deposit::auth(alice, bob), fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltURI_TOKEN
{
std::string const uri(256, 'A');
auto const id =
keylet::uritoken(alice, Blob(uri.begin(), uri.end())).key;
env(uritoken::mint(alice, uri), fee(XRP(1)));
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltRIPPLE_STATE: bal < 0
{
auto const alice2 = Account("alice2");
env.fund(XRP(1000), alice2);
env.close();
env.trust(USD(10000), alice2);
auto const id = keylet::line(alice2, USD).key;
env(pay(gw, alice2, USD(1000)));
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltRIPPLE_STATE: bal > 0
{
auto const carol0 = Account("carol0");
env.fund(XRP(1000), carol0);
env.close();
env.trust(USD(10000), carol0);
auto const id = keylet::line(carol0, USD).key;
env(pay(gw, carol0, USD(1000)));
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltRIPPLE_STATE: highReserve
{
auto const dan1 = Account("dan1");
env.fund(XRP(1000), dan1);
env.close();
env.trust(USD(1000), dan1);
auto const id = keylet::line(dan1, USD).key;
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
// ltRIPPLE_STATE: lowReserve
{
auto const bob0 = Account("bob0");
env.fund(XRP(1000), bob0);
env.close();
env.trust(USD(1000), bob0);
auto const id = keylet::line(bob0, USD).key;
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
env.close();
validateRemarks(*env.current(), id, marks);
}
}
void
testWithFeats(FeatureBitset features)
{
testEnabled(features);
testPreflightInvalid(features);
testPreclaimInvalid(features);
testDoApplyInvalid(features);
testDelete(features);
testLedgerObjects(features);
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
}
};
BEAST_DEFINE_TESTSUITE(SetRemarks, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -57,6 +57,7 @@
#include <test/jtx/quality.h>
#include <test/jtx/rate.h>
#include <test/jtx/regkey.h>
#include <test/jtx/remarks.h>
#include <test/jtx/remit.h>
#include <test/jtx/require.h>
#include <test/jtx/requires.h>

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 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/remarks.h>
namespace ripple {
namespace test {
namespace jtx {
namespace remarks {
Json::Value
setRemarks(
jtx::Account const& account,
uint256 const& id,
std::vector<remark> const& marks)
{
using namespace jtx;
Json::Value jv;
jv[jss::TransactionType] = jss::SetRemarks;
jv[jss::Account] = account.human();
jv[sfObjectID.jsonName] = strHex(id);
auto& ja = jv[sfRemarks.getJsonName()];
for (std::size_t i = 0; i < marks.size(); ++i)
{
ja[i][sfRemark.jsonName] = Json::Value{};
ja[i][sfRemark.jsonName][sfRemarkName.jsonName] = marks[i].name;
if (marks[i].value)
ja[i][sfRemark.jsonName][sfRemarkValue.jsonName] = *marks[i].value;
if (marks[i].flags)
ja[i][sfRemark.jsonName][sfFlags.jsonName] = *marks[i].flags;
}
jv[sfRemarks.jsonName] = ja;
return jv;
}
} // namespace remarks
} // namespace jtx
} // namespace test
} // namespace ripple

64
src/test/jtx/remarks.h Normal file
View File

@@ -0,0 +1,64 @@
//------------------------------------------------------------------------------
/*
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_TEST_JTX_REMARKS_H_INCLUDED
#define RIPPLE_TEST_JTX_REMARKS_H_INCLUDED
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
namespace ripple {
namespace test {
namespace jtx {
namespace remarks {
struct remark
{
std::string name;
std::optional<std::string> value;
std::optional<std::uint32_t> flags;
remark(
std::string name_,
std::optional<std::string> value_ = std::nullopt,
std::optional<std::uint32_t> flags_ = std::nullopt)
: name(name_), value(value_), flags(flags_)
{
if (value_)
value = *value_;
if (flags_)
flags = *flags_;
}
};
Json::Value
setRemarks(
jtx::Account const& account,
uint256 const& id,
std::vector<remark> const& marks);
} // namespace remarks
} // namespace jtx
} // namespace test
} // namespace ripple
#endif // RIPPLE_TEST_JTX_REMARKS_H_INCLUDED