-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Introduce NFT support (XLS-20) #4101
Conversation
Renaming global types (in this case the serialized field types) inside an amendment commit is inadvisable. I realise this amendment introduces new types but renaming existing types inside it breaks any number of other backlogged PRs (including probably mine) which all need to be individually updated to be merge-compatible again. Instead change the code to continue with the existing naming convention and do the rename in a separate non-amendment PR after backlog has been cleared. That way we can update type names once, "one and done" so to speak, without needlessly multiplying work. I'm speaking about I think as a rule amendment PR's should not change anything that isn't explicitly and exactly needed for that amendment to function. There should be a clear delineation between writing new functionality into the codebase and refactoring existing functionality for the reasons above (multiplication of work) |
static bool | ||
compareTokens(uint256 const& a, uint256 const& b) | ||
{ | ||
return (a & nft::pageMask) < (b & nft::pageMask); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a latent bug here. We need every server to produce identically sorted NFTPage
s. But this comparison only sorts on the low 96-bits. If there are NFTs with identical low 96-bits then the sort order becomes nondeterministic.
So I think this function needs to look more like this:
// The sort of NFTokens needs to be fully deterministic, but the sort
// is weird because we sort on the low 96-bits first. But if the low
// 96-bits are identical we still need a fully deterministic sort.
// So we sort on the low 96-bits first. If those are equal we sort on
// the whole thing.
int const lowBitsCmp = compare(a & nft::pageMask, b & nft::pageMask);
if (lowBitsCmp != 0)
return lowBitsCmp < 0;
return a < b;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch. I also like the exposition in the comment which explains the need for this curious construct.
Minor nit, I'd prefer:
if (auto const lc = compare(a & nft::pageMask, b & nft::pageMask); lc != 0)
return lc < 0;
return a < b;
I agree with you, and in a perfect world this wouldn't have happened. My original set of commits had a separate commit for this, but somewhere along the way I squashed it down into a single commit.
I could unsquash it but I don't know if it's worth the trouble to do this. Rationale: the renaming here doesn't alter the binary protocol; it's a purely mechanical change and any conflicts with other PRs are likely to be very simple to resolve.
I think that going forward, it's a good rule for contributors and maintainers to adopt: changes to global types should be a standalone commit. I also agree with your rationale about some mechanism to avoid conflict between different PRs that introduce new fields and/or types. While I don't think we need to do this for this commit, if you feel strongly enough that this should be done here, I'll put in the work to do it. Let me know. |
src/ripple/protocol/Protocol.h
Outdated
std::uint16_t constexpr maxTransferFee = 50000; | ||
|
||
/** The maximum length of a URI inside an NFT */ | ||
std::size_t constexpr maxTokenURILength = 512; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 512
limit does not match the spec or the documentation, which sets the limit at 256
. This value should be changed to 256
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job on this! Lots of tests and the code is really well done. I left some comments. The biggest things I'd like to tackle are:
-
Naming. I'd like to have consistent nomenclature for this new functionality. I prefer
nft
, nottoken
orNFToken
. I'd also prefer to rename the nft functionality that overlaps with existing functionality - like transfer fees and offers. -
IOU transfer fees should probably be paid by the seller, not the buyer.
-
One "typo" bug in "invariants.
-
The rest is the usual collection of minor issues.
Edit: One more minor comment. I'd rather the code was attributed to whoever wrote it, rather than "XLS-20 developers". git-blame
to a specific developer is useful because it lets us direct questions. git-blame
to some group is less useful.
src/ripple/app/misc/NetworkOPs.cpp
Outdated
@@ -1172,6 +1172,7 @@ NetworkOPsImp::processTransaction( | |||
if ((newFlags & SF_BAD) != 0) | |||
{ | |||
// cached bad | |||
std::cerr << transaction->getID() << ": cached bad!\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use log instead of writting to `cerr
ReadView const& view, | ||
beast::Journal const& j) | ||
{ | ||
if (tx.getTxnType() == ttNFTOKEN_MINT && tx.getTxnType() == ttNFTOKEN_BURN) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will never be true. Should be:
if (tx.getTxnType() != ttNFTOKEN_MINT && tx.getTxnType() != ttNFTOKEN_BURN)
NFTokenAcceptOffer::pay( | ||
AccountID const& from, | ||
AccountID const& to, | ||
STAmount amount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass by const&
if (amount < beast::zero) | ||
return tecINTERNAL; | ||
|
||
return accountSend(view(), from, to, amount, j_); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As written, the destination pays the IOU transfer fee. I don't think this is what we want. In payments, the source account pays the fee. We should probably do the same thing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is okay as written, but maybe you'll convince me otherwise. Let me walk through the steps.
- The total paid for the NFT always equals or exceeds all fees associated with the transfer -- both the transfer fee and the broker fee.
- If there is a broker, the broker is paid their fee first. That is subtracted from the total paid.
- If the seller specified a selling price then the remainder, after the broker is paid, must equal or exceed the specified selling price.
- The issuer's portion is determined as a percentage of this remainder. We now pay that portion to the issuer. The amount paid to the issuer is then subtracted from the total paid.
- At this point, the last bit of the remainder may be below the amount requested by the seller. We consider that to be okay because the seller is paying the transfer fee to the issuer.
- These last remaining funds are then transferred to the seller.
So the effect is that the seller is paying the transfer fee, even though the funds did not actually transfer from buyer to seller to issuer. The final effect is that the seller is paying the transfer fee to the issuer.
I'm open to further discussion if this doesn't seem right to you.
if (!nft) | ||
return tecINTERNAL; | ||
|
||
if (auto const ret = nft::removeToken(view(), seller, tokenID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do the work of finding the token twice. Maybe findToken
can return a hint with the page so removeToken
doesn't need to repeat this work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it stands right now, nft::findToken()
can't do this work, because it takes a ReadView
parameter instead of an ApplyView
. What I'll try is adding a new free function, nft::findTokenAndPage
, that will take an ApplyView
and return both the token object and a std::shared_ptr<SLE>
to the page. That may help. And it may be useful elsewhere. We'll see.
src/test/app/NFToken_test.cpp
Outdated
} | ||
}; | ||
|
||
BEAST_DEFINE_TESTSUITE_PRIO(NFToken, tx, ripple, 3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A 5000 like file is pretty big. We might want to split this up.
@@ -46,14 +46,25 @@ Account::Account( | |||
} | |||
|
|||
Account | |||
Account::fromCache(std::string name, KeyType type) | |||
Account::fromCache(AcctStringType stringType, std::string name, KeyType type) | |||
{ | |||
auto p = std::make_pair(name, type); // non-const so it can be moved from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll note that this allocates, which isn't great for a function that's meant to be a cache. But not new code, so no change needed. But we may want to clean this up in another PR.
src/test/jtx/token.h
Outdated
std::uint16_t xferFee_; | ||
|
||
public: | ||
xferFee(std::uint16_t fee) : xferFee_(fee) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
explicit
(the other constructors in this file should be explicit
as well)
class issuer | ||
{ | ||
private: | ||
std::string issuer_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we're storing string representation of the wrapped object rather then the object itself (here and in the other classes). I'd probably store the object - they are 20-byte object, there's not much cost to copying them and storing them. But it's also fine as-is if you prefer it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing it as a string makes it a little easier to use, since we're just about to use it to build JSON. I'll leave as is. But thanks for the comment.
src/test/jtx/token.h
Outdated
std::string issuer_; | ||
|
||
public: | ||
issuer(jtx::Account const& issue) : issuer_(issue.human()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
explicit
{rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method."}, | ||
{rpcSENDMAX_MALFORMED, "sendMaxMalformed", "SendMax amount malformed."}, | ||
{rpcOBJECT_NOT_FOUND, "objectNotFound", "The requested object was not found."}}; | ||
// clang-format off |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be clang-format on
.
static uint256 constexpr accountBits( | ||
"ffffffffffffffffffffffffffffffffffffffff000000000000000000000000"sv); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use nft::pageMask
instead of local definition.
src/ripple/protocol/SField.h
Outdated
@@ -150,7 +153,8 @@ class SField | |||
const char* fn, | |||
int meta = sMD_Default, | |||
IsSigning signing = IsSigning::yes); | |||
explicit SField(private_access_tag_t, int fc); | |||
explicit SField(private_access_tag_t, int fc) = delete; | |||
explicit SField(private_access_tag_t, int fc, bool); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bool
parameter in this signature is unused. Remove the bool
, which takes this signature back to the original signature.
src/ripple/protocol/TER.h
Outdated
|
||
temUNCERTAIN, // An internal intermediate result; should never be returned. | ||
temUNKNOWN, // An internal intermediate result; should never be returned. | ||
|
||
temSEQ_AND_TICKET, | ||
tefNOT_POSSIBLE, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tefNOT_POSSIBLE
was added to the wrong enum
!
@seelabs, thank you very much for the thorough review. I've just added a commit that I believe addresses all of your comments that did not require renames. Today or tomorrow I'll submit a comment here that can be a starting place for the rename discussion. Renames will be complicated by the fact that we should probably not change API-facing names that are in the spec. That means a number of the names are cast in stone. Non-API facing names should harmonize with the names we can't change. More thoughts to follow. I'm assuming I should hold off on rebasing until the review is complete. |
I have been looking for a way to address the naming concerns raised by @seelabs. I agree with @seelabs that we have some serious naming inconsistencies and opportunities for developer confusion. I've pushed a couple of commits that address non-
If someone has concerns with those two commits they should be easy to remove. That leaves changes to Since I'm less confident that there is appetite for changing the
I'd appreciate reviewers weighing in on whether or not we should change the Thanks. |
All name changes are incorporated. Rebased to 1.9.0-b2 and squashed. |
High Level Overview of Change
Support for XLS-20 NFTs on the XRP Ledger.
Context of Change
If integrated, this branch provides native NFT support on the XRP Ledger.
Please see XRPLF/XRPL-Standards#46 for the spec and further details.
Type of Change