From f0a0f585d65ebd9e5e9a1cd750043b8261b707fc Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 23 Jan 2025 17:31:55 +0530 Subject: [PATCH 01/51] models for SAV transactions and requests --- xrpl/models/requests/ledger_entry.py | 4 ++ .../transactions/types/transaction_type.py | 6 +++ xrpl/models/transactions/vault_clawback.py | 28 +++++++++++ xrpl/models/transactions/vault_create.py | 47 +++++++++++++++++++ xrpl/models/transactions/vault_delete.py | 25 ++++++++++ xrpl/models/transactions/vault_deposit.py | 28 +++++++++++ xrpl/models/transactions/vault_set.py | 29 ++++++++++++ xrpl/models/transactions/vault_withdraw.py | 29 ++++++++++++ 8 files changed, 196 insertions(+) create mode 100644 xrpl/models/transactions/vault_clawback.py create mode 100644 xrpl/models/transactions/vault_create.py create mode 100644 xrpl/models/transactions/vault_delete.py create mode 100644 xrpl/models/transactions/vault_deposit.py create mode 100644 xrpl/models/transactions/vault_set.py create mode 100644 xrpl/models/transactions/vault_withdraw.py diff --git a/xrpl/models/requests/ledger_entry.py b/xrpl/models/requests/ledger_entry.py index 4499d9e83..637f0a1ad 100644 --- a/xrpl/models/requests/ledger_entry.py +++ b/xrpl/models/requests/ledger_entry.py @@ -39,6 +39,7 @@ class LedgerEntryType(str, Enum): ORACLE = "oracle" PAYMENT_CHANNEL = "payment_channel" SIGNER_LIST = "signer_list" + SINGLE_ASSET_VAULT = "vault_id" STATE = "state" TICKET = "ticket" MPT_ISSUANCE = "mpt_issuance" @@ -300,6 +301,8 @@ class LedgerEntry(Request, LookupByLedgerRequest): oracle: Optional[Oracle] = None payment_channel: Optional[str] = None ripple_state: Optional[RippleState] = None + # Single Asset Vault ledger-object can be retrieved by its index only + vault_id: Optional[str] = None ticket: Optional[Union[str, Ticket]] = None bridge_account: Optional[str] = None bridge: Optional[XChainBridge] = None @@ -333,6 +336,7 @@ def _get_errors(self: Self) -> Dict[str, str]: self.oracle, self.payment_channel, self.ripple_state, + self.vault_id, self.ticket, self.xchain_claim_id, self.xchain_create_account_claim_id, diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 1d2119a90..4cbe9680c 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -48,6 +48,12 @@ class TransactionType(str, Enum): SIGNER_LIST_SET = "SignerListSet" TICKET_CREATE = "TicketCreate" TRUST_SET = "TrustSet" + VAULT_CREATE = "VaultCreate" + VAULT_CLAWBACK = "VaultClawback" + VAULT_DEPOSIT = "VaultDeposit" + VAULT_DELETE = "VaultDelete" + VAULT_SET = "VaultSet" + VAULT_WITHDRAW = "VaultWithdraw" XCHAIN_ACCOUNT_CREATE_COMMIT = "XChainAccountCreateCommit" XCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION = "XChainAddAccountCreateAttestation" XCHAIN_ADD_CLAIM_ATTESTATION = "XChainAddClaimAttestation" diff --git a/xrpl/models/transactions/vault_clawback.py b/xrpl/models/transactions/vault_clawback.py new file mode 100644 index 000000000..342925f6b --- /dev/null +++ b/xrpl/models/transactions/vault_clawback.py @@ -0,0 +1,28 @@ +""" +Represents a VaultClawback transaction on the XRP Ledger. +""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +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 + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultClawback(Transaction): + """ + Represents a VaultClawback transaction on the XRP Ledger. + """ + + vault_id: str = REQUIRED # type: ignore + holder: str = REQUIRED # type: ignore + amount: Optional[int] = None + + transaction_type: TransactionType = field( + default=TransactionType.VAULT_CLAWBACK, + init=False, + ) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py new file mode 100644 index 000000000..5b9de194f --- /dev/null +++ b/xrpl/models/transactions/vault_create.py @@ -0,0 +1,47 @@ +""" +Represents a VaultCreate transaction on the XRP Ledger. +""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional, Union + +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.flags import FlagInterface +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 + + +class VaultCreateFlag(int, Enum): + + TF_VAULT_PRIVATE = 0x0001 + TF_VAULT_SHARE_NON_TRANSFERABLE = 0x0002 + + +class VaultCreateFlagInterface(FlagInterface): + + TF_VAULT_PRIVATE: bool + TF_VAULT_SHARE_NON_TRANSFERABLE: bool + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultCreate(Transaction): + """ + Represents a VaultCreate transaction on the XRP Ledger. + """ + + data: Optional[str] = None + # Keshava: Is this an accurate representation of the asset object type, does it + # capture MPT? + asset: Union[str, IssuedCurrencyAmount] = REQUIRED # type: ignore + asset_maximum: Optional[str] = None + mptoken_metadata: Optional[str] = None + permissioned_domain_id: Optional[str] = None + + transaction_type: TransactionType = field( + default=TransactionType.VAULT_CREATE, + init=False, + ) diff --git a/xrpl/models/transactions/vault_delete.py b/xrpl/models/transactions/vault_delete.py new file mode 100644 index 000000000..4d690badf --- /dev/null +++ b/xrpl/models/transactions/vault_delete.py @@ -0,0 +1,25 @@ +""" +Represents a VaultDelete transaction on the XRP Ledger. +""" + +from dataclasses import dataclass, field + +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 + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultDelete(Transaction): + """ + Represents a VaultDelete transaction on the XRP Ledger. + """ + + vault_id: str = REQUIRED # type: ignore + + transaction_type: TransactionType = field( + default=TransactionType.VAULT_DELETE, + init=False, + ) diff --git a/xrpl/models/transactions/vault_deposit.py b/xrpl/models/transactions/vault_deposit.py new file mode 100644 index 000000000..ca1882bcb --- /dev/null +++ b/xrpl/models/transactions/vault_deposit.py @@ -0,0 +1,28 @@ +""" +Represents a VaultDeposit transaction on the XRP Ledger. +""" + +from dataclasses import dataclass, field +from typing import Union + +from xrpl.models.amounts import IssuedCurrencyAmount +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 + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultDeposit(Transaction): + """ + Represents a VaultDeposit transaction on the XRP Ledger. + """ + + vault_id: str = REQUIRED # type: ignore + amount: Union[str, IssuedCurrencyAmount] = REQUIRED # type: ignore + + transaction_type: TransactionType = field( + default=TransactionType.VAULT_DEPOSIT, + init=False, + ) diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py new file mode 100644 index 000000000..03eab3162 --- /dev/null +++ b/xrpl/models/transactions/vault_set.py @@ -0,0 +1,29 @@ +""" +Represents a VaultSet transaction on the XRP Ledger. +""" + +from dataclasses import dataclass, field +from typing import Optional + +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 + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultSet(Transaction): + """ + Represents a VaultSet transaction on the XRP Ledger. + """ + + vault_id: str = REQUIRED # type: ignore + domain_id: Optional[str] = None + data: Optional[str] = None + asset_maximum: Optional[str] = None + + transaction_type: TransactionType = field( + default=TransactionType.VAULT_SET, + init=False, + ) diff --git a/xrpl/models/transactions/vault_withdraw.py b/xrpl/models/transactions/vault_withdraw.py new file mode 100644 index 000000000..3e92a5e16 --- /dev/null +++ b/xrpl/models/transactions/vault_withdraw.py @@ -0,0 +1,29 @@ +""" +Represents a VaultWithdraw transaction on the XRP Ledger. +""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +from xrpl.models.amounts import IssuedCurrencyAmount +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 + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultWithdraw(Transaction): + """ + Represents a VaultWithdraw transaction on the XRP Ledger. + """ + + vault_id: str = REQUIRED # type: ignore + amount: Union[str, IssuedCurrencyAmount] = REQUIRED # type: ignore + destination: Optional[str] = None + + transaction_type: TransactionType = field( + default=TransactionType.VAULT_WITHDRAW, + init=False, + ) From e4e9ec55042f1410c7520d942793d04ca6cd1266 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 24 Jan 2025 21:10:45 +0530 Subject: [PATCH 02/51] initial framework of integration tests; definitions file --- .ci-config/rippled.cfg | 1 + tests/integration/transactions/test_sav.py | 41 +++++++++++++ .../binarycodec/definitions/definitions.json | 60 ++++++++++++++++++- xrpl/models/transactions/__init__.py | 12 ++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/integration/transactions/test_sav.py diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 3e57aaf0a..df89a1097 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -194,6 +194,7 @@ fixNFTokenPageLinks fixInnerObjTemplate2 fixEnforceNFTokenTrustline fixReducedOffersV2 +SingleAssetVault # This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode [voting] diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py new file mode 100644 index 000000000..7e56f059e --- /dev/null +++ b/tests/integration/transactions/test_sav.py @@ -0,0 +1,41 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import WALLET +from xrpl.models import VaultCreate +from xrpl.models.response import ResponseStatus + + +class TestSingleAssetVault(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_sav_lifecycle(self, client): + + # Create a vault + tx = VaultCreate( + account=WALLET.address, + asset="100", + asset_maximum="1000", + ) + response = await sign_and_reliable_submission_async(tx, WALLET, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + # Verify the existence of the vault with account_objects RPC call + + # Update the characteristics of the vault with VaultSet transaction + + # Execute a VaultDeposit transaction + + # Execute a VaultWithdraw transaction + + # Execute a VaultClawback transaction + + # Delete the Vault with VaultDelete transaction + + # # confirm that the DID was actually created + # account_objects_response = await client.request( + # AccountObjects(account=WALLET.address, type=AccountObjectType.DID) + # ) + # self.assertEqual(len(account_objects_response.result["account_objects"]), 1) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index c6940aba8..279208b1d 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -1250,6 +1250,16 @@ "type": "Hash256" } ], + [ + "VaultID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 35, + "type": "Hash256" + } + ], [ "hash", { @@ -2020,6 +2030,46 @@ "type": "AccountID" } ], + [ + "AssetAvailable", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Number" + } + ], + [ + "AssetMaximum", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 3, + "type": "Number" + } + ], + [ + "AssetTotal", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 4, + "type": "Number" + } + ], + [ + "LossUnrealized", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 5, + "type": "Number" + } + ], [ "TransactionMetaData", { @@ -2866,6 +2916,7 @@ "RippleState": 114, "SignerList": 83, "Ticket": 84, + "Vault" : 131, "XChainOwnedClaimID": 113, "XChainOwnedCreateAccountClaimID": 116 }, @@ -3099,6 +3150,12 @@ "TicketCreate": 10, "TrustSet": 20, "UNLModify": 102, + "VaultCreate": 64, + "VaultClawback": 69, + "VaultDeposit": 67, + "VaultDelete": 66, + "VaultSet": 65, + "VaultWithdraw": 68, "XChainAccountCreateCommit": 44, "XChainAddAccountCreateAttestation": 46, "XChainAddClaimAttestation": 45, @@ -3122,6 +3179,7 @@ "LedgerEntry": 10002, "Metadata": 10004, "NotPresent": 0, + "Number": 9, "PathSet": 18, "STArray": 15, "STObject": 14, @@ -3138,4 +3196,4 @@ "Vector256": 19, "XChainBridge": 25 } -} \ No newline at end of file +} diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index e59363d0d..c1b6a940a 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -92,6 +92,12 @@ TrustSetFlag, TrustSetFlagInterface, ) +from xrpl.models.transactions.vault_clawback import VaultClawback +from xrpl.models.transactions.vault_create import VaultCreate +from xrpl.models.transactions.vault_delete import VaultDelete +from xrpl.models.transactions.vault_deposit import VaultDeposit +from xrpl.models.transactions.vault_set import VaultSet +from xrpl.models.transactions.vault_withdraw import VaultWithdraw from xrpl.models.transactions.xchain_account_create_commit import ( XChainAccountCreateCommit, ) @@ -185,6 +191,12 @@ "TrustSet", "TrustSetFlag", "TrustSetFlagInterface", + "VaultClawback", + "VaultCreate", + "VaultDelete", + "VaultDeposit", + "VaultSet", + "VaultWithdraw", "XChainAccountCreateCommit", "XChainAddAccountCreateAttestation", "XChainAddClaimAttestation", From 5d015a3f192ee40394ad49d01c1aee024027e107 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 4 Feb 2025 14:31:20 -0800 Subject: [PATCH 03/51] provide MPT support for SAV transaction fields --- .../binarycodec/definitions/definitions.json | 20 +++++++++++++ xrpl/core/binarycodec/types/number.py | 30 +++++++++++++++++++ xrpl/models/transactions/vault_clawback.py | 5 ++-- xrpl/models/transactions/vault_create.py | 8 ++--- xrpl/models/transactions/vault_deposit.py | 5 ++-- xrpl/models/transactions/vault_withdraw.py | 6 ++-- 6 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 xrpl/core/binarycodec/types/number.py diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 279208b1d..74d98d069 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -1250,6 +1250,16 @@ "type": "Hash256" } ], + [ + "DomainID", + { + "isSerialized" : true, + "isSigningField" : true, + "isVLEncoded" : false, + "nth" : 34, + "type" : "Hash256" + } + ], [ "VaultID", { @@ -2030,6 +2040,16 @@ "type": "AccountID" } ], + [ + "Number", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 1, + "type": "Number" + } + ], [ "AssetAvailable", { diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py new file mode 100644 index 000000000..c0a29f29a --- /dev/null +++ b/xrpl/core/binarycodec/types/number.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional, Type + +from typing_extensions import Self + +from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser +from xrpl.core.binarycodec.types.serialized_type import SerializedType + + +class Number(SerializedType): + """Codec for serializing and deserializing Number fields.""" + + def __init__(self: Self, buffer: bytes) -> None: + """Construct a Number from given bytes.""" + super().__init__(buffer) + + @classmethod + def from_parser( # noqa: D102 + cls: Type[Self], + parser: BinaryParser, + # length_hint is Any so that subclasses can choose whether or not to require it. + length_hint: Any, # noqa: ANN401 + ) -> Self: + pass + + @classmethod + def from_value(cls: Type[Self], value: Dict[str, Any]) -> Self: + return cls(bytes(value)) + + # def to_json(self: Self) -> Dict[str, Any]: + # pass diff --git a/xrpl/models/transactions/vault_clawback.py b/xrpl/models/transactions/vault_clawback.py index 342925f6b..f0660c481 100644 --- a/xrpl/models/transactions/vault_clawback.py +++ b/xrpl/models/transactions/vault_clawback.py @@ -3,8 +3,9 @@ """ from dataclasses import dataclass, field -from typing import Optional, Union +from typing import Optional +from xrpl.models.amounts import Amount from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType @@ -20,7 +21,7 @@ class VaultClawback(Transaction): vault_id: str = REQUIRED # type: ignore holder: str = REQUIRED # type: ignore - amount: Optional[int] = None + amount: Optional[Amount] = None transaction_type: TransactionType = field( default=TransactionType.VAULT_CLAWBACK, diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 5b9de194f..1ba3db60c 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -4,9 +4,9 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Optional, Union +from typing import Optional -from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.amounts import Amount from xrpl.models.flags import FlagInterface from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -34,9 +34,7 @@ class VaultCreate(Transaction): """ data: Optional[str] = None - # Keshava: Is this an accurate representation of the asset object type, does it - # capture MPT? - asset: Union[str, IssuedCurrencyAmount] = REQUIRED # type: ignore + asset: Amount = REQUIRED # type: ignore asset_maximum: Optional[str] = None mptoken_metadata: Optional[str] = None permissioned_domain_id: Optional[str] = None diff --git a/xrpl/models/transactions/vault_deposit.py b/xrpl/models/transactions/vault_deposit.py index ca1882bcb..6281eef33 100644 --- a/xrpl/models/transactions/vault_deposit.py +++ b/xrpl/models/transactions/vault_deposit.py @@ -3,9 +3,8 @@ """ from dataclasses import dataclass, field -from typing import Union -from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.amounts import Amount from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType @@ -20,7 +19,7 @@ class VaultDeposit(Transaction): """ vault_id: str = REQUIRED # type: ignore - amount: Union[str, IssuedCurrencyAmount] = REQUIRED # type: ignore + amount: Amount = REQUIRED # type: ignore transaction_type: TransactionType = field( default=TransactionType.VAULT_DEPOSIT, diff --git a/xrpl/models/transactions/vault_withdraw.py b/xrpl/models/transactions/vault_withdraw.py index 3e92a5e16..db45acdc8 100644 --- a/xrpl/models/transactions/vault_withdraw.py +++ b/xrpl/models/transactions/vault_withdraw.py @@ -3,9 +3,9 @@ """ from dataclasses import dataclass, field -from typing import Optional, Union +from typing import Optional -from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.amounts import Amount from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType @@ -20,7 +20,7 @@ class VaultWithdraw(Transaction): """ vault_id: str = REQUIRED # type: ignore - amount: Union[str, IssuedCurrencyAmount] = REQUIRED # type: ignore + amount: Amount = REQUIRED # type: ignore destination: Optional[str] = None transaction_type: TransactionType = field( From 6ea396570564dfd359dd57f6523674b213df434a Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 4 Feb 2025 14:32:28 -0800 Subject: [PATCH 04/51] Test extreme values in the serialzation of STAmount --- tests/unit/core/binarycodec/types/test_amount.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/core/binarycodec/types/test_amount.py b/tests/unit/core/binarycodec/types/test_amount.py index 00246d377..72addd03c 100644 --- a/tests/unit/core/binarycodec/types/test_amount.py +++ b/tests/unit/core/binarycodec/types/test_amount.py @@ -108,6 +108,17 @@ def test_assert_xrp_is_valid_passes(self): amount.verify_xrp_value(valid_zero) amount.verify_xrp_value(valid_amount) + # Note: these values are obtained from the following rippled unit test: + # https://github.com/XRPLF/rippled/blob/33e1c42599857336d792effc753795911bdb13f0/src/test/protocol/STAmount_test.cpp#L513 + # However, the limiting values allowed by the STAmount type are much higher and + # smaller. + def test_large_small_values(self): + small_value = "5499999999999999e-95" + large_value = "15499999999999999e79" + + amount.verify_xrp_value(small_value) + amount.verify_xrp_value(large_value) + def test_assert_xrp_is_valid_raises(self): invalid_amount_large = "1e20" invalid_amount_small = "1e-7" From f06f9095d1e08848bacde2b4de0b2be448dec745 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 4 Feb 2025 14:38:53 -0800 Subject: [PATCH 05/51] Problems in STAmount: Unable to (de)serialize extreme values --- tests/unit/core/binarycodec/types/test_amount.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/core/binarycodec/types/test_amount.py b/tests/unit/core/binarycodec/types/test_amount.py index 72addd03c..c0fa4f2d1 100644 --- a/tests/unit/core/binarycodec/types/test_amount.py +++ b/tests/unit/core/binarycodec/types/test_amount.py @@ -114,7 +114,13 @@ def test_assert_xrp_is_valid_passes(self): # smaller. def test_large_small_values(self): small_value = "5499999999999999e-95" + + serialized_representation = amount.Amount.from_value(small_value) + self.assertEqual(amount.Amount.to_json(serialized_representation), small_value) + large_value = "15499999999999999e79" + serialized_representation = amount.Amount.from_value(large_value) + self.assertEqual(amount.Amount.to_json(serialized_representation), large_value) amount.verify_xrp_value(small_value) amount.verify_xrp_value(large_value) From 2858df22ec4a50b5f3736834a0a93df94fb0fa90 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 4 Feb 2025 15:59:37 -0800 Subject: [PATCH 06/51] [WIP] use struct library to pack Number data --- .../core/binarycodec/types/test_number.py | 9 +++++++ xrpl/core/binarycodec/types/__init__.py | 2 ++ xrpl/core/binarycodec/types/number.py | 26 ++++++++++++------- 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 tests/unit/core/binarycodec/types/test_number.py diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py new file mode 100644 index 000000000..2a5e815bd --- /dev/null +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -0,0 +1,9 @@ +import unittest + +from xrpl.core.binarycodec.types.number import Number + + +class TestNumber(unittest.TestCase): + def test_serialization_and_deserialization(self): + number_bytes = Number.from_value(124) + self.assertEqual(Number(number_bytes).to_json(), "124") diff --git a/xrpl/core/binarycodec/types/__init__.py b/xrpl/core/binarycodec/types/__init__.py index f2dedab06..66e14cc0d 100644 --- a/xrpl/core/binarycodec/types/__init__.py +++ b/xrpl/core/binarycodec/types/__init__.py @@ -10,6 +10,7 @@ from xrpl.core.binarycodec.types.hash192 import Hash192 from xrpl.core.binarycodec.types.hash256 import Hash256 from xrpl.core.binarycodec.types.issue import Issue +from xrpl.core.binarycodec.types.number import Number from xrpl.core.binarycodec.types.path_set import PathSet from xrpl.core.binarycodec.types.st_array import STArray from xrpl.core.binarycodec.types.st_object import STObject @@ -32,6 +33,7 @@ "Hash192", "Hash256", "Issue", + "Number", "PathSet", "STObject", "STArray", diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index c0a29f29a..e7cf324d0 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -1,8 +1,10 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Type +import struct +from typing import Optional, Type from typing_extensions import Self from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser +from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException from xrpl.core.binarycodec.types.serialized_type import SerializedType @@ -17,14 +19,20 @@ def __init__(self: Self, buffer: bytes) -> None: def from_parser( # noqa: D102 cls: Type[Self], parser: BinaryParser, - # length_hint is Any so that subclasses can choose whether or not to require it. - length_hint: Any, # noqa: ANN401 + length_hint: Optional[int] = None, # noqa: ANN401 ) -> Self: - pass + # Number type consists of two cpp std::uint_64t (mantissa) and + # std::uint_32t (exponent) types which are 8 bytes and 4 bytes respectively + return cls(parser.read(12)) @classmethod - def from_value(cls: Type[Self], value: Dict[str, Any]) -> Self: - return cls(bytes(value)) - - # def to_json(self: Self) -> Dict[str, Any]: - # pass + def from_value(cls: Type[Self], value: str) -> Self: + return cls(struct.pack(">d", float(value))) + + def to_json(self: Self) -> str: + unpack_elems = struct.unpack(">d", self.buffer) + if len(unpack_elems) != 1: + raise XRPLBinaryCodecException( + "Deserialization of Number type did not produce exactly one element" + ) + return str(unpack_elems[0]) From ea1ef24f023e45b5a8cb5d4b7e22f6104664a7dc Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 27 Feb 2025 14:33:26 -0800 Subject: [PATCH 07/51] [WIP] VaultClawback integ tests need to be completed; Errors regarding serialization of Number need to be solved; --- tests/integration/transactions/test_sav.py | 98 ++++++++++++++++--- .../core/binarycodec/types/test_number.py | 8 +- xrpl/asyncio/transaction/main.py | 1 + xrpl/models/requests/account_objects.py | 1 + xrpl/models/transactions/vault_create.py | 4 +- 5 files changed, 95 insertions(+), 17 deletions(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index 7e56f059e..0c7ee29d7 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -1,41 +1,109 @@ from tests.integration.integration_test_case import IntegrationTestCase from tests.integration.it_utils import ( + fund_wallet_async, sign_and_reliable_submission_async, test_async_and_sync, ) from tests.integration.reusable_values import WALLET -from xrpl.models import VaultCreate +from xrpl.models import ( + TrustSet, + VaultCreate, + VaultDelete, + VaultDeposit, + VaultSet, + VaultWithdraw, +) +from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.requests import AccountObjects +from xrpl.models.requests.account_objects import AccountObjectType from xrpl.models.response import ResponseStatus +from xrpl.utils import str_to_hex +from xrpl.wallet import Wallet class TestSingleAssetVault(IntegrationTestCase): @test_async_and_sync(globals()) async def test_sav_lifecycle(self, client): - # Create a vault + vault_owner = Wallet.create() + await fund_wallet_async(vault_owner) + + # # Prerequisites: Set up the IOU trust lines + # tx = TrustSet( + # account=vault_owner.address, + # limit_amount=IssuedCurrencyAmount( + # currency="USD", issuer=WALLET.address, value="1000" + # ), + # ) + # response = await sign_and_reliable_submission_async(tx, vault_owner, client) + # self.assertEqual(response.status, ResponseStatus.SUCCESS) + # self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + # Step-1: Create a vault tx = VaultCreate( - account=WALLET.address, - asset="100", - asset_maximum="1000", + account=vault_owner.address, + asset=XRP(), + # asset=IssuedCurrency(currency="USD", issuer=WALLET.address), + # TODO: This throws a Number::normalize 1 exception in rippled, why ?? + # Possible errors in serialization of Number type + # asset_maximum="1000", ) - response = await sign_and_reliable_submission_async(tx, WALLET, client) + response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") # Verify the existence of the vault with account_objects RPC call + account_objects_response = await client.request( + AccountObjects(account=vault_owner.address, type=AccountObjectType.VAULT) + ) + self.assertEqual(len(account_objects_response.result["account_objects"]), 1) - # Update the characteristics of the vault with VaultSet transaction + VAULT_ID = account_objects_response.result["account_objects"][0]["index"] - # Execute a VaultDeposit transaction + # Step-2: Update the characteristics of the vault with VaultSet transaction + tx = VaultSet( + account=vault_owner.address, + vault_id=VAULT_ID, + data=str_to_hex("auxilliary data pertaining to the vault"), + ) + response = await sign_and_reliable_submission_async(tx, vault_owner, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + # Step-3: Execute a VaultDeposit transaction + tx = VaultDeposit( + account=WALLET.address, + vault_id=VAULT_ID, + amount="10", + # amount=IssuedCurrencyAmount( + # currency="USD", issuer=WALLET.address, value="10" + # ), + ) + response = await sign_and_reliable_submission_async(tx, WALLET, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") # Execute a VaultWithdraw transaction + tx = VaultWithdraw( + account=WALLET.address, + vault_id=VAULT_ID, + amount="10", + # amount=IssuedCurrencyAmount( + # currency="USD", issuer=WALLET.address, value="10" + # ), + ) + response = await sign_and_reliable_submission_async(tx, WALLET, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") - # Execute a VaultClawback transaction + # TODO: Execute a VaultClawback transaction # Delete the Vault with VaultDelete transaction - - # # confirm that the DID was actually created - # account_objects_response = await client.request( - # AccountObjects(account=WALLET.address, type=AccountObjectType.DID) - # ) - # self.assertEqual(len(account_objects_response.result["account_objects"]), 1) + tx = VaultDelete( + account=vault_owner.address, + vault_id=account_objects_response.result["account_objects"][0]["index"], + ) + response = await sign_and_reliable_submission_async(tx, vault_owner, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py index 2a5e815bd..fb9898308 100644 --- a/tests/unit/core/binarycodec/types/test_number.py +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -6,4 +6,10 @@ class TestNumber(unittest.TestCase): def test_serialization_and_deserialization(self): number_bytes = Number.from_value(124) - self.assertEqual(Number(number_bytes).to_json(), "124") + self.assertEqual((number_bytes).to_json(), "124.0") + + number_bytes = Number.from_value(0) + self.assertEqual((number_bytes).to_json(), "0.0") + + number_bytes = Number.from_value(-10) + self.assertEqual((number_bytes).to_json(), "-10.0") diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index a9f0f6f9c..48eabe276 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -483,6 +483,7 @@ async def _calculate_fee_per_transaction_type( if transaction.transaction_type in ( TransactionType.ACCOUNT_DELETE, TransactionType.AMM_CREATE, + TransactionType.VAULT_CREATE, ): base_fee = await _fetch_owner_reserve_fee(client) diff --git a/xrpl/models/requests/account_objects.py b/xrpl/models/requests/account_objects.py index 2ce9f1798..bd41cae4c 100644 --- a/xrpl/models/requests/account_objects.py +++ b/xrpl/models/requests/account_objects.py @@ -36,6 +36,7 @@ class AccountObjectType(str, Enum): SIGNER_LIST = "signer_list" STATE = "state" TICKET = "ticket" + VAULT = "vault" XCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID = "xchain_owned_create_account_claim_id" XCHAIN_OWNED_CLAIM_ID = "xchain_owned_claim_id" diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 1ba3db60c..3919ac24e 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -7,6 +7,7 @@ from typing import Optional from xrpl.models.amounts import Amount +from xrpl.models.currencies import Currency from xrpl.models.flags import FlagInterface from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -34,7 +35,8 @@ class VaultCreate(Transaction): """ data: Optional[str] = None - asset: Amount = REQUIRED # type: ignore + # Keshava: TODO: Include MPT Issue in Asset field + asset: Currency = REQUIRED # type: ignore asset_maximum: Optional[str] = None mptoken_metadata: Optional[str] = None permissioned_domain_id: Optional[str] = None From 6966ae71c18dbc363dfa58187d873eb4bfa1ecbe Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 27 Feb 2025 15:53:13 -0800 Subject: [PATCH 08/51] add integ test for VaultClawback transaction --- tests/integration/transactions/test_sav.py | 77 +++++++++++++++------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index 0c7ee29d7..0496e13e7 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -6,7 +6,9 @@ ) from tests.integration.reusable_values import WALLET from xrpl.models import ( + Payment, TrustSet, + VaultClawback, VaultCreate, VaultDelete, VaultDeposit, @@ -14,7 +16,7 @@ VaultWithdraw, ) from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount -from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.currencies import IssuedCurrency from xrpl.models.requests import AccountObjects from xrpl.models.requests.account_objects import AccountObjectType from xrpl.models.response import ResponseStatus @@ -29,22 +31,36 @@ async def test_sav_lifecycle(self, client): vault_owner = Wallet.create() await fund_wallet_async(vault_owner) - # # Prerequisites: Set up the IOU trust lines - # tx = TrustSet( - # account=vault_owner.address, - # limit_amount=IssuedCurrencyAmount( - # currency="USD", issuer=WALLET.address, value="1000" - # ), - # ) - # response = await sign_and_reliable_submission_async(tx, vault_owner, client) - # self.assertEqual(response.status, ResponseStatus.SUCCESS) - # self.assertEqual(response.result["engine_result"], "tesSUCCESS") + issuer_wallet = Wallet.create() + await fund_wallet_async(issuer_wallet) + + # Step-0.a: Prerequisites: Set up the IOU trust line + tx = TrustSet( + account=WALLET.address, + limit_amount=IssuedCurrencyAmount( + currency="USD", issuer=issuer_wallet.address, value="1000" + ), + ) + response = await sign_and_reliable_submission_async(tx, WALLET, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + # Step-0.b: Send the payment of IOUs from issuer_wallet to WALLET + tx = Payment( + account=issuer_wallet.address, + amount=IssuedCurrencyAmount( + currency="USD", issuer=issuer_wallet.address, value="1000" + ), + destination=WALLET.address, + ) + response = await sign_and_reliable_submission_async(tx, issuer_wallet, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") # Step-1: Create a vault tx = VaultCreate( account=vault_owner.address, - asset=XRP(), - # asset=IssuedCurrency(currency="USD", issuer=WALLET.address), + asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), # TODO: This throws a Number::normalize 1 exception in rippled, why ?? # Possible errors in serialization of Number type # asset_maximum="1000", @@ -75,34 +91,45 @@ async def test_sav_lifecycle(self, client): tx = VaultDeposit( account=WALLET.address, vault_id=VAULT_ID, - amount="10", - # amount=IssuedCurrencyAmount( - # currency="USD", issuer=WALLET.address, value="10" - # ), + amount=IssuedCurrencyAmount( + currency="USD", issuer=issuer_wallet.address, value="10" + ), ) response = await sign_and_reliable_submission_async(tx, WALLET, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") - # Execute a VaultWithdraw transaction + # Step-4: Execute a VaultWithdraw transaction tx = VaultWithdraw( account=WALLET.address, vault_id=VAULT_ID, - amount="10", - # amount=IssuedCurrencyAmount( - # currency="USD", issuer=WALLET.address, value="10" - # ), + amount=IssuedCurrencyAmount( + currency="USD", issuer=issuer_wallet.address, value="9" + ), ) response = await sign_and_reliable_submission_async(tx, WALLET, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") - # TODO: Execute a VaultClawback transaction + # Step-5: Execute a VaultClawback transaction from issuer_wallet + tx = VaultClawback( + holder=WALLET.address, + account=issuer_wallet.address, + vault_id=VAULT_ID, + # Note: Although the amount is specified as 9, 1 unit of the IOU will be + # clawed back, because that is the remaining balance in the vault + amount=IssuedCurrencyAmount( + currency="USD", issuer=issuer_wallet.address, value="9" + ), + ) + response = await sign_and_reliable_submission_async(tx, issuer_wallet, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") - # Delete the Vault with VaultDelete transaction + # Step-6: Delete the Vault with VaultDelete transaction tx = VaultDelete( account=vault_owner.address, - vault_id=account_objects_response.result["account_objects"][0]["index"], + vault_id=VAULT_ID, ) response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) From 24b4df0d0bc63817c03aeb72d47122978578e15c Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 4 Mar 2025 15:51:13 -0800 Subject: [PATCH 09/51] Enforce proper serialization of Number types --- tests/integration/transactions/test_sav.py | 4 +- .../core/binarycodec/types/test_number.py | 33 ++- xrpl/core/binarycodec/types/number.py | 202 +++++++++++++++++- 3 files changed, 221 insertions(+), 18 deletions(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index 0496e13e7..8d97c18e0 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -61,9 +61,7 @@ async def test_sav_lifecycle(self, client): tx = VaultCreate( account=vault_owner.address, asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), - # TODO: This throws a Number::normalize 1 exception in rippled, why ?? - # Possible errors in serialization of Number type - # asset_maximum="1000", + asset_maximum="1000", ) response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py index fb9898308..6eb930a11 100644 --- a/tests/unit/core/binarycodec/types/test_number.py +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -5,11 +5,32 @@ class TestNumber(unittest.TestCase): def test_serialization_and_deserialization(self): - number_bytes = Number.from_value(124) - self.assertEqual((number_bytes).to_json(), "124.0") + serialized_number = Number.from_value("124") + self.assertEqual(serialized_number.to_json(), "1240000000000000e-13") - number_bytes = Number.from_value(0) - self.assertEqual((number_bytes).to_json(), "0.0") + serialized_number = Number.from_value("1000") + self.assertEqual(serialized_number.to_json(), "1000000000000000e-12") - number_bytes = Number.from_value(-10) - self.assertEqual((number_bytes).to_json(), "-10.0") + serialized_number = Number.from_value("0") + self.assertEqual(serialized_number.to_json(), "0") + + serialized_number = Number.from_value("-1") + self.assertEqual(serialized_number.to_json(), "-1000000000000000e-15") + + serialized_number = Number.from_value("-10") + self.assertEqual(serialized_number.to_json(), "-1000000000000000e-14") + + serialized_number = Number.from_value("123.456") + self.assertEqual(serialized_number.to_json(), "1234560000000000e-13") + + serialized_number = Number.from_value("1.456e-45") + self.assertEqual(serialized_number.to_json(), "1456000000000000e-60") + + serialized_number = Number.from_value("0.456e34") + self.assertEqual(serialized_number.to_json(), "4560000000000000e18") + + serialized_number = Number.from_value("4e34") + self.assertEqual(serialized_number.to_json(), "4000000000000000e19") + + def extreme_limits(self): + pass diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index e7cf324d0..229108764 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -1,5 +1,5 @@ -import struct -from typing import Optional, Type +import re +from typing import Optional, Pattern, Tuple, Type from typing_extensions import Self @@ -7,6 +7,175 @@ from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException from xrpl.core.binarycodec.types.serialized_type import SerializedType +# Note: Much of the ideas and constants in this file are borrowed from the rippled +# implementation of the `Number` and `STNumber` class. Please refer to the cpp code. + +# Limits of representation after normalization of mantissa and exponent +_MIN_MANTISSA = 1000000000000000 +_MAX_MANTISSA = 9999999999999999 + +_MIN_EXPONENT = -32768 +_MAX_EXPONENT = 32768 + + +def normalize(mantissa: int, exponent: int) -> Tuple[int, int]: + """Normalize the mantissa and exponent of a number. + + Args: + mantissa: The mantissa of the input number + exponent: The exponent of the input number + + Returns: + A tuple containing the normalized mantissa and exponent + """ + is_negative = mantissa < 0 + m = abs(mantissa) + + while m < _MIN_MANTISSA and exponent > _MIN_EXPONENT: + exponent -= 1 + m *= 10 + + # Note: This code rounds the normalized mantissa "towards_zero". If your use case + # needs other rounding modes -- to_nearest, up (or) down, let us know with an + # appropriate bug report + while m > _MAX_MANTISSA: + if exponent >= _MAX_EXPONENT: + raise XRPLBinaryCodecException("Mantissa and exponent are too large.") + + exponent += 1 + m //= 10 + + if is_negative: + m = -m + + return (m, exponent) + + +def add32(value: int) -> bytes: + """Add a 32-bit integer to a bytes object. + + Args: + value: The integer to add + + Returns: + A bytes object containing the serialized integer + """ + serialized_bytes = bytes() + serialized_bytes += (value >> 24 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 16 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 8 & 0xFF).to_bytes(1) + serialized_bytes += (value & 0xFF).to_bytes(1) + + return serialized_bytes + + +def add64(value: int) -> bytes: + """Add a 64-bit integer to a bytes object. + + Args: + value: The integer to add + + Returns: + A bytes object containing the serialized integer + """ + serialized_bytes = bytes() + serialized_bytes += (value >> 56 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 48 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 40 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 32 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 24 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 16 & 0xFF).to_bytes(1) + serialized_bytes += (value >> 8 & 0xFF).to_bytes(1) + serialized_bytes += (value & 0xFF).to_bytes(1) + + return serialized_bytes + + +class NumberParts: + """Class representing the parts of a number: mantissa, exponent and sign.""" + + def __init__(self: Self, mantissa: int, exponent: int, is_negative: bool) -> None: + """Initialize a NumberParts instance. + + Args: + mantissa: The mantissa (significant digits) of the number + exponent: The exponent indicating the position of the decimal point + is_negative: Boolean indicating if the number is negative + """ + self.mantissa = mantissa + self.exponent = exponent + self.is_negative = is_negative + + +def extractNumberPartsFromString(value: str) -> NumberParts: + """Extract the mantissa, exponent and sign from a string. + + Args: + value: The string to extract the number parts from + + Returns: + A NumberParts instance containing the mantissa, exponent and sign + """ + VALID_NUMBER_REGEX: Pattern[str] = re.compile( + r"^" # the beginning of the string + + r"([-+]?)" # (optional) + or - character + + r"(0|[1-9][0-9]*)" # mantissa: a number (no leading zeroes, unless 0) + + r"(\.([0-9]+))?" # (optional) decimal point and fractional part + + r"([eE]([+-]?)([0-9]+))?" # (optional) E/e, optional + or -, any number + + r"$" # the end of the string + ) + + matches = re.fullmatch(VALID_NUMBER_REGEX, value) + + if not matches: + raise XRPLBinaryCodecException("Unable to parse number from the input string") + + # Match fields: + # 0 = whole input + # 1 = sign + # 2 = integer portion + # 3 = whole fraction (with '.') + # 4 = fraction (without '.') + # 5 = whole exponent (with 'e') + # 6 = exponent sign + # 7 = exponent number + + is_negative: bool = matches.group(1) == "-" + + # integer only + if matches.group(3) is None and matches.group(5) is None: + mantissa = int(matches.group(2)) + exponent = 0 + + # integer and fraction + if matches.group(3) is not None and matches.group(5) is None: + mantissa = int(matches.group(2) + matches.group(4)) + exponent = -len(matches.group(4)) + + # integer and exponent + if matches.group(3) is None and matches.group(5) is not None: + mantissa = int(matches.group(2)) + exponent = int(matches.group(7)) + + if matches.group(6) == "-": + exponent = -exponent + + # integer, fraction and exponent + if matches.group(3) is not None and matches.group(5) is not None: + mantissa = int(matches.group(2) + matches.group(4)) + implied_exponent = -len(matches.group(4)) + explicit_exponent = int(matches.group(7)) + + if matches.group(6) == "-": + explicit_exponent = -explicit_exponent + + exponent = implied_exponent + explicit_exponent + + if is_negative: + mantissa = -mantissa + + return NumberParts(mantissa, exponent, is_negative) + class Number(SerializedType): """Codec for serializing and deserializing Number fields.""" @@ -23,16 +192,31 @@ def from_parser( # noqa: D102 ) -> Self: # Number type consists of two cpp std::uint_64t (mantissa) and # std::uint_32t (exponent) types which are 8 bytes and 4 bytes respectively + + # Note: Normalization is not required here. It is assumed that the serialized + # format was obtained through correct procedure. return cls(parser.read(12)) @classmethod def from_value(cls: Type[Self], value: str) -> Self: - return cls(struct.pack(">d", float(value))) + number_parts: NumberParts = extractNumberPartsFromString(value) + normalized_mantissa, normalized_exponent = normalize( + number_parts.mantissa, number_parts.exponent + ) + + serialized_mantissa = add64(normalized_mantissa) + serialized_exponent = add32(normalized_exponent) + + assert len(serialized_mantissa) == 8 + assert len(serialized_exponent) == 4 + + return cls(serialized_mantissa + serialized_exponent) def to_json(self: Self) -> str: - unpack_elems = struct.unpack(">d", self.buffer) - if len(unpack_elems) != 1: - raise XRPLBinaryCodecException( - "Deserialization of Number type did not produce exactly one element" - ) - return str(unpack_elems[0]) + mantissa = int.from_bytes(self.buffer[:8], byteorder="big", signed=True) + exponent = int.from_bytes(self.buffer[8:], byteorder="big", signed=True) + + if exponent == 0: + return str(mantissa) + + return f"{mantissa}e{exponent}" From 36469097ca9d2c093160bfd11686c431de697c85 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 4 Mar 2025 16:09:19 -0800 Subject: [PATCH 10/51] docs explanation of transaction models --- xrpl/core/binarycodec/types/number.py | 9 ++++++--- xrpl/models/transactions/vault_clawback.py | 16 +++++++++++++++- xrpl/models/transactions/vault_create.py | 11 ++++++++++- xrpl/models/transactions/vault_delete.py | 3 ++- xrpl/models/transactions/vault_deposit.py | 5 ++++- xrpl/models/transactions/vault_set.py | 11 ++++++++++- xrpl/models/transactions/vault_withdraw.py | 7 ++++++- 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index 229108764..bda0fcaf9 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -1,3 +1,9 @@ +"""Codec for the Number type. + +Note: Much of the ideas and constants in this file are borrowed from the rippled +implementation of the `Number` and `STNumber` class. Please refer to the cpp code. +""" + import re from typing import Optional, Pattern, Tuple, Type @@ -7,9 +13,6 @@ from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException from xrpl.core.binarycodec.types.serialized_type import SerializedType -# Note: Much of the ideas and constants in this file are borrowed from the rippled -# implementation of the `Number` and `STNumber` class. Please refer to the cpp code. - # Limits of representation after normalization of mantissa and exponent _MIN_MANTISSA = 1000000000000000 _MAX_MANTISSA = 9999999999999999 diff --git a/xrpl/models/transactions/vault_clawback.py b/xrpl/models/transactions/vault_clawback.py index f0660c481..75bf322d7 100644 --- a/xrpl/models/transactions/vault_clawback.py +++ b/xrpl/models/transactions/vault_clawback.py @@ -16,12 +16,26 @@ @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultClawback(Transaction): """ - Represents a VaultClawback transaction on the XRP Ledger. + The VaultClawback transaction performs a Clawback from the Vault, exchanging the + shares of an account. Conceptually, the transaction performs VaultWithdraw on + behalf of the Holder, sending the funds to the Issuer account of the asset. + + In case there are insufficient funds for the entire Amount the transaction will + perform a partial Clawback, up to the Vault.AssetAvailable. + + The Clawback transaction must respect any future fees or penalties. """ vault_id: str = REQUIRED # type: ignore + """The ID of the vault from which assets are withdrawn.""" + holder: str = REQUIRED # type: ignore + """The account ID from which to clawback the assets.""" + amount: Optional[Amount] = None + """The asset amount to clawback. When Amount is 0 clawback all funds, up to the + total shares the Holder owns. + """ transaction_type: TransactionType = field( default=TransactionType.VAULT_CLAWBACK, diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 3919ac24e..ab7937301 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -31,15 +31,24 @@ class VaultCreateFlagInterface(FlagInterface): @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultCreate(Transaction): """ - Represents a VaultCreate transaction on the XRP Ledger. + The VaultCreate transaction creates a new Vault object. """ data: Optional[str] = None + """Arbitrary Vault metadata, limited to 256 bytes.""" + # Keshava: TODO: Include MPT Issue in Asset field asset: Currency = REQUIRED # type: ignore + """The asset (XRP, IOU or MPT) of the Vault.""" + asset_maximum: Optional[str] = None + """The maximum asset amount that can be held in a vault.""" + mptoken_metadata: Optional[str] = None + """Arbitrary metadata about the share MPT, in hex format, limited to 1024 bytes.""" + permissioned_domain_id: Optional[str] = None + """The PermissionedDomain object ID associated with the shares of this Vault.""" transaction_type: TransactionType = field( default=TransactionType.VAULT_CREATE, diff --git a/xrpl/models/transactions/vault_delete.py b/xrpl/models/transactions/vault_delete.py index 4d690badf..9c797c403 100644 --- a/xrpl/models/transactions/vault_delete.py +++ b/xrpl/models/transactions/vault_delete.py @@ -14,10 +14,11 @@ @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultDelete(Transaction): """ - Represents a VaultDelete transaction on the XRP Ledger. + The VaultDelete transaction deletes an existing vault object. """ vault_id: str = REQUIRED # type: ignore + """The ID of the vault to be deleted.""" transaction_type: TransactionType = field( default=TransactionType.VAULT_DELETE, diff --git a/xrpl/models/transactions/vault_deposit.py b/xrpl/models/transactions/vault_deposit.py index 6281eef33..ce21c677c 100644 --- a/xrpl/models/transactions/vault_deposit.py +++ b/xrpl/models/transactions/vault_deposit.py @@ -15,11 +15,14 @@ @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultDeposit(Transaction): """ - Represents a VaultDeposit transaction on the XRP Ledger. + The VaultDeposit transaction adds Liqudity in exchange for vault shares. """ vault_id: str = REQUIRED # type: ignore + """The ID of the vault to which the assets are deposited.""" + amount: Amount = REQUIRED # type: ignore + """Asset amount to deposit.""" transaction_type: TransactionType = field( default=TransactionType.VAULT_DEPOSIT, diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index 03eab3162..0ec215b66 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -15,13 +15,22 @@ @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultSet(Transaction): """ - Represents a VaultSet transaction on the XRP Ledger. + The VaultSet updates an existing Vault ledger object. """ vault_id: str = REQUIRED # type: ignore + """The ID of the Vault to be modified. Must be included when updating the Vault.""" + domain_id: Optional[str] = None + """The PermissionedDomain object ID associated with the shares of this Vault.""" + data: Optional[str] = None + """Arbitrary Vault metadata, limited to 256 bytes.""" + asset_maximum: Optional[str] = None + """The maximum asset amount that can be held in a vault. The value cannot be lower + than the current AssetTotal unless the value is 0. + """ transaction_type: TransactionType = field( default=TransactionType.VAULT_SET, diff --git a/xrpl/models/transactions/vault_withdraw.py b/xrpl/models/transactions/vault_withdraw.py index db45acdc8..39992d435 100644 --- a/xrpl/models/transactions/vault_withdraw.py +++ b/xrpl/models/transactions/vault_withdraw.py @@ -16,12 +16,17 @@ @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultWithdraw(Transaction): """ - Represents a VaultWithdraw transaction on the XRP Ledger. + The VaultWithdraw transaction withdraws assets in exchange for the vault's shares. """ vault_id: str = REQUIRED # type: ignore + """The ID of the vault from which assets are withdrawn.""" + amount: Amount = REQUIRED # type: ignore + """The exact amount of Vault asset to withdraw.""" + destination: Optional[str] = None + """An account to receive the assets. It must be able to receive the asset.""" transaction_type: TransactionType = field( default=TransactionType.VAULT_WITHDRAW, From 307c515d310032ffee7523f01ecc839d009088ca Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 5 Mar 2025 10:32:57 -0800 Subject: [PATCH 11/51] handle 0 case in Number type --- .../core/binarycodec/types/test_number.py | 8 +++++- xrpl/core/binarycodec/types/number.py | 28 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py index 6eb930a11..7e9a0304c 100644 --- a/tests/unit/core/binarycodec/types/test_number.py +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -33,4 +33,10 @@ def test_serialization_and_deserialization(self): self.assertEqual(serialized_number.to_json(), "4000000000000000e19") def extreme_limits(self): - pass + lowest_mantissa = "-9223372036854776" + serialized_number = Number.from_value(lowest_mantissa + "e3") + self.assertEqual(serialized_number.hex(), "FFDF3B645A1CAC0800000003") + + highest_mantissa = "9223372036854776" + serialized_number = Number.from_value(highest_mantissa + "e3") + self.assertEqual(serialized_number.hex(), "0020C49BA5E353F700000003") diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index bda0fcaf9..34224668b 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -187,6 +187,12 @@ def __init__(self: Self, buffer: bytes) -> None: """Construct a Number from given bytes.""" super().__init__(buffer) + def display_serialized_hex(self: Self) -> str: + """Display the serialized hex representation of the number. Utility function + for debugging. + """ + return self.buffer.hex().upper() + @classmethod def from_parser( # noqa: D102 cls: Type[Self], @@ -203,9 +209,21 @@ def from_parser( # noqa: D102 @classmethod def from_value(cls: Type[Self], value: str) -> Self: number_parts: NumberParts = extractNumberPartsFromString(value) - normalized_mantissa, normalized_exponent = normalize( - number_parts.mantissa, number_parts.exponent - ) + + # `0` value is represented as a mantissa of 0 and an exponent of -2147483648 + # This is an artifact of the rippled implementation. To ensure compatibility of + # the codec, we mirror this behavior. + if ( + number_parts.mantissa == 0 + and number_parts.exponent == 0 + and not number_parts.is_negative + ): + normalized_mantissa = 0 + normalized_exponent = -2147483648 + else: + normalized_mantissa, normalized_exponent = normalize( + number_parts.mantissa, number_parts.exponent + ) serialized_mantissa = add64(normalized_mantissa) serialized_exponent = add32(normalized_exponent) @@ -222,4 +240,8 @@ def to_json(self: Self) -> str: if exponent == 0: return str(mantissa) + # `0` value is represented as a mantissa of 0 and an exponent of -2147483648 + if mantissa == 0 and exponent == -2147483648: + return "0" + return f"{mantissa}e{exponent}" From e97a4b70c90522fdf382eb6682232b68c7ad2412 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 5 Mar 2025 10:43:31 -0800 Subject: [PATCH 12/51] simplify the extractNumberParts logic; Avoid using magic numbers; --- xrpl/core/binarycodec/types/number.py | 49 +++++++++++---------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index 34224668b..d2d3e8bf9 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -20,6 +20,8 @@ _MIN_EXPONENT = -32768 _MAX_EXPONENT = 32768 +_DEFAULT_VALUE_EXPONENT = -2147483648 + def normalize(mantissa: int, exponent: int) -> Tuple[int, int]: """Normalize the mantissa and exponent of a number. @@ -146,33 +148,20 @@ def extractNumberPartsFromString(value: str) -> NumberParts: is_negative: bool = matches.group(1) == "-" # integer only - if matches.group(3) is None and matches.group(5) is None: + if matches.group(3) is None: mantissa = int(matches.group(2)) exponent = 0 - - # integer and fraction - if matches.group(3) is not None and matches.group(5) is None: + else: + # handle the fraction input mantissa = int(matches.group(2) + matches.group(4)) exponent = -len(matches.group(4)) - # integer and exponent - if matches.group(3) is None and matches.group(5) is not None: - mantissa = int(matches.group(2)) - exponent = int(matches.group(7)) - - if matches.group(6) == "-": - exponent = -exponent - - # integer, fraction and exponent - if matches.group(3) is not None and matches.group(5) is not None: - mantissa = int(matches.group(2) + matches.group(4)) - implied_exponent = -len(matches.group(4)) - explicit_exponent = int(matches.group(7)) - + # exponent is specified in the input + if matches.group(5) is not None: if matches.group(6) == "-": - explicit_exponent = -explicit_exponent - - exponent = implied_exponent + explicit_exponent + exponent -= int(matches.group(7)) + else: + exponent += int(matches.group(7)) if is_negative: mantissa = -mantissa @@ -199,9 +188,6 @@ def from_parser( # noqa: D102 parser: BinaryParser, length_hint: Optional[int] = None, # noqa: ANN401 ) -> Self: - # Number type consists of two cpp std::uint_64t (mantissa) and - # std::uint_32t (exponent) types which are 8 bytes and 4 bytes respectively - # Note: Normalization is not required here. It is assumed that the serialized # format was obtained through correct procedure. return cls(parser.read(12)) @@ -210,16 +196,16 @@ def from_parser( # noqa: D102 def from_value(cls: Type[Self], value: str) -> Self: number_parts: NumberParts = extractNumberPartsFromString(value) - # `0` value is represented as a mantissa of 0 and an exponent of -2147483648 - # This is an artifact of the rippled implementation. To ensure compatibility of - # the codec, we mirror this behavior. + # `0` value is represented as a mantissa with 0 and an exponent of + # _DEFAULT_VALUE_EXPONENT. This is an artifact of the rippled implementation. + # To ensure compatibility of the codec, we mirror this behavior. if ( number_parts.mantissa == 0 and number_parts.exponent == 0 and not number_parts.is_negative ): normalized_mantissa = 0 - normalized_exponent = -2147483648 + normalized_exponent = _DEFAULT_VALUE_EXPONENT else: normalized_mantissa, normalized_exponent = normalize( number_parts.mantissa, number_parts.exponent @@ -228,6 +214,8 @@ def from_value(cls: Type[Self], value: str) -> Self: serialized_mantissa = add64(normalized_mantissa) serialized_exponent = add32(normalized_exponent) + # Number type consists of two cpp std::uint_64t (mantissa) and + # std::uint_32t (exponent) types which are 8 bytes and 4 bytes respectively assert len(serialized_mantissa) == 8 assert len(serialized_exponent) == 4 @@ -240,8 +228,9 @@ def to_json(self: Self) -> str: if exponent == 0: return str(mantissa) - # `0` value is represented as a mantissa of 0 and an exponent of -2147483648 - if mantissa == 0 and exponent == -2147483648: + # `0` value is represented as a mantissa with 0 and an + # exponent of _DEFAULT_VALUE_EXPONENT + if mantissa == 0 and exponent == _DEFAULT_VALUE_EXPONENT: return "0" return f"{mantissa}e{exponent}" From 1617cad46ad7672007f33560c612383fb8da7142 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 5 Mar 2025 13:05:51 -0800 Subject: [PATCH 13/51] introduce model for MPTIssue --- xrpl/models/amounts/mpt_amount.py | 13 +++++++++++++ xrpl/models/transactions/vault_create.py | 7 +++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/xrpl/models/amounts/mpt_amount.py b/xrpl/models/amounts/mpt_amount.py index ca6f48f21..e0bb11a01 100644 --- a/xrpl/models/amounts/mpt_amount.py +++ b/xrpl/models/amounts/mpt_amount.py @@ -39,3 +39,16 @@ def to_dict(self: Self) -> Dict[str, str]: The dictionary representation of an MPTAmount. """ return {**super().to_dict(), "value": str(self.value)} + + +class MPTIssue: + """ + This class represents an MPT issue. It is similar to the Issue class, but + it is used with MPT amounts. + """ + + mpt_id: str = REQUIRED # type: ignore + """ + MPTID is a 192-bit concatenation of a 32-bit account sequence and a 160-bit + account id. + """ diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index ab7937301..ee7b2f6a4 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -4,9 +4,9 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Optional +from typing import Optional, Union -from xrpl.models.amounts import Amount +from xrpl.models.amounts.mpt_amount import MPTIssue from xrpl.models.currencies import Currency from xrpl.models.flags import FlagInterface from xrpl.models.required import REQUIRED @@ -37,8 +37,7 @@ class VaultCreate(Transaction): data: Optional[str] = None """Arbitrary Vault metadata, limited to 256 bytes.""" - # Keshava: TODO: Include MPT Issue in Asset field - asset: Currency = REQUIRED # type: ignore + asset: Union[Currency, MPTIssue] = REQUIRED # type: ignore """The asset (XRP, IOU or MPT) of the Vault.""" asset_maximum: Optional[str] = None From 671690a78c22247e0b4d7f959530c6d7a32fd17c Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 5 Mar 2025 16:06:11 -0800 Subject: [PATCH 14/51] Update VaultCreate model to include Withdrawal Policy --- tests/integration/transactions/test_sav.py | 1 + xrpl/core/binarycodec/definitions/definitions.json | 10 ++++++++++ xrpl/models/transactions/vault_create.py | 12 +++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index 8d97c18e0..10c13f52c 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -62,6 +62,7 @@ async def test_sav_lifecycle(self, client): account=vault_owner.address, asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), asset_maximum="1000", + withdrawal_policy=1, ) response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 5bdb4f60b..c51164ae0 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -2699,6 +2699,16 @@ "type": "UInt8" } ], + [ + "WithdrawalPolicy", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 20, + "type": "UInt8" + } + ], [ "TakerPaysCurrency", { diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index ee7b2f6a4..a7e08248f 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -46,9 +46,19 @@ class VaultCreate(Transaction): mptoken_metadata: Optional[str] = None """Arbitrary metadata about the share MPT, in hex format, limited to 1024 bytes.""" - permissioned_domain_id: Optional[str] = None + domain_id: Optional[str] = None """The PermissionedDomain object ID associated with the shares of this Vault.""" + withdrawal_policy: Optional[int] = None + """Indicates the withdrawal strategy used by the Vault. + + The below withdrawal policy is supported: + + Strategy Name Value Description + strFirstComeFirstServe 1 Requests are processed on a first-come-first- + serve basis. + """ + transaction_type: TransactionType = field( default=TransactionType.VAULT_CREATE, init=False, From b09b37a1623113f46768b651e2fb20f14c741210 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 5 Mar 2025 16:08:01 -0800 Subject: [PATCH 15/51] remove irrelevant tests for amount binary codec --- .../unit/core/binarycodec/types/test_amount.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/unit/core/binarycodec/types/test_amount.py b/tests/unit/core/binarycodec/types/test_amount.py index c0fa4f2d1..00246d377 100644 --- a/tests/unit/core/binarycodec/types/test_amount.py +++ b/tests/unit/core/binarycodec/types/test_amount.py @@ -108,23 +108,6 @@ def test_assert_xrp_is_valid_passes(self): amount.verify_xrp_value(valid_zero) amount.verify_xrp_value(valid_amount) - # Note: these values are obtained from the following rippled unit test: - # https://github.com/XRPLF/rippled/blob/33e1c42599857336d792effc753795911bdb13f0/src/test/protocol/STAmount_test.cpp#L513 - # However, the limiting values allowed by the STAmount type are much higher and - # smaller. - def test_large_small_values(self): - small_value = "5499999999999999e-95" - - serialized_representation = amount.Amount.from_value(small_value) - self.assertEqual(amount.Amount.to_json(serialized_representation), small_value) - - large_value = "15499999999999999e79" - serialized_representation = amount.Amount.from_value(large_value) - self.assertEqual(amount.Amount.to_json(serialized_representation), large_value) - - amount.verify_xrp_value(small_value) - amount.verify_xrp_value(large_value) - def test_assert_xrp_is_valid_raises(self): invalid_amount_large = "1e20" invalid_amount_small = "1e-7" From 537cc2f40a66b1c1874c53346127d487a4f3d99b Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 6 Mar 2025 10:30:24 -0800 Subject: [PATCH 16/51] reorder the REQUIRED fields to be at the top of the transactions model; Update the changelog file --- CHANGELOG.md | 1 + xrpl/models/transactions/vault_create.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a784b1c12..18596cfbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Improved validation for models to also check param types +- Support for Single Asset Vault (XLS-65d) ## [4.1.0] - 2025-2-13 diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index a7e08248f..4252378b4 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -34,12 +34,12 @@ class VaultCreate(Transaction): The VaultCreate transaction creates a new Vault object. """ - data: Optional[str] = None - """Arbitrary Vault metadata, limited to 256 bytes.""" - asset: Union[Currency, MPTIssue] = REQUIRED # type: ignore """The asset (XRP, IOU or MPT) of the Vault.""" + data: Optional[str] = None + """Arbitrary Vault metadata, limited to 256 bytes.""" + asset_maximum: Optional[str] = None """The maximum asset amount that can be held in a vault.""" From 6c4f8011823241dd80daee755e62ecdc1a76f3a0 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 6 Mar 2025 12:55:00 -0800 Subject: [PATCH 17/51] pretty print of Number values --- .../core/binarycodec/types/test_number.py | 10 ++--- xrpl/core/binarycodec/types/number.py | 41 ++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py index 7e9a0304c..86160edad 100644 --- a/tests/unit/core/binarycodec/types/test_number.py +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -6,22 +6,22 @@ class TestNumber(unittest.TestCase): def test_serialization_and_deserialization(self): serialized_number = Number.from_value("124") - self.assertEqual(serialized_number.to_json(), "1240000000000000e-13") + self.assertEqual(serialized_number.to_json(), "124") serialized_number = Number.from_value("1000") - self.assertEqual(serialized_number.to_json(), "1000000000000000e-12") + self.assertEqual(serialized_number.to_json(), "1000") serialized_number = Number.from_value("0") self.assertEqual(serialized_number.to_json(), "0") serialized_number = Number.from_value("-1") - self.assertEqual(serialized_number.to_json(), "-1000000000000000e-15") + self.assertEqual(serialized_number.to_json(), "-1") serialized_number = Number.from_value("-10") - self.assertEqual(serialized_number.to_json(), "-1000000000000000e-14") + self.assertEqual(serialized_number.to_json(), "-10") serialized_number = Number.from_value("123.456") - self.assertEqual(serialized_number.to_json(), "1234560000000000e-13") + self.assertEqual(serialized_number.to_json(), "123.456") serialized_number = Number.from_value("1.456e-45") self.assertEqual(serialized_number.to_json(), "1456000000000000e-60") diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index d2d3e8bf9..0f9c82a16 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -222,6 +222,15 @@ def from_value(cls: Type[Self], value: str) -> Self: return cls(serialized_mantissa + serialized_exponent) def to_json(self: Self) -> str: + """Convert the Number to a JSON string. + + Note: This method is faithful to rippled's `Number::to_string()` method. + This ensures API compatibility between rippled and xrpl-py regarding the JSON + representation of Number objects. + + Returns: + A JSON string representing the Number + """ mantissa = int.from_bytes(self.buffer[:8], byteorder="big", signed=True) exponent = int.from_bytes(self.buffer[8:], byteorder="big", signed=True) @@ -233,4 +242,34 @@ def to_json(self: Self) -> str: if mantissa == 0 and exponent == _DEFAULT_VALUE_EXPONENT: return "0" - return f"{mantissa}e{exponent}" + # Use scientific notation for very small or large numbers + if exponent < -25 or exponent > -5: + return f"{mantissa}e{exponent}" + + is_negative = mantissa < 0 + mantissa = abs(mantissa) + + # The below padding values are influenced by the exponent range of [-25, -5] + # in the above if-condition. Values outside of this range use the scientific + # notation and do not go through the below logic. + PAD_PREFIX = 27 + PAD_SUFFIX = 23 + + raw_value: str = "0" * PAD_PREFIX + str(mantissa) + "0" * PAD_SUFFIX + + # Note: The rationale for choosing 43 is that the highest mantissa has 16 + # digits in decimal representation and the PAD_PREFIX has 27 characters. + # 27 + 16 sums upto 43 characters. + OFFSET = exponent + 43 + assert OFFSET > 0, "Exponent is below acceptable limit" + + generate_mantissa: str = raw_value[:OFFSET].lstrip("0") + + if generate_mantissa == "": + generate_mantissa = "0" + + generate_exponent: str = raw_value[OFFSET:].rstrip("0") + if generate_exponent != "": + generate_exponent = "." + generate_exponent + + return f"{'-' if is_negative else ''}{generate_mantissa}{generate_exponent}" From 27659370ac8aab26bc54c21d38bf0c95a5af7249 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 6 Mar 2025 14:20:58 -0800 Subject: [PATCH 18/51] update the data member name of the MPTIssue class --- xrpl/models/amounts/mpt_amount.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xrpl/models/amounts/mpt_amount.py b/xrpl/models/amounts/mpt_amount.py index e0bb11a01..5810a3558 100644 --- a/xrpl/models/amounts/mpt_amount.py +++ b/xrpl/models/amounts/mpt_amount.py @@ -47,8 +47,8 @@ class MPTIssue: it is used with MPT amounts. """ - mpt_id: str = REQUIRED # type: ignore + mpt_issuance_id: str = REQUIRED # type: ignore """ - MPTID is a 192-bit concatenation of a 32-bit account sequence and a 160-bit - account id. + mpt_issuance_id is a 192-bit concatenation of a 32-bit account sequence and a + 160-bit account id. """ From 18bfd1c8651505c8ffbd97488f33c58e7cab79e0 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 6 Mar 2025 14:29:01 -0800 Subject: [PATCH 19/51] pacify linter errors --- xrpl/core/binarycodec/types/number.py | 8 ++++ xrpl/models/amounts/mpt_amount.py | 2 +- xrpl/models/transactions/vault_clawback.py | 6 +-- xrpl/models/transactions/vault_create.py | 50 +++++++++++++++++----- xrpl/models/transactions/vault_delete.py | 8 +--- xrpl/models/transactions/vault_deposit.py | 8 +--- xrpl/models/transactions/vault_set.py | 10 ++--- xrpl/models/transactions/vault_withdraw.py | 8 ++-- 8 files changed, 60 insertions(+), 40 deletions(-) diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index 0f9c82a16..212c26634 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -194,6 +194,14 @@ def from_parser( # noqa: D102 @classmethod def from_value(cls: Type[Self], value: str) -> Self: + """Construct a Number from a string. + + Args: + value: The string to construct the Number from + + Returns: + A Number instance + """ number_parts: NumberParts = extractNumberPartsFromString(value) # `0` value is represented as a mantissa with 0 and an exponent of diff --git a/xrpl/models/amounts/mpt_amount.py b/xrpl/models/amounts/mpt_amount.py index 5810a3558..b6b912bbb 100644 --- a/xrpl/models/amounts/mpt_amount.py +++ b/xrpl/models/amounts/mpt_amount.py @@ -49,6 +49,6 @@ class MPTIssue: mpt_issuance_id: str = REQUIRED # type: ignore """ - mpt_issuance_id is a 192-bit concatenation of a 32-bit account sequence and a + mpt_issuance_id is a 192-bit concatenation of a 32-bit account sequence and a 160-bit account id. """ diff --git a/xrpl/models/transactions/vault_clawback.py b/xrpl/models/transactions/vault_clawback.py index 75bf322d7..5f2576f8a 100644 --- a/xrpl/models/transactions/vault_clawback.py +++ b/xrpl/models/transactions/vault_clawback.py @@ -1,6 +1,4 @@ -""" -Represents a VaultClawback transaction on the XRP Ledger. -""" +"""Represents a VaultClawback transaction on the XRP Ledger.""" from dataclasses import dataclass, field from typing import Optional @@ -33,7 +31,7 @@ class VaultClawback(Transaction): """The account ID from which to clawback the assets.""" amount: Optional[Amount] = None - """The asset amount to clawback. When Amount is 0 clawback all funds, up to the + """The asset amount to clawback. When Amount is 0 clawback all funds, up to the total shares the Holder owns. """ diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 4252378b4..50a6db03c 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -1,6 +1,4 @@ -""" -Represents a VaultCreate transaction on the XRP Ledger. -""" +"""Represents a VaultCreate transaction on the XRP Ledger.""" from dataclasses import dataclass, field from enum import Enum @@ -16,23 +14,54 @@ class VaultCreateFlag(int, Enum): + """Flags for the VaultCreate transaction.""" - TF_VAULT_PRIVATE = 0x0001 - TF_VAULT_SHARE_NON_TRANSFERABLE = 0x0002 + TF_FREEZE = 0x0001 + """ + Indicates that the vault should be frozen. + """ + TF_UNFREEZE = 0x0002 + """ + Indicates that the vault should be unfrozen. + """ + + TF_VAULT_PRIVATE = 0x0003 + """ + Indicates that the vault is private. It can only be set during Vault creation. + """ + TF_VAULT_SHARE_NON_TRANSFERABLE = 0x0004 + """ + Indicates the vault share is non-transferable. It can only be set during Vault + creation. + """ class VaultCreateFlagInterface(FlagInterface): + """Interface for the VaultCreate transaction flags.""" + TF_FREEZE: bool + """ + Indicates that the vault should be frozen. + """ + TF_UNFREEZE: bool + """ + Indicates that the vault should be unfrozen. + """ TF_VAULT_PRIVATE: bool + """ + Indicates that the vault is private. It can only be set during Vault creation. + """ TF_VAULT_SHARE_NON_TRANSFERABLE: bool + """ + Indicates the vault share is non-transferable. It can only be set during Vault + creation. + """ @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultCreate(Transaction): - """ - The VaultCreate transaction creates a new Vault object. - """ + """The VaultCreate transaction creates a new Vault object.""" asset: Union[Currency, MPTIssue] = REQUIRED # type: ignore """The asset (XRP, IOU or MPT) of the Vault.""" @@ -50,9 +79,8 @@ class VaultCreate(Transaction): """The PermissionedDomain object ID associated with the shares of this Vault.""" withdrawal_policy: Optional[int] = None - """Indicates the withdrawal strategy used by the Vault. - - The below withdrawal policy is supported: + """Indicates the withdrawal strategy used by the Vault. The below withdrawal policy + is supported: Strategy Name Value Description strFirstComeFirstServe 1 Requests are processed on a first-come-first- diff --git a/xrpl/models/transactions/vault_delete.py b/xrpl/models/transactions/vault_delete.py index 9c797c403..c36376b5e 100644 --- a/xrpl/models/transactions/vault_delete.py +++ b/xrpl/models/transactions/vault_delete.py @@ -1,6 +1,4 @@ -""" -Represents a VaultDelete transaction on the XRP Ledger. -""" +"""Represents a VaultDelete transaction on the XRP Ledger.""" from dataclasses import dataclass, field @@ -13,9 +11,7 @@ @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultDelete(Transaction): - """ - The VaultDelete transaction deletes an existing vault object. - """ + """The VaultDelete transaction deletes an existing vault object.""" vault_id: str = REQUIRED # type: ignore """The ID of the vault to be deleted.""" diff --git a/xrpl/models/transactions/vault_deposit.py b/xrpl/models/transactions/vault_deposit.py index ce21c677c..4ea87d48e 100644 --- a/xrpl/models/transactions/vault_deposit.py +++ b/xrpl/models/transactions/vault_deposit.py @@ -1,6 +1,4 @@ -""" -Represents a VaultDeposit transaction on the XRP Ledger. -""" +"""Represents a VaultDeposit transaction on the XRP Ledger.""" from dataclasses import dataclass, field @@ -14,9 +12,7 @@ @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultDeposit(Transaction): - """ - The VaultDeposit transaction adds Liqudity in exchange for vault shares. - """ + """The VaultDeposit transaction adds Liqudity in exchange for vault shares.""" vault_id: str = REQUIRED # type: ignore """The ID of the vault to which the assets are deposited.""" diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index 0ec215b66..9ccce20a6 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -1,6 +1,4 @@ -""" -Represents a VaultSet transaction on the XRP Ledger. -""" +"""Represents a VaultSet transaction on the XRP Ledger.""" from dataclasses import dataclass, field from typing import Optional @@ -14,9 +12,7 @@ @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultSet(Transaction): - """ - The VaultSet updates an existing Vault ledger object. - """ + """The VaultSet updates an existing Vault ledger object.""" vault_id: str = REQUIRED # type: ignore """The ID of the Vault to be modified. Must be included when updating the Vault.""" @@ -28,7 +24,7 @@ class VaultSet(Transaction): """Arbitrary Vault metadata, limited to 256 bytes.""" asset_maximum: Optional[str] = None - """The maximum asset amount that can be held in a vault. The value cannot be lower + """The maximum asset amount that can be held in a vault. The value cannot be lower than the current AssetTotal unless the value is 0. """ diff --git a/xrpl/models/transactions/vault_withdraw.py b/xrpl/models/transactions/vault_withdraw.py index 39992d435..0e4ddb7cc 100644 --- a/xrpl/models/transactions/vault_withdraw.py +++ b/xrpl/models/transactions/vault_withdraw.py @@ -1,6 +1,4 @@ -""" -Represents a VaultWithdraw transaction on the XRP Ledger. -""" +"""Represents a VaultWithdraw transaction on the XRP Ledger.""" from dataclasses import dataclass, field from typing import Optional @@ -15,8 +13,8 @@ @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultWithdraw(Transaction): - """ - The VaultWithdraw transaction withdraws assets in exchange for the vault's shares. + """The VaultWithdraw transaction withdraws assets in exchange for the vault's + shares. """ vault_id: str = REQUIRED # type: ignore From f967f2366914368f20a668dfd858eb12be264f48 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Thu, 6 Mar 2025 14:47:30 -0800 Subject: [PATCH 20/51] Update tests/unit/core/binarycodec/types/test_number.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/unit/core/binarycodec/types/test_number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py index 86160edad..a98a38e74 100644 --- a/tests/unit/core/binarycodec/types/test_number.py +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -32,7 +32,7 @@ def test_serialization_and_deserialization(self): serialized_number = Number.from_value("4e34") self.assertEqual(serialized_number.to_json(), "4000000000000000e19") - def extreme_limits(self): + def test_extreme_limits(self): lowest_mantissa = "-9223372036854776" serialized_number = Number.from_value(lowest_mantissa + "e3") self.assertEqual(serialized_number.hex(), "FFDF3B645A1CAC0800000003") From eb785533a1ea1e5c0f8ca0ca7b709c63e34bb6c3 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 6 Mar 2025 16:03:25 -0800 Subject: [PATCH 21/51] Accept mypy suggestions --- xrpl/core/binarycodec/types/number.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index 212c26634..80d6e2dc9 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -66,10 +66,10 @@ def add32(value: int) -> bytes: A bytes object containing the serialized integer """ serialized_bytes = bytes() - serialized_bytes += (value >> 24 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 16 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 8 & 0xFF).to_bytes(1) - serialized_bytes += (value & 0xFF).to_bytes(1) + serialized_bytes += (value >> 24 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 16 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 8 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value & 0xFF).to_bytes(1, "big") return serialized_bytes @@ -84,14 +84,14 @@ def add64(value: int) -> bytes: A bytes object containing the serialized integer """ serialized_bytes = bytes() - serialized_bytes += (value >> 56 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 48 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 40 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 32 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 24 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 16 & 0xFF).to_bytes(1) - serialized_bytes += (value >> 8 & 0xFF).to_bytes(1) - serialized_bytes += (value & 0xFF).to_bytes(1) + serialized_bytes += (value >> 56 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 48 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 40 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 32 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 24 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 16 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value >> 8 & 0xFF).to_bytes(1, "big") + serialized_bytes += (value & 0xFF).to_bytes(1, "big") return serialized_bytes From 4cb1d0226599d104bb38b0b358c16c3c4244aabd Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 6 Mar 2025 16:09:52 -0800 Subject: [PATCH 22/51] use custom method of Number class for debugging unit tests --- tests/unit/core/binarycodec/types/test_number.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/core/binarycodec/types/test_number.py b/tests/unit/core/binarycodec/types/test_number.py index a98a38e74..830af2b29 100644 --- a/tests/unit/core/binarycodec/types/test_number.py +++ b/tests/unit/core/binarycodec/types/test_number.py @@ -35,8 +35,12 @@ def test_serialization_and_deserialization(self): def test_extreme_limits(self): lowest_mantissa = "-9223372036854776" serialized_number = Number.from_value(lowest_mantissa + "e3") - self.assertEqual(serialized_number.hex(), "FFDF3B645A1CAC0800000003") + self.assertEqual( + serialized_number.display_serialized_hex(), "FFDF3B645A1CAC0800000003" + ) highest_mantissa = "9223372036854776" serialized_number = Number.from_value(highest_mantissa + "e3") - self.assertEqual(serialized_number.hex(), "0020C49BA5E353F700000003") + self.assertEqual( + serialized_number.display_serialized_hex(), "0020C49BA5E353F800000003" + ) From f4830a08b7241c3647f4b9b7e6b3ddca008576f4 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 3 Jun 2025 15:53:52 -0700 Subject: [PATCH 23/51] Update definitions, transaction-models with the latest cpp implementation --- .ci-config/rippled.cfg | 1 + tests/integration/transactions/test_sav.py | 39 +++++++++++++++++-- .../binarycodec/definitions/definitions.json | 34 +++++++++++----- xrpl/models/requests/ledger_entry.py | 29 ++++++++++++-- xrpl/models/transactions/vault_create.py | 23 ++--------- xrpl/models/transactions/vault_set.py | 2 +- 6 files changed, 89 insertions(+), 39 deletions(-) diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 636c8b3f4..ed34d9713 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -196,6 +196,7 @@ fixEnforceNFTokenTrustline fixReducedOffersV2 DeepFreeze PermissionedDomains +# (proposed, not yet released) 2.5.0 Amendments SingleAssetVault # This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index 10c13f52c..d58fdb63b 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -6,6 +6,8 @@ ) from tests.integration.reusable_values import WALLET from xrpl.models import ( + AccountSet, + AccountSetAsfFlag, Payment, TrustSet, VaultClawback, @@ -17,7 +19,7 @@ ) from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount from xrpl.models.currencies import IssuedCurrency -from xrpl.models.requests import AccountObjects +from xrpl.models.requests import AccountObjects, LedgerEntry from xrpl.models.requests.account_objects import AccountObjectType from xrpl.models.response import ResponseStatus from xrpl.utils import str_to_hex @@ -34,6 +36,30 @@ async def test_sav_lifecycle(self, client): issuer_wallet = Wallet.create() await fund_wallet_async(issuer_wallet) + # Set up the relevant flags on the issuer_wallet account -- This is + # a pre-requisite for a Vault to hold the Issued Currency Asset + # This test uses an IOU to demonstrate the VaultClawback functionality. + # Clawback is not possible with the native XRP asset. + response = await sign_and_reliable_submission_async( + AccountSet( + account=issuer_wallet.classic_address, + set_flag=AccountSetAsfFlag.ASF_DEFAULT_RIPPLE, + ), + issuer_wallet, + ) + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + response = await sign_and_reliable_submission_async( + AccountSet( + account=issuer_wallet.classic_address, + set_flag=AccountSetAsfFlag.ASF_ALLOW_TRUSTLINE_CLAWBACK, + ), + issuer_wallet, + ) + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + # Step-0.a: Prerequisites: Set up the IOU trust line tx = TrustSet( account=WALLET.address, @@ -57,18 +83,18 @@ async def test_sav_lifecycle(self, client): self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") - # Step-1: Create a vault + # Step-1.a: Create a vault tx = VaultCreate( account=vault_owner.address, asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), - asset_maximum="1000", + assets_maximum="1000", withdrawal_policy=1, ) response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") - # Verify the existence of the vault with account_objects RPC call + # Step-1.b: Verify the existence of the vault with account_objects RPC call account_objects_response = await client.request( AccountObjects(account=vault_owner.address, type=AccountObjectType.VAULT) ) @@ -76,7 +102,12 @@ async def test_sav_lifecycle(self, client): VAULT_ID = account_objects_response.result["account_objects"][0]["index"] + # Step-1.c: Verify the existence of the vault with ledger_entry RPC call + ledger_entry_response = await client.request(LedgerEntry(index=VAULT_ID)) + self.assertEqual(ledger_entry_response.status, ResponseStatus.SUCCESS) + # Step-2: Update the characteristics of the vault with VaultSet transaction + # print(await client.request(AccountInfo(account=vault_owner.address))) tx = VaultSet( account=vault_owner.address, vault_id=VAULT_ID, diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index c51164ae0..42786207c 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -2051,7 +2051,7 @@ } ], [ - "AssetAvailable", + "AssetsAvailable", { "isSerialized": true, "isSigningField": true, @@ -2061,7 +2061,7 @@ } ], [ - "AssetMaximum", + "AssetsMaximum", { "isSerialized": true, "isSigningField": true, @@ -2071,7 +2071,7 @@ } ], [ - "AssetTotal", + "AssetsTotal", { "isSerialized": true, "isSigningField": true, @@ -2819,6 +2819,16 @@ "type": "Hash192" } ], + [ + "ShareMPTID", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 2, + "type": "Hash192" + } + ], [ "LockingChainIssue", { @@ -2956,7 +2966,7 @@ "RippleState": 114, "SignerList": 83, "Ticket": 84, - "Vault" : 131, + "Vault" : 132, "XChainOwnedClaimID": 113, "XChainOwnedCreateAccountClaimID": 116 }, @@ -3022,6 +3032,9 @@ "tecUNFUNDED_AMM": 162, "tecUNFUNDED_OFFER": 103, "tecUNFUNDED_PAYMENT": 104, + "tecWRONG_ASSET": 194, + "tecLIMIT_EXCEEDED": 195, + "tecPSEUDO_ACCOUNT": 196, "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, "tecXCHAIN_BAD_CLAIM_ID": 172, @@ -3132,6 +3145,7 @@ "terLAST": -91, "terNO_ACCOUNT": -96, "terNO_AMM": -87, + "tedADDRESS_COLLISION": -86, "terNO_AUTH": -95, "terNO_LINE": -94, "terNO_RIPPLE": -90, @@ -3194,12 +3208,12 @@ "TicketCreate": 10, "TrustSet": 20, "UNLModify": 102, - "VaultCreate": 64, - "VaultClawback": 69, - "VaultDeposit": 67, - "VaultDelete": 66, - "VaultSet": 65, - "VaultWithdraw": 68, + "VaultCreate": 65, + "VaultClawback": 70, + "VaultDeposit": 68, + "VaultDelete": 67, + "VaultSet": 66, + "VaultWithdraw": 69, "XChainAccountCreateCommit": 44, "XChainAddAccountCreateAttestation": 46, "XChainAddClaimAttestation": 45, diff --git a/xrpl/models/requests/ledger_entry.py b/xrpl/models/requests/ledger_entry.py index 85fa5d799..6990de052 100644 --- a/xrpl/models/requests/ledger_entry.py +++ b/xrpl/models/requests/ledger_entry.py @@ -41,7 +41,7 @@ class LedgerEntryType(str, Enum): PAYMENT_CHANNEL = "payment_channel" PERMISSIONED_DOMAIN = "permissioned_domain" SIGNER_LIST = "signer_list" - SINGLE_ASSET_VAULT = "vault_id" + SINGLE_ASSET_VAULT = "vault" STATE = "state" TICKET = "ticket" MPT_ISSUANCE = "mpt_issuance" @@ -258,6 +258,28 @@ class Ticket(BaseModel): """ +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class Vault(BaseModel): + """Required fields for requesting a Vault ledger-object if not querying by + object ID. + """ + + owner: str = REQUIRED # type: ignore + """ + This field is required. + + :meta hide-value: + """ + + seq: int = REQUIRED # type: ignore + """ + This field is required. + + :meta hide-value: + """ + + @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class XChainClaimID(XChainBridge): @@ -316,8 +338,7 @@ class LedgerEntry(Request, LookupByLedgerRequest): payment_channel: Optional[str] = None permissioned_domain: Optional[Union[str, PermissionedDomain]] = None ripple_state: Optional[RippleState] = None - # Single Asset Vault ledger-object can be retrieved by its index only - vault_id: Optional[str] = None + vault: Optional[Union[str, Vault]] = None ticket: Optional[Union[str, Ticket]] = None bridge_account: Optional[str] = None bridge: Optional[XChainBridge] = None @@ -352,7 +373,7 @@ def _get_errors(self: Self) -> Dict[str, str]: self.payment_channel, self.permissioned_domain, self.ripple_state, - self.vault_id, + self.vault, self.ticket, self.xchain_claim_id, self.xchain_create_account_claim_id, diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 50a6db03c..246ac25e9 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -16,20 +16,11 @@ class VaultCreateFlag(int, Enum): """Flags for the VaultCreate transaction.""" - TF_FREEZE = 0x0001 - """ - Indicates that the vault should be frozen. - """ - TF_UNFREEZE = 0x0002 - """ - Indicates that the vault should be unfrozen. - """ - - TF_VAULT_PRIVATE = 0x0003 + TF_VAULT_PRIVATE = 0x0001 """ Indicates that the vault is private. It can only be set during Vault creation. """ - TF_VAULT_SHARE_NON_TRANSFERABLE = 0x0004 + TF_VAULT_SHARE_NON_TRANSFERABLE = 0x0002 """ Indicates the vault share is non-transferable. It can only be set during Vault creation. @@ -39,14 +30,6 @@ class VaultCreateFlag(int, Enum): class VaultCreateFlagInterface(FlagInterface): """Interface for the VaultCreate transaction flags.""" - TF_FREEZE: bool - """ - Indicates that the vault should be frozen. - """ - TF_UNFREEZE: bool - """ - Indicates that the vault should be unfrozen. - """ TF_VAULT_PRIVATE: bool """ Indicates that the vault is private. It can only be set during Vault creation. @@ -69,7 +52,7 @@ class VaultCreate(Transaction): data: Optional[str] = None """Arbitrary Vault metadata, limited to 256 bytes.""" - asset_maximum: Optional[str] = None + assets_maximum: Optional[str] = None """The maximum asset amount that can be held in a vault.""" mptoken_metadata: Optional[str] = None diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index 9ccce20a6..3899a11a0 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -23,7 +23,7 @@ class VaultSet(Transaction): data: Optional[str] = None """Arbitrary Vault metadata, limited to 256 bytes.""" - asset_maximum: Optional[str] = None + assets_maximum: Optional[str] = None """The maximum asset amount that can be held in a vault. The value cannot be lower than the current AssetTotal unless the value is 0. """ From 0abade7c913f4d536206f872007dbeef3b47cb7d Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:57:35 -0700 Subject: [PATCH 24/51] Update CHANGELOG.md Co-authored-by: achowdhry-ripple --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18596cfbc..69d772262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Improved validation for models to also check param types -- Support for Single Asset Vault (XLS-65d) +- Support for `Single Asset Vault` (XLS-65d) ## [4.1.0] - 2025-2-13 From a1073b357014d71c5b38e18e2f0d226099866e77 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 3 Jun 2025 17:09:41 -0700 Subject: [PATCH 25/51] Model and Integ-tests for VaultInfo Request --- tests/integration/reqs/test_vault_info.py | 55 +++++++++++++++++++++++ xrpl/models/requests/__init__.py | 2 + xrpl/models/requests/request.py | 3 ++ xrpl/models/requests/vault_info.py | 31 +++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 tests/integration/reqs/test_vault_info.py create mode 100644 xrpl/models/requests/vault_info.py diff --git a/tests/integration/reqs/test_vault_info.py b/tests/integration/reqs/test_vault_info.py new file mode 100644 index 000000000..9a6196a72 --- /dev/null +++ b/tests/integration/reqs/test_vault_info.py @@ -0,0 +1,55 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + fund_wallet_async, + sign_and_reliable_submission_async, + test_async_and_sync, +) +from xrpl.models.currencies import XRP +from xrpl.models.requests import AccountObjects, VaultInfo +from xrpl.models.response import ResponseStatus +from xrpl.models.transactions import VaultCreate +from xrpl.wallet import Wallet + + +class TestVaultInfo(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + + VAULT_OWNER = Wallet.create() + await fund_wallet_async(VAULT_OWNER) + + # Create a vault + tx = VaultCreate( + account=VAULT_OWNER.address, + asset=XRP(), + withdrawal_policy=1, + ) + response = await sign_and_reliable_submission_async(tx, VAULT_OWNER, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + # Note: Due to the setup of the integration testing framework, it is difficult + # to obtain the next-sequence number of an account (in sync Client tests). + # Hence, AccountObjects request is used to fetch the `index` of the vault + # ledger-object. + vault_object = await client.request( + AccountObjects( + account=VAULT_OWNER.address, + type="vault", + ) + ) + self.assertEqual(len(vault_object.result["account_objects"]), 1) + self.assertEqual( + vault_object.result["account_objects"][0]["LedgerEntryType"], "Vault" + ) + vault_object_hash = vault_object.result["account_objects"][0]["index"] + + # Fetch information about the vault using VaultInfo request + response = await client.request( + VaultInfo( + vault_id=vault_object_hash, + ) + ) + self.assertTrue(response.is_successful()) + self.assertEqual(response.result["vault"]["Owner"], VAULT_OWNER.address) + self.assertEqual(response.result["vault"]["index"], vault_object_hash) diff --git a/xrpl/models/requests/__init__.py b/xrpl/models/requests/__init__.py index 73c53b591..f9bc577ad 100644 --- a/xrpl/models/requests/__init__.py +++ b/xrpl/models/requests/__init__.py @@ -51,6 +51,7 @@ from xrpl.models.requests.transaction_entry import TransactionEntry from xrpl.models.requests.tx import Tx from xrpl.models.requests.unsubscribe import Unsubscribe +from xrpl.models.requests.vault_info import VaultInfo __all__ = [ "AccountChannels", @@ -110,4 +111,5 @@ "TransactionEntry", "Tx", "Unsubscribe", + "VaultInfo", ] diff --git a/xrpl/models/requests/request.py b/xrpl/models/requests/request.py index c11ff1272..28fea7502 100644 --- a/xrpl/models/requests/request.py +++ b/xrpl/models/requests/request.py @@ -90,6 +90,9 @@ class RequestMethod(str, Enum): # price oracle methods GET_AGGREGATE_PRICE = "get_aggregate_price" + # vault methods + VAULT_INFO = "vault_info" + # generic unknown/unsupported request # (there is no XRPL analog, this model is specific to xrpl-py) GENERIC_REQUEST = "zzgeneric_request" diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py new file mode 100644 index 000000000..b9296910b --- /dev/null +++ b/xrpl/models/requests/vault_info.py @@ -0,0 +1,31 @@ +""" +This request retrieves information about a Single Asset Vault. + +All information retrieved is relative to a particular version of the ledger. +""" + +from dataclasses import dataclass, field +from typing import Optional + +from xrpl.models.requests.request import LookupByLedgerRequest, Request, RequestMethod +from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class VaultInfo(Request, LookupByLedgerRequest): + """ + This request retrieves information about a Single Asset Vault. + + All information retrieved is relative to a particular version of the ledger. + + Information about a vault ledger-object can be fetched by providing either the + vault_id or both owner and seq values. Please check the documentation for more + details. + """ + + vault_id: Optional[str] = None # type: ignore + owner: Optional[str] = None # type: ignore + seq: Optional[int] = None # type: ignore + + method: RequestMethod = field(default=RequestMethod.VAULT_INFO, init=False) From 4e7343b3e63d32fe92bbb8c2b521110536cfb77d Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 3 Jun 2025 17:17:59 -0700 Subject: [PATCH 26/51] fix linter errors --- xrpl/models/amounts/mpt_amount.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xrpl/models/amounts/mpt_amount.py b/xrpl/models/amounts/mpt_amount.py index c6d0b76c1..074d89f7f 100644 --- a/xrpl/models/amounts/mpt_amount.py +++ b/xrpl/models/amounts/mpt_amount.py @@ -50,6 +50,7 @@ def to_currency(self: Self) -> MPTCurrency: """ return MPTCurrency(mpt_issuance_id=self.mpt_issuance_id) + class MPTIssue: """ This class represents an MPT issue. It is similar to the Issue class, but From edd54001b682ae247688fd968e60b08bfdb65470 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 3 Jun 2025 17:20:35 -0700 Subject: [PATCH 27/51] remove unused type:ignore directives --- xrpl/models/requests/vault_info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index b9296910b..949ccc994 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -24,8 +24,8 @@ class VaultInfo(Request, LookupByLedgerRequest): details. """ - vault_id: Optional[str] = None # type: ignore - owner: Optional[str] = None # type: ignore - seq: Optional[int] = None # type: ignore + vault_id: Optional[str] = None + owner: Optional[str] = None + seq: Optional[int] = None method: RequestMethod = field(default=RequestMethod.VAULT_INFO, init=False) From c90479c0aaf491a22e29b1d75cf50054198c91ff Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 13 Jun 2025 11:35:44 +0800 Subject: [PATCH 28/51] Use updated name for withdrawal policy --- xrpl/models/transactions/vault_create.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 246ac25e9..f2c1e1476 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -65,9 +65,9 @@ class VaultCreate(Transaction): """Indicates the withdrawal strategy used by the Vault. The below withdrawal policy is supported: - Strategy Name Value Description - strFirstComeFirstServe 1 Requests are processed on a first-come-first- - serve basis. + Strategy Name Value Description + vaultStrategyFirstComeFirstServe 1 Requests are processed on a first- + come-first-serve basis. """ transaction_type: TransactionType = field( From f00a4c5cbf28f966bd2641e1573cfd41e18320ae Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 13 Jun 2025 12:01:18 +0800 Subject: [PATCH 29/51] update flag values in VaultCreate transaction --- tests/integration/reqs/test_vault_info.py | 4 ++++ xrpl/models/transactions/vault_create.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration/reqs/test_vault_info.py b/tests/integration/reqs/test_vault_info.py index 9a6196a72..915e0677b 100644 --- a/tests/integration/reqs/test_vault_info.py +++ b/tests/integration/reqs/test_vault_info.py @@ -8,6 +8,7 @@ from xrpl.models.requests import AccountObjects, VaultInfo from xrpl.models.response import ResponseStatus from xrpl.models.transactions import VaultCreate +from xrpl.models.transactions.vault_create import VaultCreateFlag from xrpl.wallet import Wallet @@ -19,10 +20,13 @@ async def test_basic_functionality(self, client): await fund_wallet_async(VAULT_OWNER) # Create a vault + # Additionally validate the usage of flags in the VaultCreate transaction tx = VaultCreate( account=VAULT_OWNER.address, asset=XRP(), withdrawal_policy=1, + flags=VaultCreateFlag.TF_VAULT_PRIVATE + | VaultCreateFlag.TF_VAULT_SHARE_NON_TRANSFERABLE, ) response = await sign_and_reliable_submission_async(tx, VAULT_OWNER, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index f2c1e1476..1f95eff40 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -16,11 +16,11 @@ class VaultCreateFlag(int, Enum): """Flags for the VaultCreate transaction.""" - TF_VAULT_PRIVATE = 0x0001 + TF_VAULT_PRIVATE = 0x00010000 """ Indicates that the vault is private. It can only be set during Vault creation. """ - TF_VAULT_SHARE_NON_TRANSFERABLE = 0x0002 + TF_VAULT_SHARE_NON_TRANSFERABLE = 0x00020000 """ Indicates the vault share is non-transferable. It can only be set during Vault creation. From afbd4e56239b578519d97e2519d422bbd7648850 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:16:25 -0700 Subject: [PATCH 30/51] Update .ci-config/rippled.cfg Co-authored-by: Omar Khan --- .ci-config/rippled.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index f1034e14e..6a0dc81c6 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -198,7 +198,7 @@ DeepFreeze DynamicNFT PermissionedDomains -# (proposed, not yet released) 2.5.0 Amendments +# 2.5.0 Amendments SingleAssetVault fixFrozenLPTokenTransfer fixInvalidTxFlags From b2c2de7c014bb33bccef6f169c3f83d96d83ff9f Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:32:09 -0700 Subject: [PATCH 31/51] Update xrpl/core/binarycodec/types/number.py Co-authored-by: Omar Khan --- xrpl/core/binarycodec/types/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index 80d6e2dc9..1672841e5 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -240,7 +240,7 @@ def to_json(self: Self) -> str: A JSON string representing the Number """ mantissa = int.from_bytes(self.buffer[:8], byteorder="big", signed=True) - exponent = int.from_bytes(self.buffer[8:], byteorder="big", signed=True) + exponent = int.from_bytes(self.buffer[8:12], byteorder="big", signed=True) if exponent == 0: return str(mantissa) From 9488ae935c2bb685c222f5804821137f770e429f Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 20 Jun 2025 10:35:13 -0700 Subject: [PATCH 32/51] remove duplicate entries in the tec-codes and transaction-types of def.json file --- xrpl/core/binarycodec/definitions/definitions.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 692a27a38..0e63482f4 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -3160,8 +3160,6 @@ "tecUNFUNDED_OFFER": 103, "tecUNFUNDED_PAYMENT": 104, "tecWRONG_ASSET": 194, - "tecLIMIT_EXCEEDED": 195, - "tecPSEUDO_ACCOUNT": 196, "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, "tecXCHAIN_BAD_CLAIM_ID": 172, @@ -3348,10 +3346,6 @@ "VaultClawback": 70, "VaultDeposit": 68, "VaultDelete": 67, - "VaultClawback": 70, - "VaultCreate": 65, - "VaultDelete": 67, - "VaultDeposit": 68, "VaultSet": 66, "VaultWithdraw": 69, "XChainAccountCreateCommit": 44, From ebb70ec7008e18409f73d8a0cf1c36c64442e4ed Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 20 Jun 2025 12:57:18 -0700 Subject: [PATCH 33/51] Address comments from Raj Patel --- .../fixtures/data/codec-fixtures.json | 11 ++++ .../models/transactions/test_vault_create.py | 64 +++++++++++++++++++ .../models/transactions/test_vault_delete.py | 27 ++++++++ .../models/transactions/test_vault_set.py | 41 ++++++++++++ .../transactions/test_vault_withdraw.py | 32 ++++++++++ xrpl/models/transactions/vault_create.py | 16 ++++- xrpl/models/transactions/vault_delete.py | 11 ++++ xrpl/models/transactions/vault_set.py | 14 +++- xrpl/models/transactions/vault_withdraw.py | 12 +++- 9 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 tests/unit/models/transactions/test_vault_create.py create mode 100644 tests/unit/models/transactions/test_vault_delete.py create mode 100644 tests/unit/models/transactions/test_vault_set.py create mode 100644 tests/unit/models/transactions/test_vault_withdraw.py diff --git a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json index 5ae4f2c7c..1a5b866dd 100644 --- a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json +++ b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json @@ -4884,6 +4884,17 @@ "TransactionType": "DelegateSet", "TxnSignature": "D05A89D0B489DEC1CECBE0D33BA656C929CDCCC75D4D41B282B378544975B87A70C3E42147D980D1F6E2E4DC6316C99D7E2D4F6335F147C71C0DAA0D6516150D" } + }, + { + "binary": "12004173008114204288D2E47F8EF6C99BCC457966320D124097119300038D7EA4C68000FFFFFFF40010140103180000000000000000000000005553440000000000204288D2E47F8EF6C99BCC457966320D12409711", + "json": { + "Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + "TransactionType": "VaultCreate", + "SigningPubKey": "", + "Asset": {"currency": "USD", "issuer": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"}, + "AssetsMaximum": "1000", + "WithdrawalPolicy": 1 + } } ], "ledgerData": [{ diff --git a/tests/unit/models/transactions/test_vault_create.py b/tests/unit/models/transactions/test_vault_create.py new file mode 100644 index 000000000..1dc670f84 --- /dev/null +++ b/tests/unit/models/transactions/test_vault_create.py @@ -0,0 +1,64 @@ +from unittest import TestCase + +from xrpl.models.currencies import IssuedCurrency +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions.vault_create import VaultCreate + +_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" + + +class TestVaultCreate(TestCase): + def test_valid(self): + tx = VaultCreate( + account=_ACCOUNT, + asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT), + assets_maximum="1000", + withdrawal_policy=1, + ) + self.assertTrue(tx.is_valid()) + + def test_long_data_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultCreate( + account=_ACCOUNT, + asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT), + assets_maximum="1000", + withdrawal_policy=1, + data="A" * 257, + ) + self.assertEqual( + e.exception.args[0], + str({"data": "Data must be less than 256 bytes."}), + ) + + def test_long_mpt_metadata_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultCreate( + account=_ACCOUNT, + asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT), + assets_maximum="1000", + withdrawal_policy=1, + # Note: MPTMetadata is associated with a Multi-Purpose token and not a + # conventional IOU token. This unit test demonstrates the validity of + # the transaction model only. It must not be misconstrued as an + # archetype of a VaultCreate transaction. + mptoken_metadata="A" * 1025, + ) + self.assertEqual( + e.exception.args[0], + str({"mptoken_metadata": "Metadata must be less than 1024 bytes."}), + ) + + def test_invalid_domain_id_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultCreate( + account=_ACCOUNT, + asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT), + assets_maximum="1000", + withdrawal_policy=1, + domain_id="A" * 14, + ) + self.assertEqual( + e.exception.args[0], + str({"domain_id": "Invalid domain ID."}), + ) diff --git a/tests/unit/models/transactions/test_vault_delete.py b/tests/unit/models/transactions/test_vault_delete.py new file mode 100644 index 000000000..e45de67e8 --- /dev/null +++ b/tests/unit/models/transactions/test_vault_delete.py @@ -0,0 +1,27 @@ +from unittest import TestCase + +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions.vault_delete import VaultDelete + +_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" +_VAULT_ID = "B982D2AAEF6014E6BE3194D939865453D56D16FF7081BB1D0ED865C708ABCEEE" + + +class TestVaultDelete(TestCase): + def test_valid(self): + tx = VaultDelete( + account=_ACCOUNT, + vault_id=_VAULT_ID, + ) + self.assertTrue(tx.is_valid()) + + def test_invalid_vault_id_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultDelete( + account=_ACCOUNT, + vault_id="0", + ) + self.assertEqual( + e.exception.args[0], + str({"vault_id": "Invalid vault ID."}), + ) diff --git a/tests/unit/models/transactions/test_vault_set.py b/tests/unit/models/transactions/test_vault_set.py new file mode 100644 index 000000000..850bbee53 --- /dev/null +++ b/tests/unit/models/transactions/test_vault_set.py @@ -0,0 +1,41 @@ +from unittest import TestCase + +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions.vault_set import VaultSet + +_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" +_VAULT_ID = "DB303FC1C7611B22C09E773B51044F6BEA02EF917DF59A2E2860871E167066A5" + + +class TestVaultSet(TestCase): + def test_valid(self): + tx = VaultSet( + account=_ACCOUNT, + vault_id=_VAULT_ID, + assets_maximum="1000", + ) + self.assertTrue(tx.is_valid()) + + def test_long_data_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultSet( + account=_ACCOUNT, + vault_id=_VAULT_ID, + data="A" * 257, + ) + self.assertEqual( + e.exception.args[0], + str({"data": "Data must be less than 256 bytes."}), + ) + + def test_invalid_domain_id_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultSet( + account=_ACCOUNT, + vault_id=_VAULT_ID, + domain_id="A" * 14, + ) + self.assertEqual( + e.exception.args[0], + str({"domain_id": "Invalid domain ID."}), + ) diff --git a/tests/unit/models/transactions/test_vault_withdraw.py b/tests/unit/models/transactions/test_vault_withdraw.py new file mode 100644 index 000000000..6364d7b7b --- /dev/null +++ b/tests/unit/models/transactions/test_vault_withdraw.py @@ -0,0 +1,32 @@ +from unittest import TestCase + +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions.vault_withdraw import VaultWithdraw + +_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" +_VAULT_ID = "B982D2AAEF6014E6BE3194D939865453D56D16FF7081BB1D0ED865C708ABCEEE" + + +class TestVaultWithdraw(TestCase): + def test_valid(self): + tx = VaultWithdraw( + account=_ACCOUNT, + vault_id=_VAULT_ID, + amount=IssuedCurrencyAmount(currency="USD", issuer=_ACCOUNT, value="100"), + ) + self.assertTrue(tx.is_valid()) + + def test_invalid_vault_id_field(self): + with self.assertRaises(XRPLModelException) as e: + VaultWithdraw( + account=_ACCOUNT, + amount=IssuedCurrencyAmount( + currency="USD", issuer=_ACCOUNT, value="100" + ), + vault_id="0", + ) + self.assertEqual( + e.exception.args[0], + str({"vault_id": "Invalid vault ID."}), + ) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 1f95eff40..c324a20e1 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -2,7 +2,9 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Optional, Union +from typing import Dict, Optional, Union + +from typing_extensions import Self from xrpl.models.amounts.mpt_amount import MPTIssue from xrpl.models.currencies import Currency @@ -74,3 +76,15 @@ class VaultCreate(Transaction): default=TransactionType.VAULT_CREATE, init=False, ) + + def _get_errors(self: Self) -> Dict[str, str]: + errors = super()._get_errors() + + if self.data is not None and len(self.data) > 256: + errors["data"] = "Data must be less than 256 bytes." + if self.mptoken_metadata is not None and len(self.mptoken_metadata) > 1024: + errors["mptoken_metadata"] = "Metadata must be less than 1024 bytes." + if self.domain_id is not None and len(self.domain_id) != 32: + errors["domain_id"] = "Invalid domain ID." + + return errors diff --git a/xrpl/models/transactions/vault_delete.py b/xrpl/models/transactions/vault_delete.py index c36376b5e..7af551ee6 100644 --- a/xrpl/models/transactions/vault_delete.py +++ b/xrpl/models/transactions/vault_delete.py @@ -1,6 +1,9 @@ """Represents a VaultDelete transaction on the XRP Ledger.""" from dataclasses import dataclass, field +from typing import Dict + +from typing_extensions import Self from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -20,3 +23,11 @@ class VaultDelete(Transaction): default=TransactionType.VAULT_DELETE, init=False, ) + + def _get_errors(self: Self) -> Dict[str, str]: + errors = super()._get_errors() + + if len(self.vault_id) != 64: + errors["vault_id"] = "Invalid vault ID." + + return errors diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index 3899a11a0..e37ee6541 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -1,7 +1,9 @@ """Represents a VaultSet transaction on the XRP Ledger.""" from dataclasses import dataclass, field -from typing import Optional +from typing import Dict, Optional + +from typing_extensions import Self from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -32,3 +34,13 @@ class VaultSet(Transaction): default=TransactionType.VAULT_SET, init=False, ) + + def _get_errors(self: Self) -> Dict[str, str]: + errors = super()._get_errors() + + if self.data is not None and len(self.data) > 256: + errors["data"] = "Data must be less than 256 bytes." + if self.domain_id is not None and len(self.domain_id) != 32: + errors["domain_id"] = "Invalid domain ID." + + return errors diff --git a/xrpl/models/transactions/vault_withdraw.py b/xrpl/models/transactions/vault_withdraw.py index 0e4ddb7cc..9518ff49e 100644 --- a/xrpl/models/transactions/vault_withdraw.py +++ b/xrpl/models/transactions/vault_withdraw.py @@ -1,7 +1,9 @@ """Represents a VaultWithdraw transaction on the XRP Ledger.""" from dataclasses import dataclass, field -from typing import Optional +from typing import Dict, Optional + +from typing_extensions import Self from xrpl.models.amounts import Amount from xrpl.models.required import REQUIRED @@ -30,3 +32,11 @@ class VaultWithdraw(Transaction): default=TransactionType.VAULT_WITHDRAW, init=False, ) + + def _get_errors(self: Self) -> Dict[str, str]: + errors = super()._get_errors() + + if len(self.vault_id) != 64: + errors["vault_id"] = "Invalid vault ID." + + return errors From 2dc7e25914bddbef6f8ec8f29a88e78169c9bd17 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 20 Jun 2025 13:11:37 -0700 Subject: [PATCH 34/51] fix: update the length of domain_id field check --- xrpl/models/transactions/vault_create.py | 2 +- xrpl/models/transactions/vault_set.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index c324a20e1..bff0eced5 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -84,7 +84,7 @@ def _get_errors(self: Self) -> Dict[str, str]: errors["data"] = "Data must be less than 256 bytes." if self.mptoken_metadata is not None and len(self.mptoken_metadata) > 1024: errors["mptoken_metadata"] = "Metadata must be less than 1024 bytes." - if self.domain_id is not None and len(self.domain_id) != 32: + if self.domain_id is not None and len(self.domain_id) != 64: errors["domain_id"] = "Invalid domain ID." return errors diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index e37ee6541..db050e3d7 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -40,7 +40,7 @@ def _get_errors(self: Self) -> Dict[str, str]: if self.data is not None and len(self.data) > 256: errors["data"] = "Data must be less than 256 bytes." - if self.domain_id is not None and len(self.domain_id) != 32: + if self.domain_id is not None and len(self.domain_id) != 64: errors["domain_id"] = "Invalid domain ID." return errors From a2619de16b09e0074692531cb0c3b370003ef428 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Fri, 20 Jun 2025 16:01:23 -0700 Subject: [PATCH 35/51] VaultCreate: Add enum for representing Withdrawal Strategy input --- tests/integration/transactions/test_sav.py | 3 ++- xrpl/models/transactions/vault_create.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index d58fdb63b..afdb4ea68 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -22,6 +22,7 @@ from xrpl.models.requests import AccountObjects, LedgerEntry from xrpl.models.requests.account_objects import AccountObjectType from xrpl.models.response import ResponseStatus +from xrpl.models.transactions.vault_create import WithdrawalPolicy from xrpl.utils import str_to_hex from xrpl.wallet import Wallet @@ -88,7 +89,7 @@ async def test_sav_lifecycle(self, client): account=vault_owner.address, asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), assets_maximum="1000", - withdrawal_policy=1, + withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE, ) response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index bff0eced5..b88783880 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -43,6 +43,13 @@ class VaultCreateFlagInterface(FlagInterface): """ +class WithdrawalPolicy(int, Enum): + """Withdrawal policy for the Vault.""" + + VAULT_STRATEGY_FIRST_COME_FIRST_SERVE = 1 + """Requests are processed on a first-come-first-serve basis.""" + + @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class VaultCreate(Transaction): @@ -63,7 +70,7 @@ class VaultCreate(Transaction): domain_id: Optional[str] = None """The PermissionedDomain object ID associated with the shares of this Vault.""" - withdrawal_policy: Optional[int] = None + withdrawal_policy: Optional[Union[int, WithdrawalPolicy]] = None """Indicates the withdrawal strategy used by the Vault. The below withdrawal policy is supported: From 56a19e8a607462ecca05edd5e084e9784ffe95e1 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 23 Jun 2025 11:10:16 -0700 Subject: [PATCH 36/51] vault_info: additional validation for request input specification' --- tests/unit/models/requests/test_vault_info.py | 32 +++++++++++++++++++ xrpl/models/requests/vault_info.py | 14 +++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/unit/models/requests/test_vault_info.py diff --git a/tests/unit/models/requests/test_vault_info.py b/tests/unit/models/requests/test_vault_info.py new file mode 100644 index 000000000..d7f0c3165 --- /dev/null +++ b/tests/unit/models/requests/test_vault_info.py @@ -0,0 +1,32 @@ +from unittest import TestCase + +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.requests import VaultInfo +from xrpl.models.requests.request import _DEFAULT_API_VERSION + +_ASSET = XRP() +_ASSET_2 = IssuedCurrency(currency="USD", issuer="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj") +_ACCOUNT = _ASSET_2.issuer + +VAULT_ID = "CE47F59928D43773A8A9CB7F525BE031977EFB72A23FF094C1C326E687D2B567" + + +class TestVaultInfo(TestCase): + def test_valid_vault_info(self): + request = VaultInfo( + vault_id=VAULT_ID, + ) + self.assertTrue(request.is_valid()) + + def test_specify_invalid_combination_of_input_fields_1(self): + with self.assertRaises(ValueError): + VaultInfo() + + def test_specify_invalid_combination_of_input_fields_2(self): + with self.assertRaises(ValueError): + VaultInfo( + vault_id=VAULT_ID, + owner="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj", + seq=1234567890, + ) diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index 949ccc994..778c03a59 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -5,7 +5,7 @@ """ from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Self from xrpl.models.requests.request import LookupByLedgerRequest, Request, RequestMethod from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init @@ -29,3 +29,15 @@ class VaultInfo(Request, LookupByLedgerRequest): seq: Optional[int] = None method: RequestMethod = field(default=RequestMethod.VAULT_INFO, init=False) + + def __post_init__(self: Self) -> None: + """Validate that either vault_id or both owner and seq are provided.""" + if self.vault_id is None and (self.owner is None or self.seq is None): + raise ValueError( + "Either vault_id must be provided, or both owner and seq must be " + "provided" + ) + if self.vault_id is not None and ( + self.owner is not None or self.seq is not None + ): + raise ValueError("Cannot provide both vault_id and owner/seq parameters") From de2131e867999155f2a8b6fd25b7517c5814e5db Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 23 Jun 2025 12:38:28 -0700 Subject: [PATCH 37/51] fix: update the import statements --- tests/unit/models/requests/test_vault_info.py | 7 ------- xrpl/models/requests/vault_info.py | 4 +++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/unit/models/requests/test_vault_info.py b/tests/unit/models/requests/test_vault_info.py index d7f0c3165..7a739c804 100644 --- a/tests/unit/models/requests/test_vault_info.py +++ b/tests/unit/models/requests/test_vault_info.py @@ -1,13 +1,6 @@ from unittest import TestCase -from xrpl.models.currencies import XRP, IssuedCurrency -from xrpl.models.exceptions import XRPLModelException from xrpl.models.requests import VaultInfo -from xrpl.models.requests.request import _DEFAULT_API_VERSION - -_ASSET = XRP() -_ASSET_2 = IssuedCurrency(currency="USD", issuer="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj") -_ACCOUNT = _ASSET_2.issuer VAULT_ID = "CE47F59928D43773A8A9CB7F525BE031977EFB72A23FF094C1C326E687D2B567" diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index 778c03a59..abcf541b7 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -5,7 +5,9 @@ """ from dataclasses import dataclass, field -from typing import Optional, Self +from typing import Optional + +from typing_extensions import Self from xrpl.models.requests.request import LookupByLedgerRequest, Request, RequestMethod from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init From 4b2084dcbe57e5b1a65d7672e7d688c08dd34f71 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:47:47 -0700 Subject: [PATCH 38/51] Update tests/unit/models/requests/test_vault_info.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/unit/models/requests/test_vault_info.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unit/models/requests/test_vault_info.py b/tests/unit/models/requests/test_vault_info.py index 7a739c804..efa9c2625 100644 --- a/tests/unit/models/requests/test_vault_info.py +++ b/tests/unit/models/requests/test_vault_info.py @@ -12,14 +12,21 @@ def test_valid_vault_info(self): ) self.assertTrue(request.is_valid()) - def test_specify_invalid_combination_of_input_fields_1(self): + def test_vault_info_requires_parameters(self): with self.assertRaises(ValueError): VaultInfo() - def test_specify_invalid_combination_of_input_fields_2(self): + def test_vault_info_rejects_conflicting_parameters(self): with self.assertRaises(ValueError): VaultInfo( vault_id=VAULT_ID, owner="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj", seq=1234567890, ) + + def test_valid_vault_info_with_owner_and_seq(self): + request = VaultInfo( + owner="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj", + seq=1234567890, + ) + self.assertTrue(request.is_valid()) From eb0260e9fd3d6e68096036709b64b70270f7a24e Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 23 Jun 2025 14:37:51 -0700 Subject: [PATCH 39/51] fix: Length field check in Hash256 fields for VaultCreate transaction --- tests/integration/transactions/test_sav.py | 19 +++++++++++++ .../models/transactions/test_vault_create.py | 27 +++++++++++++++---- xrpl/models/transactions/vault_create.py | 21 ++++++++++----- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index afdb4ea68..b67749aed 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -165,3 +165,22 @@ async def test_sav_lifecycle(self, client): response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") + + @test_async_and_sync(globals()) + async def test_encoding_non_hex_data_field(self, client): + + vault_owner = Wallet.create() + await fund_wallet_async(vault_owner) + + issuer_wallet = Wallet.create() + await fund_wallet_async(issuer_wallet) + tx = VaultCreate( + account=vault_owner.address, + asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), + assets_maximum="1000", + withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE, + data="z", + ) + response = await sign_and_reliable_submission_async(tx, vault_owner, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") diff --git a/tests/unit/models/transactions/test_vault_create.py b/tests/unit/models/transactions/test_vault_create.py index 1dc670f84..d3a67497d 100644 --- a/tests/unit/models/transactions/test_vault_create.py +++ b/tests/unit/models/transactions/test_vault_create.py @@ -3,6 +3,7 @@ from xrpl.models.currencies import IssuedCurrency from xrpl.models.exceptions import XRPLModelException from xrpl.models.transactions.vault_create import VaultCreate +from xrpl.utils import str_to_hex _ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" @@ -14,6 +15,7 @@ def test_valid(self): asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT), assets_maximum="1000", withdrawal_policy=1, + data=str_to_hex("A" * 256), ) self.assertTrue(tx.is_valid()) @@ -24,11 +26,16 @@ def test_long_data_field(self): asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT), assets_maximum="1000", withdrawal_policy=1, - data="A" * 257, + data=str_to_hex("A" * 257), ) self.assertEqual( e.exception.args[0], - str({"data": "Data must be less than 256 bytes."}), + str( + { + "data": "Data must be less than 256 bytes " + "(alternatively, 512 hex characters)." + } + ), ) def test_long_mpt_metadata_field(self): @@ -42,11 +49,16 @@ def test_long_mpt_metadata_field(self): # conventional IOU token. This unit test demonstrates the validity of # the transaction model only. It must not be misconstrued as an # archetype of a VaultCreate transaction. - mptoken_metadata="A" * 1025, + mptoken_metadata=str_to_hex("A" * 1025), ) self.assertEqual( e.exception.args[0], - str({"mptoken_metadata": "Metadata must be less than 1024 bytes."}), + str( + { + "mptoken_metadata": "Metadata must be less than 1024 bytes " + "(alternatively, 2048 hex characters)." + } + ), ) def test_invalid_domain_id_field(self): @@ -60,5 +72,10 @@ def test_invalid_domain_id_field(self): ) self.assertEqual( e.exception.args[0], - str({"domain_id": "Invalid domain ID."}), + str( + { + "domain_id": "Invalid domain ID: Length must be 32 characters " + "(64 hex characters)." + } + ), ) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index b88783880..3db84da54 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -87,11 +87,20 @@ class VaultCreate(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if self.data is not None and len(self.data) > 256: - errors["data"] = "Data must be less than 256 bytes." - if self.mptoken_metadata is not None and len(self.mptoken_metadata) > 1024: - errors["mptoken_metadata"] = "Metadata must be less than 1024 bytes." - if self.domain_id is not None and len(self.domain_id) != 64: - errors["domain_id"] = "Invalid domain ID." + if self.data is not None and len(self.data) > (256 * 2): + errors["data"] = ( + "Data must be less than 256 bytes (alternatively, 512 hex characters)." + ) + if self.mptoken_metadata is not None and len(self.mptoken_metadata) > ( + 1024 * 2 + ): + errors["mptoken_metadata"] = ( + "Metadata must be less than 1024 bytes " + "(alternatively, 2048 hex characters)." + ) + if self.domain_id is not None and len(self.domain_id) != 32 * 2: + errors["domain_id"] = ( + "Invalid domain ID: Length must be 32 characters (64 hex characters)." + ) return errors From 4e9a80599ee6ae21cf647f1ae0961191f31e5787 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 23 Jun 2025 14:40:55 -0700 Subject: [PATCH 40/51] fix: Update length checkk on VaultSet transaction --- .../unit/models/transactions/test_vault_set.py | 18 +++++++++++++++--- xrpl/models/transactions/vault_set.py | 12 ++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/unit/models/transactions/test_vault_set.py b/tests/unit/models/transactions/test_vault_set.py index 850bbee53..ef63dfe94 100644 --- a/tests/unit/models/transactions/test_vault_set.py +++ b/tests/unit/models/transactions/test_vault_set.py @@ -2,6 +2,7 @@ from xrpl.models.exceptions import XRPLModelException from xrpl.models.transactions.vault_set import VaultSet +from xrpl.utils import str_to_hex _ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW" _VAULT_ID = "DB303FC1C7611B22C09E773B51044F6BEA02EF917DF59A2E2860871E167066A5" @@ -13,6 +14,7 @@ def test_valid(self): account=_ACCOUNT, vault_id=_VAULT_ID, assets_maximum="1000", + data=str_to_hex("A" * 256), ) self.assertTrue(tx.is_valid()) @@ -21,11 +23,16 @@ def test_long_data_field(self): VaultSet( account=_ACCOUNT, vault_id=_VAULT_ID, - data="A" * 257, + data=str_to_hex("A" * 257), ) self.assertEqual( e.exception.args[0], - str({"data": "Data must be less than 256 bytes."}), + str( + { + "data": "Data must be less than 256 bytes " + "(alternatively, 512 hex characters)." + } + ), ) def test_invalid_domain_id_field(self): @@ -37,5 +44,10 @@ def test_invalid_domain_id_field(self): ) self.assertEqual( e.exception.args[0], - str({"domain_id": "Invalid domain ID."}), + str( + { + "domain_id": "Invalid domain ID: Length must be 32 characters " + "(64 hex characters)." + } + ), ) diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index db050e3d7..f68c576d4 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -38,9 +38,13 @@ class VaultSet(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if self.data is not None and len(self.data) > 256: - errors["data"] = "Data must be less than 256 bytes." - if self.domain_id is not None and len(self.domain_id) != 64: - errors["domain_id"] = "Invalid domain ID." + if self.data is not None and len(self.data) > (256 * 2): + errors["data"] = ( + "Data must be less than 256 bytes (alternatively, 512 hex characters)." + ) + if self.domain_id is not None and len(self.domain_id) != 32 * 2: + errors["domain_id"] = ( + "Invalid domain ID: Length must be 32 characters (64 hex characters)." + ) return errors From 9c11f463858fcec5689e1378ede7efe2b136c5cc Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 23 Jun 2025 17:12:39 -0700 Subject: [PATCH 41/51] remove integ test -- code is already covered under binary_codec tests --- tests/integration/transactions/test_sav.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_sav.py index b67749aed..afdb4ea68 100644 --- a/tests/integration/transactions/test_sav.py +++ b/tests/integration/transactions/test_sav.py @@ -165,22 +165,3 @@ async def test_sav_lifecycle(self, client): response = await sign_and_reliable_submission_async(tx, vault_owner, client) self.assertEqual(response.status, ResponseStatus.SUCCESS) self.assertEqual(response.result["engine_result"], "tesSUCCESS") - - @test_async_and_sync(globals()) - async def test_encoding_non_hex_data_field(self, client): - - vault_owner = Wallet.create() - await fund_wallet_async(vault_owner) - - issuer_wallet = Wallet.create() - await fund_wallet_async(issuer_wallet) - tx = VaultCreate( - account=vault_owner.address, - asset=IssuedCurrency(currency="USD", issuer=issuer_wallet.address), - assets_maximum="1000", - withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE, - data="z", - ) - response = await sign_and_reliable_submission_async(tx, vault_owner, client) - self.assertEqual(response.status, ResponseStatus.SUCCESS) - self.assertEqual(response.result["engine_result"], "tesSUCCESS") From e6f591f90fab36c7f7e6def3996ff3d92a6438f7 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 25 Jun 2025 10:38:47 -0700 Subject: [PATCH 42/51] vault_clawback: do not allow XRP amounts inside VaultClawback transaction --- xrpl/models/transactions/vault_clawback.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/xrpl/models/transactions/vault_clawback.py b/xrpl/models/transactions/vault_clawback.py index 5f2576f8a..c5489084d 100644 --- a/xrpl/models/transactions/vault_clawback.py +++ b/xrpl/models/transactions/vault_clawback.py @@ -1,9 +1,10 @@ """Represents a VaultClawback transaction on the XRP Ledger.""" from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, Union -from xrpl.models.amounts import Amount +from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount +from xrpl.models.amounts.mpt_amount import MPTAmount from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType @@ -30,7 +31,7 @@ class VaultClawback(Transaction): holder: str = REQUIRED # type: ignore """The account ID from which to clawback the assets.""" - amount: Optional[Amount] = None + amount: Optional[Union[IssuedCurrencyAmount, MPTAmount]] = None """The asset amount to clawback. When Amount is 0 clawback all funds, up to the total shares the Holder owns. """ From 3661d9ac4477a42dae041eec10bb6b1c0c76735d Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 30 Jun 2025 12:07:36 -0700 Subject: [PATCH 43/51] address comments from Phu --- .../models/transactions/test_vault_delete.py | 7 ++++++- tests/unit/models/transactions/test_vault_set.py | 16 ++++++++++++++++ .../models/transactions/test_vault_withdraw.py | 7 ++++++- xrpl/models/requests/vault_info.py | 3 +++ xrpl/models/transactions/vault_create.py | 10 +++++++--- xrpl/models/transactions/vault_delete.py | 8 ++++++-- xrpl/models/transactions/vault_set.py | 14 ++++++++++++-- xrpl/models/transactions/vault_withdraw.py | 7 +++++-- 8 files changed, 61 insertions(+), 11 deletions(-) diff --git a/tests/unit/models/transactions/test_vault_delete.py b/tests/unit/models/transactions/test_vault_delete.py index e45de67e8..e9ce1c939 100644 --- a/tests/unit/models/transactions/test_vault_delete.py +++ b/tests/unit/models/transactions/test_vault_delete.py @@ -23,5 +23,10 @@ def test_invalid_vault_id_field(self): ) self.assertEqual( e.exception.args[0], - str({"vault_id": "Invalid vault ID."}), + str( + { + "vault_id": "Invalid vault ID: Length must be 32 characters " + "(64 hex characters)." + } + ), ) diff --git a/tests/unit/models/transactions/test_vault_set.py b/tests/unit/models/transactions/test_vault_set.py index ef63dfe94..e562d99ca 100644 --- a/tests/unit/models/transactions/test_vault_set.py +++ b/tests/unit/models/transactions/test_vault_set.py @@ -18,6 +18,22 @@ def test_valid(self): ) self.assertTrue(tx.is_valid()) + def test_invalid_vault_id_field_length(self): + with self.assertRaises(XRPLModelException) as e: + VaultSet( + account=_ACCOUNT, + vault_id=_VAULT_ID[:-1], + ) + self.assertEqual( + e.exception.args[0], + str( + { + "vault_id": "Invalid vault ID: Length must be 32 characters " + "(64 hex characters)." + } + ), + ) + def test_long_data_field(self): with self.assertRaises(XRPLModelException) as e: VaultSet( diff --git a/tests/unit/models/transactions/test_vault_withdraw.py b/tests/unit/models/transactions/test_vault_withdraw.py index 6364d7b7b..abe719f89 100644 --- a/tests/unit/models/transactions/test_vault_withdraw.py +++ b/tests/unit/models/transactions/test_vault_withdraw.py @@ -28,5 +28,10 @@ def test_invalid_vault_id_field(self): ) self.assertEqual( e.exception.args[0], - str({"vault_id": "Invalid vault ID."}), + str( + { + "vault_id": "Invalid vault ID: Length must be 32 characters " + "(64 hex characters)." + } + ), ) diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index abcf541b7..ead781893 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -27,8 +27,11 @@ class VaultInfo(Request, LookupByLedgerRequest): """ vault_id: Optional[str] = None + """The object ID of the Vault to be returned.""" owner: Optional[str] = None + """The account address of the Vault Owner.""" seq: Optional[int] = None + """The transaction sequence number that created the vault.""" method: RequestMethod = field(default=RequestMethod.VAULT_INFO, init=False) diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 3db84da54..8051808d4 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -14,6 +14,10 @@ from xrpl.models.transactions.types import TransactionType from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init +_MAX_DATA_LENGTH = 256 * 2 +_MAX_MPTOKEN_METADATA_LENGTH = 1024 * 2 +_MAX_DOMAIN_ID_LENGTH = 32 * 2 + class VaultCreateFlag(int, Enum): """Flags for the VaultCreate transaction.""" @@ -87,18 +91,18 @@ class VaultCreate(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if self.data is not None and len(self.data) > (256 * 2): + if self.data is not None and len(self.data) > _MAX_DATA_LENGTH: errors["data"] = ( "Data must be less than 256 bytes (alternatively, 512 hex characters)." ) if self.mptoken_metadata is not None and len(self.mptoken_metadata) > ( - 1024 * 2 + _MAX_MPTOKEN_METADATA_LENGTH ): errors["mptoken_metadata"] = ( "Metadata must be less than 1024 bytes " "(alternatively, 2048 hex characters)." ) - if self.domain_id is not None and len(self.domain_id) != 32 * 2: + if self.domain_id is not None and len(self.domain_id) != _MAX_DOMAIN_ID_LENGTH: errors["domain_id"] = ( "Invalid domain ID: Length must be 32 characters (64 hex characters)." ) diff --git a/xrpl/models/transactions/vault_delete.py b/xrpl/models/transactions/vault_delete.py index 7af551ee6..1397b71d7 100644 --- a/xrpl/models/transactions/vault_delete.py +++ b/xrpl/models/transactions/vault_delete.py @@ -10,6 +10,8 @@ from xrpl.models.transactions.types import TransactionType from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init +_MAX_VAULT_ID_LENGTH = 32 * 2 + @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) @@ -27,7 +29,9 @@ class VaultDelete(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if len(self.vault_id) != 64: - errors["vault_id"] = "Invalid vault ID." + if len(self.vault_id) != _MAX_VAULT_ID_LENGTH: + errors["vault_id"] = ( + "Invalid vault ID: Length must be 32 characters (64 hex characters)." + ) return errors diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index f68c576d4..6b648a4e4 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -8,6 +8,11 @@ from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType +from xrpl.models.transactions.vault_create import ( + _MAX_DATA_LENGTH, + _MAX_DOMAIN_ID_LENGTH, +) +from xrpl.models.transactions.vault_delete import _MAX_VAULT_ID_LENGTH from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init @@ -38,13 +43,18 @@ class VaultSet(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if self.data is not None and len(self.data) > (256 * 2): + if self.data is not None and len(self.data) > _MAX_DATA_LENGTH: errors["data"] = ( "Data must be less than 256 bytes (alternatively, 512 hex characters)." ) - if self.domain_id is not None and len(self.domain_id) != 32 * 2: + if self.domain_id is not None and len(self.domain_id) != _MAX_DOMAIN_ID_LENGTH: errors["domain_id"] = ( "Invalid domain ID: Length must be 32 characters (64 hex characters)." ) + if len(self.vault_id) != _MAX_VAULT_ID_LENGTH: + errors["vault_id"] = ( + "Invalid vault ID: Length must be 32 characters (64 hex characters)." + ) + return errors diff --git a/xrpl/models/transactions/vault_withdraw.py b/xrpl/models/transactions/vault_withdraw.py index 9518ff49e..ea4bd7b57 100644 --- a/xrpl/models/transactions/vault_withdraw.py +++ b/xrpl/models/transactions/vault_withdraw.py @@ -9,6 +9,7 @@ from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType +from xrpl.models.transactions.vault_delete import _MAX_VAULT_ID_LENGTH from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init @@ -36,7 +37,9 @@ class VaultWithdraw(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if len(self.vault_id) != 64: - errors["vault_id"] = "Invalid vault ID." + if len(self.vault_id) != _MAX_VAULT_ID_LENGTH: + errors["vault_id"] = ( + "Invalid vault ID: Length must be 32 characters (64 hex characters)." + ) return errors From 217cacecb5d79e6b20b14f5494a39f1dd6f51416 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:11:31 -0700 Subject: [PATCH 44/51] Update xrpl/models/requests/vault_info.py Co-authored-by: Omar Khan --- xrpl/models/requests/vault_info.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index ead781893..24129f428 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -28,8 +28,10 @@ class VaultInfo(Request, LookupByLedgerRequest): vault_id: Optional[str] = None """The object ID of the Vault to be returned.""" + owner: Optional[str] = None """The account address of the Vault Owner.""" + seq: Optional[int] = None """The transaction sequence number that created the vault.""" From 4e728b5a877ad224b176f32653111e6a6ecd58e9 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:11:50 -0700 Subject: [PATCH 45/51] Update xrpl/models/transactions/vault_delete.py Co-authored-by: Omar Khan --- xrpl/models/transactions/vault_delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/models/transactions/vault_delete.py b/xrpl/models/transactions/vault_delete.py index 1397b71d7..9b9a36947 100644 --- a/xrpl/models/transactions/vault_delete.py +++ b/xrpl/models/transactions/vault_delete.py @@ -10,7 +10,7 @@ from xrpl.models.transactions.types import TransactionType from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init -_MAX_VAULT_ID_LENGTH = 32 * 2 +_MAX_VAULT_ID_LENGTH = 64 @require_kwargs_on_init From 6a1be36cb6a0bafd25c5215d2a5a64e871123374 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:12:14 -0700 Subject: [PATCH 46/51] Update xrpl/core/binarycodec/definitions/definitions.json Co-authored-by: Omar Khan --- xrpl/core/binarycodec/definitions/definitions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 0e63482f4..9fdd5ff6a 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -3088,7 +3088,7 @@ "RippleState": 114, "SignerList": 83, "Ticket": 84, - "Vault" : 132, + "Vault": 132, "XChainOwnedClaimID": 113, "XChainOwnedCreateAccountClaimID": 116 }, From dfc19ffe37ab401fa07be3ec696adb304cb1fe9a Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 2 Jul 2025 14:24:37 -0700 Subject: [PATCH 47/51] address PR comments from Omart --- .../{test_sav.py => test_single_asset_vault.py} | 0 xrpl/core/binarycodec/types/number.py | 4 +++- xrpl/models/amounts/__init__.py | 5 +++++ xrpl/models/amounts/mpt_amount.py | 13 ------------- xrpl/models/requests/vault_info.py | 1 - xrpl/models/transactions/clawback.py | 11 +++-------- xrpl/models/transactions/vault_clawback.py | 7 +++---- xrpl/models/transactions/vault_create.py | 3 +-- 8 files changed, 15 insertions(+), 29 deletions(-) rename tests/integration/transactions/{test_sav.py => test_single_asset_vault.py} (100%) diff --git a/tests/integration/transactions/test_sav.py b/tests/integration/transactions/test_single_asset_vault.py similarity index 100% rename from tests/integration/transactions/test_sav.py rename to tests/integration/transactions/test_single_asset_vault.py diff --git a/xrpl/core/binarycodec/types/number.py b/xrpl/core/binarycodec/types/number.py index 1672841e5..1244b0c85 100644 --- a/xrpl/core/binarycodec/types/number.py +++ b/xrpl/core/binarycodec/types/number.py @@ -133,7 +133,9 @@ def extractNumberPartsFromString(value: str) -> NumberParts: matches = re.fullmatch(VALID_NUMBER_REGEX, value) if not matches: - raise XRPLBinaryCodecException("Unable to parse number from the input string") + raise XRPLBinaryCodecException( + f"Unable to parse number from the input string: {value}" + ) # Match fields: # 0 = whole input diff --git a/xrpl/models/amounts/__init__.py b/xrpl/models/amounts/__init__.py index 4b9e2df0d..d2f48c3a5 100644 --- a/xrpl/models/amounts/__init__.py +++ b/xrpl/models/amounts/__init__.py @@ -4,6 +4,8 @@ that holds the value. For XRP, there is no counterparty. """ +from typing import Union + from xrpl.models.amounts.amount import ( Amount, get_amount_value, @@ -14,6 +16,8 @@ from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount from xrpl.models.amounts.mpt_amount import MPTAmount +ClawbackAmount = Union[IssuedCurrencyAmount | MPTAmount] + __all__ = [ "Amount", "IssuedCurrencyAmount", @@ -22,4 +26,5 @@ "is_mpt", "get_amount_value", "MPTAmount", + "ClawbackAmount", ] diff --git a/xrpl/models/amounts/mpt_amount.py b/xrpl/models/amounts/mpt_amount.py index 074d89f7f..6954b0d48 100644 --- a/xrpl/models/amounts/mpt_amount.py +++ b/xrpl/models/amounts/mpt_amount.py @@ -49,16 +49,3 @@ def to_currency(self: Self) -> MPTCurrency: The MPTCurrency for this MPTAmount. """ return MPTCurrency(mpt_issuance_id=self.mpt_issuance_id) - - -class MPTIssue: - """ - This class represents an MPT issue. It is similar to the Issue class, but - it is used with MPT amounts. - """ - - mpt_issuance_id: str = REQUIRED # type: ignore - """ - mpt_issuance_id is a 192-bit concatenation of a 32-bit account sequence and a - 160-bit account id. - """ diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index 24129f428..27739a199 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -31,7 +31,6 @@ class VaultInfo(Request, LookupByLedgerRequest): owner: Optional[str] = None """The account address of the Vault Owner.""" - seq: Optional[int] = None """The transaction sequence number that created the vault.""" diff --git a/xrpl/models/transactions/clawback.py b/xrpl/models/transactions/clawback.py index 8174c624d..3ac272ee4 100644 --- a/xrpl/models/transactions/clawback.py +++ b/xrpl/models/transactions/clawback.py @@ -3,16 +3,11 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, Optional, Union +from typing import Dict, Optional from typing_extensions import Self -from xrpl.models.amounts import ( - IssuedCurrencyAmount, - MPTAmount, - is_issued_currency, - is_xrp, -) +from xrpl.models.amounts import ClawbackAmount, is_issued_currency, is_xrp from xrpl.models.amounts.amount import is_mpt from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -25,7 +20,7 @@ class Clawback(Transaction): """The clawback transaction claws back issued funds from token holders.""" - amount: Union[IssuedCurrencyAmount, MPTAmount] = REQUIRED # type: ignore + amount: ClawbackAmount = REQUIRED # type: ignore """ The amount of currency to claw back. The issuer field is used for the token holder's address, from whom the tokens will be clawed back. diff --git a/xrpl/models/transactions/vault_clawback.py b/xrpl/models/transactions/vault_clawback.py index c5489084d..7def3df61 100644 --- a/xrpl/models/transactions/vault_clawback.py +++ b/xrpl/models/transactions/vault_clawback.py @@ -1,10 +1,9 @@ """Represents a VaultClawback transaction on the XRP Ledger.""" from dataclasses import dataclass, field -from typing import Optional, Union +from typing import Optional -from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount -from xrpl.models.amounts.mpt_amount import MPTAmount +from xrpl.models.amounts import ClawbackAmount from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType @@ -31,7 +30,7 @@ class VaultClawback(Transaction): holder: str = REQUIRED # type: ignore """The account ID from which to clawback the assets.""" - amount: Optional[Union[IssuedCurrencyAmount, MPTAmount]] = None + amount: Optional[ClawbackAmount] = None """The asset amount to clawback. When Amount is 0 clawback all funds, up to the total shares the Holder owns. """ diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 8051808d4..6a4fe2b5f 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -6,7 +6,6 @@ from typing_extensions import Self -from xrpl.models.amounts.mpt_amount import MPTIssue from xrpl.models.currencies import Currency from xrpl.models.flags import FlagInterface from xrpl.models.required import REQUIRED @@ -59,7 +58,7 @@ class WithdrawalPolicy(int, Enum): class VaultCreate(Transaction): """The VaultCreate transaction creates a new Vault object.""" - asset: Union[Currency, MPTIssue] = REQUIRED # type: ignore + asset: Currency = REQUIRED # type: ignore """The asset (XRP, IOU or MPT) of the Vault.""" data: Optional[str] = None From 34cee1835e3f472f7b2344c0b1adfa5747cbaa8c Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 2 Jul 2025 14:36:26 -0700 Subject: [PATCH 48/51] revert clawback transaction model --- xrpl/models/transactions/clawback.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/xrpl/models/transactions/clawback.py b/xrpl/models/transactions/clawback.py index 3ac272ee4..8174c624d 100644 --- a/xrpl/models/transactions/clawback.py +++ b/xrpl/models/transactions/clawback.py @@ -3,11 +3,16 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, Optional +from typing import Dict, Optional, Union from typing_extensions import Self -from xrpl.models.amounts import ClawbackAmount, is_issued_currency, is_xrp +from xrpl.models.amounts import ( + IssuedCurrencyAmount, + MPTAmount, + is_issued_currency, + is_xrp, +) from xrpl.models.amounts.amount import is_mpt from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -20,7 +25,7 @@ class Clawback(Transaction): """The clawback transaction claws back issued funds from token holders.""" - amount: ClawbackAmount = REQUIRED # type: ignore + amount: Union[IssuedCurrencyAmount, MPTAmount] = REQUIRED # type: ignore """ The amount of currency to claw back. The issuer field is used for the token holder's address, from whom the tokens will be clawed back. From 16bbc80fc3f0378e61b980cce9007f1a41b50dd6 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 2 Jul 2025 14:42:10 -0700 Subject: [PATCH 49/51] fix typo in the declaration of Clawback Amount --- xrpl/models/amounts/__init__.py | 2 +- xrpl/models/transactions/clawback.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/xrpl/models/amounts/__init__.py b/xrpl/models/amounts/__init__.py index d2f48c3a5..295c432fd 100644 --- a/xrpl/models/amounts/__init__.py +++ b/xrpl/models/amounts/__init__.py @@ -16,7 +16,7 @@ from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount from xrpl.models.amounts.mpt_amount import MPTAmount -ClawbackAmount = Union[IssuedCurrencyAmount | MPTAmount] +ClawbackAmount = Union[IssuedCurrencyAmount, MPTAmount] __all__ = [ "Amount", diff --git a/xrpl/models/transactions/clawback.py b/xrpl/models/transactions/clawback.py index 8174c624d..cf6d54211 100644 --- a/xrpl/models/transactions/clawback.py +++ b/xrpl/models/transactions/clawback.py @@ -7,12 +7,7 @@ from typing_extensions import Self -from xrpl.models.amounts import ( - IssuedCurrencyAmount, - MPTAmount, - is_issued_currency, - is_xrp, -) +from xrpl.models.amounts import ClawbackAmount, is_issued_currency, is_xrp from xrpl.models.amounts.amount import is_mpt from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Transaction @@ -25,7 +20,7 @@ class Clawback(Transaction): """The clawback transaction claws back issued funds from token holders.""" - amount: Union[IssuedCurrencyAmount, MPTAmount] = REQUIRED # type: ignore + amount: ClawbackAmount = REQUIRED # type: ignore """ The amount of currency to claw back. The issuer field is used for the token holder's address, from whom the tokens will be clawed back. From 25e204e9ceb648ebd79c8d8ae3b4de7350ffc6d9 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 2 Jul 2025 14:46:16 -0700 Subject: [PATCH 50/51] remove unneeded import --- xrpl/models/transactions/clawback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/models/transactions/clawback.py b/xrpl/models/transactions/clawback.py index cf6d54211..3ac272ee4 100644 --- a/xrpl/models/transactions/clawback.py +++ b/xrpl/models/transactions/clawback.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, Optional, Union +from typing import Dict, Optional from typing_extensions import Self From 9144d2c576917bb5e2cce0442a7005c1c2d5f63c Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 3 Jul 2025 10:59:17 -0700 Subject: [PATCH 51/51] address Omar PR suggestions --- xrpl/models/amounts/__init__.py | 5 +---- xrpl/models/amounts/clawback_amount.py | 12 ++++++++++++ xrpl/models/requests/vault_info.py | 1 + xrpl/models/transactions/vault_create.py | 15 +++++++++------ xrpl/models/transactions/vault_set.py | 11 +++++++---- 5 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 xrpl/models/amounts/clawback_amount.py diff --git a/xrpl/models/amounts/__init__.py b/xrpl/models/amounts/__init__.py index 295c432fd..6a2fc8489 100644 --- a/xrpl/models/amounts/__init__.py +++ b/xrpl/models/amounts/__init__.py @@ -4,8 +4,6 @@ that holds the value. For XRP, there is no counterparty. """ -from typing import Union - from xrpl.models.amounts.amount import ( Amount, get_amount_value, @@ -13,11 +11,10 @@ is_mpt, is_xrp, ) +from xrpl.models.amounts.clawback_amount import ClawbackAmount from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount from xrpl.models.amounts.mpt_amount import MPTAmount -ClawbackAmount = Union[IssuedCurrencyAmount, MPTAmount] - __all__ = [ "Amount", "IssuedCurrencyAmount", diff --git a/xrpl/models/amounts/clawback_amount.py b/xrpl/models/amounts/clawback_amount.py new file mode 100644 index 000000000..0353aded0 --- /dev/null +++ b/xrpl/models/amounts/clawback_amount.py @@ -0,0 +1,12 @@ +""" +A ClawbackAmount is an object specifying a currency, a quantity of that currency, and +the counterparty (issuer) on the trustline that holds the value. Clawback is possible +only for IOU Tokens and MPT Tokens. XRP Amounts cannot be clawed back. +""" + +from typing import Union + +from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount +from xrpl.models.amounts.mpt_amount import MPTAmount + +ClawbackAmount = Union[IssuedCurrencyAmount, MPTAmount] diff --git a/xrpl/models/requests/vault_info.py b/xrpl/models/requests/vault_info.py index 27739a199..6e2755b93 100644 --- a/xrpl/models/requests/vault_info.py +++ b/xrpl/models/requests/vault_info.py @@ -31,6 +31,7 @@ class VaultInfo(Request, LookupByLedgerRequest): owner: Optional[str] = None """The account address of the Vault Owner.""" + seq: Optional[int] = None """The transaction sequence number that created the vault.""" diff --git a/xrpl/models/transactions/vault_create.py b/xrpl/models/transactions/vault_create.py index 6a4fe2b5f..01dc92ec3 100644 --- a/xrpl/models/transactions/vault_create.py +++ b/xrpl/models/transactions/vault_create.py @@ -13,9 +13,9 @@ from xrpl.models.transactions.types import TransactionType from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init -_MAX_DATA_LENGTH = 256 * 2 -_MAX_MPTOKEN_METADATA_LENGTH = 1024 * 2 -_MAX_DOMAIN_ID_LENGTH = 32 * 2 +VAULT_MAX_DATA_LENGTH = 256 * 2 +VAULT_MAX_DOMAIN_ID_LENGTH = 1024 * 2 +_VAULT_MAX_MPTOKEN_METADATA_LENGTH = 32 * 2 class VaultCreateFlag(int, Enum): @@ -90,18 +90,21 @@ class VaultCreate(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if self.data is not None and len(self.data) > _MAX_DATA_LENGTH: + if self.data is not None and len(self.data) > VAULT_MAX_DATA_LENGTH: errors["data"] = ( "Data must be less than 256 bytes (alternatively, 512 hex characters)." ) if self.mptoken_metadata is not None and len(self.mptoken_metadata) > ( - _MAX_MPTOKEN_METADATA_LENGTH + _VAULT_MAX_MPTOKEN_METADATA_LENGTH ): errors["mptoken_metadata"] = ( "Metadata must be less than 1024 bytes " "(alternatively, 2048 hex characters)." ) - if self.domain_id is not None and len(self.domain_id) != _MAX_DOMAIN_ID_LENGTH: + if ( + self.domain_id is not None + and len(self.domain_id) != VAULT_MAX_DOMAIN_ID_LENGTH + ): errors["domain_id"] = ( "Invalid domain ID: Length must be 32 characters (64 hex characters)." ) diff --git a/xrpl/models/transactions/vault_set.py b/xrpl/models/transactions/vault_set.py index 6b648a4e4..a6ac715b2 100644 --- a/xrpl/models/transactions/vault_set.py +++ b/xrpl/models/transactions/vault_set.py @@ -9,8 +9,8 @@ from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types import TransactionType from xrpl.models.transactions.vault_create import ( - _MAX_DATA_LENGTH, - _MAX_DOMAIN_ID_LENGTH, + VAULT_MAX_DATA_LENGTH, + VAULT_MAX_DOMAIN_ID_LENGTH, ) from xrpl.models.transactions.vault_delete import _MAX_VAULT_ID_LENGTH from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init @@ -43,11 +43,14 @@ class VaultSet(Transaction): def _get_errors(self: Self) -> Dict[str, str]: errors = super()._get_errors() - if self.data is not None and len(self.data) > _MAX_DATA_LENGTH: + if self.data is not None and len(self.data) > VAULT_MAX_DATA_LENGTH: errors["data"] = ( "Data must be less than 256 bytes (alternatively, 512 hex characters)." ) - if self.domain_id is not None and len(self.domain_id) != _MAX_DOMAIN_ID_LENGTH: + if ( + self.domain_id is not None + and len(self.domain_id) != VAULT_MAX_DOMAIN_ID_LENGTH + ): errors["domain_id"] = ( "Invalid domain ID: Length must be 32 characters (64 hex characters)." )