Skip to content

Commit 96a3f07

Browse files
authored
Merge branch 'main' into mptDex
2 parents df6982b + d5dab24 commit 96a3f07

File tree

18 files changed

+430
-9
lines changed

18 files changed

+430
-9
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added `sign_loan_set_by_counterparty` helper function to sign LoanSet transactions as the counterparty (supports both single-sign and multi-sign modes)
1313
- Added `combine_loanset_counterparty_signers` helper function to combine multiple counterparty signatures for multi-sign LoanSet transactions
1414
- Added `compute_signature` helper function to compute transaction signatures with optional multi-sign support
15+
- 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.
16+
17+
### Fixed
18+
1519
- Updates `Number` codec with mantissa range normalization ([10^18, 10^19 - 1]) and appropriate overflow/underflow checks
20+
- Fix `Request.from_xrpl` by aliasing it to `Request.from_dict`
1621

1722
## [[4.4.0]] - 2025-12-16
1823

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)

tests/unit/models/requests/test_requests.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ def test_from_dict(self):
2929
}
3030
self.assertDictEqual(obj.to_dict(), expected)
3131

32+
def test_from_xrpl(self):
33+
req = {"method": "account_tx", "account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj"}
34+
obj = Request.from_xrpl(req)
35+
self.assertEqual(obj.__class__.__name__, "AccountTx")
36+
expected = {
37+
**req,
38+
"binary": False,
39+
"forward": False,
40+
"api_version": _DEFAULT_API_VERSION,
41+
}
42+
self.assertDictEqual(obj.to_dict(), expected)
43+
3244
def test_from_dict_no_method(self):
3345
req = {"account": "rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj"}
3446
with self.assertRaises(XRPLModelException):
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",

xrpl/core/binarycodec/types/int.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Base class for serializing and deserializing signed integers.
2+
See `Int Fields <https://xrpl.org/serialization.html#int-fields>`_
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing_extensions import Self
8+
9+
from xrpl.core.binarycodec.types.uint import UInt
10+
11+
12+
class Int(UInt):
13+
"""Base class for serializing and deserializing signed integers.
14+
See `Int Fields <https://xrpl.org/serialization.html#int-fields>`_
15+
"""
16+
17+
@property
18+
def value(self: Self) -> int:
19+
"""
20+
Get the value of the Int represented by `self.buffer`.
21+
22+
Returns:
23+
The int value of the Int.
24+
"""
25+
return int.from_bytes(self.buffer, byteorder="big", signed=True)

0 commit comments

Comments
 (0)