diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 18a9b9498aa..8821d531ff0 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -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 diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index c293798f7d7..9502db7e3ea 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -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; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 31fc90cef80..287a1368a72 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -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) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 4f4c8f12595..99e741036aa 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -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, ({ diff --git a/include/xrpl/protocol/nft.h b/include/xrpl/protocol/nft.h index 839d872a63a..97c004ae3a1 100644 --- a/include/xrpl/protocol/nft.h +++ b/include/xrpl/protocol/nft.h @@ -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) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 0d4786ae72e..3460fef0a91 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -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 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite static FeatureBitset const all{supported_amendments()}; static FeatureBitset const fixNFTDir{fixNFTokenDirV1}; - static std::array const feats{ + static std::array 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 diff --git a/src/test/jtx/impl/token.cpp b/src/test/jtx/impl/token.cpp index 5faf56185b9..49f473fcd82 100644 --- a/src/test/jtx/impl/token.cpp +++ b/src/test/jtx/impl/token.cpp @@ -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 diff --git a/src/test/jtx/token.h b/src/test/jtx/token.h index 9b4f81b7061..f22a1a01dae 100644 --- a/src/test/jtx/token.h +++ b/src/test/jtx/token.h @@ -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 diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 5699a29c039..76e561cfc3f 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -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; diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp new file mode 100644 index 00000000000..74ca1baef4f --- /dev/null +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 diff --git a/src/xrpld/app/tx/detail/NFTokenModify.h b/src/xrpld/app/tx/detail/NFTokenModify.h new file mode 100644 index 00000000000..0d1e72ade1a --- /dev/null +++ b/src/xrpld/app/tx/detail/NFTokenModify.h @@ -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 + +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 diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 07edfe1b79a..04eb53ae764 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -34,7 +34,7 @@ namespace ripple { namespace nft { static std::shared_ptr -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 -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 const& uri) +{ + std::shared_ptr 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) diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.h b/src/xrpld/app/tx/detail/NFTokenUtils.h index 97d109b8318..f5232630eef 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.h +++ b/src/xrpld/app/tx/detail/NFTokenUtils.h @@ -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 const& uri); + /** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */ NotTEC tokenOfferCreatePreflight( diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index bf492f540ba..02a7e070ab4 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include