Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [[Unreleased]]

### Fixed
- add `MPTAmount` support in `Issue` (rippled internal type)
- add `MPTCurrency` support in `Issue` (rippled internal type)
- Fixed the implementation error in get_latest_open_ledger_sequence method. The change uses the "current" ledger for extracting sequence number.

### Added
Expand Down
2 changes: 0 additions & 2 deletions tests/unit/core/binarycodec/types/test_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ def test_from_value_mpt(self):
# Test Issue creation for an MPT amount.
# Use a valid 48-character hex string (24 bytes) for mpt_issuance_id.
test_input = {
"value": "100", # MPT amounts must be an integer string (no decimal point)
"mpt_issuance_id": "BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D",
}
issue_obj = Issue.from_value(test_input)
Expand Down Expand Up @@ -77,7 +76,6 @@ def test_from_parser_non_standard_currency(self):
def test_from_parser_mpt(self):
# Test round-trip: serialize an MPT Issue and then parse it back.
test_input = {
"value": "100",
"mpt_issuance_id": "BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D",
}
issue_obj = Issue.from_value(test_input)
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/models/currencies/test_mpt_currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from unittest import TestCase

from xrpl.models.currencies import MPTCurrency
from xrpl.models.exceptions import XRPLModelException

_MPTID = "00002403C84A0A28E0190E208E982C352BBD5006600555CF"


class TestMPTCurrency(TestCase):
def test_correct_mptid_format(self):
obj = MPTCurrency(
mpt_issuance_id=_MPTID,
)
self.assertTrue(obj.is_valid())

def test_lower_mptid_format(self):
obj = MPTCurrency(
mpt_issuance_id=_MPTID.lower(),
)
self.assertTrue(obj.is_valid())

def test_invalid_length(self):
with self.assertRaises(XRPLModelException):
MPTCurrency(mpt_issuance_id=_MPTID[:40])

with self.assertRaises(XRPLModelException):
MPTCurrency(mpt_issuance_id=_MPTID + "AA")

def test_incorrect_hex_format(self):
# the "+" is not allowed in a currency format"
with self.assertRaises(XRPLModelException):
MPTCurrency(
mpt_issuance_id="ABCD" * 11 + "XXXX",
)

def test_to_amount(self):
amount = "12"
MPT_currency = MPTCurrency(mpt_issuance_id=_MPTID)
MPT_currency_amount = MPT_currency.to_amount(amount)

self.assertEqual(MPT_currency_amount.mpt_issuance_id, _MPTID)
self.assertEqual(MPT_currency_amount.value, amount)
2 changes: 2 additions & 0 deletions xrpl/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class XRPLException(Exception):
:meta private:
"""

HEX_MPTID_REGEX: Final[Pattern[str]] = re.compile(r"^[0-9A-Fa-f]{48}$")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was under the belief that mpt_issuance_id is represented by 192-bits, as explained here: https://github.com/XRPLF/rippled/blob/6cf37c4abef80fb535d1d7577a548665e139f635/include/xrpl/protocol/MPTIssue.h#L28

However, the size of mpt_issuance_id is much larger than 192 bits in the examples. It's four times larger in the RPC outputs.

  1. Where can I find the cpp implementation of this MPTCurrency type?
  2. Which transactions use the MPTCurrency type? I'd like to test the correctness of serialization/de-serialization of the MPTCurrency type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

192 bits is 24 bytes, which is a hex string of length 48.

  1. MPTCurrency is equivalent to MPTIssue in rippled.
  2. Not sure there are any. The only two features that use STIssue types are XChainBridge and AMM and I don't think either of them support MPTs right now (though support for AMM is being added in XLS-82d in Add MPT support to DEX rippled#5285).

Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. Can we retain the same name MPTIssue in the client libraries. Is there a reason you prefer MPTCurrency ?
  2. I wish there were a way to test the correctness of this serialization. Unless we communicate a serialized MPTIssue value to rippled, we can not validate the correctness of the client library binary codec.

As a new feature, if rippled could expose an echo(serialized_input) => deserialized_output -like of utility command, that would have been helpful. I'm sure there are other SFields which haven't been used in transactions yet.

192 bits is 24 bytes, which is a hex string of length 48.

Hmm yes, you are right. I had mis-understood the calculation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

1.MPTCurrency aligns more with what we have in the client library and makes more sense in English. I don't have a strong opinion on this, though.
2. rippled can serialize transactions via the sign and submit fields locally. You could try with the XLS-82d branch above.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Alright, I would slightly prefer the name MPTIssue over MPTCurrency, it aligns well with the documentation too.

The MPT amendment has been merged into the rippled codebase, I should be able to use the develop branch to test the sign, submit commands right?

rippled can serialize transactions via the sign and submit fields locally

Hmm, this will help me with serialization, but not the other way around i.e. I'm trying to understand how I can deserialize a blob of MPTIssue and verify that the xrpl-py library does it correctly?


# Constants for validating amounts.
MIN_IOU_EXPONENT: Final[int] = -96
"""
Expand Down
8 changes: 4 additions & 4 deletions xrpl/core/binarycodec/types/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from xrpl.core.binarycodec.types.currency import Currency
from xrpl.core.binarycodec.types.hash192 import HASH192_BYTES, Hash192
from xrpl.core.binarycodec.types.serialized_type import SerializedType
from xrpl.models.amounts.mpt_amount import MPTAmount
from xrpl.models.currencies import XRP as XRPModel
from xrpl.models.currencies import IssuedCurrency as IssuedCurrencyModel
from xrpl.models.currencies import MPTCurrency as MPTCurrencyModel


class Issue(SerializedType):
Expand Down Expand Up @@ -54,13 +54,13 @@ def from_value(cls: Type[Self], value: Dict[str, str]) -> Self:
issuer_bytes = bytes(AccountID.from_value(value["issuer"]))
return cls(currency_bytes + issuer_bytes)

if MPTAmount.is_dict_of_model(value):
if MPTCurrencyModel.is_dict_of_model(value):
mpt_issuance_id_bytes = bytes(Hash192.from_value(value["mpt_issuance_id"]))
return cls(bytes(mpt_issuance_id_bytes))

raise XRPLBinaryCodecException(
"Invalid type to construct an Issue: expected XRP, IssuedCurrency or "
f"MPTAmount as a str or dict, received {value.__class__.__name__}."
f"MPTCurrency as a str or dict, received {value.__class__.__name__}."
)

@classmethod
Expand All @@ -80,7 +80,7 @@ def from_parser(
Returns:
The Issue object constructed from a parser.
"""
# Check if it's an MPTAmount by checking mpt_issuance_id byte size
# Check if it's an MPTIssue by checking mpt_issuance_id byte size
if length_hint == HASH192_BYTES:
mpt_bytes = parser.read(HASH192_BYTES)
return cls(mpt_bytes)
Expand Down
18 changes: 12 additions & 6 deletions xrpl/models/amounts/amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
counterparty.
"""

from typing import Union, cast
from typing import Union

from typing_extensions import TypeGuard

from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount
from xrpl.models.amounts.mpt_amount import MPTAmount

Amount = Union[IssuedCurrencyAmount, MPTAmount, str]


def is_xrp(amount: Amount) -> bool:
def is_xrp(amount: Amount) -> TypeGuard[str]:
"""
Returns whether amount is an XRP value, as opposed to an issued currency
or MPT value.
Expand All @@ -26,7 +28,7 @@ def is_xrp(amount: Amount) -> bool:
return isinstance(amount, str)


def is_issued_currency(amount: Amount) -> bool:
def is_issued_currency(amount: Amount) -> TypeGuard[IssuedCurrencyAmount]:
"""
Returns whether amount is an issued currency value, as opposed to an XRP
or MPT value.
Expand All @@ -40,7 +42,7 @@ def is_issued_currency(amount: Amount) -> bool:
return isinstance(amount, IssuedCurrencyAmount)


def is_mpt(amount: Amount) -> bool:
def is_mpt(amount: Amount) -> TypeGuard[MPTAmount]:
"""
Returns whether amount is an MPT value, as opposed to an XRP or
an issued currency value.
Expand All @@ -65,5 +67,9 @@ def get_amount_value(amount: Amount) -> float:
The value of the amount irrespective of its currency.
"""
if is_xrp(amount):
return float(cast(str, amount))
return float(cast(IssuedCurrencyAmount, amount).value)
return float(amount)
if is_issued_currency(amount):
return float(amount.value)
if is_mpt(amount):
return float(amount.value)
raise ValueError(f"Invalid amount: {repr(amount)}")
10 changes: 10 additions & 0 deletions xrpl/models/amounts/mpt_amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing_extensions import Self

from xrpl.models.base_model import BaseModel
from xrpl.models.currencies.mpt_currency import MPTCurrency
from xrpl.models.required import REQUIRED
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init

Expand Down Expand Up @@ -39,3 +40,12 @@ def to_dict(self: Self) -> Dict[str, str]:
The dictionary representation of an MPTAmount.
"""
return {**super().to_dict(), "value": str(self.value)}

def to_currency(self: Self) -> MPTCurrency:
"""
Build an MPTCurrency from this MPTAmount.

Returns:
The MPTCurrency for this MPTAmount.
"""
return MPTCurrency(mpt_issuance_id=self.mpt_issuance_id)
7 changes: 4 additions & 3 deletions xrpl/models/currencies/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""
The XRP Ledger has two kinds of money: XRP, and issued
currencies. Both types have high precision, although their
formats are different.
The XRP Ledger has three kinds of money: XRP, issued currencies, and MPTs. All types
have high precision, although their formats are different.
"""

from xrpl.models.currencies.currency import Currency
from xrpl.models.currencies.issued_currency import IssuedCurrency
from xrpl.models.currencies.mpt_currency import MPTCurrency
from xrpl.models.currencies.xrp import XRP

__all__ = [
"Currency",
"IssuedCurrency",
"MPTCurrency",
"XRP",
]
3 changes: 2 additions & 1 deletion xrpl/models/currencies/currency.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Union

from xrpl.models.currencies.issued_currency import IssuedCurrency
from xrpl.models.currencies.mpt_currency import MPTCurrency
from xrpl.models.currencies.xrp import XRP

Currency = Union[IssuedCurrency, XRP]
Currency = Union[IssuedCurrency, MPTCurrency, XRP]
63 changes: 63 additions & 0 deletions xrpl/models/currencies/mpt_currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Specifies an amount in an issued currency, but without a value field.
This format is used for some book order requests.

See https://xrpl.org/currency-formats.html#specifying-currency-amounts
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, Union

from typing_extensions import Self

import xrpl.models.amounts # not a direct import, to get around circular imports
from xrpl.constants import HEX_MPTID_REGEX
from xrpl.models.base_model import BaseModel
from xrpl.models.required import REQUIRED
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init


def _is_valid_mptid(candidate: str) -> bool:
return bool(HEX_MPTID_REGEX.fullmatch(candidate))


@require_kwargs_on_init
@dataclass(frozen=True, **KW_ONLY_DATACLASS)
class MPTCurrency(BaseModel):
"""
Specifies an amount in an MPT, but without a value field.
This format is used for some book order requests.

See https://xrpl.org/currency-formats.html#specifying-currency-amounts
"""

mpt_issuance_id: str = REQUIRED # type: ignore
"""
This field is required.

:meta hide-value:
"""

def _get_errors(self: Self) -> Dict[str, str]:
errors = super()._get_errors()
if not _is_valid_mptid(self.mpt_issuance_id):
errors["mpt_issuance_id"] = (
f"Invalid mpt_issuance_id {self.mpt_issuance_id}"
)
return errors

def to_amount(self: Self, value: Union[str, int]) -> xrpl.models.amounts.MPTAmount:
"""
Converts an MPTCurrency to an MPTAmount.

Args:
value: The amount of MPTs in the MPTAmount.

Returns:
An MPTAmount that represents the MPT and the provided value.
"""
return xrpl.models.amounts.MPTAmount(
mpt_issuance_id=self.mpt_issuance_id, value=str(value)
)
10 changes: 4 additions & 6 deletions xrpl/models/transactions/clawback.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,15 @@ def _get_errors(self: Self) -> Dict[str, str]:
# Amount transaction errors
if is_xrp(self.amount):
errors["amount"] = "``amount`` cannot be XRP."

if is_issued_currency(self.amount):
elif is_issued_currency(self.amount):
if self.holder is not None:
errors["amount"] = "Cannot have Holder for currency."
if self.account == self.amount.issuer: # type:ignore
if self.account == self.amount.issuer:
errors["amount"] = "Holder's address is wrong."

if is_mpt(self.amount):
elif is_mpt(self.amount):
if self.holder is None:
errors["amount"] = "Missing Holder."
if self.account == self.holder:
elif self.account == self.holder:
errors["amount"] = "Invalid Holder account."

return errors
13 changes: 6 additions & 7 deletions xrpl/models/transactions/xchain_claim.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@

from typing_extensions import Self

from xrpl.models.amounts import Amount
from xrpl.models.amounts.amount import is_issued_currency, is_mpt, is_xrp
from xrpl.models.currencies import XRP
from xrpl.models.amounts import Amount, is_issued_currency, is_mpt, is_xrp
from xrpl.models.currencies import XRP, Currency
from xrpl.models.required import REQUIRED
from xrpl.models.transactions.transaction import Transaction
from xrpl.models.transactions.types import TransactionType
Expand Down Expand Up @@ -81,13 +80,13 @@ def _get_errors(self: Self) -> Dict[str, str]:

amount = self.amount
if is_xrp(amount):
currency = XRP()
currency: Currency = XRP()
elif is_issued_currency(amount):
currency = amount.to_currency() # type: ignore
currency = amount.to_currency()
elif is_mpt(amount):
currency = amount.mpt_issuance_id # type: ignore
currency = amount.to_currency()
else:
errors["amount"] = "currency can't be derived."
errors["amount"] = "Currency can't be derived."

if (
currency != bridge.locking_chain_issue
Expand Down