Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XLS-46: DynamicNFT #5048

Open
wants to merge 32 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
598a0e3
XLS-46 dNFTs PR first
Nov 27, 2023
d851678
dNFTs initial
xVet Nov 27, 2023
e30addc
added dnft amendment and placed guards
xVet Nov 28, 2023
0848a98
Merge branch 'XRPLF:develop' into dNFTs-XLS-46
xVet Nov 28, 2023
624e11e
added featureDNFT in Feature.h
xVet Nov 28, 2023
4cbde96
Merge branch 'dNFTs-XLS-46' of https://github.com/xVet/rippled into d…
xVet Nov 28, 2023
afaa675
Merge remote-tracking branch 'upstream/develop' into dNFTs-XLS-46
tequdev Feb 16, 2024
f6418aa
dNFTs
tequdev Feb 16, 2024
44790ab
rename dNFT to DynamicNFT
tequdev Feb 16, 2024
ef6ba44
clang-format
tequdev Feb 16, 2024
b9becc4
fix flag test
tequdev Feb 16, 2024
8fd988d
add standard license/disclaimer text
tequdev Feb 16, 2024
68a51e1
Merge remote-tracking branch 'upstream/develop' into dNFTs-XLS-46
tequdev May 31, 2024
750adda
chore
tequdev Jun 18, 2024
4b3480c
fix tests
tequdev Jun 18, 2024
f4ab8c6
Merge remote-tracking branch 'upstream/develop' into featureDynamicNFT
tequdev Jun 18, 2024
47106cc
Merge branch 'develop' into featureDynamicNFT
tequdev Jun 18, 2024
4ca5105
Merge branch 'develop' into featureDynamicNFT
tequdev Jun 21, 2024
579dc25
[FOLD] An incremental approach to the tfNFTokenMintMask variants
scottschurr Jun 27, 2024
cae9b62
[FOLD] Extend testNFTokenModify unit test
scottschurr Jun 27, 2024
23ecb99
[FOLD] Avoid NFTPage coalescing and splitting when URI changes
scottschurr Jun 27, 2024
2eb7559
fix sorting testcase
tequdev Jun 29, 2024
e9dd06c
Merge branch 'develop' into featureDynamicNFT
tequdev Jun 29, 2024
c89f41d
Merge branch 'develop' into featureDynamicNFT
tequdev Jul 3, 2024
1b2cf6d
Merge branch 'develop' into featureDynamicNFT
tequdev Jul 9, 2024
d52b659
Merge branch 'develop' into featureDynamicNFT
tequdev Jul 12, 2024
90aeef9
fix testcase name
tequdev Jul 12, 2024
4eea716
chore: fix typo
tequdev Jul 12, 2024
321459d
Merge branch 'develop' into featureDynamicNFT
tequdev Sep 23, 2024
b41b98a
Merge branch 'develop' into featureDynamicNFT
tequdev Sep 23, 2024
fc81bcc
clang-format
tequdev Sep 23, 2024
0f02668
Merge branch 'develop' into featureDynamicNFT
tequdev Sep 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion include/xrpl/protocol/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 76;
static constexpr std::size_t numFeatures = 77;

/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
Expand Down Expand Up @@ -369,6 +369,7 @@ extern uint256 const fixAMMv1_1;
extern uint256 const featureNFTokenMintOffer;
extern uint256 const fixReducedOffersV2;
extern uint256 const fixEnforceNFTokenTrustline;
extern uint256 const featureDynamicNFT;

} // namespace ripple

Expand Down
7 changes: 7 additions & 0 deletions include/xrpl/protocol/TxFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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;

// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between
// accounts allowed a TrustLine to be added to the issuer of that token
Expand All @@ -149,6 +150,12 @@ constexpr std::uint32_t const tfNFTokenMintOldMask =
constexpr std::uint32_t const tfNFTokenMintMask =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTransferable);

// if featureDynamicNFT enabled then new flag allowing mutable URI available.
constexpr std::uint32_t const tfNFTokenMintOldMaskWithMutable =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTrustLine | tfTransferable | tfMutable);
constexpr std::uint32_t const tfNFTokenMintMaskWithMutable =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTransferable | tfMutable);

tequdev marked this conversation as resolved.
Show resolved Hide resolved
// NFTokenCreateOffer flags:
constexpr std::uint32_t const tfSellNFToken = 0x00000001;
constexpr std::uint32_t const tfNFTokenCreateOfferMask =
Expand Down
4 changes: 3 additions & 1 deletion include/xrpl/protocol/TxFormats.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,15 @@ enum TxType : std::uint16_t
/** This transaction type deletes a DID */
ttDID_DELETE = 50,


/** This transaction type creates an Oracle instance */
ttORACLE_SET = 51,

/** This transaction type deletes an Oracle instance */
ttORACLE_DELETE = 52,

/** This transaction type modify a NFToken */
ttNFTOKEN_MODIFY = 53,

/** This system-generated transaction type is used to update the status of the various amendments.

For details, see: https://xrpl.org/amendments.html
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/protocol/jss.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ JSS(NFTokenOffer); // ledger type.
JSS(NFTokenAcceptOffer); // transaction type.
JSS(NFTokenCancelOffer); // transaction type.
JSS(NFTokenCreateOffer); // transaction type.
JSS(NFTokenModify); // transaction type.
JSS(NFTokenPage); // ledger type.
JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens
JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/protocol/nft.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/libxrpl/protocol/Feature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ REGISTER_FIX (fixAMMv1_1, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(NFTokenMintOffer, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixReducedOffersV2, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixEnforceNFTokenTrustline, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo);

// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.
Expand Down
9 changes: 9 additions & 0 deletions src/libxrpl/protocol/TxFormats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,15 @@ TxFormats::TxFormats()
},
commonFields);

add(jss::NFTokenModify,
ttNFTOKEN_MODIFY,
{
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
{sfURI, soeOPTIONAL},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec (XRPLF/XRPL-Standards#130 August 18, 2023 version) specifies that the sfURI field is required. Here it is listed as soeOPTIONAL. I think that soeOPTIONAL is a good choice, because that matches the behavior of NFTokenMint. Perhaps the spec needs to be updated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you pointed it out, as of Jun 29 the sfURI field has been updated to an optional field.

Copy link
Collaborator

@shawnxie999 shawnxie999 Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any use case for removing the URI from an existing NFT? Is there any difference from burning the NFT compared to an NFT without a URI?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such a use case may not be common, but it is not impossible to say that there is no use case.

Since the NFTokenMint transaction can be executed with the URI field unspecified, it is reasonable that the NFTokenModify transaction can change the URI field to unspecified.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a flag for removing the URI from the NFT, like with NFTokenMinter, instead of just using its absence?

},
commonFields);

add(jss::Clawback,
ttCLAWBACK,
{
Expand Down
229 changes: 221 additions & 8 deletions src/test/app/NFToken_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7744,6 +7744,205 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
}
}

void
testNFTokenModify(FeatureBitset features)
{
testcase("Test modify");
tequdev marked this conversation as resolved.
Show resolved Hide resolved

using namespace test::jtx;

Account const minter{"minter"};
tequdev marked this conversation as resolved.
Show resolved Hide resolved
Account const alice("alice");
Account const bob("bob");

bool modifyEnabled = features[featureDynamicNFT];
tequdev marked this conversation as resolved.
Show resolved Hide resolved

{
// Mint with tfMutable
Env env{*this, features};
env.fund(XRP(10000), minter);
env.close();

auto const expectedTer =
modifyEnabled ? TER{tesSUCCESS} : TER{temINVALID_FLAG};
env(token::mint(minter, 0u), txflags(tfMutable), ter(expectedTer));
env.close();
}
{
Env env{*this, features};
env.fund(XRP(10000), minter);
env.close();

// Modify a nftoken
uint256 const nftId{token::getNextID(env, minter, 0u, tfMutable)};
if (modifyEnabled)
{
env(token::mint(minter, 0u), txflags(tfMutable));
env.close();
BEAST_EXPECT(ownerCount(env, minter) == 1);
env(token::modify(minter, nftId));
BEAST_EXPECT(ownerCount(env, minter) == 1);
}
else
{
env(token::mint(minter, 0u));
env.close();
env(token::modify(minter, nftId), ter(temDISABLED));
env.close();
}
}
if (!modifyEnabled)
return;

{
Env env{*this, features};
env.fund(XRP(10000), minter);
env.close();

uint256 const nftId{token::getNextID(env, minter, 0u, tfMutable)};
env(token::mint(minter, 0u), txflags(tfMutable));
env.close();

// Invalid Owner
scottschurr marked this conversation as resolved.
Show resolved Hide resolved
env(token::modify(minter, nftId),
token::owner(minter),
ter(temMALFORMED));
env.close();

// Invalid URI length = 0
env(token::modify(minter, nftId),
token::uri(""),
ter(temMALFORMED));
env.close();

// Invalid URI length > 256
env(token::modify(minter, nftId),
token::uri(std::string(maxTokenURILength + 1, 'q')),
ter(temMALFORMED));
env.close();
}
{
Env env{*this, features};
env.fund(XRP(10000), minter, alice);
env.close();

uint256 const nftId_not_exists{
tequdev marked this conversation as resolved.
Show resolved Hide resolved
token::getNextID(env, minter, 0u, tfMutable)};
env.close();

// NFToken not exists
env(token::modify(minter, nftId_not_exists), ter(tecNO_ENTRY));
env.close();

uint256 const nftId_not_modifiable{
token::getNextID(env, minter, 0u)};
env(token::mint(minter, 0u));
env.close();

// Invalid NFToken flag
env(token::modify(minter, nftId_not_modifiable),
ter(tecNO_PERMISSION));
env.close();

// Invalid NFTokenMinter
env(token::modify(alice, nftId_not_modifiable),
tequdev marked this conversation as resolved.
Show resolved Hide resolved
token::owner(minter),
ter(tecNO_PERMISSION));
env.close();
}
{
Env env{*this, features};
env.fund(XRP(10000), minter, alice, bob);
env.close();

auto accountNFTs = [&](Env& env, Account const& acct) {
tequdev marked this conversation as resolved.
Show resolved Hide resolved
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];
};

uint256 const nftId{token::getNextID(env, minter, 0u, tfMutable)};
env.close();

env(token::mint(minter, 0u), txflags(tfMutable), token::uri("uri"));
env.close();
{
tequdev marked this conversation as resolved.
Show resolved Hide resolved
auto nfts = accountNFTs(env, minter);
BEAST_EXPECT(nfts.size() == 1);
BEAST_EXPECT(
nfts[0u][sfURI.jsonName] == strHex(std::string("uri")));
}

// set URI Field
env(token::modify(minter, nftId), token::uri("new_uri"));
env.close();
{
auto nfts = accountNFTs(env, minter);
BEAST_EXPECT(nfts.size() == 1);
BEAST_EXPECT(
nfts[0u][sfURI.jsonName] == strHex(std::string("new_uri")));
}
// unset URI Field
env(token::modify(minter, nftId));
env.close();
{
auto nfts = accountNFTs(env, minter);
BEAST_EXPECT(nfts.size() == 1);
BEAST_EXPECT(!nfts[0u].isMember(sfURI.jsonName));
}
// set URI Field
env(token::modify(minter, nftId), token::uri("uri"));
env.close();
{
auto nfts = accountNFTs(env, minter);
BEAST_EXPECT(nfts.size() == 1);
BEAST_EXPECT(
nfts[0u][sfURI.jsonName] == strHex(std::string("uri")));
}

// Account != Owner
uint256 const offerID =
keylet::nftoffer(minter, env.seq(minter)).key;
env(token::createOffer(minter, nftId, XRP(0)),
txflags(tfSellNFToken));
env.close();
env(token::acceptSellOffer(alice, offerID));
env.close();

BEAST_EXPECT(ownerCount(env, minter) == 0);
BEAST_EXPECT(ownerCount(env, alice) == 1);
env(token::modify(minter, nftId),
token::owner(alice),
token::uri("new_uri"));
env.close();
BEAST_EXPECT(ownerCount(env, minter) == 0);
BEAST_EXPECT(ownerCount(env, alice) == 1);

env(token::modify(minter, nftId), token::owner(alice));
env.close();

env(token::modify(minter, nftId),
token::owner(alice),
token::uri("uri"));
env.close();

// Modify by Issuer
env(token::setMinter(minter, bob));
env.close();
env(token::modify(bob, nftId),
token::owner(alice),
token::uri("new_uri"));
env(token::modify(bob, nftId), token::owner(alice));
env(token::modify(bob, nftId),
token::owner(alice),
token::uri("uri"));
env.close();
}
}

void
testWithFeats(FeatureBitset features)
{
Expand Down Expand Up @@ -7781,6 +7980,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
testFixNFTokenBuyerReserve(features);
testUnaskedForAutoTrustline(features);
testNFTIssuerIsIOUIssuer(features);
testNFTokenModify(features);
}

public:
Expand All @@ -7791,17 +7991,20 @@ class NFTokenBaseUtil_test : public beast::unit_test::suite
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()))
Expand Down Expand Up @@ -7863,12 +8066,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);
}
};

Expand All @@ -7877,6 +8089,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(NFTokenDisallowIncoming, tx, ripple, 2);
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(NFTokenWOModify, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenWOMintOffer, tx, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAllFeatures, tx, ripple, 2);

Expand Down
10 changes: 10 additions & 0 deletions src/test/jtx/impl/token.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/test/jtx/token.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading