Repository: XRPLF/xrpl-py

Issue:
Delete data, URI, DIDDocument fields in DID object
It is not possible to delete Data, URI and DIDDocument fields in a DID object. Users encounter a "data" : "Must be a hex" error. This is contrary to the expected behavior in the docs: 
```
Note: To delete the Data, DIDDocument, or URI field from an existing DID ledger entry, add the field as an empty string.
```
Source: https://xrpl.org/docs/references/protocol/transactions/types/didset/

A unit test to reproduce this issue can be found at the tip of this branch: https://github.com/ckeshava/xrpl-py/tree/DIDemptyData

Many thanks to this Discord user (0xΞarl) for bringing this to our attention: https://discord.com/channels/886050993802985492/886095603669544991/1251546703602126888

Discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08d50bd..28447f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,8 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [[Unreleased]]
+
+### Added
 - Add support for the DeliverMax field in Payment transactions
 
+### Fixed
+- Allow empty strings for the purpose of removing fields in DIDSet transaction
+
 ## [2.6.0] - 2024-06-03
 
 ### Added
diff --git a/xrpl/models/transactions/did_set.py b/xrpl/models/transactions/did_set.py
index 4fced01..36f0492 100644
--- a/xrpl/models/transactions/did_set.py
+++ b/xrpl/models/transactions/did_set.py
@@ -12,7 +12,7 @@ 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
 
-HEX_REGEX: Final[Pattern[str]] = re.compile("[a-fA-F0-9]+")
+HEX_REGEX: Final[Pattern[str]] = re.compile("[a-fA-F0-9]*")
 
 
 @require_kwargs_on_init
@@ -21,8 +21,26 @@ class DIDSet(Transaction):
     """Represents a DIDSet transaction."""
 
     did_document: Optional[str] = None
+    """
+    The DID document associated with the DID.
+
+    To delete the Data, DIDDocument, or URI field from an existing DID ledger
+    entry, add the field as an empty string.
+    """
+
     data: Optional[str] = None
+    """
+    The public attestations of identity credentials associated with the DID.
+    To delete the Data, DIDDocument, or URI field from an existing DID ledger
+    entry, add the field as an empty string.
+    """
+
     uri: Optional[str] = None
+    """
+    The Universal Resource Identifier associated with the DID.
+    To delete the Data, DIDDocument, or URI field from an existing DID ledger
+    entry, add the field as an empty string.
+    """
 
     transaction_type: TransactionType = field(
         default=TransactionType.DID_SET,
@@ -37,6 +55,14 @@ class DIDSet(Transaction):
             # Can return here because there are no fields to process
             return errors
 
+        if self.did_document == "" and self.data == "" and self.uri == "":
+            errors["did_set"] = (
+                "At least one of the fields `did_document`, `data`, and `uri` "
+                + "must have a length greater than zero"
+            )
+
+            return errors
+
         def _process_field(name: str, value: Optional[str]) -> None:
             if value is not None:
                 error_strs = []


Test patch:
diff --git a/tests/unit/models/transactions/test_did_set.py b/tests/unit/models/transactions/test_did_set.py
index 3a55cb6..5c1d185 100644
--- a/tests/unit/models/transactions/test_did_set.py
+++ b/tests/unit/models/transactions/test_did_set.py
@@ -62,3 +62,15 @@ class TestDIDSet(TestCase):
             error.exception.args[0],
             "{'did_set': 'Must have one of `did_document`, `data`, and `uri`.'}",
         )
+
+    def test_create_did_object_all_empty_fields(self):
+        with self.assertRaises(XRPLModelException):
+            DIDSet(account=_ACCOUNT, data="", did_document="", uri="")
+
+    def test_empty_data_field(self):
+        # create a valid DID object
+        tx = DIDSet(
+            account=_ACCOUNT,
+            data="",
+        )
+        self.assertTrue(tx.is_valid())