Skip to content

Commit cd6b7a2

Browse files
authored
feat: add support for Batch amendment (#757)
1 parent 40835d5 commit cd6b7a2

40 files changed

+985
-146
lines changed

.ci-config/rippled.cfg

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,32 +171,37 @@ AMM
171171
Clawback
172172
fixReducedOffersV1
173173
fixNFTokenRemint
174-
# 2.0.0-b4 Amendments
174+
# 2.0.0 Amendments
175175
XChainBridge
176176
DID
177-
# 2.2.0-b3 Amendments
177+
# 2.2.0 Amendments
178178
fixNFTokenReserve
179179
fixInnerObjTemplate
180180
fixAMMOverflowOffer
181181
PriceOracle
182182
fixEmptyDID
183183
fixXChainRewardRounding
184184
fixPreviousTxnID
185-
# 2.3.0 Amendments
186185
fixAMMv1_1
187-
fixAMMv1_2
186+
# 2.3.0 Amendments
188187
AMMClawback
189-
InvariantsV1_1
190188
Credentials
191189
NFTokenMintOffer
192190
MPTokensV1
191+
fixAMMv1_2
193192
fixNFTokenPageLinks
194193
fixInnerObjTemplate2
195194
fixEnforceNFTokenTrustline
196195
fixReducedOffersV2
196+
# 2.4.0 Amendments
197197
DeepFreeze
198+
DynamicNFT
198199
PermissionedDomains
200+
fixFrozenLPTokenTransfer
201+
fixInvalidTxFlags
202+
# 2.5.0 Amendments
199203
PermissionDelegation
204+
Batch
200205

201206
# This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode
202207
[voting]

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
"aiounittest",
55
"altnet",
66
"asyncio",
7+
"autofilling",
78
"autofills",
89
"binarycodec",
910
"Clawback",
1011
"isnumeric",
1112
"keypair",
1213
"keypairs",
14+
"multiaccount",
1315
"multisign",
16+
"multisigned",
17+
"multisigning",
1418
"nftoken",
1519
"PATHSET",
1620
"rippletest",

CHANGELOG.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [[Unreleased]]
99

10-
### Fixed
11-
- Added `MPTCurrency` support in `Issue` (rippled internal type)
12-
- Fixed the implementation error in get_latest_open_ledger_sequence method. The change uses the "current" ledger for extracting sequence number.
13-
- Increase default maximum payload size for websocket client
14-
- Fixed the default behavior of flags field when preparing transactions. By default, flags are not part of the transaction if not explicitly provided.
15-
- Added support for `amm_info` to `Request.from_dict`
16-
- Improved error handling for `amm_info`
17-
1810
### Added
1911
- Improved validation for models to also check param types
2012
- Support for `Account Permission` and `Account Permission Delegation` (XLS-74d, XLS-75d)
13+
- Support for the `Batch` amendment (XLS-56d)
14+
15+
### Fixed
16+
- Add `MPTCurrency` support in `Issue` (rippled internal type)
17+
- Fix the implementation error in get_latest_open_ledger_sequence method. The change uses the "current" ledger for extracting sequence number
18+
- Increase default maximum payload size for websocket client
19+
- Fix the default behavior of flags field when preparing transactions. By default, flags are not part of the transaction if not explicitly provided
20+
- Add support for `amm_info` to `Request.from_dict`
21+
- Improve error handling for `amm_info`
22+
- Handle autofilling better when multisigning transactions
23+
- Improve typing for transaction-related helper functions
24+
- Improve handling of `TicketSequence`
25+
- Fix issue with failing on a higher than expected fee
26+
- Improve multi-sign fee calculations
2127

2228
## [4.1.0] - 2025-2-13
2329

tests/integration/it_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def fund_wallet(wallet: Wallet) -> None:
110110
destination=wallet.address,
111111
amount=FUNDING_AMOUNT,
112112
)
113-
sign_and_submit(payment, client, MASTER_WALLET, check_fee=True)
113+
sign_and_submit(payment, client, MASTER_WALLET)
114114
client.request(LEDGER_ACCEPT_REQUEST)
115115

116116

@@ -122,7 +122,7 @@ async def fund_wallet_async(
122122
destination=wallet.address,
123123
amount=FUNDING_AMOUNT,
124124
)
125-
await sign_and_submit_async(payment, client, MASTER_WALLET, check_fee=True)
125+
await sign_and_submit_async(payment, client, MASTER_WALLET)
126126
await client.request(LEDGER_ACCEPT_REQUEST)
127127

128128

tests/integration/sugar/test_transaction.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
)
77
from tests.integration.reusable_values import DESTINATION as DESTINATION_WALLET
88
from tests.integration.reusable_values import WALLET
9+
from xrpl.asyncio.account import get_next_valid_seq_number
910
from xrpl.asyncio.ledger import get_fee, get_latest_validated_ledger_sequence
1011
from xrpl.asyncio.transaction import (
1112
XRPLReliableSubmissionException,
@@ -24,7 +25,15 @@
2425
from xrpl.models.amounts.issued_currency_amount import IssuedCurrencyAmount
2526
from xrpl.models.exceptions import XRPLException
2627
from xrpl.models.requests import ServerState, Tx
27-
from xrpl.models.transactions import AccountDelete, AccountSet, EscrowFinish, Payment
28+
from xrpl.models.transactions import (
29+
AccountDelete,
30+
AccountSet,
31+
Batch,
32+
DepositPreauth,
33+
EscrowFinish,
34+
Payment,
35+
TransactionFlag,
36+
)
2837
from xrpl.utils import xrp_to_drops
2938

3039
ACCOUNT = WALLET.address
@@ -326,6 +335,37 @@ async def test_simulate(self, client):
326335
self.assertEqual(response.result["engine_result_code"], 0)
327336
self.assertFalse(response.result["applied"])
328337

338+
@test_async_and_sync(
339+
globals(),
340+
["xrpl.transaction.autofill", "xrpl.account.get_next_valid_seq_number"],
341+
)
342+
async def test_batch_autofill(self, client):
343+
tx = Batch(
344+
account="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
345+
raw_transactions=[
346+
DepositPreauth(
347+
account=WALLET.address,
348+
authorize="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
349+
),
350+
DepositPreauth(
351+
account=WALLET.address,
352+
authorize="rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
353+
),
354+
],
355+
)
356+
transaction = await autofill(tx, client)
357+
358+
sequence = await get_next_valid_seq_number(WALLET.address, client)
359+
for i in range(len(transaction.raw_transactions)):
360+
raw_tx = transaction.raw_transactions[i]
361+
self.assertTrue(raw_tx.has_flag(TransactionFlag.TF_INNER_BATCH_TXN))
362+
self.assertEqual(raw_tx.sequence, sequence + i)
363+
self.assertEqual(raw_tx.network_id, 63456)
364+
self.assertIsNone(raw_tx.last_ledger_sequence)
365+
self.assertEqual(raw_tx.fee, "0")
366+
self.assertEqual(raw_tx.signing_pub_key, "")
367+
self.assertEqual(raw_tx.txn_signature, None)
368+
329369

330370
class TestSubmitAndWait(IntegrationTestCase):
331371
@test_async_and_sync(
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
sign_and_reliable_submission_async,
4+
test_async_and_sync,
5+
)
6+
from tests.integration.reusable_values import DESTINATION, WALLET
7+
from xrpl.asyncio.transaction import autofill
8+
from xrpl.models import Batch, BatchFlag, Payment
9+
from xrpl.models.response import ResponseStatus
10+
from xrpl.transaction.batch_signers import sign_multiaccount_batch
11+
12+
13+
class TestBatch(IntegrationTestCase):
14+
@test_async_and_sync(globals())
15+
async def test_basic_functionality(self, client):
16+
payment = Payment(
17+
account=WALLET.address,
18+
amount="1",
19+
destination=DESTINATION.address,
20+
)
21+
batch = Batch(
22+
account=WALLET.address,
23+
flags=BatchFlag.TF_ALL_OR_NOTHING,
24+
raw_transactions=[payment, payment],
25+
)
26+
response = await sign_and_reliable_submission_async(batch, WALLET, client)
27+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
28+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
29+
30+
@test_async_and_sync(globals(), ["xrpl.transaction.autofill"])
31+
async def test_multisign(self, client):
32+
payment = Payment(
33+
account=WALLET.address,
34+
amount="1",
35+
destination=DESTINATION.address,
36+
)
37+
payment2 = Payment(
38+
account=DESTINATION.address,
39+
amount="1",
40+
destination=WALLET.address,
41+
)
42+
batch = Batch(
43+
account=WALLET.address,
44+
flags=BatchFlag.TF_ALL_OR_NOTHING,
45+
raw_transactions=[payment, payment2],
46+
)
47+
autofilled = await autofill(batch, client, 1)
48+
signed = sign_multiaccount_batch(DESTINATION, autofilled)
49+
response = await sign_and_reliable_submission_async(signed, WALLET, client)
50+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
51+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

tests/unit/core/binarycodec/test_main.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
encode,
1212
encode_for_multisigning,
1313
encode_for_signing,
14+
encode_for_signing_batch,
1415
encode_for_signing_claim,
1516
)
1617

@@ -401,6 +402,30 @@ def test_claim(self):
401402
)
402403
self.assertEqual(encode_for_signing_claim(json), expected)
403404

405+
def test_batch(self):
406+
flags = 1
407+
transaction_ids = [
408+
"ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA",
409+
"795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4",
410+
]
411+
412+
json = {"flags": flags, "transaction_ids": transaction_ids}
413+
actual = encode_for_signing_batch(json)
414+
self.assertEqual(
415+
actual,
416+
(
417+
# hash prefix
418+
"42434800"
419+
# flags
420+
"00000001"
421+
# transaction_ids length
422+
"00000002"
423+
# transaction_ids
424+
"ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA"
425+
"795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4"
426+
),
427+
)
428+
404429
def test_multisig(self):
405430
signing_account = "rJZdUusLDtY9NEsGea7ijqhVrXv98rYBYN"
406431
multisig_json = {**signing_json, "SigningPubKey": ""}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models import Batch, BatchFlag, Payment, TransactionFlag
4+
from xrpl.models.exceptions import XRPLModelException
5+
6+
_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
7+
_DESTINATION = "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg"
8+
9+
10+
class TestBatch(TestCase):
11+
def test_basic(self):
12+
payment = Payment(
13+
account=_ACCOUNT,
14+
amount="1",
15+
destination=_DESTINATION,
16+
)
17+
batch = Batch(
18+
account=_ACCOUNT,
19+
flags=BatchFlag.TF_ALL_OR_NOTHING,
20+
raw_transactions=[payment, payment],
21+
)
22+
self.assertTrue(batch.is_valid())
23+
self.assertTrue(
24+
batch.raw_transactions[0].has_flag(TransactionFlag.TF_INNER_BATCH_TXN)
25+
)
26+
self.assertTrue(
27+
batch.raw_transactions[1].has_flag(TransactionFlag.TF_INNER_BATCH_TXN)
28+
)
29+
30+
def test_too_few_inner_transactions(self):
31+
payment = Payment(
32+
account=_ACCOUNT,
33+
amount="1",
34+
destination=_DESTINATION,
35+
)
36+
with self.assertRaises(XRPLModelException):
37+
Batch(
38+
account=_ACCOUNT,
39+
flags=BatchFlag.TF_ALL_OR_NOTHING,
40+
raw_transactions=[payment],
41+
)

tests/unit/models/transactions/test_delegate_set.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_account_and_delegate_are_the_same(self):
9595
+ "'}",
9696
)
9797

98-
def test_non_delegatable_transactions(self):
98+
def test_non_delegable_transactions(self):
9999
with self.assertRaises(XRPLModelException) as error:
100100
DelegateSet(
101101
account=_ACCOUNT,
@@ -109,6 +109,6 @@ def test_non_delegatable_transactions(self):
109109
)
110110
self.assertEqual(
111111
error.exception.args[0],
112-
"{'permissions': \"Non-delegatable transactions found in `permissions` "
112+
"{'permissions': \"Non-delegable transactions found in `permissions` "
113113
"list: {<TransactionType.ACCOUNT_DELETE: 'AccountDelete'>}.\"}",
114114
)

0 commit comments

Comments
 (0)