mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 18:45:52 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
90
src/xrpld/app/tx/detail/NFTokenModify.cpp
Normal file
90
src/xrpld/app/tx/detail/NFTokenModify.cpp
Normal 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
|
||||
48
src/xrpld/app/tx/detail/NFTokenModify.h
Normal file
48
src/xrpld/app/tx/detail/NFTokenModify.h
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user