Skip to content

Commit c731c16

Browse files
mvadariLimpidCrypto
authored andcommitted
fix: add MPTCurrency type instead of MPTAmount for Issues (XRPLF#822)
1 parent b12ae0e commit c731c16

File tree

12 files changed

+150
-30
lines changed

12 files changed

+150
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [[Unreleased]]
99

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

1414
### Added

tests/unit/core/binarycodec/types/test_issue.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def test_from_value_mpt(self):
3232
# Test Issue creation for an MPT amount.
3333
# Use a valid 48-character hex string (24 bytes) for mpt_issuance_id.
3434
test_input = {
35-
"value": "100", # MPT amounts must be an integer string (no decimal point)
3635
"mpt_issuance_id": "BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D",
3736
}
3837
issue_obj = Issue.from_value(test_input)
@@ -77,7 +76,6 @@ def test_from_parser_non_standard_currency(self):
7776
def test_from_parser_mpt(self):
7877
# Test round-trip: serialize an MPT Issue and then parse it back.
7978
test_input = {
80-
"value": "100",
8179
"mpt_issuance_id": "BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D",
8280
}
8381
issue_obj = Issue.from_value(test_input)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models.currencies import MPTCurrency
4+
from xrpl.models.exceptions import XRPLModelException
5+
6+
_MPTID = "00002403C84A0A28E0190E208E982C352BBD5006600555CF"
7+
8+
9+
class TestMPTCurrency(TestCase):
10+
def test_correct_mptid_format(self):
11+
obj = MPTCurrency(
12+
mpt_issuance_id=_MPTID,
13+
)
14+
self.assertTrue(obj.is_valid())
15+
16+
def test_lower_mptid_format(self):
17+
obj = MPTCurrency(
18+
mpt_issuance_id=_MPTID.lower(),
19+
)
20+
self.assertTrue(obj.is_valid())
21+
22+
def test_invalid_length(self):
23+
with self.assertRaises(XRPLModelException):
24+
MPTCurrency(mpt_issuance_id=_MPTID[:40])
25+
26+
with self.assertRaises(XRPLModelException):
27+
MPTCurrency(mpt_issuance_id=_MPTID + "AA")
28+
29+
def test_incorrect_hex_format(self):
30+
# the "+" is not allowed in a currency format"
31+
with self.assertRaises(XRPLModelException):
32+
MPTCurrency(
33+
mpt_issuance_id="ABCD" * 11 + "XXXX",
34+
)
35+
36+
def test_to_amount(self):
37+
amount = "12"
38+
MPT_currency = MPTCurrency(mpt_issuance_id=_MPTID)
39+
MPT_currency_amount = MPT_currency.to_amount(amount)
40+
41+
self.assertEqual(MPT_currency_amount.mpt_issuance_id, _MPTID)
42+
self.assertEqual(MPT_currency_amount.value, amount)

xrpl/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class XRPLException(Exception):
4242
:meta private:
4343
"""
4444

45+
HEX_MPTID_REGEX: Final[Pattern[str]] = re.compile(r"^[0-9A-Fa-f]{48}$")
46+
4547
# Constants for validating amounts.
4648
MIN_IOU_EXPONENT: Final[int] = -96
4749
"""

xrpl/core/binarycodec/types/issue.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
from xrpl.core.binarycodec.types.currency import Currency
1313
from xrpl.core.binarycodec.types.hash192 import HASH192_BYTES, Hash192
1414
from xrpl.core.binarycodec.types.serialized_type import SerializedType
15-
from xrpl.models.amounts.mpt_amount import MPTAmount
1615
from xrpl.models.currencies import XRP as XRPModel
1716
from xrpl.models.currencies import IssuedCurrency as IssuedCurrencyModel
17+
from xrpl.models.currencies import MPTCurrency as MPTCurrencyModel
1818

1919

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

57-
if MPTAmount.is_dict_of_model(value):
57+
if MPTCurrencyModel.is_dict_of_model(value):
5858
mpt_issuance_id_bytes = bytes(Hash192.from_value(value["mpt_issuance_id"]))
5959
return cls(bytes(mpt_issuance_id_bytes))
6060

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

6666
@classmethod
@@ -80,7 +80,7 @@ def from_parser(
8080
Returns:
8181
The Issue object constructed from a parser.
8282
"""
83-
# Check if it's an MPTAmount by checking mpt_issuance_id byte size
83+
# Check if it's an MPTIssue by checking mpt_issuance_id byte size
8484
if length_hint == HASH192_BYTES:
8585
mpt_bytes = parser.read(HASH192_BYTES)
8686
return cls(mpt_bytes)

xrpl/models/amounts/amount.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
counterparty.
55
"""
66

7-
from typing import Union, cast
7+
from typing import Union
8+
9+
from typing_extensions import TypeGuard
810

911
from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount
1012
from xrpl.models.amounts.mpt_amount import MPTAmount
1113

1214
Amount = Union[IssuedCurrencyAmount, MPTAmount, str]
1315

1416

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

2830

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

4244

43-
def is_mpt(amount: Amount) -> bool:
45+
def is_mpt(amount: Amount) -> TypeGuard[MPTAmount]:
4446
"""
4547
Returns whether amount is an MPT value, as opposed to an XRP or
4648
an issued currency value.
@@ -65,5 +67,9 @@ def get_amount_value(amount: Amount) -> float:
6567
The value of the amount irrespective of its currency.
6668
"""
6769
if is_xrp(amount):
68-
return float(cast(str, amount))
69-
return float(cast(IssuedCurrencyAmount, amount).value)
70+
return float(amount)
71+
if is_issued_currency(amount):
72+
return float(amount.value)
73+
if is_mpt(amount):
74+
return float(amount.value)
75+
raise ValueError(f"Invalid amount: {repr(amount)}")

xrpl/models/amounts/mpt_amount.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing_extensions import Self
99

1010
from xrpl.models.base_model import BaseModel
11+
from xrpl.models.currencies.mpt_currency import MPTCurrency
1112
from xrpl.models.required import REQUIRED
1213
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init
1314

@@ -39,3 +40,12 @@ def to_dict(self: Self) -> Dict[str, str]:
3940
The dictionary representation of an MPTAmount.
4041
"""
4142
return {**super().to_dict(), "value": str(self.value)}
43+
44+
def to_currency(self: Self) -> MPTCurrency:
45+
"""
46+
Build an MPTCurrency from this MPTAmount.
47+
48+
Returns:
49+
The MPTCurrency for this MPTAmount.
50+
"""
51+
return MPTCurrency(mpt_issuance_id=self.mpt_issuance_id)

xrpl/models/currencies/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
"""
2-
The XRP Ledger has two kinds of money: XRP, and issued
3-
currencies. Both types have high precision, although their
4-
formats are different.
2+
The XRP Ledger has three kinds of money: XRP, issued currencies, and MPTs. All types
3+
have high precision, although their formats are different.
54
"""
65

76
from xrpl.models.currencies.currency import Currency
87
from xrpl.models.currencies.issued_currency import IssuedCurrency
8+
from xrpl.models.currencies.mpt_currency import MPTCurrency
99
from xrpl.models.currencies.xrp import XRP
1010

1111
__all__ = [
1212
"Currency",
1313
"IssuedCurrency",
14+
"MPTCurrency",
1415
"XRP",
1516
]

xrpl/models/currencies/currency.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Union
88

99
from xrpl.models.currencies.issued_currency import IssuedCurrency
10+
from xrpl.models.currencies.mpt_currency import MPTCurrency
1011
from xrpl.models.currencies.xrp import XRP
1112

12-
Currency = Union[IssuedCurrency, XRP]
13+
Currency = Union[IssuedCurrency, MPTCurrency, XRP]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Specifies an amount in an issued currency, but without a value field.
3+
This format is used for some book order requests.
4+
5+
See https://xrpl.org/currency-formats.html#specifying-currency-amounts
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass
11+
from typing import Dict, Union
12+
13+
from typing_extensions import Self
14+
15+
import xrpl.models.amounts # not a direct import, to get around circular imports
16+
from xrpl.constants import HEX_MPTID_REGEX
17+
from xrpl.models.base_model import BaseModel
18+
from xrpl.models.required import REQUIRED
19+
from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init
20+
21+
22+
def _is_valid_mptid(candidate: str) -> bool:
23+
return bool(HEX_MPTID_REGEX.fullmatch(candidate))
24+
25+
26+
@require_kwargs_on_init
27+
@dataclass(frozen=True, **KW_ONLY_DATACLASS)
28+
class MPTCurrency(BaseModel):
29+
"""
30+
Specifies an amount in an MPT, but without a value field.
31+
This format is used for some book order requests.
32+
33+
See https://xrpl.org/currency-formats.html#specifying-currency-amounts
34+
"""
35+
36+
mpt_issuance_id: str = REQUIRED # type: ignore
37+
"""
38+
This field is required.
39+
40+
:meta hide-value:
41+
"""
42+
43+
def _get_errors(self: Self) -> Dict[str, str]:
44+
errors = super()._get_errors()
45+
if not _is_valid_mptid(self.mpt_issuance_id):
46+
errors["mpt_issuance_id"] = (
47+
f"Invalid mpt_issuance_id {self.mpt_issuance_id}"
48+
)
49+
return errors
50+
51+
def to_amount(self: Self, value: Union[str, int]) -> xrpl.models.amounts.MPTAmount:
52+
"""
53+
Converts an MPTCurrency to an MPTAmount.
54+
55+
Args:
56+
value: The amount of MPTs in the MPTAmount.
57+
58+
Returns:
59+
An MPTAmount that represents the MPT and the provided value.
60+
"""
61+
return xrpl.models.amounts.MPTAmount(
62+
mpt_issuance_id=self.mpt_issuance_id, value=str(value)
63+
)

0 commit comments

Comments
 (0)