Compare commits

...

44 Commits

Author SHA1 Message Date
Richard Holland
6b49032436 feature count 2025-04-15 20:19:44 +10:00
RichardAH
7a62559da9 Merge branch 'dev' into remarks 2025-04-15 20:09:33 +10:00
Denis Angell
d7dd6196e8 fix test 2025-04-15 11:06:56 +02:00
Richard Holland
d3cfd46af3 add 1 to feature count 2025-04-15 17:15:40 +10:00
Richard Holland
94fab7d58b tx flags 2025-04-15 14:36:07 +10:00
Richard Holland
53b3b543a7 cleanup 2025-04-15 14:14:07 +10:00
Richard Holland
69e72ecb91 ensure numerically 0 value blob isnt a deletion 2025-04-15 13:59:42 +10:00
Richard Holland
98a33d11e0 change tem code 2025-04-15 13:54:59 +10:00
Richard Holland
c908018647 re-order check 2025-04-15 13:51:45 +10:00
RichardAH
c6ddd6d2c4 Merge branch 'dev' into remarks 2025-04-15 13:42:28 +10:00
RichardAH
78906ee086 Merge branch 'dev' into remarks 2025-04-09 17:14:04 +10:00
RichardAH
987247ddc1 Merge branch 'dev' into remarks 2024-11-20 12:15:32 +10:00
RichardAH
a5e2fd0699 Merge branch 'dev' into remarks 2024-11-09 15:27:55 +10:00
RichardAH
d92403ce35 Merge branch 'dev' into remarks 2024-11-09 13:45:31 +10:00
Denis Angell
6fb8fef883 clang-format 2024-09-19 16:29:18 +02:00
Denis Angell
a8a4774232 add tests 2024-09-19 16:27:33 +02:00
Denis Angell
eaec08471b Merge branch 'dev' into remarks 2024-09-19 14:41:53 +02:00
Denis Angell
caffeea6fc Merge branch 'dev' into remarks 2024-07-08 09:59:26 +02:00
Denis Angell
23d49d0548 Merge branch 'dev' into remarks 2024-05-23 08:23:54 +02:00
Denis Angell
519ab34e4f add more tests 2024-04-03 15:28:23 +02:00
Denis Angell
bdc59ac4ec apply sandbox and fixup 2024-04-03 15:28:07 +02:00
Denis Angell
96bb67bfe5 clang-format 2024-04-02 17:12:59 +02:00
Denis Angell
798212f87c add tests 2024-04-02 17:08:46 +02:00
Denis Angell
a3d61c0fbf make sfRemarkValue Optional 2024-04-02 16:55:31 +02:00
Denis Angell
3e926c9946 add remark fee 2024-04-02 16:55:15 +02:00
Denis Angell
4392342c99 update error warning 2024-04-02 16:54:47 +02:00
Denis Angell
f4fe7b7d9a add jtx helper 2024-04-02 16:53:33 +02:00
Richard Holland
d268638a39 whoops 2024-03-27 03:36:01 +00:00
Richard Holland
b1447afcc0 refactor, feature enable check 2024-03-27 02:22:02 +00:00
Denis Angell
f40621c662 Update mulDiv.cpp 2024-03-25 22:18:01 +01:00
Denis Angell
36ff48474a Revert "fix muldiv"
This reverts commit 63b0245d06.
2024-03-25 22:05:09 +01:00
Denis Angell
2adc234bf1 Update SetRemarks_test.cpp 2024-03-25 17:39:43 +01:00
Denis Angell
89bcacca5b use sandbox and peak 2024-03-25 17:30:26 +01:00
Denis Angell
6d496cc16f create set remarks test 2024-03-25 17:06:51 +01:00
Denis Angell
63b0245d06 fix muldiv 2024-03-25 17:06:39 +01:00
Denis Angell
fdf02a3853 Update SetRemarks.cpp 2024-03-25 17:06:32 +01:00
Denis Angell
9edf7ae67a create SetRemarks header 2024-03-25 17:06:29 +01:00
Denis Angell
533ba7ab75 nit: remit headerfile 2024-03-25 17:05:37 +01:00
Denis Angell
4e10d7d61f fix applySteps headers 2024-03-25 17:05:25 +01:00
Denis Angell
01e7caa0d6 fix applyHook headers 2024-03-25 17:05:15 +01:00
Denis Angell
349f4d2d68 reorder cmake 2024-03-25 17:05:04 +01:00
Richard Holland
24ac5d5f51 bug fixes, but levelisation issues 2024-03-25 06:58:31 +00:00
Richard Holland
8522c6684b transactor 2024-03-25 00:55:42 +00:00
Richard Holland
7efc26a8b1 initial version of remarks 2024-03-25 00:54:08 +00:00
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/Remit.cpp
src/ripple/app/tx/impl/SetAccount.cpp src/ripple/app/tx/impl/SetAccount.cpp
src/ripple/app/tx/impl/SetHook.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/SetRegularKey.cpp
src/ripple/app/tx/impl/SetSignerList.cpp src/ripple/app/tx/impl/SetSignerList.cpp
src/ripple/app/tx/impl/SetTrust.cpp src/ripple/app/tx/impl/SetTrust.cpp
@@ -752,7 +753,10 @@ if (tests)
src/test/app/Remit_test.cpp src/test/app/Remit_test.cpp
src/test/app/SHAMapStore_test.cpp src/test/app/SHAMapStore_test.cpp
src/test/app/SetAuth_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/SetRegularKey_test.cpp
src/test/app/SetRemarks_test.cpp
src/test/app/SetTrust_test.cpp src/test/app/SetTrust_test.cpp
src/test/app/Taker_test.cpp src/test/app/Taker_test.cpp
src/test/app/TheoreticalQuality_test.cpp src/test/app/TheoreticalQuality_test.cpp
@@ -765,8 +769,6 @@ if (tests)
src/test/app/ValidatorKeys_test.cpp src/test/app/ValidatorKeys_test.cpp
src/test/app/ValidatorList_test.cpp src/test/app/ValidatorList_test.cpp
src/test/app/ValidatorSite_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/Wildcard_test.cpp
src/test/app/XahauGenesis_test.cpp src/test/app/XahauGenesis_test.cpp
src/test/app/tx/apply_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/rate.cpp
src/test/jtx/impl/regkey.cpp src/test/jtx/impl/regkey.cpp
src/test/jtx/impl/reward.cpp src/test/jtx/impl/reward.cpp
src/test/jtx/impl/remarks.cpp
src/test/jtx/impl/remit.cpp src/test/jtx/impl/remit.cpp
src/test/jtx/impl/sendmax.cpp src/test/jtx/impl/sendmax.cpp
src/test/jtx/impl/seq.cpp src/test/jtx/impl/seq.cpp

View File

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

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how // 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 // 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. // 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. /** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated 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 fixPageCap;
extern uint256 const fix240911; extern uint256 const fix240911;
extern uint256 const fixFloatDivide; extern uint256 const fixFloatDivide;
extern uint256 const featureRemarks;
extern uint256 const featureTouch; extern uint256 const featureTouch;
extern uint256 const fixReduceImport; extern uint256 const fixReduceImport;
extern uint256 const fixXahauV3; extern uint256 const fixXahauV3;

View File

@@ -315,6 +315,9 @@ enum LedgerSpecificFlags {
// ltURI_TOKEN // ltURI_TOKEN
lsfBurnable = 0x00000001, // True, issuer can burn the 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 sfEmitParentTxnID;
extern SF_UINT256 const sfEmitNonce; extern SF_UINT256 const sfEmitNonce;
extern SF_UINT256 const sfEmitHookHash; extern SF_UINT256 const sfEmitHookHash;
extern SF_UINT256 const sfObjectID;
// 256-bit (uncommon) // 256-bit (uncommon)
extern SF_UINT256 const sfBookDirectory; extern SF_UINT256 const sfBookDirectory;
@@ -539,6 +540,8 @@ extern SF_VL const sfHookReturnString;
extern SF_VL const sfHookParameterName; extern SF_VL const sfHookParameterName;
extern SF_VL const sfHookParameterValue; extern SF_VL const sfHookParameterValue;
extern SF_VL const sfBlob; extern SF_VL const sfBlob;
extern SF_VL const sfRemarkName;
extern SF_VL const sfRemarkValue;
// account // account
extern SF_ACCOUNT const sfAccount; extern SF_ACCOUNT const sfAccount;
@@ -596,6 +599,7 @@ extern SField const sfImportVLKey;
extern SField const sfHookEmission; extern SField const sfHookEmission;
extern SField const sfMintURIToken; extern SField const sfMintURIToken;
extern SField const sfAmountEntry; extern SField const sfAmountEntry;
extern SField const sfRemark;
// array of objects (common) // array of objects (common)
// ARRAY/1 is reserved for end of array // ARRAY/1 is reserved for end of array
@@ -624,6 +628,7 @@ extern SField const sfActiveValidators;
extern SField const sfImportVLKeys; extern SField const sfImportVLKeys;
extern SField const sfHookEmissions; extern SField const sfHookEmissions;
extern SField const sfAmounts; extern SField const sfAmounts;
extern SField const sfRemarks;
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

View File

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

View File

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

View File

@@ -146,6 +146,9 @@ enum TxType : std::uint16_t
ttURITOKEN_CREATE_SELL_OFFER = 48, ttURITOKEN_CREATE_SELL_OFFER = 48,
ttURITOKEN_CANCEL_SELL_OFFER = 49, 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 /* A payment transactor that delivers only the exact amounts specified, creating accounts and TLs as needed
* that the sender pays for. */ * that the sender pays for. */
ttREMIT = 95, ttREMIT = 95,

View File

@@ -468,6 +468,7 @@ REGISTER_FIX (fix240819, Supported::yes, VoteBehavior::De
REGISTER_FIX (fixPageCap, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixPageCap, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixFloatDivide, 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_FEATURE(Touch, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultYes);

View File

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

View File

@@ -31,6 +31,7 @@ LedgerFormats::LedgerFormats()
{sfLedgerIndex, soeOPTIONAL}, {sfLedgerIndex, soeOPTIONAL},
{sfLedgerEntryType, soeREQUIRED}, {sfLedgerEntryType, soeREQUIRED},
{sfFlags, soeREQUIRED}, {sfFlags, soeREQUIRED},
{sfRemarks, soeOPTIONAL},
}; };
add(jss::AccountRoot, 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(sfEmitParentTxnID, "EmitParentTxnID", UINT256, 11);
CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", UINT256, 12); CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", UINT256, 12);
CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", UINT256, 13); CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", UINT256, 13);
CONSTRUCT_TYPED_SFIELD(sfObjectID, "ObjectID", UINT256, 14);
// 256-bit (uncommon) // 256-bit (uncommon)
CONSTRUCT_TYPED_SFIELD(sfBookDirectory, "BookDirectory", UINT256, 16); 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(sfHookParameterName, "HookParameterName", VL, 24);
CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25);
CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26); CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26);
CONSTRUCT_TYPED_SFIELD(sfRemarkValue, "RemarkValue", VL, 98);
CONSTRUCT_TYPED_SFIELD(sfRemarkName, "RemarkName", VL, 99);
// account // account
CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); 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(sfHookDefinition, "HookDefinition", OBJECT, 22);
CONSTRUCT_UNTYPED_SFIELD(sfHookParameter, "HookParameter", OBJECT, 23); CONSTRUCT_UNTYPED_SFIELD(sfHookParameter, "HookParameter", OBJECT, 23);
CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, 24); CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, 24);
CONSTRUCT_UNTYPED_SFIELD(sfRemark, "Remark", OBJECT, 97);
CONSTRUCT_UNTYPED_SFIELD(sfGenesisMint, "GenesisMint", OBJECT, 96); CONSTRUCT_UNTYPED_SFIELD(sfGenesisMint, "GenesisMint", OBJECT, 96);
CONSTRUCT_UNTYPED_SFIELD(sfActiveValidator, "ActiveValidator", OBJECT, 95); CONSTRUCT_UNTYPED_SFIELD(sfActiveValidator, "ActiveValidator", OBJECT, 95);
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, 94); 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(sfHookExecutions, "HookExecutions", ARRAY, 18);
CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19); CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19);
CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20); CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20);
CONSTRUCT_UNTYPED_SFIELD(sfRemarks, "Remarks", ARRAY, 97);
CONSTRUCT_UNTYPED_SFIELD(sfGenesisMints, "GenesisMints", ARRAY, 96); CONSTRUCT_UNTYPED_SFIELD(sfGenesisMints, "GenesisMints", ARRAY, 96);
CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, 95); CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, 95);
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94); 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(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(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(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(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."), MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."),

View File

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

View File

@@ -122,6 +122,7 @@ JSS(Remit); // transaction type.
JSS(RippleState); // ledger type. JSS(RippleState); // ledger type.
JSS(SLE_hit_rate); // out: GetCounts. JSS(SLE_hit_rate); // out: GetCounts.
JSS(SetFee); // transaction type. JSS(SetFee); // transaction type.
JSS(SetRemarks); // transaction type
JSS(UNLModify); // transaction type. JSS(UNLModify); // transaction type.
JSS(UNLReport); // transaction type. JSS(UNLReport); // transaction type.
JSS(SettleDelay); // in: TransactionSign 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/quality.h>
#include <test/jtx/rate.h> #include <test/jtx/rate.h>
#include <test/jtx/regkey.h> #include <test/jtx/regkey.h>
#include <test/jtx/remarks.h>
#include <test/jtx/remit.h> #include <test/jtx/remit.h>
#include <test/jtx/require.h> #include <test/jtx/require.h>
#include <test/jtx/requires.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