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

support DynamicNFT XLS-46 #799

Merged
merged 19 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `nfts_by_issuer` clio-only API definition
- Included `ctid` field in the `tx` request.
- `from_xrpl` method accepts input dictionary keys exclusively in the proper XRPL format.
- Add `NFTokenModify` transaction and add `TF_MUTABLE` flag in `NFTokenMintFlag`
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved

### Fixed
- Added support for `XChainModifyBridge` flag maps (fixing an issue with `NFTokenCreateOffer` flag names)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def test_nftoken_mint_flags(self):
TF_ONLY_XRP=True,
TF_TRANSFERABLE=True,
TF_TRUSTLINE=True,
TF_MUTABLE=True,
),
)
self.assertTrue(actual.has_flag(flag=0x00000001))
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/models/transactions/test_nftoken_modify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from unittest import TestCase

from xrpl.models.exceptions import XRPLModelException
from xrpl.models.transactions.nftoken_modify import NFTokenModify

_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
_SEQUENCE = 19048
_FEE = "0.00001"
_URI = "abc"
_NFTOKEN_ID = "00090032B5F762798A53D543A014CAF8B297CFF8F2F937E844B17C9E00000003"


class TestNFTokenModify(TestCase):
def test_nftoken_miss(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
uri=_ACCOUNT,
)

def test_uri_empty(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri="",
)

def test_uri_too_long(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri=_ACCOUNT * 1000,
)

def test_valid(self):
obj = NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
uri=_URI,
nftoken_id=_NFTOKEN_ID,
)
self.assertTrue(obj.is_valid())
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add more test cases for comprehensive coverage.

The current test suite should be expanded to include:

  1. Validation of hex-encoded URI format
  2. Validation of NFTokenID format
  3. Integration test with TF_MUTABLE flag
+    def test_uri_not_hex(self):
+        with self.assertRaises(XRPLModelException):
+            NFTokenModify(
+                account=_ACCOUNT,
+                owner=_ACCOUNT,
+                sequence=_SEQUENCE,
+                fee=_FEE,
+                nftoken_id=_NFTOKEN_ID,
+                uri="not-hex-encoded",
+            )
+
+    def test_invalid_nftoken_id_format(self):
+        with self.assertRaises(XRPLModelException):
+            NFTokenModify(
+                account=_ACCOUNT,
+                owner=_ACCOUNT,
+                sequence=_SEQUENCE,
+                fee=_FEE,
+                nftoken_id="invalid",
+                uri=_URI,
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class TestNFTokenModify(TestCase):
def test_nftoken_miss(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
uri=_ACCOUNT,
)
def test_uri_empty(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri="",
)
def test_uri_too_long(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri=_ACCOUNT * 1000,
)
def test_valid(self):
obj = NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
uri=_URI,
nftoken_id=_NFTOKEN_ID,
)
self.assertTrue(obj.is_valid())
class TestNFTokenModify(TestCase):
def test_nftoken_miss(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
uri=_ACCOUNT,
)
def test_uri_empty(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri="",
)
def test_uri_too_long(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri=_ACCOUNT * 1000,
)
def test_uri_not_hex(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id=_NFTOKEN_ID,
uri="not-hex-encoded",
)
def test_invalid_nftoken_id_format(self):
with self.assertRaises(XRPLModelException):
NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
nftoken_id="invalid",
uri=_URI,
)
def test_valid(self):
obj = NFTokenModify(
account=_ACCOUNT,
owner=_ACCOUNT,
sequence=_SEQUENCE,
fee=_FEE,
uri=_URI,
nftoken_id=_NFTOKEN_ID,
)
self.assertTrue(obj.is_valid())

3 changes: 2 additions & 1 deletion xrpl/core/binarycodec/definitions/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3085,6 +3085,7 @@
"NFTokenCancelOffer": 28,
"NFTokenCreateOffer": 27,
"NFTokenMint": 25,
"NFTokenModify": 61,
"OfferCancel": 8,
"OfferCreate": 7,
"OracleDelete": 52,
Expand Down Expand Up @@ -3138,4 +3139,4 @@
"Vector256": 19,
"XChainBridge": 25
}
}
}
2 changes: 2 additions & 0 deletions xrpl/models/transactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
NFTokenMintFlag,
NFTokenMintFlagInterface,
)
from xrpl.models.transactions.nftoken_modify import NFTokenModify
from xrpl.models.transactions.offer_cancel import OfferCancel
from xrpl.models.transactions.offer_create import (
OfferCreate,
Expand Down Expand Up @@ -161,6 +162,7 @@
"NFTokenMint",
"NFTokenMintFlag",
"NFTokenMintFlagInterface",
"NFTokenModify",
"OfferCancel",
"OfferCreate",
"OfferCreateFlag",
Expand Down
6 changes: 6 additions & 0 deletions xrpl/models/transactions/nftoken_mint.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ class NFTokenMintFlag(int, Enum):
issuer.
"""

TF_MUTABLE = 0x00000010
"""
If set, indicates that this NFT's URI can be modified.
"""


class NFTokenMintFlagInterface(FlagInterface):
"""Transaction Flags for an NFTokenMint Transaction."""
Expand All @@ -54,6 +59,7 @@ class NFTokenMintFlagInterface(FlagInterface):
TF_ONLY_XRP: bool
TF_TRUSTLINE: bool
TF_TRANSFERABLE: bool
TF_MUTABLE: bool


@require_kwargs_on_init
Expand Down
69 changes: 69 additions & 0 deletions xrpl/models/transactions/nftoken_modify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Model for NFTokenModify transaction type."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, Optional

from typing_extensions import Final, Self

from xrpl.models.required import REQUIRED
from xrpl.models.transactions.transaction import Transaction
from xrpl.models.transactions.types import TransactionType
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init

_MAX_URI_LENGTH: Final[int] = 512


@require_kwargs_on_init
@dataclass(frozen=True, **KW_ONLY_DATACLASS)
class NFTokenModify(Transaction):
"""
The NFTokenModify transaction modifies an NFToken's URI
if its tfMutable is set to true.
"""

nftoken_id: str = REQUIRED # type: ignore
"""
Identifies the TokenID of the NFToken object that the
offer references. This field is required.
"""
Comment on lines +26 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add NFTokenID format validation.

The nftoken_id field should validate that it matches the XRP Ledger's NFTokenID format to prevent runtime errors.

+_NFTOKEN_ID_REGEX = re.compile(r'^[0-9A-F]{64}$', re.IGNORECASE)
+
 @require_kwargs_on_init
 @dataclass(frozen=True, **KW_ONLY_DATACLASS)
 class NFTokenModify(Transaction):
     # ...
     def _get_errors(self: Self) -> Dict[str, str]:
         return {
             key: value
             for key, value in {
                 **super()._get_errors(),
                 "uri": self._get_uri_error(),
+                "nftoken_id": self._get_nftoken_id_error(),
             }.items()
             if value is not None
         }

+    def _get_nftoken_id_error(self: Self) -> Optional[str]:
+        if not _NFTOKEN_ID_REGEX.match(self.nftoken_id):
+            return "NFTokenID must be a 64-character hexadecimal string"
+        return None

Committable suggestion skipped: line range outside the PR's diff.


owner: Optional[str] = None
"""
Indicates the AccountID of the account that owns the
corresponding NFToken.
"""

uri: Optional[str] = None
"""
URI that points to the data and/or metadata associated with the NFT.
This field need not be an HTTP or HTTPS URL; it could be an IPFS URI, a
magnet link, immediate data encoded as an RFC2379 "data" URL, or even an
opaque issuer-specific encoding. The URI is not checked for validity.

This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to
convert a UTF-8 string to hex.
"""

transaction_type: TransactionType = field(
default=TransactionType.NFTOKEN_MODIFY,
init=False,
)

def _get_errors(self: Self) -> Dict[str, str]:
return {
key: value
for key, value in {
**super()._get_errors(),
"uri": self._get_uri_error(),
}.items()
if value is not None
}

def _get_uri_error(self: Self) -> Optional[str]:
if not self.uri:
return "URI must not be empty string"
elif len(self.uri) > _MAX_URI_LENGTH:
return f"Must not be longer than {_MAX_URI_LENGTH} characters"
yinyiqian1 marked this conversation as resolved.
Show resolved Hide resolved
return None
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for hex-encoded URI format.

The URI documentation states it must be hex-encoded, but the _get_uri_error method only validates length and emptiness. Consider adding validation for hex format.

 def _get_uri_error(self: Self) -> Optional[str]:
     if not self.uri:
         return "URI must not be empty string"
     elif len(self.uri) > _MAX_URI_LENGTH:
         return f"Must not be longer than {_MAX_URI_LENGTH} characters"
+    elif not all(c in '0123456789ABCDEFabcdef' for c in self.uri):
+        return "URI must be hex-encoded"
     return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _get_uri_error(self: Self) -> Optional[str]:
if not self.uri:
return "URI must not be empty string"
elif len(self.uri) > _MAX_URI_LENGTH:
return f"Must not be longer than {_MAX_URI_LENGTH} characters"
return None
def _get_uri_error(self: Self) -> Optional[str]:
if not self.uri:
return "URI must not be empty string"
elif len(self.uri) > _MAX_URI_LENGTH:
return f"Must not be longer than {_MAX_URI_LENGTH} characters"
elif not all(c in '0123456789ABCDEFabcdef' for c in self.uri):
return "URI must be hex-encoded"
return None

1 change: 1 addition & 0 deletions xrpl/models/transactions/types/transaction_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class TransactionType(str, Enum):
NFTOKEN_CANCEL_OFFER = "NFTokenCancelOffer"
NFTOKEN_CREATE_OFFER = "NFTokenCreateOffer"
NFTOKEN_MINT = "NFTokenMint"
NFTOKEN_MODIFY = "NFTokenModify"
OFFER_CANCEL = "OfferCancel"
OFFER_CREATE = "OfferCreate"
ORACLE_SET = "OracleSet"
Expand Down