XLS-46: DynamicNFT (#5048)

This Amendment adds functionality to update the URI of NFToken objects as described in the XLS-46d: Dynamic Non Fungible Tokens (dNFTs) spec.
This commit is contained in:
tequ
2025-01-10 01:22:11 +09:00
committed by GitHub
parent 040cd23e4a
commit 58af62f388
14 changed files with 522 additions and 26 deletions

View File

@@ -80,7 +80,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 = 83;
static constexpr std::size_t numFeatures = 84;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated

View File

@@ -132,6 +132,7 @@ constexpr std::uint32_t const tfBurnable = 0x00000001;
constexpr std::uint32_t const tfOnlyXRP = 0x00000002;
constexpr std::uint32_t const tfTrustLine = 0x00000004;
constexpr std::uint32_t const tfTransferable = 0x00000008;
constexpr std::uint32_t const tfMutable = 0x00000010;
// MPTokenIssuanceCreate flags:
// NOTE - there is intentionally no flag here for lsfMPTLocked, which this transaction cannot mutate.
@@ -169,12 +170,19 @@ constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal;
// The fixRemoveNFTokenAutoTrustLine amendment disables minting with the
// tfTrustLine flag as a way to prevent the attack. But until the
// amendment passes we still need to keep the old behavior available.
constexpr std::uint32_t const tfNFTokenMintOldMask =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTrustLine | tfTransferable);
constexpr std::uint32_t const tfNFTokenMintMask =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTransferable);
constexpr std::uint32_t const tfNFTokenMintOldMask =
~( ~tfNFTokenMintMask | tfTrustLine);
// if featureDynamicNFT enabled then new flag allowing mutable URI available.
constexpr std::uint32_t const tfNFTokenMintOldMaskWithMutable =
~( ~tfNFTokenMintOldMask | tfMutable);
constexpr std::uint32_t const tfNFTokenMintMaskWithMutable =
~( ~tfNFTokenMintMask | tfMutable);
// NFTokenCreateOffer flags:
constexpr std::uint32_t const tfSellNFToken = 0x00000001;
constexpr std::uint32_t const tfNFTokenCreateOfferMask =
@@ -187,17 +195,17 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
// Clawback flags:
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
// AMM Flags:
constexpr std::uint32_t tfLPToken = 0x00010000;
constexpr std::uint32_t tfWithdrawAll = 0x00020000;
constexpr std::uint32_t tfOneAssetWithdrawAll = 0x00040000;
constexpr std::uint32_t tfSingleAsset = 0x00080000;
constexpr std::uint32_t tfTwoAsset = 0x00100000;
constexpr std::uint32_t tfOneAssetLPToken = 0x00200000;
constexpr std::uint32_t tfLimitLPToken = 0x00400000;
constexpr std::uint32_t tfTwoAssetIfEmpty = 0x00800000;
constexpr std::uint32_t tfLPToken = 0x00010000;
constexpr std::uint32_t tfWithdrawAll = 0x00020000;
constexpr std::uint32_t tfOneAssetWithdrawAll = 0x00040000;
constexpr std::uint32_t tfSingleAsset = 0x00080000;
constexpr std::uint32_t tfTwoAsset = 0x00100000;
constexpr std::uint32_t tfOneAssetLPToken = 0x00200000;
constexpr std::uint32_t tfLimitLPToken = 0x00400000;
constexpr std::uint32_t tfTwoAssetIfEmpty = 0x00800000;
constexpr std::uint32_t tfWithdrawSubTx =
tfLPToken | tfSingleAsset | tfTwoAsset | tfOneAssetLPToken |
tfLimitLPToken | tfWithdrawAll | tfOneAssetWithdrawAll;

View File

@@ -29,6 +29,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_2, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -447,6 +447,13 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, ({
{sfCredentialType, soeREQUIRED},
}))
/** This transaction type modify a NFToken */
TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, ({
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
{sfURI, soeOPTIONAL},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
@@ -458,7 +465,6 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, ({
}))
/** This system-generated transaction type is used to update the network's fee settings.
For details, see: https://xrpl.org/fee-voting.html
*/
TRANSACTION(ttFEE, 101, SetFee, ({

View File

@@ -54,6 +54,7 @@ constexpr std::uint16_t const flagBurnable = 0x0001;
constexpr std::uint16_t const flagOnlyXRP = 0x0002;
constexpr std::uint16_t const flagCreateTrustLines = 0x0004;
constexpr std::uint16_t const flagTransferable = 0x0008;
constexpr std::uint16_t const flagMutable = 0x0010;
inline std::uint16_t
getFlags(uint256 const& id)

View File

@@ -7734,6 +7734,273 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
}
}
void
testNFTokenModify(FeatureBitset features)
{
testcase("Test NFTokenModify");
using namespace test::jtx;
Account const issuer{"issuer"};
Account const alice("alice");
Account const bob("bob");
bool const modifyEnabled = features[featureDynamicNFT];
{
// Mint with tfMutable
Env env{*this, features};
env.fund(XRP(10000), issuer);
env.close();
auto const expectedTer =
modifyEnabled ? TER{tesSUCCESS} : TER{temINVALID_FLAG};
env(token::mint(issuer, 0u), txflags(tfMutable), ter(expectedTer));
env.close();
}
{
Env env{*this, features};
env.fund(XRP(10000), issuer);
env.close();
// Modify a nftoken
uint256 const nftId{token::getNextID(env, issuer, 0u, tfMutable)};
if (modifyEnabled)
{
env(token::mint(issuer, 0u), txflags(tfMutable));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 1);
env(token::modify(issuer, nftId));
BEAST_EXPECT(ownerCount(env, issuer) == 1);
}
else
{
env(token::mint(issuer, 0u));
env.close();
env(token::modify(issuer, nftId), ter(temDISABLED));
env.close();
}
}
if (!modifyEnabled)
return;
{
Env env{*this, features};
env.fund(XRP(10000), issuer);
env.close();
uint256 const nftId{token::getNextID(env, issuer, 0u, tfMutable)};
env(token::mint(issuer, 0u), txflags(tfMutable));
env.close();
// Set a negative fee. Exercises invalid preflight1.
env(token::modify(issuer, nftId),
fee(STAmount(10ull, true)),
ter(temBAD_FEE));
env.close();
// Invalid Owner
env(token::modify(issuer, nftId),
token::owner(issuer),
ter(temMALFORMED));
env.close();
// Invalid URI length = 0
env(token::modify(issuer, nftId),
token::uri(""),
ter(temMALFORMED));
env.close();
// Invalid URI length > 256
env(token::modify(issuer, nftId),
token::uri(std::string(maxTokenURILength + 1, 'q')),
ter(temMALFORMED));
env.close();
}
{
Env env{*this, features};
env.fund(XRP(10000), issuer, alice, bob);
env.close();
{
// NFToken not exists
uint256 const nftIDNotExists{
token::getNextID(env, issuer, 0u, tfMutable)};
env.close();
env(token::modify(issuer, nftIDNotExists), ter(tecNO_ENTRY));
env.close();
}
{
// Invalid NFToken flag
uint256 const nftIDNotModifiable{
token::getNextID(env, issuer, 0u)};
env(token::mint(issuer, 0u));
env.close();
env(token::modify(issuer, nftIDNotModifiable),
ter(tecNO_PERMISSION));
env.close();
}
{
// Unauthorized account
uint256 const nftId{
token::getNextID(env, issuer, 0u, tfMutable)};
env(token::mint(issuer, 0u), txflags(tfMutable));
env.close();
env(token::modify(bob, nftId),
token::owner(issuer),
ter(tecNO_PERMISSION));
env.close();
env(token::setMinter(issuer, alice));
env.close();
env(token::modify(bob, nftId),
token::owner(issuer),
ter(tecNO_PERMISSION));
env.close();
}
}
{
Env env{*this, features};
env.fund(XRP(10000), issuer, alice, bob);
env.close();
// lambda that returns the JSON form of NFTokens held by acct
auto accountNFTs = [&env](Account const& acct) {
Json::Value params;
params[jss::account] = acct.human();
params[jss::type] = "state";
auto response =
env.rpc("json", "account_nfts", to_string(params));
return response[jss::result][jss::account_nfts];
};
// lambda that checks for the expected URI value of an NFToken
auto checkURI = [&accountNFTs, this](
Account const& acct,
char const* uri,
int line) {
auto const nfts = accountNFTs(acct);
if (nfts.size() == 1)
pass();
else
{
std::ostringstream text;
text << "checkURI: unexpected NFT count on line " << line;
fail(text.str(), __FILE__, line);
return;
}
if (uri == nullptr)
{
if (!nfts[0u].isMember(sfURI.jsonName))
pass();
else
{
std::ostringstream text;
text << "checkURI: unexpected URI present on line "
<< line;
fail(text.str(), __FILE__, line);
}
return;
}
if (nfts[0u][sfURI.jsonName] == strHex(std::string(uri)))
pass();
else
{
std::ostringstream text;
text << "checkURI: unexpected URI contents on line "
<< line;
fail(text.str(), __FILE__, line);
}
};
uint256 const nftId{token::getNextID(env, issuer, 0u, tfMutable)};
env.close();
env(token::mint(issuer, 0u), txflags(tfMutable), token::uri("uri"));
env.close();
checkURI(issuer, "uri", __LINE__);
// set URI Field
env(token::modify(issuer, nftId), token::uri("new_uri"));
env.close();
checkURI(issuer, "new_uri", __LINE__);
// unset URI Field
env(token::modify(issuer, nftId));
env.close();
checkURI(issuer, nullptr, __LINE__);
// set URI Field
env(token::modify(issuer, nftId), token::uri("uri"));
env.close();
checkURI(issuer, "uri", __LINE__);
// Account != Owner
uint256 const offerID =
keylet::nftoffer(issuer, env.seq(issuer)).key;
env(token::createOffer(issuer, nftId, XRP(0)),
txflags(tfSellNFToken));
env.close();
env(token::acceptSellOffer(alice, offerID));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, alice) == 1);
checkURI(alice, "uri", __LINE__);
// Modify by owner fails.
env(token::modify(alice, nftId),
token::uri("new_uri"),
ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, alice) == 1);
checkURI(alice, "uri", __LINE__);
env(token::modify(issuer, nftId),
token::owner(alice),
token::uri("new_uri"));
env.close();
BEAST_EXPECT(ownerCount(env, issuer) == 0);
BEAST_EXPECT(ownerCount(env, alice) == 1);
checkURI(alice, "new_uri", __LINE__);
env(token::modify(issuer, nftId), token::owner(alice));
env.close();
checkURI(alice, nullptr, __LINE__);
env(token::modify(issuer, nftId),
token::owner(alice),
token::uri("uri"));
env.close();
checkURI(alice, "uri", __LINE__);
// Modify by authorized minter
env(token::setMinter(issuer, bob));
env.close();
env(token::modify(bob, nftId),
token::owner(alice),
token::uri("new_uri"));
env.close();
checkURI(alice, "new_uri", __LINE__);
env(token::modify(bob, nftId), token::owner(alice));
env.close();
checkURI(alice, nullptr, __LINE__);
env(token::modify(bob, nftId),
token::owner(alice),
token::uri("uri"));
env.close();
checkURI(alice, "uri", __LINE__);
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -7771,6 +8038,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
testFixNFTokenBuyerReserve(features);
testUnaskedForAutoTrustline(features);
testNFTIssuerIsIOUIssuer(features);
testNFTokenModify(features);
}
public:
@@ -7781,17 +8049,20 @@ public:
static FeatureBitset const all{supported_amendments()};
static FeatureBitset const fixNFTDir{fixNFTokenDirV1};
static std::array<FeatureBitset, 7> const feats{
static std::array<FeatureBitset, 8> const feats{
all - fixNFTDir - fixNonFungibleTokensV1_2 - fixNFTokenRemint -
fixNFTokenReserve - featureNFTokenMintOffer,
fixNFTokenReserve - featureNFTokenMintOffer - featureDynamicNFT,
all - disallowIncoming - fixNonFungibleTokensV1_2 -
fixNFTokenRemint - fixNFTokenReserve - featureNFTokenMintOffer,
fixNFTokenRemint - fixNFTokenReserve - featureNFTokenMintOffer -
featureDynamicNFT,
all - fixNonFungibleTokensV1_2 - fixNFTokenRemint -
fixNFTokenReserve - featureNFTokenMintOffer,
fixNFTokenReserve - featureNFTokenMintOffer - featureDynamicNFT,
all - fixNFTokenRemint - fixNFTokenReserve -
featureNFTokenMintOffer,
all - fixNFTokenReserve - featureNFTokenMintOffer,
all - featureNFTokenMintOffer,
featureNFTokenMintOffer - featureDynamicNFT,
all - fixNFTokenReserve - featureNFTokenMintOffer -
featureDynamicNFT,
all - featureNFTokenMintOffer - featureDynamicNFT,
all - featureDynamicNFT,
all};
if (BEAST_EXPECT(instance < feats.size()))
@@ -7853,12 +8124,21 @@ class NFTokenWOMintOffer_test : public NFTokenBaseUtil_test
}
};
class NFTokenWOModify_test : public NFTokenBaseUtil_test
{
void
run() override
{
NFTokenBaseUtil_test::run(6);
}
};
class NFTokenAllFeatures_test : public NFTokenBaseUtil_test
{
void
run() override
{
NFTokenBaseUtil_test::run(6, true);
NFTokenBaseUtil_test::run(7, true);
}
};
@@ -7868,6 +8148,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOfixV1, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOTokenRemint, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOTokenReserve, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOMintOffer, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOModify, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAllFeatures, tx, ripple, 2);
} // namespace ripple

View File

@@ -232,6 +232,16 @@ clearMinter(jtx::Account const& account)
return fclear(account, asfAuthorizedNFTokenMinter);
}
Json::Value
modify(jtx::Account const& account, uint256 const& nftokenID)
{
Json::Value jv;
jv[sfAccount.jsonName] = account.human();
jv[sfNFTokenID.jsonName] = to_string(nftokenID);
jv[jss::TransactionType] = jss::NFTokenModify;
return jv;
}
} // namespace token
} // namespace jtx
} // namespace test

View File

@@ -237,6 +237,10 @@ setMinter(jtx::Account const& account, jtx::Account const& minter);
Json::Value
clearMinter(jtx::Account const& account);
/** Modify an NFToken. */
Json::Value
modify(jtx::Account const& account, uint256 const& nftokenID);
} // namespace token
} // namespace jtx

View File

@@ -67,8 +67,14 @@ NFTokenMint::preflight(PreflightContext const& ctx)
// tfTrustLine flag as a way to prevent the attack. But until the
// amendment passes we still need to keep the old behavior available.
std::uint32_t const NFTokenMintMask =
ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine) ? tfNFTokenMintMask
: tfNFTokenMintOldMask;
ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine)
// if featureDynamicNFT enabled then new flag allowing mutable URI
// available
? ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintMaskWithMutable
: tfNFTokenMintMask
: ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintOldMaskWithMutable
: tfNFTokenMintOldMask;
if (ctx.tx.getFlags() & NFTokenMintMask)
return temINVALID_FLAG;

View File

@@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
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 <xrpld/app/tx/detail/NFTokenModify.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
namespace ripple {
NotTEC
NFTokenModify::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureNonFungibleTokensV1_1) ||
!ctx.rules.enabled(featureDynamicNFT))
return temDISABLED;
if (NotTEC const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (auto owner = ctx.tx[~sfOwner]; owner == ctx.tx[sfAccount])
return temMALFORMED;
if (auto uri = ctx.tx[~sfURI])
{
if (uri->length() == 0 || uri->length() > maxTokenURILength)
return temMALFORMED;
}
return preflight2(ctx);
}
TER
NFTokenModify::preclaim(PreclaimContext const& ctx)
{
AccountID const account = ctx.tx[sfAccount];
AccountID const owner =
ctx.tx[ctx.tx.isFieldPresent(sfOwner) ? sfOwner : sfAccount];
if (!nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID]))
return tecNO_ENTRY;
// Check if the NFT is mutable
if (!(nft::getFlags(ctx.tx[sfNFTokenID]) & nft::flagMutable))
return tecNO_PERMISSION;
// Verify permissions for the issuer
if (AccountID const issuer = nft::getIssuer(ctx.tx[sfNFTokenID]);
issuer != account)
{
auto const sle = ctx.view.read(keylet::account(issuer));
if (!sle)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto const minter = (*sle)[~sfNFTokenMinter]; minter != account)
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
NFTokenModify::doApply()
{
uint256 const nftokenID = ctx_.tx[sfNFTokenID];
AccountID const owner =
ctx_.tx[ctx_.tx.isFieldPresent(sfOwner) ? sfOwner : sfAccount];
return nft::changeTokenURI(view(), owner, nftokenID, ctx_.tx[~sfURI]);
}
} // namespace ripple

View File

@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
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_NFTOKENMODIFY_H_INCLUDED
#define RIPPLE_TX_NFTOKENMODIFY_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class NFTokenModify : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit NFTokenModify(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -34,7 +34,7 @@ namespace ripple {
namespace nft {
static std::shared_ptr<SLE const>
locatePage(ReadView const& view, AccountID owner, uint256 const& id)
locatePage(ReadView const& view, AccountID const& owner, uint256 const& id)
{
auto const first = keylet::nftpage(keylet::nftpage_min(owner), id);
auto const last = keylet::nftpage_max(owner);
@@ -48,7 +48,7 @@ locatePage(ReadView const& view, AccountID owner, uint256 const& id)
}
static std::shared_ptr<SLE>
locatePage(ApplyView& view, AccountID owner, uint256 const& id)
locatePage(ApplyView& view, AccountID const& owner, uint256 const& id)
{
auto const first = keylet::nftpage(keylet::nftpage_min(owner), id);
auto const last = keylet::nftpage_max(owner);
@@ -241,6 +241,39 @@ compareTokens(uint256 const& a, uint256 const& b)
return a < b;
}
TER
changeTokenURI(
ApplyView& view,
AccountID const& owner,
uint256 const& nftokenID,
std::optional<ripple::Slice> const& uri)
{
std::shared_ptr<SLE> const page = locatePage(view, owner, nftokenID);
// If the page couldn't be found, the given NFT isn't owned by this account
if (!page)
return tecINTERNAL; // LCOV_EXCL_LINE
// Locate the NFT in the page
STArray& arr = page->peekFieldArray(sfNFTokens);
auto const nftIter =
std::find_if(arr.begin(), arr.end(), [&nftokenID](STObject const& obj) {
return (obj[sfNFTokenID] == nftokenID);
});
if (nftIter == arr.end())
return tecINTERNAL; // LCOV_EXCL_LINE
if (uri)
nftIter->setFieldVL(sfURI, *uri);
else if (nftIter->isFieldPresent(sfURI))
nftIter->makeFieldAbsent(sfURI);
view.update(page);
return tesSUCCESS;
}
/** Insert the token in the owner's token directory. */
TER
insertToken(ApplyView& view, AccountID owner, STObject&& nft)

View File

@@ -105,6 +105,13 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner);
bool
compareTokens(uint256 const& a, uint256 const& b);
TER
changeTokenURI(
ApplyView& view,
AccountID const& owner,
uint256 const& nftokenID,
std::optional<ripple::Slice> const& uri);
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */
NotTEC
tokenOfferCreatePreflight(

View File

@@ -50,6 +50,7 @@
#include <xrpld/app/tx/detail/NFTokenCancelOffer.h>
#include <xrpld/app/tx/detail/NFTokenCreateOffer.h>
#include <xrpld/app/tx/detail/NFTokenMint.h>
#include <xrpld/app/tx/detail/NFTokenModify.h>
#include <xrpld/app/tx/detail/PayChan.h>
#include <xrpld/app/tx/detail/Payment.h>
#include <xrpld/app/tx/detail/SetAccount.h>