Compare commits

...

7 Commits

Author SHA1 Message Date
RichardAH
bbacb6296d Merge branch 'dev' into patch-ctid 2025-04-24 16:38:40 +10:00
tequ
372f25d09b Remove #ifndef DEBUG guards and exception handling wrappers (#496) 2025-04-24 16:38:14 +10:00
Denis Angell
401395a204 patch remarks (#497) 2025-04-24 16:36:57 +10:00
Denis Angell
eef47da061 fix: ledger_index 2025-04-24 01:58:41 +02:00
tequ
4221dcf568 Add tests for SetRemarks (#491) 2025-04-18 09:34:44 +10:00
tequ
989532702d Update clang-format workflow (#490) 2025-04-17 16:16:59 +10:00
RichardAH
f9cd2e0d21 Remarks amendment (#301)
Co-authored-by: Denis Angell <dangell@transia.co>
2025-04-16 08:42:04 +10:00
28 changed files with 1306 additions and 42 deletions

View File

@@ -4,21 +4,32 @@ on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
env:
CLANG_VERSION: 10
steps:
- uses: actions/checkout@v3
- name: Install clang-format
# - name: Install clang-format
# run: |
# codename=$( lsb_release --codename --short )
# sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null <<EOF
# deb http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${CLANG_VERSION} main
# deb-src http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${CLANG_VERSION} main
# EOF
# wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add
# sudo apt-get update -y
# sudo apt-get install -y clang-format-${CLANG_VERSION}
# Temporary fix until this commit is merged
# https://github.com/XRPLF/rippled/commit/552377c76f55b403a1c876df873a23d780fcc81c
- name: Download and install clang-format
run: |
codename=$( lsb_release --codename --short )
sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null <<EOF
deb http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${CLANG_VERSION} main
deb-src http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${CLANG_VERSION} main
EOF
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add
sudo apt-get update
sudo apt-get install clang-format-${CLANG_VERSION}
sudo apt-get update -y
sudo apt-get install -y libtinfo5
curl -LO https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.1/clang+llvm-10.0.1-x86_64-linux-gnu-ubuntu-16.04.tar.xz
tar -xf clang+llvm-10.0.1-x86_64-linux-gnu-ubuntu-16.04.tar.xz
sudo mv clang+llvm-10.0.1-x86_64-linux-gnu-ubuntu-16.04 /opt/clang-10
sudo ln -s /opt/clang-10/bin/clang-format /usr/local/bin/clang-format-10
- name: Format src/ripple
run: find src/ripple -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.ipp' \) -print0 | xargs -0 clang-format-${CLANG_VERSION} -i
- name: Format src/test

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

@@ -498,15 +498,11 @@ RCLConsensus::Adaptor::doAccept(
for (auto const& item : *result.txns.map_)
{
#ifndef DEBUG
try
{
#endif
retriableTxs.insert(
std::make_shared<STTx const>(SerialIter{item.slice()}));
JLOG(j_.debug()) << " Tx: " << item.key();
#ifndef DEBUG
}
catch (std::exception const& ex)
{
@@ -514,7 +510,6 @@ RCLConsensus::Adaptor::doAccept(
JLOG(j_.warn())
<< " Tx: " << item.key() << " throws: " << ex.what();
}
#endif
}
auto built = buildLCL(

View File

@@ -116,10 +116,8 @@ applyTransactions(
{
auto const txid = it->first.getTXID();
#ifndef DEBUG
try
{
#endif
if (pass == 0 && built->txExists(txid))
{
it = txns.erase(it);
@@ -142,7 +140,6 @@ applyTransactions(
case ApplyResult::Retry:
++it;
}
#ifndef DEBUG
}
catch (std::exception const& ex)
{
@@ -151,7 +148,6 @@ applyTransactions(
failed.insert(txid);
it = txns.erase(it);
}
#endif
}
JLOG(j.debug()) << (certainRetry ? "Pass: " : "Final pass: ") << pass

View File

@@ -44,8 +44,7 @@ convertBlobsToTxResult(
auto tr = std::make_shared<Transaction>(txn, reason, app);
auto metaset =
std::make_shared<TxMeta>(tr->getID(), tr->getLedger(), rawMeta);
auto metaset = std::make_shared<TxMeta>(tr->getID(), ledger_index, rawMeta);
// if properly formed meta is available we can use it to generate ctid
if (metaset->getAsObject().isFieldPresent(sfTransactionIndex))

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/TxFlags.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()
{
Sandbox sb(&ctx_.view());
auto const sle = sb.read(keylet::account(account_));
if (!sle)
return tefINTERNAL;
auto const objID = ctx_.tx[sfObjectID];
auto sleO = sb.peek(keylet::unchecked(objID));
if (!sleO)
return tefINTERNAL;
std::optional<AccountID> issuer = getRemarksIssuer(sleO);
if (!issuer || *issuer != account_)
return tefINTERNAL;
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 tefINTERNAL;
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

@@ -168,10 +168,8 @@ applyTransaction(
JLOG(j.debug()) << "TXN " << txn.getTransactionID()
<< (retryAssured ? "/retry" : "/final");
#ifndef DEBUG
try
{
#endif
auto const result = apply(app, view, txn, flags, j);
if (result.second)
{
@@ -191,14 +189,12 @@ applyTransaction(
JLOG(j.debug()) << "Transaction retry: " << transHuman(result.first);
return ApplyResult::Retry;
#ifndef DEBUG
}
catch (std::exception const& ex)
{
JLOG(j.warn()) << "Throws: " << ex.what();
return ApplyResult::Fail;
}
#endif
}
} // namespace ripple

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:
@@ -580,19 +591,15 @@ preflight(
{
PreflightContext const pfctx(app, tx, rules, flags, j);
#ifndef DEBUG
try
{
#endif
return {pfctx, invoke_preflight(pfctx)};
#ifndef DEBUG
}
catch (std::exception const& e)
{
JLOG(j.fatal()) << "apply: " << e.what();
return {pfctx, {tefEXCEPTION, TxConsequences{tx}}};
}
#endif
}
PreclaimResult
@@ -629,21 +636,17 @@ preclaim(
preflightResult.j);
}
#ifndef DEBUG
try
{
#endif
if (!isTesSuccess(ctx->preflightResult))
return {*ctx, ctx->preflightResult};
return {*ctx, invoke_preclaim(*ctx)};
#ifndef DEBUG
}
catch (std::exception const& e)
{
JLOG(ctx->j.fatal()) << "apply: " << e.what();
return {*ctx, tefEXCEPTION};
}
#endif
}
XRPAmount
@@ -667,10 +670,8 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view)
// info to recover.
return {tefEXCEPTION, false};
}
#ifndef DEBUG
try
{
#endif
if (!preclaimResult.likelyToClaimFee)
return {preclaimResult.ter, false};
@@ -683,14 +684,12 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view)
preclaimResult.flags,
preclaimResult.j);
return invoke_apply(ctx);
#ifndef DEBUG
}
catch (std::exception const& e)
{
JLOG(preclaimResult.j.fatal()) << "apply: " << e.what();
return {tefEXCEPTION, false};
}
#endif
}
} // namespace ripple

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

@@ -396,6 +396,8 @@ private:
return "SetRegularKey";
if (inp == "HookSet")
return "SetHook";
if (inp == "RemarksSet")
return "SetRemarks";
return inp;
};

View File

@@ -0,0 +1,589 @@
//------------------------------------------------------------------------------
/*
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
// All checks in doApply are done in preclaim.
BEAST_EXPECT(1);
}
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