diff --git a/CHANGELOG.md b/CHANGELOG.md index ebca6cecb..14738d5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 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]] +- Add support for the DeliverMax field in Payment transactions ## [2.6.0] - 2024-06-03 diff --git a/tests/integration/transactions/test_payment.py b/tests/integration/transactions/test_payment.py index 7e308debb..a4b33f914 100644 --- a/tests/integration/transactions/test_payment.py +++ b/tests/integration/transactions/test_payment.py @@ -4,6 +4,7 @@ test_async_and_sync, ) from tests.integration.reusable_values import DESTINATION, WALLET +from xrpl.models.exceptions import XRPLModelException from xrpl.models.transactions import Payment @@ -20,3 +21,116 @@ async def test_basic_functionality(self, client): client, ) self.assertTrue(response.is_successful()) + + @test_async_and_sync(globals()) + async def test_deliver_max_alias_field(self, client): + delivered_amount = "200000" + + # Case-1: Only Amount field is present in the Payment transaction + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "Amount": delivered_amount, + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + payment_txn = Payment.from_xrpl(payment_tx_json) + self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) + + response = await sign_and_reliable_submission_async( + payment_txn, + WALLET, + client, + ) + self.assertTrue(response.is_successful()) + + # Case-2: Only deliver_max field is present in the Payment transaction + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "DeliverMax": delivered_amount, + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + payment_txn = Payment.from_xrpl(payment_tx_json) + self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) + + response = await sign_and_reliable_submission_async( + payment_txn, + WALLET, + client, + ) + self.assertTrue(response.is_successful()) + + # Case-3a: Both fields are present, with identical values + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "DeliverMax": delivered_amount, + "Amount": delivered_amount, + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + payment_txn = Payment.from_xrpl(payment_tx_json) + self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) + + response = await sign_and_reliable_submission_async( + payment_txn, + WALLET, + client, + ) + self.assertTrue(response.is_successful()) + + # Case-3b: Both fields are present, with different values + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "DeliverMax": delivered_amount, + "Amount": "10", + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + with self.assertRaises(XRPLModelException): + payment_txn = Payment.from_xrpl(payment_tx_json) + response = await sign_and_reliable_submission_async( + payment_txn, + WALLET, + client, + ) + self.assertFalse(response.is_successful()) + + # Case-4: Both fields are absent in the Payment transaction + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + with self.assertRaises(XRPLModelException): + payment_txn = Payment.from_xrpl(payment_tx_json) + response = await sign_and_reliable_submission_async( + payment_txn, + WALLET, + client, + ) + self.assertFalse(response.is_successful()) diff --git a/tests/unit/models/transactions/test_transaction.py b/tests/unit/models/transactions/test_transaction.py index 40bd3e913..dea0bb267 100644 --- a/tests/unit/models/transactions/test_transaction.py +++ b/tests/unit/models/transactions/test_transaction.py @@ -2,7 +2,7 @@ from xrpl.asyncio.transaction.main import sign from xrpl.models.exceptions import XRPLModelException -from xrpl.models.transactions import AccountSet, OfferCreate +from xrpl.models.transactions import AccountSet, OfferCreate, Payment from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types.transaction_type import TransactionType from xrpl.transaction.multisign import multisign @@ -157,3 +157,69 @@ def test_is_signed_for_multisigned_transaction(self): multisigned_tx = multisign(tx, [tx_1, tx_2]) self.assertTrue(multisigned_tx.is_signed()) + + # test the usage of DeliverMax field in Payment transactions + def test_payment_txn_API_no_deliver_max(self): + delivered_amount = "200000" + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "Amount": delivered_amount, + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + payment_txn = Payment.from_xrpl(payment_tx_json) + self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) + + def test_payment_txn_API_no_amount(self): + delivered_amount = "200000" + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "DeliverMax": delivered_amount, + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + payment_txn = Payment.from_xrpl(payment_tx_json) + self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) + + def test_payment_txn_API_different_amount_and_deliver_max(self): + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "DeliverMax": "200000", + "Amount": "200010", + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + with self.assertRaises(XRPLModelException): + Payment.from_xrpl(payment_tx_json) + + def test_payment_txn_API_identical_amount_and_deliver_max(self): + delivered_amount = "200000" + payment_tx_json = { + "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", + "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "TransactionType": "Payment", + "DeliverMax": delivered_amount, + "Amount": delivered_amount, + "Fee": "15", + "Flags": 0, + "Sequence": 144, + "LastLedgerSequence": 6220218, + } + + payment_txn = Payment.from_xrpl(payment_tx_json) + self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) diff --git a/xrpl/models/base_model.py b/xrpl/models/base_model.py index 583a564b2..87c790f34 100644 --- a/xrpl/models/base_model.py +++ b/xrpl/models/base_model.py @@ -213,6 +213,30 @@ def _from_dict_single_param( ) raise XRPLModelException(error_message) + @classmethod + def _process_xrpl_json( + cls: Type[BM], value: Union[str, Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Creates a dictionary object based on a JSON or dictionary in the standard XRPL + format. + + Args: + value: The dictionary or JSON string to be processed. + + Returns: + A formatted dictionary instantiated from the input. + """ + if isinstance(value, str): + value = json.loads(value) + + formatted_dict = { + _key_to_json(k): _value_to_json(v) + for (k, v) in cast(Dict[str, XRPL_VALUE_TYPE], value).items() + } + + return formatted_dict + @classmethod def _get_only_init_args(cls: Type[BM], args: Dict[str, Any]) -> Dict[str, Any]: init_keys = {field.name for field in fields(cls) if field.init is True} @@ -232,13 +256,7 @@ def from_xrpl(cls: Type[BM], value: Union[str, Dict[str, Any]]) -> BM: Returns: A BaseModel object instantiated from the input. """ - if isinstance(value, str): - value = json.loads(value) - - formatted_dict = { - _key_to_json(k): _value_to_json(v) - for (k, v) in cast(Dict[str, XRPL_VALUE_TYPE], value).items() - } + formatted_dict = cls._process_xrpl_json(value) return cls.from_dict(formatted_dict) diff --git a/xrpl/models/transactions/transaction.py b/xrpl/models/transactions/transaction.py index a9745e932..40925217b 100644 --- a/xrpl/models/transactions/transaction.py +++ b/xrpl/models/transactions/transaction.py @@ -468,3 +468,45 @@ def from_blob(tx_blob: str) -> Transaction: The formatted transaction. """ return Transaction.from_xrpl(decode(tx_blob)) + + @classmethod + def from_xrpl(cls: Type[T], value: Union[str, Dict[str, Any]]) -> T: + """ + Creates a Transaction object based on a JSON or JSON-string representation of + data + + In Payment transactions, the DeliverMax field is renamed to the Amount field. + + Args: + value: The dictionary or JSON string to be instantiated. + + Returns: + A Transaction object instantiated from the input. + + Raises: + XRPLModelException: If Payment transactions have different values for + amount and deliver_max fields + """ + processed_value = cls._process_xrpl_json(value) + + # handle the deliver_max alias in Payment transactions + if ( + "transaction_type" in processed_value + and processed_value["transaction_type"] == "Payment" + ) and "deliver_max" in processed_value: + if ( + "amount" in processed_value + and processed_value["amount"] != processed_value["deliver_max"] + ): + raise XRPLModelException( + "Error: amount and deliver_max fields must be equal if both are " + + "provided" + ) + else: + processed_value["amount"] = processed_value["deliver_max"] + + # deliver_max field is not recognised in the Payment Request format, + # nor is it supported in the serialization operations. + del processed_value["deliver_max"] + + return cls.from_dict(processed_value)