Skip to content

Commit f922e1f

Browse files
committed
Merge branch 'main' into request-from-xrpl
2 parents d0dc6cc + 8f74e2d commit f922e1f

File tree

14 files changed

+359
-3
lines changed

14 files changed

+359
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
### Fixed
1717

1818
- Updates `Number` codec with mantissa range normalization ([10^18, 10^19 - 1]) and appropriate overflow/underflow checks
19+
- Introduce the binary-codec for the new rippled type titled "Int32". This type is used to represent the LoanScale field relating to the Lending Protocol.
1920
- Fix `Request.from_xrpl` by aliasing it to `Request.from_dict`
2021

2122
## [[4.4.0]] - 2025-12-16
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from unittest import TestCase
2+
3+
from xrpl.core.binarycodec import XRPLBinaryCodecException
4+
from xrpl.core.binarycodec.types.int32 import Int32
5+
6+
7+
class TestInt32(TestCase):
8+
def test_compare_INT32(self):
9+
value1 = Int32.from_value(124)
10+
value2 = Int32.from_value(123)
11+
value3 = Int32.from_value(124)
12+
13+
self.assertGreater(value1, value2)
14+
self.assertLess(value2, value1)
15+
self.assertNotEqual(value1, value2)
16+
self.assertEqual(value1, value3)
17+
18+
def test_from_value(self):
19+
self.assertEqual(Int32.from_value(0).to_json(), 0)
20+
self.assertEqual(Int32.from_value(110).to_json(), 110)
21+
self.assertEqual(Int32.from_value(-123).to_json(), -123)
22+
23+
def test_limits(self):
24+
self.assertEqual(Int32.from_value(-2147483648).to_json(), -2147483648)
25+
self.assertEqual(Int32.from_value(2147483647).to_json(), 2147483647)
26+
27+
def test_compare(self):
28+
value1 = Int32.from_value(124)
29+
30+
self.assertEqual(value1, 124)
31+
self.assertLess(value1, 1000)
32+
self.assertGreater(value1, -1000)
33+
34+
def test_raises_invalid_value_type(self):
35+
invalid_value = [1, 2, 3]
36+
self.assertRaises(XRPLBinaryCodecException, Int32.from_value, invalid_value)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models.transactions import LoanBrokerCoverWithdraw
4+
5+
_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"
6+
_LOAN_BROKER_ID = "DB303FC1C7611B22C09E773B51044F6BEA02EF917DF59A2E2860871E167066A5"
7+
_DESTINATION = "rf7HPydP4ihkFkSRHWFq34b4SXRc7GvPCR"
8+
_DESTINATION_TAG = 2345
9+
10+
11+
class TestLoanBrokerCoverWithdraw(TestCase):
12+
def test_valid(self):
13+
tx = LoanBrokerCoverWithdraw(
14+
account=_ACCOUNT,
15+
loan_broker_id=_LOAN_BROKER_ID,
16+
amount="1000",
17+
destination=_DESTINATION,
18+
destination_tag=_DESTINATION_TAG,
19+
)
20+
self.assertTrue(tx.is_valid())
21+
22+
def test_valid_minimal_fields(self):
23+
tx = LoanBrokerCoverWithdraw(
24+
account=_ACCOUNT,
25+
loan_broker_id=_LOAN_BROKER_ID,
26+
amount="1000",
27+
)
28+
self.assertTrue(tx.is_valid())
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models.exceptions import XRPLModelException
4+
from xrpl.models.transactions import LoanPay, LoanPayFlag
5+
6+
_SOURCE = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
7+
_LOAN_ID = "78381E3BA82D0464D27B6E7D968657EB8EFF95625A4FE623840127F61F468D4C"
8+
9+
10+
class TestLoanPay(TestCase):
11+
def test_invalid_multiple_flags_enabled(self):
12+
with self.assertRaises(XRPLModelException) as error:
13+
LoanPay(
14+
account=_SOURCE,
15+
loan_id=_LOAN_ID,
16+
amount="1000",
17+
flags=LoanPayFlag.TF_LOAN_OVERPAYMENT
18+
| LoanPayFlag.TF_LOAN_FULL_PAYMENT,
19+
)
20+
self.assertEqual(
21+
error.exception.args[0],
22+
"{'LoanPay:Flags': 'Only one flag must be enabled in the LoanPay "
23+
+ "transaction'}",
24+
)
25+
26+
def test_invalid_flag_input(self):
27+
with self.assertRaises(XRPLModelException) as error:
28+
LoanPay(
29+
account=_SOURCE,
30+
loan_id=_LOAN_ID,
31+
amount="1000",
32+
flags=0x00080000,
33+
)
34+
self.assertEqual(
35+
error.exception.args[0],
36+
"{'LoanPay:Flags': 'Unrecognised flag in the LoanPay " + "transaction'}",
37+
)
38+
39+
def test_valid_loan_pay(self):
40+
tx = LoanPay(
41+
account=_SOURCE,
42+
loan_id=_LOAN_ID,
43+
amount="1000",
44+
)
45+
self.assertTrue(tx.is_valid())

tests/unit/models/transactions/test_vault_create.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def test_valid(self):
1818
assets_maximum="1000",
1919
withdrawal_policy=1,
2020
data=str_to_hex("A" * 256),
21+
scale=4,
2122
)
2223
self.assertTrue(tx.is_valid())
2324

@@ -121,3 +122,33 @@ def test_tx_emits_warning_for_missing_icon_metadata(self):
121122
"- uris/us: should be an array of objects each "
122123
"with uri/u, category/c, and title/t properties.",
123124
)
125+
126+
def test_scale_field_too_large(self):
127+
with self.assertRaises(XRPLModelException) as error:
128+
VaultCreate(
129+
account=_ACCOUNT,
130+
asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT),
131+
assets_maximum="1000",
132+
withdrawal_policy=1,
133+
data=str_to_hex("A" * 256),
134+
scale=18 + 1,
135+
)
136+
self.assertEqual(
137+
error.exception.args[0],
138+
"{'VaultCreate': 'Scale field is higher than the allowed limit (18)'}",
139+
)
140+
141+
def test_scale_field_too_small(self):
142+
with self.assertRaises(XRPLModelException) as error:
143+
VaultCreate(
144+
account=_ACCOUNT,
145+
asset=IssuedCurrency(currency="USD", issuer=_ACCOUNT),
146+
assets_maximum="1000",
147+
withdrawal_policy=1,
148+
data=str_to_hex("A" * 256),
149+
scale=-1,
150+
)
151+
self.assertEqual(
152+
error.exception.args[0],
153+
"{'VaultCreate': 'Scale field is lower than the allowed limit (0)'}",
154+
)

tests/unit/models/transactions/test_vault_withdraw.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"
88
_VAULT_ID = "B982D2AAEF6014E6BE3194D939865453D56D16FF7081BB1D0ED865C708ABCEEE"
9+
_DESTINATION = "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
910

1011

1112
class TestVaultWithdraw(TestCase):
@@ -17,6 +18,16 @@ def test_valid(self):
1718
)
1819
self.assertTrue(tx.is_valid())
1920

21+
def test_valid_with_destination_tag(self):
22+
tx = VaultWithdraw(
23+
account=_ACCOUNT,
24+
vault_id=_VAULT_ID,
25+
amount=IssuedCurrencyAmount(currency="USD", issuer=_ACCOUNT, value="100"),
26+
destination=_DESTINATION,
27+
destination_tag=3000,
28+
)
29+
self.assertTrue(tx.is_valid())
30+
2031
def test_invalid_vault_id_field(self):
2132
with self.assertRaises(XRPLModelException) as e:
2233
VaultWithdraw(

xrpl/core/binarycodec/definitions/definitions.json

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2351,7 +2351,7 @@
23512351
}
23522352
],
23532353
[
2354-
"DummyInt32",
2354+
"DebtTotal",
23552355
{
23562356
"isSerialized": true,
23572357
"isSigningField": true,
@@ -2439,6 +2439,36 @@
24392439
"type": "Number"
24402440
}
24412441
],
2442+
[
2443+
"TotalValueOutstanding",
2444+
{
2445+
"isSerialized": true,
2446+
"isSigningField": true,
2447+
"isVLEncoded": false,
2448+
"nth": 15,
2449+
"type": "Number"
2450+
}
2451+
],
2452+
[
2453+
"PeriodicPayment",
2454+
{
2455+
"isSerialized": true,
2456+
"isSigningField": true,
2457+
"isVLEncoded": false,
2458+
"nth": 16,
2459+
"type": "Number"
2460+
}
2461+
],
2462+
[
2463+
"ManagementFeeOutstanding",
2464+
{
2465+
"isSerialized": true,
2466+
"isSigningField": true,
2467+
"isVLEncoded": false,
2468+
"nth": 17,
2469+
"type": "Number"
2470+
}
2471+
],
24422472
[
24432473
"TransactionMetaData",
24442474
{
@@ -3059,6 +3089,16 @@
30593089
"type": "UInt8"
30603090
}
30613091
],
3092+
[
3093+
"LoanScale",
3094+
{
3095+
"isSerialized": true,
3096+
"isSigningField": true,
3097+
"isVLEncoded": false,
3098+
"nth": 1,
3099+
"type": "Int32"
3100+
}
3101+
],
30623102
[
30633103
"Method",
30643104
{

xrpl/core/binarycodec/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from xrpl.core.binarycodec.types.hash160 import Hash160
1010
from xrpl.core.binarycodec.types.hash192 import Hash192
1111
from xrpl.core.binarycodec.types.hash256 import Hash256
12+
from xrpl.core.binarycodec.types.int32 import Int32
1213
from xrpl.core.binarycodec.types.issue import Issue
1314
from xrpl.core.binarycodec.types.number import Number
1415
from xrpl.core.binarycodec.types.path_set import PathSet
@@ -32,6 +33,7 @@
3233
"Hash160",
3334
"Hash192",
3435
"Hash256",
36+
"Int32",
3537
"Issue",
3638
"Number",
3739
"PathSet",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Class for serializing and deserializing a signed 32-bit integer."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Optional, Type
6+
7+
from typing_extensions import Final, Self
8+
9+
from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser
10+
from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException
11+
from xrpl.core.binarycodec.types.serialized_type import SerializedType
12+
13+
_WIDTH: Final[int] = 4 # 32 / 8
14+
15+
16+
class Int32(SerializedType):
17+
"""Class for serializing and deserializing a signed 32-bit integer."""
18+
19+
def __init__(self: Self, buffer: bytes = bytes(_WIDTH)) -> None:
20+
"""Construct a new Int32 type from a ``bytes`` value."""
21+
super().__init__(buffer)
22+
23+
@property
24+
def value(self: Self) -> int:
25+
"""Get the value of the Int32 represented by `self.buffer`."""
26+
return int.from_bytes(self.buffer, byteorder="big", signed=True)
27+
28+
@classmethod
29+
def from_parser(
30+
cls: Type[Self], parser: BinaryParser, _length_hint: Optional[int] = None
31+
) -> Self:
32+
"""Construct a new Int32 type from a BinaryParser."""
33+
return cls(parser.read(_WIDTH))
34+
35+
@classmethod
36+
def from_value(cls: Type[Self], value: int) -> Self:
37+
"""Construct a new Int32 type from an integer."""
38+
if not isinstance(value, int):
39+
raise XRPLBinaryCodecException(
40+
f"Invalid type to construct Int32: expected int, "
41+
f"received {value.__class__.__name__}."
42+
)
43+
return cls(value.to_bytes(_WIDTH, byteorder="big", signed=True))
44+
45+
def to_json(self: Self) -> int:
46+
"""Convert the Int32 to JSON (returns the integer value)."""
47+
return self.value
48+
49+
def __eq__(self: Self, other: object) -> bool:
50+
"""Determine whether two Int32 objects are equal."""
51+
if isinstance(other, int):
52+
return self.value == other
53+
if isinstance(other, Int32):
54+
return self.value == other.value
55+
return NotImplemented
56+
57+
def __lt__(self: Self, other: object) -> bool:
58+
"""Determine whether this Int32 is less than another."""
59+
if isinstance(other, int):
60+
return self.value < other
61+
if isinstance(other, Int32):
62+
return self.value < other.value
63+
return NotImplemented
64+
65+
def __gt__(self: Self, other: object) -> bool:
66+
"""Determine whether this Int32 is greater than another."""
67+
if isinstance(other, int):
68+
return self.value > other
69+
if isinstance(other, Int32):
70+
return self.value > other.value
71+
return NotImplemented

xrpl/models/transactions/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from xrpl.models.transactions.loan_broker_set import LoanBrokerSet
4848
from xrpl.models.transactions.loan_delete import LoanDelete
4949
from xrpl.models.transactions.loan_manage import LoanManage
50-
from xrpl.models.transactions.loan_pay import LoanPay
50+
from xrpl.models.transactions.loan_pay import LoanPay, LoanPayFlag, LoanPayFlagInterface
5151
from xrpl.models.transactions.loan_set import LoanSet
5252
from xrpl.models.transactions.metadata import TransactionMetadata
5353
from xrpl.models.transactions.mptoken_authorize import (
@@ -182,6 +182,8 @@
182182
"LoanDelete",
183183
"LoanManage",
184184
"LoanPay",
185+
"LoanPayFlag",
186+
"LoanPayFlagInterface",
185187
"LoanSet",
186188
"Memo",
187189
"MPTokenAuthorize",

0 commit comments

Comments
 (0)