From 8c1f79524a762d1aee04042f72008ca22e09a741 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 2 Oct 2024 19:32:14 -0400 Subject: [PATCH 01/33] update definitions.json --- .../binarycodec/definitions/definitions.json | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 797be9ce2..07d56e5b5 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -260,6 +260,16 @@ "type": "UInt8" } ], + [ + "BatchIndex", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], [ "LedgerEntryType", { @@ -370,6 +380,16 @@ "type": "UInt16" } ], + [ + "LedgerFixType", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], [ "NetworkID", { @@ -1980,6 +2000,16 @@ "type": "Blob" } ], + [ + "InnerResult", + { + "nth": 30, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2140,6 +2170,16 @@ "type": "AccountID" } ], + [ + "OuterAccount", + { + "nth": 24, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], [ "Indexes", { @@ -2180,6 +2220,16 @@ "type": "Vector256" } ], + [ + "TxIDs", + { + "nth": 5, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Vector256" + } + ], [ "Paths", { @@ -2550,6 +2600,46 @@ "type": "STObject" } ], + [ + "RawTransaction", + { + "nth": 33, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "BatchExecution", + { + "nth": 34, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "BatchTxn", + { + "nth": 35, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "BatchSigner", + { + "nth": 36, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2739,6 +2829,36 @@ "isSigningField": true, "type": "STArray" } + ], + [ + "BatchExecutions", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "RawTransactions", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "BatchSigners", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": false, + "type": "STArray" + } ] ], "TRANSACTION_RESULTS": { @@ -2808,6 +2928,7 @@ "temEMPTY_DID": -254, "temARRAY_EMPTY": -253, "temARRAY_TOO_LARGE": -252, + "temINVALID_BATCH": -251, "tefFAILURE": -199, "tefALREADY": -198, @@ -2830,6 +2951,7 @@ "tefTOO_BIG": -181, "tefNO_TICKET": -180, "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, + "tefINVALID_LEDGER_FIX_TYPE": -178, "terRETRY": -99, "terFUNDS_SPENT": -98, @@ -2923,7 +3045,8 @@ "tecINVALID_UPDATE_TIME": 188, "tecTOKEN_PAIR_NOT_FOUND": 189, "tecARRAY_EMPTY": 190, - "tecARRAY_TOO_LARGE": 191 + "tecARRAY_TOO_LARGE": 191, + "tecBATCH_FAILURE": 192 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2974,6 +3097,8 @@ "DIDDelete": 50, "OracleSet": 51, "OracleDelete": 52, + "LedgerStateFix": 53, + "Batch": 54, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 From ea28bc4136e50ae479bdd5b1c1a3dcdde5d5a25e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 2 Oct 2024 19:41:20 -0400 Subject: [PATCH 02/33] update scripts after rippled refactor --- tools/generate_tx_models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/generate_tx_models.py b/tools/generate_tx_models.py index 28b0dcbef..25bc2af8c 100644 --- a/tools/generate_tx_models.py +++ b/tools/generate_tx_models.py @@ -26,7 +26,7 @@ def _parse_rippled_source( folder: str, ) -> Tuple[Dict[str, List[str]], Dict[str, List[Tuple[str, ...]]]]: # Get SFields - sfield_cpp = _read_file(os.path.join(folder, "src/ripple/protocol/impl/SField.cpp")) + sfield_cpp = _read_file(os.path.join(folder, "src/libxrpl/protocol/SField.cpp")) sfield_hits = re.findall( r'^ *CONSTRUCT_[^\_]+_SFIELD *\( *[^,\n]*,[ \n]*"([^\"\n ]+)"[ \n]*,[ \n]*' + r"([^, \n]+)[ \n]*,[ \n]*([0-9]+)(,.*?(notSigning))?", @@ -37,7 +37,7 @@ def _parse_rippled_source( # Get TxFormats tx_formats_cpp = _read_file( - os.path.join(folder, "src/ripple/protocol/impl/TxFormats.cpp") + os.path.join(folder, "src/libxrpl/protocol/TxFormats.cpp") ) tx_formats_hits = re.findall( r"^ *add\(jss::([^\"\n, ]+),[ \n]*tt[A-Z_]+,[ \n]*{[ \n]*(({sf[A-Za-z0-9]+, " @@ -102,6 +102,7 @@ def _main( existing_library_txs = {m.value for m in TransactionType} | { m.value for m in PseudoTransactionType } + print(sorted(existing_library_txs)) for tx in tx_formats: if tx not in existing_library_txs: txs_to_add.append((tx, _key_to_json(tx))) @@ -130,7 +131,7 @@ def _generate_param_line(param: str, is_required: bool) -> str: param_lines.sort(key=lambda x: "REQUIRED" not in x) params = "\n".join(param_lines) model = f"""@require_kwargs_on_init -@dataclass(frozen=True, **KW_ONLY_DATACLASS) +@dataclass(frozen=True, **KW_ONLY_DATACLASS) class {tx}(Transaction): \"\"\"Represents a {tx} transaction.\"\"\" @@ -184,6 +185,8 @@ class {tx}(Transaction): if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: poetry run python generate_tx_models.py path/to/rippled") folder = sys.argv[1] sfields, tx_formats = _parse_rippled_source(folder) _main(sfields, tx_formats) From dbedfdc17f55e37ff845e2d822852dce08c612e8 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 2 Oct 2024 19:41:44 -0400 Subject: [PATCH 03/33] add LedgerStateFix --- xrpl/models/transactions/__init__.py | 2 ++ xrpl/models/transactions/ledger_state_fix.py | 25 +++++++++++++++++++ .../transactions/types/transaction_type.py | 1 + 3 files changed, 28 insertions(+) create mode 100644 xrpl/models/transactions/ledger_state_fix.py diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index ee3304f1b..9fa6a8b7b 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -34,6 +34,7 @@ from xrpl.models.transactions.escrow_cancel import EscrowCancel from xrpl.models.transactions.escrow_create import EscrowCreate from xrpl.models.transactions.escrow_finish import EscrowFinish +from xrpl.models.transactions.ledger_state_fix import LedgerStateFix from xrpl.models.transactions.metadata import TransactionMetadata from xrpl.models.transactions.nftoken_accept_offer import NFTokenAcceptOffer from xrpl.models.transactions.nftoken_burn import NFTokenBurn @@ -119,6 +120,7 @@ "EscrowCancel", "EscrowCreate", "EscrowFinish", + "LedgerStateFix", "Memo", "NFTokenAcceptOffer", "NFTokenBurn", diff --git a/xrpl/models/transactions/ledger_state_fix.py b/xrpl/models/transactions/ledger_state_fix.py new file mode 100644 index 000000000..fbc03dd9d --- /dev/null +++ b/xrpl/models/transactions/ledger_state_fix.py @@ -0,0 +1,25 @@ +"""Model for LedgerStateFix transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional + +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class LedgerStateFix(Transaction): + """Represents a LedgerStateFix transaction.""" + + ledger_fix_type: int = REQUIRED # type: ignore + owner: Optional[str] = None + + transaction_type: TransactionType = field( + default=TransactionType.LEDGER_STATE_FIX, + init=False, + ) diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 0d14e2775..a4aaa501f 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -24,6 +24,7 @@ class TransactionType(str, Enum): ESCROW_CANCEL = "EscrowCancel" ESCROW_CREATE = "EscrowCreate" ESCROW_FINISH = "EscrowFinish" + LEDGER_STATE_FIX = "LedgerStateFix" NFTOKEN_ACCEPT_OFFER = "NFTokenAcceptOffer" NFTOKEN_BURN = "NFTokenBurn" NFTOKEN_CANCEL_OFFER = "NFTokenCancelOffer" From 703f4c8755a082b56474477e6f42b59cd17a15d4 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 2 Oct 2024 19:52:30 -0400 Subject: [PATCH 04/33] add basic batch models --- xrpl/models/transactions/__init__.py | 2 + xrpl/models/transactions/batch.py | 63 +++++++++++++++++++ .../transactions/types/transaction_type.py | 1 + 3 files changed, 66 insertions(+) create mode 100644 xrpl/models/transactions/batch.py diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index 9fa6a8b7b..df49e3bb1 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -24,6 +24,7 @@ AMMWithdrawFlag, AMMWithdrawFlagInterface, ) +from xrpl.models.transactions.batch import Batch from xrpl.models.transactions.check_cancel import CheckCancel from xrpl.models.transactions.check_cash import CheckCash from xrpl.models.transactions.check_create import CheckCreate @@ -110,6 +111,7 @@ "AMMWithdrawFlag", "AMMWithdrawFlagInterface", "AuthAccount", + "Batch", "CheckCancel", "CheckCash", "CheckCreate", diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py new file mode 100644 index 000000000..de7edd273 --- /dev/null +++ b/xrpl/models/transactions/batch.py @@ -0,0 +1,63 @@ +"""Model for Batch transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + +from xrpl.models.nested_model import NestedModel +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Signer, Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class BatchSigner(NestedModel): + """Represents a Batch signer.""" + + account: str = REQUIRED # type: ignore + + signing_pub_key: Optional[str] = None + + txn_signature: Optional[str] = None + + signers: Optional[List[Signer]] = None + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class BatchTxn(NestedModel): + """Represents the info indicating a Batch transaction.""" + + outer_account: str = REQUIRED # type: ignore + + sequence: Optional[int] = None + + ticket_sequence: Optional[int] = None + + batch_index: int = REQUIRED # type: ignore + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class BatchInnerTransaction(Transaction): + """Represents a Batch inner transaction.""" + + BatchTxn: BatchTxn = REQUIRED # type: ignore + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class Batch(Transaction): + """Represents a Batch transaction.""" + + raw_transactions: List[BatchInnerTransaction] = REQUIRED # type: ignore + tx_ids: List[str] = REQUIRED # type: ignore + batch_signers: Optional[List[BatchSigner]] = None + + transaction_type: TransactionType = field( + default=TransactionType.BATCH, + init=False, + ) diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index a4aaa501f..f6cef90cd 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -14,6 +14,7 @@ class TransactionType(str, Enum): AMM_DEPOSIT = "AMMDeposit" AMM_VOTE = "AMMVote" AMM_WITHDRAW = "AMMWithdraw" + BATCH = "Batch" CHECK_CANCEL = "CheckCancel" CHECK_CASH = "CheckCash" CHECK_CREATE = "CheckCreate" From 429d6fc6e665d562410b665ef2b1116c816385d3 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 2 Oct 2024 20:10:32 -0400 Subject: [PATCH 05/33] add autofill --- xrpl/asyncio/transaction/main.py | 43 ++++++++++++++++++++++++- xrpl/models/transactions/transaction.py | 7 +++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 3c7bb8a52..1767505fe 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -1,6 +1,7 @@ """High-level transaction methods with XRPL transactions.""" + import math -from typing import Any, Dict, Optional, cast +from typing import Any, Dict, List, Optional, Tuple, cast from typing_extensions import Final @@ -14,6 +15,7 @@ from xrpl.models.requests import ServerInfo, ServerState, SubmitOnly from xrpl.models.response import Response from xrpl.models.transactions import EscrowFinish +from xrpl.models.transactions.batch import Batch, BatchInnerTransaction from xrpl.models.transactions.transaction import Signer, Transaction from xrpl.models.transactions.transaction import ( transaction_json_to_binary_codec_form as model_transaction_to_binary_codec, @@ -244,6 +246,11 @@ async def autofill( if "last_ledger_sequence" not in transaction_json: ledger_sequence = await get_latest_validated_ledger_sequence(client) transaction_json["last_ledger_sequence"] = ledger_sequence + _LEDGER_OFFSET + if transaction.transaction_type == TransactionType.BATCH: + inner_txs, tx_ids = await _autofill_batch(client, cast(Batch, transaction)) + transaction_json["raw_transactions"] = inner_txs + if "tx_ids" not in transaction_json: + transaction_json["tx_ids"] = tx_ids return Transaction.from_dict(transaction_json) @@ -482,3 +489,37 @@ async def _fetch_owner_reserve_fee(client: Client) -> int: server_state = await client._request_impl(ServerState()) fee = server_state.result["state"]["validated_ledger"]["reserve_inc"] return int(fee) + + +async def _autofill_batch( + client: Client, transaction: Batch +) -> Tuple[List[BatchInnerTransaction], List[str]]: + account_sequences: Dict[str, int] = {} + batch_index = 0 + tx_ids: List[str] = [] + inner_txs: List[BatchInnerTransaction] = [] + + for raw_txn in transaction.raw_transactions: + if raw_txn.BatchTxn is not None: + inner_txs.append(raw_txn) + continue + + batch_txn = {"outer_account": transaction.account} + + if raw_txn.account in account_sequences: + batch_txn["sequence"] = account_sequences[raw_txn.account] + account_sequences[raw_txn.account] += 1 + else: + sequence = await get_next_valid_seq_number(raw_txn.account, client) + account_sequences[raw_txn.account] = sequence + 1 + batch_txn["sequence"] = sequence + + batch_txn["batch_index"] = batch_index + batch_index += 1 + + raw_txn_dict = raw_txn.to_dict() + raw_txn_dict["RawTransaction"]["BatchTxn"] = batch_txn + inner_txs.append(BatchInnerTransaction.from_dict(raw_txn_dict)) + tx_ids.append(raw_txn.get_hash()) + + return inner_txs, tx_ids diff --git a/xrpl/models/transactions/transaction.py b/xrpl/models/transactions/transaction.py index ea25aab8c..e22e6d160 100644 --- a/xrpl/models/transactions/transaction.py +++ b/xrpl/models/transactions/transaction.py @@ -16,6 +16,7 @@ from xrpl.models.nested_model import NestedModel from xrpl.models.requests import PathStep from xrpl.models.required import REQUIRED +from xrpl.models.transactions.batch import BatchInnerTransaction from xrpl.models.transactions.types import PseudoTransactionType, TransactionType from xrpl.models.types import XRPL_VALUE_TYPE from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init @@ -411,7 +412,11 @@ def get_hash(self: Self) -> str: Raises: XRPLModelException: if the Transaction is unsigned. """ - if self.txn_signature is None and self.signers is None: + if ( + self.txn_signature is None + and self.signers is None + and not isinstance(self, BatchInnerTransaction) + ): raise XRPLModelException( "Cannot get the hash from an unsigned Transaction." ) From 267758406f8b02139120283cb99c011dfeb548cf Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 2 Oct 2024 23:18:32 -0400 Subject: [PATCH 06/33] add autofill tests --- tests/integration/sugar/test_transaction.py | 46 +++++++++++++++++++-- xrpl/asyncio/transaction/main.py | 28 +++++++------ xrpl/models/transactions/batch.py | 26 +----------- xrpl/models/transactions/transaction.py | 19 ++++++++- 4 files changed, 78 insertions(+), 41 deletions(-) diff --git a/tests/integration/sugar/test_transaction.py b/tests/integration/sugar/test_transaction.py index e9e245789..6e831d73f 100644 --- a/tests/integration/sugar/test_transaction.py +++ b/tests/integration/sugar/test_transaction.py @@ -6,22 +6,30 @@ ) from tests.integration.reusable_values import DESTINATION as DESTINATION_WALLET from tests.integration.reusable_values import WALLET +from xrpl.asyncio.account import get_next_valid_seq_number from xrpl.asyncio.ledger import get_fee, get_latest_validated_ledger_sequence from xrpl.asyncio.transaction import ( XRPLReliableSubmissionException, autofill, autofill_and_sign, sign, + sign_and_submit, ) from xrpl.asyncio.transaction import submit as submit_transaction_alias_async from xrpl.asyncio.transaction import submit_and_wait -from xrpl.asyncio.transaction.main import sign_and_submit from xrpl.clients import XRPLRequestFailureException from xrpl.core.addresscodec import classic_address_to_xaddress -from xrpl.core.binarycodec.main import encode +from xrpl.core.binarycodec import encode from xrpl.models.exceptions import XRPLException from xrpl.models.requests import ServerState, Tx -from xrpl.models.transactions import AccountDelete, AccountSet, EscrowFinish, Payment +from xrpl.models.transactions import ( + AccountDelete, + AccountSet, + Batch, + DepositPreauth, + EscrowFinish, + Payment, +) from xrpl.utils import xrp_to_drops ACCOUNT = WALLET.address @@ -249,6 +257,38 @@ async def test_networkid_reserved_networks(self, client): self.assertIsNone(transaction.network_id) self.assertEqual(client.network_id, 1) + @test_async_and_sync( + globals(), + ["xrpl.transaction.autofill", "xrpl.account.get_next_valid_seq_number"], + ) + async def test_batch_autofill(self, client): + tx = Batch( + account="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + raw_transactions=[ + DepositPreauth( + account=WALLET.address, + authorize="rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + ), + DepositPreauth( + account=WALLET.address, + authorize="rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + ), + ], + ) + transaction = await autofill(tx, client) + self.assertEqual(len(transaction.tx_ids), 2) + + sequence = await get_next_valid_seq_number(WALLET.address, client) + for i in range(len(transaction.raw_transactions)): + raw_tx = transaction.raw_transactions[i] + self.assertIsNotNone(raw_tx.batch_txn) + self.assertEqual( + raw_tx.batch_txn.outer_account, "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + ) + self.assertEqual(raw_tx.batch_txn.sequence, sequence + i) + self.assertEqual(raw_tx.batch_txn.batch_index, i) + self.assertEqual(raw_tx.get_hash(), transaction.tx_ids[i]) + class TestSubmitAndWait(IntegrationTestCase): @test_async_and_sync( diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 1767505fe..8c791a5b3 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -3,7 +3,7 @@ import math from typing import Any, Dict, List, Optional, Tuple, cast -from typing_extensions import Final +from typing_extensions import Final, TypeVar from xrpl.asyncio.account import get_next_valid_seq_number from xrpl.asyncio.clients import Client, XRPLRequestFailureException @@ -15,7 +15,7 @@ from xrpl.models.requests import ServerInfo, ServerState, SubmitOnly from xrpl.models.response import Response from xrpl.models.transactions import EscrowFinish -from xrpl.models.transactions.batch import Batch, BatchInnerTransaction +from xrpl.models.transactions.batch import Batch from xrpl.models.transactions.transaction import Signer, Transaction from xrpl.models.transactions.transaction import ( transaction_json_to_binary_codec_form as model_transaction_to_binary_codec, @@ -214,9 +214,12 @@ def _prepare_transaction( return transaction_json +T = TypeVar("T", bound=Transaction) + + async def autofill( - transaction: Transaction, client: Client, signers_count: Optional[int] = None -) -> Transaction: + transaction: T, client: Client, signers_count: Optional[int] = None +) -> T: """ Autofills fields in a transaction. This will set `sequence`, `fee`, and `last_ledger_sequence` according to the current state of the server this Client is @@ -251,7 +254,7 @@ async def autofill( transaction_json["raw_transactions"] = inner_txs if "tx_ids" not in transaction_json: transaction_json["tx_ids"] = tx_ids - return Transaction.from_dict(transaction_json) + return cast(T, Transaction.from_dict(transaction_json)) async def _get_network_id_and_build_version(client: Client) -> None: @@ -493,18 +496,18 @@ async def _fetch_owner_reserve_fee(client: Client) -> int: async def _autofill_batch( client: Client, transaction: Batch -) -> Tuple[List[BatchInnerTransaction], List[str]]: +) -> Tuple[List[Transaction], List[str]]: account_sequences: Dict[str, int] = {} batch_index = 0 tx_ids: List[str] = [] - inner_txs: List[BatchInnerTransaction] = [] + inner_txs: List[Transaction] = [] for raw_txn in transaction.raw_transactions: - if raw_txn.BatchTxn is not None: + if raw_txn.batch_txn is not None: inner_txs.append(raw_txn) continue - batch_txn = {"outer_account": transaction.account} + batch_txn: Dict[str, Any] = {"outer_account": transaction.account} if raw_txn.account in account_sequences: batch_txn["sequence"] = account_sequences[raw_txn.account] @@ -518,8 +521,9 @@ async def _autofill_batch( batch_index += 1 raw_txn_dict = raw_txn.to_dict() - raw_txn_dict["RawTransaction"]["BatchTxn"] = batch_txn - inner_txs.append(BatchInnerTransaction.from_dict(raw_txn_dict)) - tx_ids.append(raw_txn.get_hash()) + raw_txn_dict["batch_txn"] = batch_txn + new_raw_txn = Transaction.from_dict(raw_txn_dict) + inner_txs.append(new_raw_txn) + tx_ids.append(new_raw_txn.get_hash()) return inner_txs, tx_ids diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py index de7edd273..ab9be1ab0 100644 --- a/xrpl/models/transactions/batch.py +++ b/xrpl/models/transactions/batch.py @@ -26,35 +26,13 @@ class BatchSigner(NestedModel): signers: Optional[List[Signer]] = None -@require_kwargs_on_init -@dataclass(frozen=True, **KW_ONLY_DATACLASS) -class BatchTxn(NestedModel): - """Represents the info indicating a Batch transaction.""" - - outer_account: str = REQUIRED # type: ignore - - sequence: Optional[int] = None - - ticket_sequence: Optional[int] = None - - batch_index: int = REQUIRED # type: ignore - - -@require_kwargs_on_init -@dataclass(frozen=True, **KW_ONLY_DATACLASS) -class BatchInnerTransaction(Transaction): - """Represents a Batch inner transaction.""" - - BatchTxn: BatchTxn = REQUIRED # type: ignore - - @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class Batch(Transaction): """Represents a Batch transaction.""" - raw_transactions: List[BatchInnerTransaction] = REQUIRED # type: ignore - tx_ids: List[str] = REQUIRED # type: ignore + raw_transactions: List[Transaction] = REQUIRED # type: ignore + tx_ids: Optional[List[str]] = None batch_signers: Optional[List[BatchSigner]] = None transaction_type: TransactionType = field( diff --git a/xrpl/models/transactions/transaction.py b/xrpl/models/transactions/transaction.py index e22e6d160..41a3bbe9d 100644 --- a/xrpl/models/transactions/transaction.py +++ b/xrpl/models/transactions/transaction.py @@ -16,7 +16,6 @@ from xrpl.models.nested_model import NestedModel from xrpl.models.requests import PathStep from xrpl.models.required import REQUIRED -from xrpl.models.transactions.batch import BatchInnerTransaction from xrpl.models.transactions.types import PseudoTransactionType, TransactionType from xrpl.models.types import XRPL_VALUE_TYPE from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init @@ -154,6 +153,20 @@ class Signer(NestedModel): """ +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class BatchTxn(NestedModel): + """Represents the info indicating a Batch transaction.""" + + outer_account: str = REQUIRED # type: ignore + + sequence: Optional[int] = None + + ticket_sequence: Optional[int] = None + + batch_index: int = REQUIRED # type: ignore + + @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class Transaction(BaseModel): @@ -251,6 +264,8 @@ class Transaction(BaseModel): network_id: Optional[int] = None """The network id of the transaction.""" + batch_txn: Optional[BatchTxn] = None + def _get_errors(self: Self) -> Dict[str, str]: # import must be here to avoid circular dependencies from xrpl.wallet.main import Wallet @@ -415,7 +430,7 @@ def get_hash(self: Self) -> str: if ( self.txn_signature is None and self.signers is None - and not isinstance(self, BatchInnerTransaction) + and self.batch_txn is None ): raise XRPLModelException( "Cannot get the hash from an unsigned Transaction." From b79c7f72da8d9420c9ffb7f52c3d2e9d5f151ae2 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 3 Oct 2024 10:36:28 -0400 Subject: [PATCH 07/33] fix multisign issue, add test --- tests/unit/models/transactions/test_transaction.py | 14 +++++++++++++- xrpl/asyncio/transaction/main.py | 14 +++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/unit/models/transactions/test_transaction.py b/tests/unit/models/transactions/test_transaction.py index dea0bb267..f9080e9ca 100644 --- a/tests/unit/models/transactions/test_transaction.py +++ b/tests/unit/models/transactions/test_transaction.py @@ -1,8 +1,9 @@ from unittest import TestCase from xrpl.asyncio.transaction.main import sign +from xrpl.core.addresscodec.main import classic_address_to_xaddress from xrpl.models.exceptions import XRPLModelException -from xrpl.models.transactions import AccountSet, OfferCreate, Payment +from xrpl.models.transactions import AccountSet, DepositPreauth, OfferCreate, Payment from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.types.transaction_type import TransactionType from xrpl.transaction.multisign import multisign @@ -158,6 +159,17 @@ def test_is_signed_for_multisigned_transaction(self): multisigned_tx = multisign(tx, [tx_1, tx_2]) self.assertTrue(multisigned_tx.is_signed()) + def test_multisigned_transaction_xaddress(self): + tx = DepositPreauth( + account=classic_address_to_xaddress(_WALLET.address, 1, False), + authorize=classic_address_to_xaddress(_ACCOUNT, 1, False), + ) + tx_1 = sign(tx, _FIRST_SIGNER, multisign=True) + tx_2 = sign(tx, _SECOND_SIGNER, multisign=True) + + multisigned_tx = multisign(tx, [tx_1, tx_2]) + self.assertTrue(multisigned_tx.is_signed()) + # test the usage of DeliverMax field in Payment transactions def test_payment_txn_API_no_deliver_max(self): delivered_amount = "200000" diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 8c791a5b3..1bdb8a8ac 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -87,11 +87,12 @@ def sign( Returns: The signed transaction blob. """ + transaction_json = _prepare_transaction(transaction, wallet) if multisign: signature = keypairs_sign( bytes.fromhex( encode_for_multisigning( - transaction.to_xrpl(), + transaction_json, wallet.address, ) ), @@ -107,7 +108,6 @@ def sign( ] return Transaction.from_dict(tx_dict) - transaction_json = _prepare_transaction(transaction, wallet) serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = keypairs_sign(serialized_bytes, wallet.private_key) @@ -194,8 +194,12 @@ def _prepare_transaction( Raises: XRPLException: if both LastLedgerSequence and `ledger_offset` are provided, or - if an address tag is provided that does not match the X-Address tag. + if an address tag is provided that does not match the X-Address tag, or if + attempting to directly sign a Batch inner transaction. """ + if transaction.batch_txn is not None: + raise XRPLException("Cannot directly sign a batch inner transaction.") + transaction_json = transaction.to_xrpl() transaction_json["SigningPubKey"] = wallet.public_key @@ -214,7 +218,7 @@ def _prepare_transaction( return transaction_json -T = TypeVar("T", bound=Transaction) +T = TypeVar("T", bound=Transaction, default=Transaction) async def autofill( @@ -385,7 +389,7 @@ def _convert_to_classic_address(json: Dict[str, Any], field: str) -> None: field: the field in `json` that may contain an X-Address """ if field in json and is_valid_xaddress(json[field]): - json[field] = xaddress_to_classic_address(json[field]) + json[field] = xaddress_to_classic_address(json[field])[0] def transaction_json_to_binary_codec_form(dictionary: Dict[str, Any]) -> Dict[str, Any]: From 2443f4ca611a429aa3f3f442a1a40d6bd27f364d Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 18:34:32 -0700 Subject: [PATCH 08/33] rename some tests --- .vscode/settings.json | 1 + tests/unit/models/transactions/test_transaction.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c6830efc1..53c45e2e6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "keypair", "keypairs", "multisign", + "multisigned", "nftoken", "PATHSET", "rippletest", diff --git a/tests/unit/models/transactions/test_transaction.py b/tests/unit/models/transactions/test_transaction.py index f9080e9ca..763f92f04 100644 --- a/tests/unit/models/transactions/test_transaction.py +++ b/tests/unit/models/transactions/test_transaction.py @@ -171,7 +171,7 @@ def test_multisigned_transaction_xaddress(self): self.assertTrue(multisigned_tx.is_signed()) # test the usage of DeliverMax field in Payment transactions - def test_payment_txn_API_no_deliver_max(self): + def test_payment_txn_api_no_deliver_max(self): delivered_amount = "200000" payment_tx_json = { "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", @@ -187,7 +187,7 @@ def test_payment_txn_API_no_deliver_max(self): payment_txn = Payment.from_xrpl(payment_tx_json) self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) - def test_payment_txn_API_no_amount(self): + def test_payment_txn_api_no_amount(self): delivered_amount = "200000" payment_tx_json = { "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", @@ -203,7 +203,7 @@ def test_payment_txn_API_no_amount(self): payment_txn = Payment.from_xrpl(payment_tx_json) self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"]) - def test_payment_txn_API_different_amount_and_deliver_max(self): + def test_payment_txn_api_different_amount_and_deliver_max(self): payment_tx_json = { "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", @@ -219,7 +219,7 @@ def test_payment_txn_API_different_amount_and_deliver_max(self): with self.assertRaises(XRPLModelException): Payment.from_xrpl(payment_tx_json) - def test_payment_txn_API_identical_amount_and_deliver_max(self): + def test_payment_txn_api_identical_amount_and_deliver_max(self): delivered_amount = "200000" payment_tx_json = { "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", From 3fa22941f82e8402b53561f756250a1ccadc237a Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 18:52:39 -0700 Subject: [PATCH 09/33] fix multisign so tests pass --- xrpl/asyncio/transaction/main.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 1bdb8a8ac..7bc35fd02 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -87,7 +87,7 @@ def sign( Returns: The signed transaction blob. """ - transaction_json = _prepare_transaction(transaction, wallet) + transaction_json = _prepare_transaction(transaction) if multisign: signature = keypairs_sign( bytes.fromhex( @@ -108,6 +108,7 @@ def sign( ] return Transaction.from_dict(tx_dict) + transaction_json["SigningPubKey"] = wallet.public_key serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = keypairs_sign(serialized_bytes, wallet.private_key) @@ -176,10 +177,7 @@ async def submit( raise XRPLRequestFailureException(response.result) -def _prepare_transaction( - transaction: Transaction, - wallet: Wallet, -) -> Dict[str, Any]: +def _prepare_transaction(transaction: Transaction) -> Dict[str, Any]: """ Prepares a Transaction by converting it to a JSON-like dictionary, converting the field names to CamelCase. If a Client is provided, then it also autofills any @@ -201,7 +199,6 @@ def _prepare_transaction( raise XRPLException("Cannot directly sign a batch inner transaction.") transaction_json = transaction.to_xrpl() - transaction_json["SigningPubKey"] = wallet.public_key _validate_account_xaddress(transaction_json, "Account", "SourceTag") if "Destination" in transaction_json: From 5a000f7d1b8353379a3b07f79a1b54209812e4b4 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 18:58:07 -0700 Subject: [PATCH 10/33] improve typing --- xrpl/asyncio/transaction/main.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 7bc35fd02..e4480e97e 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -35,6 +35,8 @@ # TODO: make this dynamic based on the current ledger fee _OWNER_RESERVE_FEE: Final[int] = int(xrp_to_drops(2)) +T = TypeVar("T", bound=Transaction, default=Transaction) + async def sign_and_submit( transaction: Transaction, @@ -72,10 +74,10 @@ async def sign_and_submit( # It to a central location as part of the xrpl-py 2.0 changes. It is aliased in # The synchronous half of the library as well. def sign( - transaction: Transaction, + transaction: T, wallet: Wallet, multisign: bool = False, -) -> Transaction: +) -> T: """ Signs a transaction locally, without trusting external rippled nodes. @@ -106,22 +108,22 @@ def sign( signing_pub_key=wallet.public_key, ) ] - return Transaction.from_dict(tx_dict) + return cast(T, Transaction.from_dict(tx_dict)) transaction_json["SigningPubKey"] = wallet.public_key serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = keypairs_sign(serialized_bytes, wallet.private_key) transaction_json["TxnSignature"] = signature - return Transaction.from_xrpl(transaction_json) + return cast(T, Transaction.from_dict(transaction_json)) async def autofill_and_sign( - transaction: Transaction, + transaction: T, client: Client, wallet: Wallet, check_fee: bool = True, -) -> Transaction: +) -> T: """ Autofills relevant fields. Then, signs a transaction locally, without trusting external rippled nodes. @@ -215,9 +217,6 @@ def _prepare_transaction(transaction: Transaction) -> Dict[str, Any]: return transaction_json -T = TypeVar("T", bound=Transaction, default=Transaction) - - async def autofill( transaction: T, client: Client, signers_count: Optional[int] = None ) -> T: From 76d3666ecbec93ebf08a63b468b299c8970d177f Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 19:06:18 -0700 Subject: [PATCH 11/33] improve tests, fix more issues --- .../models/transactions/test_transaction.py | 5 +++++ xrpl/asyncio/transaction/main.py | 22 +++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/unit/models/transactions/test_transaction.py b/tests/unit/models/transactions/test_transaction.py index 763f92f04..f9d3aae84 100644 --- a/tests/unit/models/transactions/test_transaction.py +++ b/tests/unit/models/transactions/test_transaction.py @@ -167,6 +167,11 @@ def test_multisigned_transaction_xaddress(self): tx_1 = sign(tx, _FIRST_SIGNER, multisign=True) tx_2 = sign(tx, _SECOND_SIGNER, multisign=True) + for tx_signed in (tx_1, tx_2): + self.assertEqual(tx_signed.account, _WALLET.address) + self.assertEqual(tx_signed.source_tag, 1) + self.assertEqual(tx_signed.authorize, _ACCOUNT) + multisigned_tx = multisign(tx, [tx_1, tx_2]) self.assertTrue(multisigned_tx.is_signed()) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index e4480e97e..71d036a6f 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -16,7 +16,7 @@ from xrpl.models.response import Response from xrpl.models.transactions import EscrowFinish from xrpl.models.transactions.batch import Batch -from xrpl.models.transactions.transaction import Signer, Transaction +from xrpl.models.transactions.transaction import Transaction from xrpl.models.transactions.transaction import ( transaction_json_to_binary_codec_form as model_transaction_to_binary_codec, ) @@ -100,22 +100,23 @@ def sign( ), wallet.private_key, ) - tx_dict = transaction.to_dict() - tx_dict["signers"] = [ - Signer( - account=wallet.address, - txn_signature=signature, - signing_pub_key=wallet.public_key, - ) + transaction_json["Signers"] = [ + { + "Signer": { + "Account": wallet.address, + "TxnSignature": signature, + "SigningPubKey": wallet.public_key, + } + } ] - return cast(T, Transaction.from_dict(tx_dict)) + return cast(T, Transaction.from_xrpl(transaction_json)) transaction_json["SigningPubKey"] = wallet.public_key serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = keypairs_sign(serialized_bytes, wallet.private_key) transaction_json["TxnSignature"] = signature - return cast(T, Transaction.from_dict(transaction_json)) + return cast(T, Transaction.from_xrpl(transaction_json)) async def autofill_and_sign( @@ -187,7 +188,6 @@ def _prepare_transaction(transaction: Transaction) -> Dict[str, Any]: Args: transaction: the Transaction to be prepared. - wallet: the wallet that will be used for signing. Returns: A JSON-like dictionary that is ready to be signed. From 5e7bc747d4401fb95e8e87f211353ada0a1bb3a0 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 19:07:34 -0700 Subject: [PATCH 12/33] more cleanup --- .../unit/models/transactions/test_transaction.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/models/transactions/test_transaction.py b/tests/unit/models/transactions/test_transaction.py index f9d3aae84..c1d11509c 100644 --- a/tests/unit/models/transactions/test_transaction.py +++ b/tests/unit/models/transactions/test_transaction.py @@ -179,8 +179,8 @@ def test_multisigned_transaction_xaddress(self): def test_payment_txn_api_no_deliver_max(self): delivered_amount = "200000" payment_tx_json = { - "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", - "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "Account": _WALLET.address, + "Destination": _ACCOUNT, "TransactionType": "Payment", "Amount": delivered_amount, "Fee": "15", @@ -195,8 +195,8 @@ def test_payment_txn_api_no_deliver_max(self): def test_payment_txn_api_no_amount(self): delivered_amount = "200000" payment_tx_json = { - "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", - "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "Account": _WALLET.address, + "Destination": _ACCOUNT, "TransactionType": "Payment", "DeliverMax": delivered_amount, "Fee": "15", @@ -210,8 +210,8 @@ def test_payment_txn_api_no_amount(self): def test_payment_txn_api_different_amount_and_deliver_max(self): payment_tx_json = { - "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", - "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "Account": _WALLET.address, + "Destination": _ACCOUNT, "TransactionType": "Payment", "DeliverMax": "200000", "Amount": "200010", @@ -227,8 +227,8 @@ def test_payment_txn_api_different_amount_and_deliver_max(self): def test_payment_txn_api_identical_amount_and_deliver_max(self): delivered_amount = "200000" payment_tx_json = { - "Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e", - "Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ", + "Account": _WALLET.address, + "Destination": _ACCOUNT, "TransactionType": "Payment", "DeliverMax": delivered_amount, "Amount": delivered_amount, From 68ba657d6abc61d18344ad9bfe46730fad454c47 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 19:08:37 -0700 Subject: [PATCH 13/33] more typing improvements --- xrpl/transaction/main.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/xrpl/transaction/main.py b/xrpl/transaction/main.py index 99f1ff43e..151f91bca 100644 --- a/xrpl/transaction/main.py +++ b/xrpl/transaction/main.py @@ -1,13 +1,18 @@ """High-level transaction methods with XRPL transactions.""" + import asyncio from typing import Optional +from typing_extensions import TypeVar + from xrpl.asyncio.transaction import main from xrpl.clients.sync_client import SyncClient from xrpl.models.response import Response from xrpl.models.transactions.transaction import Transaction from xrpl.wallet.main import Wallet +T = TypeVar("T", bound=Transaction, default=Transaction) + def sign_and_submit( transaction: Transaction, @@ -77,11 +82,11 @@ def submit( def autofill_and_sign( - transaction: Transaction, + transaction: T, client: SyncClient, wallet: Wallet, check_fee: bool = True, -) -> Transaction: +) -> T: """ Signs a transaction locally, without trusting external rippled nodes. Autofills relevant fields. @@ -107,8 +112,8 @@ def autofill_and_sign( def autofill( - transaction: Transaction, client: SyncClient, signers_count: Optional[int] = None -) -> Transaction: + transaction: T, client: SyncClient, signers_count: Optional[int] = None +) -> T: """ Autofills fields in a transaction. This will set `sequence`, `fee`, and `last_ledger_sequence` according to the current state of the server this Client is From 7aae9d8f32e1cf63242706136016cf7c4723590d Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 19:09:47 -0700 Subject: [PATCH 14/33] update changelog --- .vscode/settings.json | 7 ++++++- CHANGELOG.md | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 53c45e2e6..2921648bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "aiounittest", "altnet", "asyncio", + "autofilling", "autofills", "binarycodec", "Clawback", @@ -12,6 +13,7 @@ "keypairs", "multisign", "multisigned", + "multisigning", "nftoken", "PATHSET", "rippletest", @@ -28,5 +30,8 @@ "source.organizeImports": "always" } }, - "isort.args": ["--profile", "black"] + "isort.args": [ + "--profile", + "black" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 391e62823..c49e5345c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### BREAKING CHANGE: - Remove Python 3.7 support to fix dependency installation and use 3.8 as new default. +### Added +- Support for the `Batch` amendment (XLS-56d). + +### Fixed +- Handle autofilling better when multisigning transactions. + ## [3.0.0] - 2024-07-16 ### BREAKING CHANGE From 786e156d3fea113f0a43d5769f1e180d6e6ac6f6 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 22:26:13 -0700 Subject: [PATCH 15/33] add binary codec batch encoding --- tests/unit/core/binarycodec/test_main.py | 27 +++++++++++++++++++++++ xrpl/core/binarycodec/main.py | 28 ++++++++++++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/tests/unit/core/binarycodec/test_main.py b/tests/unit/core/binarycodec/test_main.py index 6c6ead083..81751bb32 100644 --- a/tests/unit/core/binarycodec/test_main.py +++ b/tests/unit/core/binarycodec/test_main.py @@ -9,6 +9,7 @@ from xrpl.core.binarycodec.main import ( decode, encode, + encode_for_batch, encode_for_multisigning, encode_for_signing, encode_for_signing_claim, @@ -401,6 +402,32 @@ def test_claim(self): ) self.assertEqual(encode_for_signing_claim(json), expected) + def test_batch(self): + flags = 1 + tx_ids = [ + "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", + "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", + ] + + json = {"flags": flags, "tx_ids": tx_ids} + actual = encode_for_batch(json) + self.assertEqual( + actual, + "".join( + [ + # hash prefix + "42434800", + # flags + "00000001", + # tx_ids length + "00000002", + # tx_ids + "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", + "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", + ] + ), + ) + def test_multisig(self): signing_account = "rJZdUusLDtY9NEsGea7ijqhVrXv98rYBYN" multisig_json = {**signing_json, "SigningPubKey": ""} diff --git a/xrpl/core/binarycodec/main.py b/xrpl/core/binarycodec/main.py index a2b5cec86..603bdd8d9 100644 --- a/xrpl/core/binarycodec/main.py +++ b/xrpl/core/binarycodec/main.py @@ -8,10 +8,7 @@ from typing_extensions import Final from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser -from xrpl.core.binarycodec.types.account_id import AccountID -from xrpl.core.binarycodec.types.hash256 import Hash256 -from xrpl.core.binarycodec.types.st_object import STObject -from xrpl.core.binarycodec.types.uint64 import UInt64 +from xrpl.core.binarycodec.types import AccountID, Hash256, STObject, UInt32, UInt64 def _num_to_bytes(num: int) -> bytes: @@ -21,6 +18,7 @@ def _num_to_bytes(num: int) -> bytes: _TRANSACTION_SIGNATURE_PREFIX: Final[bytes] = _num_to_bytes(0x53545800) _PAYMENT_CHANNEL_CLAIM_PREFIX: Final[bytes] = _num_to_bytes(0x434C4D00) _TRANSACTION_MULTISIG_PREFIX: Final[bytes] = _num_to_bytes(0x534D5400) +_BATCH_PREFIX: Final[bytes] = _num_to_bytes(0x42434800) def encode(json: Dict[str, Any]) -> str: @@ -73,6 +71,28 @@ def encode_for_signing_claim(json: Dict[str, Any]) -> str: return buffer.hex().upper() +def encode_for_batch(json: Dict[str, Any]) -> str: + """ + Encode a Batch transaction's data to be signed. + + Args: + json: A JSON-like dictionary representation of Batch data. + + Returns: + The binary-encoded Batch data, ready to be signed. + """ + prefix = _BATCH_PREFIX + flags = UInt32.from_value(json["flags"]) + tx_ids = json["tx_ids"] + len_tx_ids = UInt32.from_value(len(tx_ids)) + + buffer = prefix + bytes(flags) + bytes(len_tx_ids) + for tx in tx_ids: + buffer += bytes(Hash256.from_value(tx)) + + return buffer.hex().upper() + + def encode_for_multisigning(json: Dict[str, Any], signing_account: str) -> str: """ Encode a transaction into binary format in preparation for providing one From 1a8939d6c66148c2d3db6c43bef8354aa220ddc9 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 9 Oct 2024 22:49:00 -0700 Subject: [PATCH 16/33] add multi-account batch signing helper function --- .vscode/settings.json | 1 + CHANGELOG.md | 1 + xrpl/asyncio/transaction/main.py | 4 +-- xrpl/core/binarycodec/__init__.py | 3 ++ xrpl/wallet/batch_signers.py | 56 +++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 xrpl/wallet/batch_signers.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 2921648bc..ca5cdb549 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "isnumeric", "keypair", "keypairs", + "multiaccount", "multisign", "multisigned", "multisigning", diff --git a/CHANGELOG.md b/CHANGELOG.md index c49e5345c..742347bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Handle autofilling better when multisigning transactions. +- Better typing for transaction-related helper functions. ## [3.0.0] - 2024-07-16 diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 71d036a6f..05b1ad485 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -14,9 +14,7 @@ from xrpl.core.keypairs.main import sign as keypairs_sign from xrpl.models.requests import ServerInfo, ServerState, SubmitOnly from xrpl.models.response import Response -from xrpl.models.transactions import EscrowFinish -from xrpl.models.transactions.batch import Batch -from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions import Batch, EscrowFinish, Transaction from xrpl.models.transactions.transaction import ( transaction_json_to_binary_codec_form as model_transaction_to_binary_codec, ) diff --git a/xrpl/core/binarycodec/__init__.py b/xrpl/core/binarycodec/__init__.py index b73ffeb68..ae5937fd0 100644 --- a/xrpl/core/binarycodec/__init__.py +++ b/xrpl/core/binarycodec/__init__.py @@ -2,10 +2,12 @@ Functions for encoding objects into the XRP Ledger's canonical binary format and decoding them. """ + from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException from xrpl.core.binarycodec.main import ( decode, encode, + encode_for_batch, encode_for_multisigning, encode_for_signing, encode_for_signing_claim, @@ -14,6 +16,7 @@ __all__ = [ "decode", "encode", + "encode_for_batch", "encode_for_multisigning", "encode_for_signing", "encode_for_signing_claim", diff --git a/xrpl/wallet/batch_signers.py b/xrpl/wallet/batch_signers.py new file mode 100644 index 000000000..33078fa73 --- /dev/null +++ b/xrpl/wallet/batch_signers.py @@ -0,0 +1,56 @@ +"""Helper functions for signing multi-account Batch transactions.""" + +from typing import Any, Dict, Optional + +from xrpl.constants import XRPLException +from xrpl.core.binarycodec import encode_for_batch +from xrpl.core.keypairs import sign +from xrpl.models.transactions import Batch, Signer +from xrpl.models.transactions.batch import BatchSigner +from xrpl.wallet import Wallet + + +def sign_multiaccount_batch( + wallet: Wallet, transaction: Batch, multisign: Optional[bool] = False +) -> Batch: + """ + Sign a multi-account Batch transaction. + + Args: + wallet: Wallet instance. + transaction: The Batch transaction to sign. + multisign: Specify true/false to use multisign. Defaults to False. + + Raises: + XRPLException: If the wallet signing the transaction doesn't have an account in + the Batch. + + Returns: + The Batch transaction with the BatchSigner included. + """ + involved_accounts = set(tx.account for tx in transaction.raw_transactions) + if wallet.address not in involved_accounts: + raise XRPLException("Must be signing for an address included in the Batch.") + + fields_to_sign: Dict[str, Any] = { + "flags": transaction.flags, + "tx_ids": transaction.tx_ids, + } + if multisign: + signer = Signer( + account=wallet.address, + signing_pub_key=wallet.public_key, + txn_signature=sign(encode_for_batch(fields_to_sign), wallet.private_key), + ) + batch_signer = BatchSigner(account=wallet.address, signers=[signer]) + else: + batch_signer = BatchSigner( + account=wallet.address, + signing_pub_key=wallet.public_key, + txn_signature=sign(encode_for_batch(fields_to_sign), wallet.private_key), + ) + + transaction_dict = transaction.to_dict() + transaction_dict["batch_signers"] = [batch_signer] + + return Batch.from_dict(transaction_dict) From c5ea66c1b9ada371954ef14cde94a18a96977873 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Oct 2024 09:01:05 -0700 Subject: [PATCH 17/33] add batch signer combine function --- tests/unit/core/binarycodec/test_main.py | 4 +- xrpl/core/binarycodec/__init__.py | 4 +- xrpl/core/binarycodec/main.py | 2 +- xrpl/wallet/batch_signers.py | 85 ++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/tests/unit/core/binarycodec/test_main.py b/tests/unit/core/binarycodec/test_main.py index 81751bb32..1bde083de 100644 --- a/tests/unit/core/binarycodec/test_main.py +++ b/tests/unit/core/binarycodec/test_main.py @@ -9,9 +9,9 @@ from xrpl.core.binarycodec.main import ( decode, encode, - encode_for_batch, encode_for_multisigning, encode_for_signing, + encode_for_signing_batch, encode_for_signing_claim, ) @@ -410,7 +410,7 @@ def test_batch(self): ] json = {"flags": flags, "tx_ids": tx_ids} - actual = encode_for_batch(json) + actual = encode_for_signing_batch(json) self.assertEqual( actual, "".join( diff --git a/xrpl/core/binarycodec/__init__.py b/xrpl/core/binarycodec/__init__.py index ae5937fd0..f7e5bcd36 100644 --- a/xrpl/core/binarycodec/__init__.py +++ b/xrpl/core/binarycodec/__init__.py @@ -7,16 +7,16 @@ from xrpl.core.binarycodec.main import ( decode, encode, - encode_for_batch, encode_for_multisigning, encode_for_signing, + encode_for_signing_batch, encode_for_signing_claim, ) __all__ = [ "decode", "encode", - "encode_for_batch", + "encode_for_signing_batch", "encode_for_multisigning", "encode_for_signing", "encode_for_signing_claim", diff --git a/xrpl/core/binarycodec/main.py b/xrpl/core/binarycodec/main.py index 603bdd8d9..a1f4a35c0 100644 --- a/xrpl/core/binarycodec/main.py +++ b/xrpl/core/binarycodec/main.py @@ -71,7 +71,7 @@ def encode_for_signing_claim(json: Dict[str, Any]) -> str: return buffer.hex().upper() -def encode_for_batch(json: Dict[str, Any]) -> str: +def encode_for_signing_batch(json: Dict[str, Any]) -> str: """ Encode a Batch transaction's data to be signed. diff --git a/xrpl/wallet/batch_signers.py b/xrpl/wallet/batch_signers.py index 33078fa73..7f786a7fa 100644 --- a/xrpl/wallet/batch_signers.py +++ b/xrpl/wallet/batch_signers.py @@ -1,11 +1,12 @@ """Helper functions for signing multi-account Batch transactions.""" -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Union, cast from xrpl.constants import XRPLException -from xrpl.core.binarycodec import encode_for_batch +from xrpl.core.addresscodec.codec import decode_classic_address +from xrpl.core.binarycodec import encode, encode_for_signing_batch from xrpl.core.keypairs import sign -from xrpl.models.transactions import Batch, Signer +from xrpl.models.transactions import Batch, Signer, Transaction from xrpl.models.transactions.batch import BatchSigner from xrpl.wallet import Wallet @@ -40,17 +41,91 @@ def sign_multiaccount_batch( signer = Signer( account=wallet.address, signing_pub_key=wallet.public_key, - txn_signature=sign(encode_for_batch(fields_to_sign), wallet.private_key), + txn_signature=sign( + encode_for_signing_batch(fields_to_sign), wallet.private_key + ), ) batch_signer = BatchSigner(account=wallet.address, signers=[signer]) else: batch_signer = BatchSigner( account=wallet.address, signing_pub_key=wallet.public_key, - txn_signature=sign(encode_for_batch(fields_to_sign), wallet.private_key), + txn_signature=sign( + encode_for_signing_batch(fields_to_sign), wallet.private_key + ), ) transaction_dict = transaction.to_dict() transaction_dict["batch_signers"] = [batch_signer] return Batch.from_dict(transaction_dict) + + +def combine_batch_signers(transactions: List[Union[Batch, str]]) -> str: + """ + Takes several transactions with BatchSigners fields (in object or blob form) and + creates a single transaction with all BatchSigners that then gets signed and + returned. + + Args: + transactions: The transactions to combine `BatchSigners` values on. + + Raises: + XRPLException: If the list of transactions provided is invalid. + + Returns: + A single signed Transaction which has all BatchSigners from transactions within + it. + """ + if len(transactions) == 0: + raise XRPLException("There were 0 transactions to combine.") + + decoded_txs: List[Transaction] = [ + Transaction.from_blob(tx) if isinstance(tx, str) else tx for tx in transactions + ] + for tx in decoded_txs: + if tx.transaction_type != "Batch": + raise XRPLException("TransactionType must be `Batch`.") + batch = cast(Batch, tx) + if batch.batch_signers is None or len(batch.batch_signers) == 0: + raise XRPLException( + "For combining Batch transaction signatures, all transactions must " + "include a BatchSigners field containing an array of signatures." + ) + if ( + tx.signing_pub_key != "" + or tx.txn_signature is not None + or tx.signers is not None + ): + raise XRPLException("Transaction must be unsigned.") + + batch_txs = cast(List[Batch], transactions) + _validate_batch_equivalence(batch_txs) + + return encode(_get_batch_with_all_signers(batch_txs).to_xrpl()) + + return "" + + +def _validate_batch_equivalence(transactions: List[Batch]) -> None: + example_tx = transactions[0] + for tx in transactions: + if tx != example_tx: + raise XRPLException( + "Flags and TxIDs is not the same for all provided transactions." + ) + + +def _get_batch_with_all_signers(transactions: List[Batch]) -> Batch: + batch_signers = [ + signer + for tx in transactions + if tx.batch_signers is not None + for signer in tx.batch_signers + if signer.account != transactions[0].account + ] + batch_signers.sort( + key=lambda signer: decode_classic_address(signer.account).hex().upper() + ) + returned_tx_dict = transactions[0].to_dict() + return Batch.from_dict({**returned_tx_dict, "batch_signers": batch_signers}) From 41fbe404ab4f1c3ebc416c6cd39a1dee2cde07eb Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Oct 2024 13:41:21 -0700 Subject: [PATCH 18/33] move to transaction --- xrpl/transaction/__init__.py | 7 +++++++ xrpl/{wallet => transaction}/batch_signers.py | 0 2 files changed, 7 insertions(+) rename xrpl/{wallet => transaction}/batch_signers.py (100%) diff --git a/xrpl/transaction/__init__.py b/xrpl/transaction/__init__.py index 0e04ea191..e3447af59 100644 --- a/xrpl/transaction/__init__.py +++ b/xrpl/transaction/__init__.py @@ -1,8 +1,13 @@ """Methods for working with transactions on the XRP Ledger.""" + from xrpl.asyncio.transaction import ( XRPLReliableSubmissionException, transaction_json_to_binary_codec_form, ) +from xrpl.transaction.batch_signers import ( + combine_batch_signers, + sign_multiaccount_batch, +) from xrpl.transaction.main import ( autofill, autofill_and_sign, @@ -22,5 +27,7 @@ "submit_and_wait", "transaction_json_to_binary_codec_form", "multisign", + "sign_multiaccount_batch", + "combine_batch_signers", "XRPLReliableSubmissionException", ] diff --git a/xrpl/wallet/batch_signers.py b/xrpl/transaction/batch_signers.py similarity index 100% rename from xrpl/wallet/batch_signers.py rename to xrpl/transaction/batch_signers.py From eb0d88bf3ac42e8b050ed9b5bc7a8fd9f025ec50 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Oct 2024 15:54:04 -0700 Subject: [PATCH 19/33] add tests, fix issues --- tests/unit/transaction/__init__.py | 0 tests/unit/transaction/test_batch_signers.py | 223 +++++++++++++++++++ xrpl/models/base_model.py | 1 + xrpl/models/transactions/batch.py | 36 ++- xrpl/transaction/batch_signers.py | 4 +- 5 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 tests/unit/transaction/__init__.py create mode 100644 tests/unit/transaction/test_batch_signers.py diff --git a/tests/unit/transaction/__init__.py b/tests/unit/transaction/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/transaction/test_batch_signers.py b/tests/unit/transaction/test_batch_signers.py new file mode 100644 index 000000000..0f7db5691 --- /dev/null +++ b/tests/unit/transaction/test_batch_signers.py @@ -0,0 +1,223 @@ +from unittest import TestCase + +from xrpl.constants import CryptoAlgorithm, XRPLException +from xrpl.core.binarycodec.main import decode +from xrpl.models.transactions import Batch +from xrpl.models.transactions.batch import BatchSigner +from xrpl.models.transactions.transaction import Transaction +from xrpl.transaction.batch_signers import ( + combine_batch_signers, + sign_multiaccount_batch, +) +from xrpl.wallet import Wallet + +secp_wallet = Wallet.from_seed( + "spkcsko6Ag3RbCSVXV2FJ8Pd4Zac1", + algorithm=CryptoAlgorithm.SECP256K1, +) +ed_wallet = Wallet.from_seed( + "spkcsko6Ag3RbCSVXV2FJ8Pd4Zac1", + algorithm=CryptoAlgorithm.ED25519, +) +submit_wallet = Wallet.from_seed( + "sEd7HmQFsoyj5TAm6d98gytM9LJA1MF", + algorithm=CryptoAlgorithm.ED25519, +) +other_wallet = Wallet.create() + + +class TestSignMultiAccountBatch(TestCase): + batch_tx = Batch.from_xrpl( + { + "Account": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Flags": 1, + "RawTransactions": [ + { + "RawTransaction": { + "Account": "rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7", + "Amount": "5000000", + "BatchTxn": { + "BatchIndex": 1, + "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Sequence": 215, + }, + "Destination": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + "Fee": "0", + "NetworkID": 21336, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + }, + }, + { + "RawTransaction": { + "Account": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + "Amount": "1000000", + "BatchTxn": { + "BatchIndex": 0, + "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Sequence": 470, + }, + "Destination": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Fee": "0", + "NetworkID": 21336, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + }, + }, + ], + "TransactionType": "Batch", + "TxIDs": [ + "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", + "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", + ], + } + ) + + def test_secp_wallet(self): + result = sign_multiaccount_batch(secp_wallet, self.batch_tx) + expected = [ + BatchSigner( + account="rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + signing_pub_key=( + "02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF80" + "8F3AFC006E3FE" + ), + txn_signature=( + "30450221008E595499C334127A23190F61FB9ADD8B8C501D543E379" + "45B11FABB66B097A6130220138C908E8C4929B47E994A46D611FAC1" + "7AB295CFB8D9E0828B32F2947B97394B" + ), + ) + ] + + self.assertIsNotNone(result.batch_signers) + self.assertEqual(result.batch_signers, expected) + + def test_ed_wallet(self): + result = sign_multiaccount_batch(ed_wallet, self.batch_tx) + expected = [ + BatchSigner( + account="rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7", + signing_pub_key=( + "ED3CC3D14FD80C213BC92A98AFE13A405A030F845EDCFD5E39528" + "6A6E9E62BA638" + ), + txn_signature=( + "E3337EE8C746523B5F96BEBE1190164B8B384EE2DC99F327D95ABC1" + "4E27F3AE16CC00DA7D61FC535DBFF0ADA3AF06394F8A703EE952A14" + "1BD871B75166C5CD0A" + ), + ) + ] + + self.assertIsNotNone(result.batch_signers) + self.assertEqual(result.batch_signers, expected) + + def test_not_included_account(self): + with self.assertRaises(XRPLException): + sign_multiaccount_batch(other_wallet, self.batch_tx) + + +class TestCombineBatchSigners(TestCase): + batch_tx = Batch.from_xrpl( + { + "Account": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Flags": 1, + "LastLedgerSequence": 14973, + "NetworkID": 21336, + "RawTransactions": [ + { + "RawTransaction": { + "Account": "rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7", + "Amount": "5000000", + "BatchTxn": { + "BatchIndex": 1, + "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Sequence": 215, + }, + "Destination": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + "Fee": "0", + "NetworkID": 21336, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + }, + }, + { + "RawTransaction": { + "Account": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + "Amount": "1000000", + "BatchTxn": { + "BatchIndex": 0, + "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Sequence": 470, + }, + "Destination": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Fee": "0", + "NetworkID": 21336, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + }, + }, + ], + "Sequence": 215, + "TransactionType": "Batch", + "TxIDs": [ + "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", + "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", + ], + } + ) + tx1 = sign_multiaccount_batch(ed_wallet, batch_tx) + tx2 = sign_multiaccount_batch(secp_wallet, batch_tx) + expected_valid = tx1.to_xrpl().get("BatchSigners", []) + tx2.to_xrpl().get( + "BatchSigners", [] + ) + + def test_valid(self): + result = combine_batch_signers([self.tx1, self.tx2]) + self.assertEqual(decode(result)["BatchSigners"], self.expected_valid) + + def test_valid_serialized(self): + result = combine_batch_signers([self.tx1.blob(), self.tx2.blob()]) + self.assertEqual(decode(result)["BatchSigners"], self.expected_valid) + + def test_valid_sorted(self): + result = combine_batch_signers([self.tx2, self.tx1]) + self.assertEqual(decode(result)["BatchSigners"], self.expected_valid) + + def test_remove_submitter_signature(self): + tx = Transaction.from_xrpl( + { + "Account": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Amount": "1000000", + "BatchTxn": { + "BatchIndex": 0, + "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", + "Sequence": 470, + }, + "Destination": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", + "Fee": "0", + "NetworkID": 21336, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + } + ) + original_dict = self.batch_tx.to_xrpl() + original_dict["RawTransactions"].append({"RawTransaction": tx.to_xrpl()}) + original_dict["TxIDs"].append(tx.get_hash()) + + batch_tx = Batch.from_xrpl(original_dict) + tx1 = sign_multiaccount_batch(ed_wallet, batch_tx) + tx2 = sign_multiaccount_batch(secp_wallet, batch_tx) + tx3 = sign_multiaccount_batch(submit_wallet, batch_tx) + + result = combine_batch_signers([tx1, tx2, tx3]) + expected_valid = tx1.to_xrpl().get("BatchSigners", []) + tx2.to_xrpl().get( + "BatchSigners", [] + ) + self.assertEqual(decode(result)["BatchSigners"], expected_valid) diff --git a/xrpl/models/base_model.py b/xrpl/models/base_model.py index 84c0416fc..22ba16304 100644 --- a/xrpl/models/base_model.py +++ b/xrpl/models/base_model.py @@ -39,6 +39,7 @@ "amm": "AMM", "did": "DID", "id": "ID", + "ids": "IDs", "lp": "LP", "nftoken": "NFToken", "unl": "UNL", diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py index ab9be1ab0..425004774 100644 --- a/xrpl/models/transactions/batch.py +++ b/xrpl/models/transactions/batch.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Any, Dict, List, Optional, Self, Type from xrpl.models.nested_model import NestedModel from xrpl.models.required import REQUIRED @@ -39,3 +39,37 @@ class Batch(Transaction): default=TransactionType.BATCH, init=False, ) + + @classmethod + def from_dict(cls: Type[Self], value: Dict[str, Any]) -> Self: + """ + Construct a new Batch from a dictionary of parameters. + + Args: + value: The value to construct the Batch from. + + Returns: + A new Batch object, constructed using the given parameters. + + Raises: + XRPLModelException: If the dictionary provided is invalid. + """ + new_value = {**value} + new_value["raw_transactions"] = [ + tx["raw_transaction"] if "raw_transaction" in tx else tx + for tx in value["raw_transactions"] + ] + return super(Transaction, cls).from_dict(new_value) + + def to_dict(self: Self) -> Dict[str, Any]: + """ + Returns the dictionary representation of a Batch. + + Returns: + The dictionary representation of a Batch. + """ + tx_dict = super().to_dict() + tx_dict["raw_transactions"] = [ + {"raw_transaction": tx} for tx in tx_dict["raw_transactions"] + ] + return tx_dict diff --git a/xrpl/transaction/batch_signers.py b/xrpl/transaction/batch_signers.py index 7f786a7fa..93c20f353 100644 --- a/xrpl/transaction/batch_signers.py +++ b/xrpl/transaction/batch_signers.py @@ -99,7 +99,7 @@ def combine_batch_signers(transactions: List[Union[Batch, str]]) -> str: ): raise XRPLException("Transaction must be unsigned.") - batch_txs = cast(List[Batch], transactions) + batch_txs = cast(List[Batch], decoded_txs) _validate_batch_equivalence(batch_txs) return encode(_get_batch_with_all_signers(batch_txs).to_xrpl()) @@ -110,7 +110,7 @@ def combine_batch_signers(transactions: List[Union[Batch, str]]) -> str: def _validate_batch_equivalence(transactions: List[Batch]) -> None: example_tx = transactions[0] for tx in transactions: - if tx != example_tx: + if tx.flags != example_tx.flags or tx.tx_ids != example_tx.tx_ids: raise XRPLException( "Flags and TxIDs is not the same for all provided transactions." ) From ca6caece682a7c3a9a1d944bb9e4d639ca30092a Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Oct 2024 15:57:17 -0700 Subject: [PATCH 20/33] fix typing issue --- xrpl/models/transactions/batch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py index 425004774..3d83cbec6 100644 --- a/xrpl/models/transactions/batch.py +++ b/xrpl/models/transactions/batch.py @@ -3,7 +3,9 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Self, Type +from typing import Any, Dict, List, Optional, Type + +from typing_extensions import Self from xrpl.models.nested_model import NestedModel from xrpl.models.required import REQUIRED From d3da22a829e05b9e5accbc2019263c45327ebade Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Oct 2024 16:08:18 -0700 Subject: [PATCH 21/33] fix tests --- xrpl/asyncio/transaction/main.py | 4 +++- xrpl/models/transactions/batch.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 05b1ad485..120183b2b 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -249,7 +249,9 @@ async def autofill( transaction_json["last_ledger_sequence"] = ledger_sequence + _LEDGER_OFFSET if transaction.transaction_type == TransactionType.BATCH: inner_txs, tx_ids = await _autofill_batch(client, cast(Batch, transaction)) - transaction_json["raw_transactions"] = inner_txs + transaction_json["raw_transactions"] = [ + {"raw_transaction": tx} for tx in inner_txs + ] if "tx_ids" not in transaction_json: transaction_json["tx_ids"] = tx_ids return cast(T, Transaction.from_dict(transaction_json)) diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py index 3d83cbec6..945de3241 100644 --- a/xrpl/models/transactions/batch.py +++ b/xrpl/models/transactions/batch.py @@ -58,7 +58,11 @@ def from_dict(cls: Type[Self], value: Dict[str, Any]) -> Self: """ new_value = {**value} new_value["raw_transactions"] = [ - tx["raw_transaction"] if "raw_transaction" in tx else tx + ( + tx["raw_transaction"] + if isinstance(tx, dict) and "raw_transaction" in tx + else tx + ) for tx in value["raw_transactions"] ] return super(Transaction, cls).from_dict(new_value) From c4417954d92d39b378399142c84fd301caf41e59 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 10 Oct 2024 16:08:49 -0700 Subject: [PATCH 22/33] Update main.py --- xrpl/asyncio/transaction/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 120183b2b..e8eec063a 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -250,7 +250,7 @@ async def autofill( if transaction.transaction_type == TransactionType.BATCH: inner_txs, tx_ids = await _autofill_batch(client, cast(Batch, transaction)) transaction_json["raw_transactions"] = [ - {"raw_transaction": tx} for tx in inner_txs + {"raw_transaction": tx.to_dict()} for tx in inner_txs ] if "tx_ids" not in transaction_json: transaction_json["tx_ids"] = tx_ids From d2119e11161ad96b728100b52d4b338d2f9b0cae Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 6 Nov 2024 19:37:59 -0500 Subject: [PATCH 23/33] remove BatchTxn field --- tests/integration/sugar/test_transaction.py | 9 ++-- xrpl/asyncio/transaction/main.py | 57 ++++++++++++--------- xrpl/models/transactions/__init__.py | 10 +++- xrpl/models/transactions/transaction.py | 31 ++++++----- 4 files changed, 63 insertions(+), 44 deletions(-) diff --git a/tests/integration/sugar/test_transaction.py b/tests/integration/sugar/test_transaction.py index 0a1477825..21b287270 100644 --- a/tests/integration/sugar/test_transaction.py +++ b/tests/integration/sugar/test_transaction.py @@ -31,6 +31,7 @@ DepositPreauth, EscrowFinish, Payment, + TransactionFlag, ) from xrpl.utils import xrp_to_drops @@ -284,12 +285,8 @@ async def test_batch_autofill(self, client): sequence = await get_next_valid_seq_number(WALLET.address, client) for i in range(len(transaction.raw_transactions)): raw_tx = transaction.raw_transactions[i] - self.assertIsNotNone(raw_tx.batch_txn) - self.assertEqual( - raw_tx.batch_txn.outer_account, "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" - ) - self.assertEqual(raw_tx.batch_txn.sequence, sequence + i) - self.assertEqual(raw_tx.batch_txn.batch_index, i) + self.assertFalse(raw_tx.has_flag(TransactionFlag.TF_INNER_BATCH_TXN)) + self.assertEqual(raw_tx.sequence, sequence + i) self.assertEqual(raw_tx.get_hash(), transaction.tx_ids[i]) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index eeb237913..4a24065ff 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -12,15 +12,22 @@ from xrpl.core.addresscodec import is_valid_xaddress, xaddress_to_classic_address from xrpl.core.binarycodec import encode, encode_for_multisigning, encode_for_signing from xrpl.core.keypairs.main import sign as keypairs_sign -from xrpl.models.requests import ServerInfo, ServerState, SubmitOnly -from xrpl.models.response import Response -from xrpl.models.transactions import Batch, EscrowFinish, Transaction +from xrpl.models import ( + Batch, + EscrowFinish, + Response, + ServerInfo, + ServerState, + SubmitOnly, + Transaction, + TransactionFlag, +) from xrpl.models.transactions.transaction import ( transaction_json_to_binary_codec_form as model_transaction_to_binary_codec, ) from xrpl.models.transactions.types.transaction_type import TransactionType from xrpl.utils import drops_to_xrp, xrp_to_drops -from xrpl.wallet.main import Wallet +from xrpl.wallet import Wallet _LEDGER_OFFSET: Final[int] = 20 # Sidechains are expected to have network IDs above this. @@ -193,7 +200,7 @@ def _prepare_transaction(transaction: Transaction) -> Dict[str, Any]: if an address tag is provided that does not match the X-Address tag, or if attempting to directly sign a Batch inner transaction. """ - if transaction.batch_txn is not None: + if transaction.has_flag(TransactionFlag.TF_INNER_BATCH_TXN): raise XRPLException("Cannot directly sign a batch inner transaction.") transaction_json = transaction.to_xrpl() @@ -231,9 +238,9 @@ async def autofill( The autofilled transaction. """ transaction_json = transaction.to_dict() - if not client.network_id: - await _get_network_id_and_build_version(client) if "network_id" not in transaction_json and _tx_needs_networkID(client): + if not client.network_id: + await _get_network_id_and_build_version(client) transaction_json["network_id"] = client.network_id if "sequence" not in transaction_json: sequence = await get_next_valid_seq_number(transaction_json["account"], client) @@ -246,7 +253,7 @@ async def autofill( ledger_sequence = await get_latest_validated_ledger_sequence(client) transaction_json["last_ledger_sequence"] = ledger_sequence + _LEDGER_OFFSET if transaction.transaction_type == TransactionType.BATCH: - inner_txs, tx_ids = await _autofill_batch(client, cast(Batch, transaction)) + inner_txs, tx_ids = await _autofill_batch(client, transaction_json) transaction_json["raw_transactions"] = [ {"raw_transaction": tx.to_dict()} for tx in inner_txs ] @@ -490,34 +497,34 @@ async def _fetch_owner_reserve_fee(client: Client) -> int: async def _autofill_batch( - client: Client, transaction: Batch + client: Client, transaction_dict: Dict[str, Any] ) -> Tuple[List[Transaction], List[str]]: - account_sequences: Dict[str, int] = {} - batch_index = 0 + transaction = Batch.from_dict(transaction_dict) + assert transaction.sequence is not None + account_sequences: Dict[str, int] = {transaction.account: transaction.sequence} tx_ids: List[str] = [] inner_txs: List[Transaction] = [] for raw_txn in transaction.raw_transactions: - if raw_txn.batch_txn is not None: + if raw_txn.has_flag(TransactionFlag.TF_INNER_BATCH_TXN): inner_txs.append(raw_txn) continue - batch_txn: Dict[str, Any] = {"outer_account": transaction.account} + if raw_txn.sequence is None and raw_txn.ticket_sequence is None: + raw_txn_dict = raw_txn.to_dict() - if raw_txn.account in account_sequences: - batch_txn["sequence"] = account_sequences[raw_txn.account] - account_sequences[raw_txn.account] += 1 - else: - sequence = await get_next_valid_seq_number(raw_txn.account, client) - account_sequences[raw_txn.account] = sequence + 1 - batch_txn["sequence"] = sequence + if raw_txn.account in account_sequences: + raw_txn_dict["sequence"] = account_sequences[raw_txn.account] + account_sequences[raw_txn.account] += 1 + else: + sequence = await get_next_valid_seq_number(raw_txn.account, client) + account_sequences[raw_txn.account] = sequence + 1 + raw_txn_dict["sequence"] = sequence - batch_txn["batch_index"] = batch_index - batch_index += 1 + new_raw_txn = Transaction.from_dict(raw_txn_dict) + else: + new_raw_txn = raw_txn - raw_txn_dict = raw_txn.to_dict() - raw_txn_dict["batch_txn"] = batch_txn - new_raw_txn = Transaction.from_dict(raw_txn_dict) inner_txs.append(new_raw_txn) tx_ids.append(new_raw_txn.get_hash()) diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index df49e3bb1..b7a352716 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -69,7 +69,13 @@ from xrpl.models.transactions.set_regular_key import SetRegularKey from xrpl.models.transactions.signer_list_set import SignerEntry, SignerListSet from xrpl.models.transactions.ticket_create import TicketCreate -from xrpl.models.transactions.transaction import Memo, Signer, Transaction +from xrpl.models.transactions.transaction import ( + Memo, + Signer, + Transaction, + TransactionFlag, + TransactionFlagInterface, +) from xrpl.models.transactions.trust_set import ( TrustSet, TrustSetFlag, @@ -153,6 +159,8 @@ "SignerListSet", "TicketCreate", "Transaction", + "TransactionFlag", + "TransactionFlagInterface", "TransactionMetadata", "TrustSet", "TrustSetFlag", diff --git a/xrpl/models/transactions/transaction.py b/xrpl/models/transactions/transaction.py index 41a3bbe9d..b1408773d 100644 --- a/xrpl/models/transactions/transaction.py +++ b/xrpl/models/transactions/transaction.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum from hashlib import sha512 from typing import Any, Dict, List, Optional, Type, Union @@ -12,7 +13,11 @@ from xrpl.models.amounts import IssuedCurrencyAmount from xrpl.models.base_model import ABBREVIATIONS, BaseModel from xrpl.models.exceptions import XRPLModelException -from xrpl.models.flags import check_false_flag_definition, interface_to_flag_list +from xrpl.models.flags import ( + FlagInterface, + check_false_flag_definition, + interface_to_flag_list, +) from xrpl.models.nested_model import NestedModel from xrpl.models.requests import PathStep from xrpl.models.required import REQUIRED @@ -153,18 +158,22 @@ class Signer(NestedModel): """ -@require_kwargs_on_init -@dataclass(frozen=True, **KW_ONLY_DATACLASS) -class BatchTxn(NestedModel): - """Represents the info indicating a Batch transaction.""" +class TransactionFlag(int, Enum): + """ + Transactions of the Transaction type support additional values in the Flags field. + This enum represents those options. + """ - outer_account: str = REQUIRED # type: ignore + TF_INNER_BATCH_TXN = 0x40000000 - sequence: Optional[int] = None - ticket_sequence: Optional[int] = None +class TransactionFlagInterface(FlagInterface): + """ + Transactions of the Transaction type support additional values in the Flags field. + This TypedDict represents those options. + """ - batch_index: int = REQUIRED # type: ignore + TF_INNER_BATCH_TXN: bool @require_kwargs_on_init @@ -264,8 +273,6 @@ class Transaction(BaseModel): network_id: Optional[int] = None """The network id of the transaction.""" - batch_txn: Optional[BatchTxn] = None - def _get_errors(self: Self) -> Dict[str, str]: # import must be here to avoid circular dependencies from xrpl.wallet.main import Wallet @@ -430,7 +437,7 @@ def get_hash(self: Self) -> str: if ( self.txn_signature is None and self.signers is None - and self.batch_txn is None + and not self.has_flag(TransactionFlag.TF_INNER_BATCH_TXN) ): raise XRPLModelException( "Cannot get the hash from an unsigned Transaction." From f9cbea77dcdbac4135e3e56a302a4d0f135cafac Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 11:03:39 -0500 Subject: [PATCH 24/33] fix tests --- tests/unit/transaction/test_batch_signers.py | 30 ++++---------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/tests/unit/transaction/test_batch_signers.py b/tests/unit/transaction/test_batch_signers.py index 0f7db5691..eec1ae3ed 100644 --- a/tests/unit/transaction/test_batch_signers.py +++ b/tests/unit/transaction/test_batch_signers.py @@ -35,12 +35,8 @@ class TestSignMultiAccountBatch(TestCase): { "RawTransaction": { "Account": "rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7", + "Flags": 1073741824, "Amount": "5000000", - "BatchTxn": { - "BatchIndex": 1, - "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", - "Sequence": 215, - }, "Destination": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "Fee": "0", "NetworkID": 21336, @@ -53,11 +49,7 @@ class TestSignMultiAccountBatch(TestCase): "RawTransaction": { "Account": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "Amount": "1000000", - "BatchTxn": { - "BatchIndex": 0, - "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", - "Sequence": 470, - }, + "Flags": 1073741824, "Destination": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", "Fee": "0", "NetworkID": 21336, @@ -132,11 +124,7 @@ class TestCombineBatchSigners(TestCase): "RawTransaction": { "Account": "rJy554HmWFFJQGnRfZuoo8nV97XSMq77h7", "Amount": "5000000", - "BatchTxn": { - "BatchIndex": 1, - "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", - "Sequence": 215, - }, + "Flags": 1073741824, "Destination": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "Fee": "0", "NetworkID": 21336, @@ -149,11 +137,7 @@ class TestCombineBatchSigners(TestCase): "RawTransaction": { "Account": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "Amount": "1000000", - "BatchTxn": { - "BatchIndex": 0, - "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", - "Sequence": 470, - }, + "Flags": 1073741824, "Destination": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", "Fee": "0", "NetworkID": 21336, @@ -194,11 +178,7 @@ def test_remove_submitter_signature(self): { "Account": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", "Amount": "1000000", - "BatchTxn": { - "BatchIndex": 0, - "OuterAccount": "rJCxK2hX9tDMzbnn3cg1GU2g19Kfmhzxkp", - "Sequence": 470, - }, + "Flags": 1073741824, "Destination": "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK", "Fee": "0", "NetworkID": 21336, From 7d842c6ab22670583ea6965344608c70c8e35547 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 11:03:49 -0500 Subject: [PATCH 25/33] fix unrelated TicketSequence bug --- xrpl/asyncio/transaction/main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 4a24065ff..555e06032 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -243,7 +243,12 @@ async def autofill( await _get_network_id_and_build_version(client) transaction_json["network_id"] = client.network_id if "sequence" not in transaction_json: - sequence = await get_next_valid_seq_number(transaction_json["account"], client) + if "ticket_sequence" in transaction_json: + sequence = 0 + else: + sequence = await get_next_valid_seq_number( + transaction_json["account"], client + ) transaction_json["sequence"] = sequence if "fee" not in transaction_json: transaction_json["fee"] = await _calculate_fee_per_transaction_type( From 2a632cf7be72f2d1083fd61a43a62640a9216b39 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 11:27:23 -0500 Subject: [PATCH 26/33] improve autofilling --- xrpl/asyncio/transaction/main.py | 67 ++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 555e06032..b8fb9a3f4 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -224,9 +224,9 @@ async def autofill( transaction: T, client: Client, signers_count: Optional[int] = None ) -> T: """ - Autofills fields in a transaction. This will set `sequence`, `fee`, and - `last_ledger_sequence` according to the current state of the server this Client is - connected to. It also converts all X-Addresses to classic addresses. + Autofills fields in a transaction. This will set all autofill-able fields according + to the current state of the server this Client is connected to. It also converts all + X-Addresses to classic addresses. Args: transaction: the transaction to be signed. @@ -234,6 +234,9 @@ async def autofill( signers_count: the expected number of signers for this transaction. Only used for multisigned transactions. + Raises: + XRPLException: If a field is pre-filled out incorrectly. + Returns: The autofilled transaction. """ @@ -259,10 +262,13 @@ async def autofill( transaction_json["last_ledger_sequence"] = ledger_sequence + _LEDGER_OFFSET if transaction.transaction_type == TransactionType.BATCH: inner_txs, tx_ids = await _autofill_batch(client, transaction_json) - transaction_json["raw_transactions"] = [ - {"raw_transaction": tx.to_dict()} for tx in inner_txs - ] - if "tx_ids" not in transaction_json: + transaction_json["raw_transactions"] = inner_txs + if "tx_ids" in transaction_json: + if transaction_json["tx_ids"] != tx_ids: + raise XRPLException( + "Batch `TxIDs` don't match what `autofill` generated." + ) + else: transaction_json["tx_ids"] = tx_ids return cast(T, Transaction.from_dict(transaction_json)) @@ -503,21 +509,21 @@ async def _fetch_owner_reserve_fee(client: Client) -> int: async def _autofill_batch( client: Client, transaction_dict: Dict[str, Any] -) -> Tuple[List[Transaction], List[str]]: +) -> Tuple[List[Dict[str, Any]], List[str]]: transaction = Batch.from_dict(transaction_dict) assert transaction.sequence is not None account_sequences: Dict[str, int] = {transaction.account: transaction.sequence} tx_ids: List[str] = [] - inner_txs: List[Transaction] = [] + inner_txs: List[Dict[str, Any]] = [] for raw_txn in transaction.raw_transactions: if raw_txn.has_flag(TransactionFlag.TF_INNER_BATCH_TXN): - inner_txs.append(raw_txn) + inner_txs.append(raw_txn.to_dict()) continue + raw_txn_dict = raw_txn.to_dict() if raw_txn.sequence is None and raw_txn.ticket_sequence is None: - raw_txn_dict = raw_txn.to_dict() - + # autofill sequence if raw_txn.account in account_sequences: raw_txn_dict["sequence"] = account_sequences[raw_txn.account] account_sequences[raw_txn.account] += 1 @@ -526,11 +532,38 @@ async def _autofill_batch( account_sequences[raw_txn.account] = sequence + 1 raw_txn_dict["sequence"] = sequence - new_raw_txn = Transaction.from_dict(raw_txn_dict) - else: - new_raw_txn = raw_txn + if raw_txn.is_signed(): + raise XRPLException("Inner Batch transactions must not be signed.") + + # validate fields that are supposed to be empty/zeroed + def _validate_field(field_name: str, expected_value: str) -> None: + if raw_txn_dict[field_name] is None: + raw_txn_dict[field_name] = expected_value + elif raw_txn_dict[field_name] != expected_value: + raise XRPLException( + f"Must have a `{field_name}` of ${repr(expected_value)} in an " + "inner Batch transaction." + ) + + _validate_field("fee", "0") + _validate_field("signing_pub_key", "") + _validate_field("txn_signature", "") + + if raw_txn.signers is not None: + raise XRPLException( + "Must not have a `signers` field in an inner Batch transaction." + ) + if raw_txn.network_id is not None: + raise XRPLException( + "Must not have a `network_id` field in an inner Batch transaction." + ) + if raw_txn.last_ledger_sequence is not None: + raise XRPLException( + "Must not have a `last_ledger_sequence` field in an inner Batch " + "transaction." + ) - inner_txs.append(new_raw_txn) - tx_ids.append(new_raw_txn.get_hash()) + inner_txs.append(raw_txn_dict) + tx_ids.append(Transaction.from_dict(raw_txn_dict).get_hash()) return inner_txs, tx_ids From 6f1cbedfe65fe5205394d68b48d5f4d4d7b173b1 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 11:39:34 -0500 Subject: [PATCH 27/33] better flag handling --- xrpl/asyncio/transaction/main.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index b8fb9a3f4..3ac0a3c44 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -517,11 +517,16 @@ async def _autofill_batch( inner_txs: List[Dict[str, Any]] = [] for raw_txn in transaction.raw_transactions: - if raw_txn.has_flag(TransactionFlag.TF_INNER_BATCH_TXN): - inner_txs.append(raw_txn.to_dict()) - continue - raw_txn_dict = raw_txn.to_dict() + + if not raw_txn.has_flag(TransactionFlag.TF_INNER_BATCH_TXN): + if isinstance(raw_txn.flags, int): + raw_txn_dict["flags"] |= TransactionFlag.TF_INNER_BATCH_TXN + elif isinstance(raw_txn.flags, dict): + raw_txn_dict["flags"]["TF_INNER_BATCH_TXN"] = True + else: # is List[int] + raw_txn_dict["flags"].append(TransactionFlag.TF_INNER_BATCH_TXN) + if raw_txn.sequence is None and raw_txn.ticket_sequence is None: # autofill sequence if raw_txn.account in account_sequences: From 8ca1005b49f5c6d241b568623e7bc0fd3badf9ec Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 11:42:32 -0500 Subject: [PATCH 28/33] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d977520ac..56b3fd915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Grab the FeeSettings values from the latest validated ledger. Remove hard-coded reference to 10 drops as the reference transaction cost. - Handle autofilling better when multisigning transactions. - Better typing for transaction-related helper functions. +- Better handling of `TicketSequence`. ## [3.0.0] - 2024-07-16 @@ -96,7 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.0.0] - 2023-07-05 ### BREAKING CHANGE - The default signing algorithm in the `Wallet` was changed from secp256k1 to ed25519 -- +- ### Added: - Wallet support for regular key compatibility - Added new ways of wallet generation: `from_seed`, `from_secret`, `from_entropy`, `from_secret_numbers` From 77fc6e1df96c285836dc11ad8917498e9e87a353 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 12:02:38 -0500 Subject: [PATCH 29/33] fix integration tests --- tests/integration/sugar/test_transaction.py | 7 ++++++- xrpl/asyncio/transaction/main.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integration/sugar/test_transaction.py b/tests/integration/sugar/test_transaction.py index 21b287270..0ee06a0f4 100644 --- a/tests/integration/sugar/test_transaction.py +++ b/tests/integration/sugar/test_transaction.py @@ -285,9 +285,14 @@ async def test_batch_autofill(self, client): sequence = await get_next_valid_seq_number(WALLET.address, client) for i in range(len(transaction.raw_transactions)): raw_tx = transaction.raw_transactions[i] - self.assertFalse(raw_tx.has_flag(TransactionFlag.TF_INNER_BATCH_TXN)) + self.assertTrue(raw_tx.has_flag(TransactionFlag.TF_INNER_BATCH_TXN)) self.assertEqual(raw_tx.sequence, sequence + i) self.assertEqual(raw_tx.get_hash(), transaction.tx_ids[i]) + self.assertIsNone(raw_tx.network_id) + self.assertIsNone(raw_tx.last_ledger_sequence) + self.assertEqual(raw_tx.fee, "0") + self.assertEqual(raw_tx.signing_pub_key, "") + self.assertEqual(raw_tx.txn_signature, "") class TestSubmitAndWait(IntegrationTestCase): diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 3ac0a3c44..135c0b73d 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -542,7 +542,7 @@ async def _autofill_batch( # validate fields that are supposed to be empty/zeroed def _validate_field(field_name: str, expected_value: str) -> None: - if raw_txn_dict[field_name] is None: + if field_name not in raw_txn_dict: raw_txn_dict[field_name] = expected_value elif raw_txn_dict[field_name] != expected_value: raise XRPLException( From 0792b3e4c56b7ea7528db2a61f0b9bd30fd9460e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 7 Nov 2024 15:14:25 -0500 Subject: [PATCH 30/33] handle batch in batch --- xrpl/asyncio/transaction/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 135c0b73d..38db86b74 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -519,6 +519,11 @@ async def _autofill_batch( for raw_txn in transaction.raw_transactions: raw_txn_dict = raw_txn.to_dict() + if raw_txn.transaction_type == TransactionType.BATCH: + raise XRPLException( + "Cannot have a Batch transaction inside a Batch transaction." + ) + if not raw_txn.has_flag(TransactionFlag.TF_INNER_BATCH_TXN): if isinstance(raw_txn.flags, int): raw_txn_dict["flags"] |= TransactionFlag.TF_INNER_BATCH_TXN From a2890a9d5280bbac079f61e96d183011516eac22 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 8 Nov 2024 10:40:25 -0500 Subject: [PATCH 31/33] get working integration test --- tests/integration/it_utils.py | 4 +- tests/integration/transactions/test_batch.py | 26 +++++++++++ xrpl/asyncio/transaction/main.py | 29 +++++++----- .../binarycodec/definitions/definitions.json | 45 +++++-------------- xrpl/models/transactions/__init__.py | 4 +- xrpl/models/transactions/batch.py | 29 ++++++++++++ 6 files changed, 90 insertions(+), 47 deletions(-) create mode 100644 tests/integration/transactions/test_batch.py diff --git a/tests/integration/it_utils.py b/tests/integration/it_utils.py index b7713c861..b75e630a9 100644 --- a/tests/integration/it_utils.py +++ b/tests/integration/it_utils.py @@ -119,7 +119,9 @@ async def fund_wallet_async( destination=wallet.address, amount=FUNDING_AMOUNT, ) - await sign_and_submit_async(payment, client, MASTER_WALLET, check_fee=True) + await sign_and_submit_async( + payment, client, MASTER_WALLET, autofill=True, check_fee=True + ) await client.request(LEDGER_ACCEPT_REQUEST) diff --git a/tests/integration/transactions/test_batch.py b/tests/integration/transactions/test_batch.py new file mode 100644 index 000000000..cc37d693d --- /dev/null +++ b/tests/integration/transactions/test_batch.py @@ -0,0 +1,26 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.it_utils import ( + sign_and_reliable_submission_async, + test_async_and_sync, +) +from tests.integration.reusable_values import DESTINATION, WALLET +from xrpl.models import Batch, BatchFlag, Payment +from xrpl.models.response import ResponseStatus + + +class TestBatch(IntegrationTestCase): + @test_async_and_sync(globals()) + async def test_basic_functionality(self, client): + payment = Payment( + account=WALLET.address, + amount="1", + destination=DESTINATION.address, + ) + batch = Batch( + account=WALLET.address, + flags=BatchFlag.TF_ALL_OR_NOTHING, + raw_transactions=[payment, payment], + ) + response = await sign_and_reliable_submission_async(batch, WALLET, client) + self.assertEqual(response.status, ResponseStatus.SUCCESS) + self.assertEqual(response.result["engine_result"], "tesSUCCESS") diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 38db86b74..fdf6e5af0 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -241,9 +241,9 @@ async def autofill( The autofilled transaction. """ transaction_json = transaction.to_dict() + if not client.network_id: + await _get_network_id_and_build_version(client) if "network_id" not in transaction_json and _tx_needs_networkID(client): - if not client.network_id: - await _get_network_id_and_build_version(client) transaction_json["network_id"] = client.network_id if "sequence" not in transaction_json: if "ticket_sequence" in transaction_json: @@ -307,7 +307,7 @@ def _tx_needs_networkID(client: Client) -> bool: Returns: bool: whether the transactions required network ID to be valid """ - if client.network_id and client.network_id > _RESTRICTED_NETWORKS: + if client.network_id is not None and client.network_id > _RESTRICTED_NETWORKS: if client.build_version and _is_not_later_rippled_version( _REQUIRED_NETWORKID_VERSION, client.build_version ): @@ -487,12 +487,18 @@ async def _calculate_fee_per_transaction_type( base_fee = math.ceil(net_fee * (33 + (len(fulfillment_bytes) / 16))) # AccountDelete Transaction - if transaction.transaction_type in ( + elif transaction.transaction_type in ( TransactionType.ACCOUNT_DELETE, TransactionType.AMM_CREATE, ): base_fee = await _fetch_owner_reserve_fee(client) + elif transaction.transaction_type == TransactionType.BATCH: + batch = cast(Batch, transaction) + base_fee = base_fee * (2 + len(batch.raw_transactions)) + if batch.batch_signers is not None: + base_fee += base_fee * len(batch.batch_signers) + # Multi-signed Transaction # BaseFee × (1 + Number of Signatures Provided) if signers_count is not None and signers_count > 0: @@ -512,7 +518,7 @@ async def _autofill_batch( ) -> Tuple[List[Dict[str, Any]], List[str]]: transaction = Batch.from_dict(transaction_dict) assert transaction.sequence is not None - account_sequences: Dict[str, int] = {transaction.account: transaction.sequence} + account_sequences: Dict[str, int] = {transaction.account: transaction.sequence + 1} tx_ids: List[str] = [] inner_txs: List[Dict[str, Any]] = [] @@ -557,16 +563,19 @@ def _validate_field(field_name: str, expected_value: str) -> None: _validate_field("fee", "0") _validate_field("signing_pub_key", "") - _validate_field("txn_signature", "") + # _validate_field("txn_signature", "") - if raw_txn.signers is not None: + if raw_txn.txn_signature is not None: raise XRPLException( - "Must not have a `signers` field in an inner Batch transaction." + "Must not have a `txn_signature` field in an inner Batch transaction." ) - if raw_txn.network_id is not None: + if raw_txn.signers is not None: raise XRPLException( - "Must not have a `network_id` field in an inner Batch transaction." + "Must not have a `signers` field in an inner Batch transaction." ) + if raw_txn.network_id is None: + if _tx_needs_networkID(client): + raw_txn_dict["network_id"] = client.network_id if raw_txn.last_ledger_sequence is not None: raise XRPLException( "Must not have a `last_ledger_sequence` field in an inner Batch " diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 07d56e5b5..96db16704 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -260,16 +260,6 @@ "type": "UInt8" } ], - [ - "BatchIndex", - { - "nth": 20, - "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, - "type": "UInt8" - } - ], [ "LedgerEntryType", { @@ -2003,7 +1993,7 @@ [ "InnerResult", { - "nth": 30, + "nth": 32, "isVLEncoded": true, "isSerialized": true, "isSigningField": true, @@ -2170,16 +2160,6 @@ "type": "AccountID" } ], - [ - "OuterAccount", - { - "nth": 24, - "isVLEncoded": true, - "isSerialized": true, - "isSigningField": true, - "type": "AccountID" - } - ], [ "Indexes", { @@ -2223,7 +2203,7 @@ [ "TxIDs", { - "nth": 5, + "nth": 6, "isVLEncoded": true, "isSerialized": true, "isSigningField": true, @@ -2603,7 +2583,7 @@ [ "RawTransaction", { - "nth": 33, + "nth": 34, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2613,7 +2593,7 @@ [ "BatchExecution", { - "nth": 34, + "nth": 35, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2623,7 +2603,7 @@ [ "BatchTxn", { - "nth": 35, + "nth": 36, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2633,7 +2613,7 @@ [ "BatchSigner", { - "nth": 36, + "nth": 37, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2833,7 +2813,7 @@ [ "BatchExecutions", { - "nth": 26, + "nth": 28, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2843,7 +2823,7 @@ [ "RawTransactions", { - "nth": 27, + "nth": 29, "isVLEncoded": false, "isSerialized": true, "isSigningField": true, @@ -2853,7 +2833,7 @@ [ "BatchSigners", { - "nth": 28, + "nth": 30, "isVLEncoded": false, "isSerialized": true, "isSigningField": false, @@ -2879,7 +2859,6 @@ "telREQUIRES_NETWORK_ID": -385, "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, "telENV_RPC_FAILED": -383, - "temMALFORMED": -299, "temBAD_AMOUNT": -298, "temBAD_CURRENCY": -297, @@ -2929,7 +2908,6 @@ "temARRAY_EMPTY": -253, "temARRAY_TOO_LARGE": -252, "temINVALID_BATCH": -251, - "tefFAILURE": -199, "tefALREADY": -198, "tefBAD_ADD_AUTH": -197, @@ -2952,7 +2930,6 @@ "tefNO_TICKET": -180, "tefNFTOKEN_IS_NOT_TRANSFERABLE": -179, "tefINVALID_LEDGER_FIX_TYPE": -178, - "terRETRY": -99, "terFUNDS_SPENT": -98, "terINSUF_FEE_B": -97, @@ -2966,9 +2943,7 @@ "terQUEUED": -89, "terPRE_TICKET": -88, "terNO_AMM": -87, - "tesSUCCESS": 0, - "tecCLAIM": 100, "tecPATH_PARTIAL": 101, "tecUNFUNDED_ADD": 102, @@ -3098,7 +3073,7 @@ "OracleSet": 51, "OracleDelete": 52, "LedgerStateFix": 53, - "Batch": 54, + "Batch": 61, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index b7a352716..c58498d51 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -24,7 +24,7 @@ AMMWithdrawFlag, AMMWithdrawFlagInterface, ) -from xrpl.models.transactions.batch import Batch +from xrpl.models.transactions.batch import Batch, BatchFlag, BatchFlagInterface from xrpl.models.transactions.check_cancel import CheckCancel from xrpl.models.transactions.check_cash import CheckCash from xrpl.models.transactions.check_create import CheckCreate @@ -118,6 +118,8 @@ "AMMWithdrawFlagInterface", "AuthAccount", "Batch", + "BatchFlag", + "BatchFlagInterface", "CheckCancel", "CheckCash", "CheckCreate", diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py index 945de3241..17a71393b 100644 --- a/xrpl/models/transactions/batch.py +++ b/xrpl/models/transactions/batch.py @@ -3,10 +3,12 @@ from __future__ import annotations from dataclasses import dataclass, field +from enum import Enum from typing import Any, Dict, List, Optional, Type from typing_extensions import Self +from xrpl.models.flags import FlagInterface from xrpl.models.nested_model import NestedModel from xrpl.models.required import REQUIRED from xrpl.models.transactions.transaction import Signer, Transaction @@ -14,6 +16,33 @@ from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init +class BatchFlag(int, Enum): + """ + Transactions of the Batch type support additional values in the Flags field. + This enum represents those options. + """ + + TF_ALL_OR_NOTHING = 0x00010000 + + TF_ONLY_ONE = 0x00020000 + + TF_UNTIL_FAILURE = 0x00040000 + + TF_INDEPENDENT = 0x00080000 + + +class BatchFlagInterface(FlagInterface): + """ + Transactions of the Batch type support additional values in the Flags field. + This TypedDict represents those options. + """ + + TF_ALL_OR_NOTHING: bool + TF_ONLY_ONE: bool + TF_UNTIL_FAILURE: bool + TF_INDEPENDENT: bool + + @require_kwargs_on_init @dataclass(frozen=True, **KW_ONLY_DATACLASS) class BatchSigner(NestedModel): From a637da3e326d3aca8764e51c6015c9e23684ab99 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 2 Jan 2025 18:02:28 -0500 Subject: [PATCH 32/33] rename field --- tests/unit/transaction/test_batch_signers.py | 6 +++--- xrpl/asyncio/transaction/main.py | 2 +- xrpl/transaction/batch_signers.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/transaction/test_batch_signers.py b/tests/unit/transaction/test_batch_signers.py index eec1ae3ed..6b4f4ec34 100644 --- a/tests/unit/transaction/test_batch_signers.py +++ b/tests/unit/transaction/test_batch_signers.py @@ -60,7 +60,7 @@ class TestSignMultiAccountBatch(TestCase): }, ], "TransactionType": "Batch", - "TxIDs": [ + "TransactionIDs": [ "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", ], @@ -149,7 +149,7 @@ class TestCombineBatchSigners(TestCase): ], "Sequence": 215, "TransactionType": "Batch", - "TxIDs": [ + "TransactionIDs": [ "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", ], @@ -189,7 +189,7 @@ def test_remove_submitter_signature(self): ) original_dict = self.batch_tx.to_xrpl() original_dict["RawTransactions"].append({"RawTransaction": tx.to_xrpl()}) - original_dict["TxIDs"].append(tx.get_hash()) + original_dict["TransactionIDs"].append(tx.get_hash()) batch_tx = Batch.from_xrpl(original_dict) tx1 = sign_multiaccount_batch(ed_wallet, batch_tx) diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 3619f8275..848a2277e 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -265,7 +265,7 @@ async def autofill( if "tx_ids" in transaction_json: if transaction_json["tx_ids"] != tx_ids: raise XRPLException( - "Batch `TxIDs` don't match what `autofill` generated." + "Batch `TransactionIDs` don't match what `autofill` generated." ) else: transaction_json["tx_ids"] = tx_ids diff --git a/xrpl/transaction/batch_signers.py b/xrpl/transaction/batch_signers.py index 93c20f353..0fa0b9f7b 100644 --- a/xrpl/transaction/batch_signers.py +++ b/xrpl/transaction/batch_signers.py @@ -112,7 +112,8 @@ def _validate_batch_equivalence(transactions: List[Batch]) -> None: for tx in transactions: if tx.flags != example_tx.flags or tx.tx_ids != example_tx.tx_ids: raise XRPLException( - "Flags and TxIDs is not the same for all provided transactions." + "Flags and TransactionIDs are not the same for all provided " + "transactions." ) From 8be504d95c33e382cb025065f1308ecbcb847038 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 2 Jan 2025 18:07:11 -0500 Subject: [PATCH 33/33] more renames --- tests/integration/sugar/test_transaction.py | 4 ++-- tests/unit/core/binarycodec/test_main.py | 8 ++++---- xrpl/asyncio/transaction/main.py | 14 +++++++------- xrpl/core/binarycodec/main.py | 8 ++++---- xrpl/models/transactions/batch.py | 2 +- xrpl/transaction/batch_signers.py | 7 +++++-- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/tests/integration/sugar/test_transaction.py b/tests/integration/sugar/test_transaction.py index 0ee06a0f4..4efc59e73 100644 --- a/tests/integration/sugar/test_transaction.py +++ b/tests/integration/sugar/test_transaction.py @@ -280,14 +280,14 @@ async def test_batch_autofill(self, client): ], ) transaction = await autofill(tx, client) - self.assertEqual(len(transaction.tx_ids), 2) + self.assertEqual(len(transaction.transaction_ids), 2) sequence = await get_next_valid_seq_number(WALLET.address, client) for i in range(len(transaction.raw_transactions)): raw_tx = transaction.raw_transactions[i] self.assertTrue(raw_tx.has_flag(TransactionFlag.TF_INNER_BATCH_TXN)) self.assertEqual(raw_tx.sequence, sequence + i) - self.assertEqual(raw_tx.get_hash(), transaction.tx_ids[i]) + self.assertEqual(raw_tx.get_hash(), transaction.transaction_ids[i]) self.assertIsNone(raw_tx.network_id) self.assertIsNone(raw_tx.last_ledger_sequence) self.assertEqual(raw_tx.fee, "0") diff --git a/tests/unit/core/binarycodec/test_main.py b/tests/unit/core/binarycodec/test_main.py index 1bde083de..3dea3f50d 100644 --- a/tests/unit/core/binarycodec/test_main.py +++ b/tests/unit/core/binarycodec/test_main.py @@ -404,12 +404,12 @@ def test_claim(self): def test_batch(self): flags = 1 - tx_ids = [ + transaction_ids = [ "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", ] - json = {"flags": flags, "tx_ids": tx_ids} + json = {"flags": flags, "transaction_ids": transaction_ids} actual = encode_for_signing_batch(json) self.assertEqual( actual, @@ -419,9 +419,9 @@ def test_batch(self): "42434800", # flags "00000001", - # tx_ids length + # transaction_ids length "00000002", - # tx_ids + # transaction_ids "ABE4871E9083DF66727045D49DEEDD3A6F166EB7F8D1E92FE868F02E76B2C5CA", "795AAC88B59E95C3497609749127E69F12958BC016C600C770AEEB1474C840B4", ] diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 848a2277e..843acab7e 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -260,15 +260,15 @@ async def autofill( ledger_sequence = await get_latest_validated_ledger_sequence(client) transaction_json["last_ledger_sequence"] = ledger_sequence + _LEDGER_OFFSET if transaction.transaction_type == TransactionType.BATCH: - inner_txs, tx_ids = await _autofill_batch(client, transaction_json) + inner_txs, transaction_ids = await _autofill_batch(client, transaction_json) transaction_json["raw_transactions"] = inner_txs - if "tx_ids" in transaction_json: - if transaction_json["tx_ids"] != tx_ids: + if "transaction_ids" in transaction_json: + if transaction_json["transaction_ids"] != transaction_ids: raise XRPLException( "Batch `TransactionIDs` don't match what `autofill` generated." ) else: - transaction_json["tx_ids"] = tx_ids + transaction_json["transaction_ids"] = transaction_ids return cast(T, Transaction.from_dict(transaction_json)) @@ -497,7 +497,7 @@ async def _autofill_batch( transaction = Batch.from_dict(transaction_dict) assert transaction.sequence is not None account_sequences: Dict[str, int] = {transaction.account: transaction.sequence + 1} - tx_ids: List[str] = [] + transaction_ids: List[str] = [] inner_txs: List[Dict[str, Any]] = [] for raw_txn in transaction.raw_transactions: @@ -561,6 +561,6 @@ def _validate_field(field_name: str, expected_value: str) -> None: ) inner_txs.append(raw_txn_dict) - tx_ids.append(Transaction.from_dict(raw_txn_dict).get_hash()) + transaction_ids.append(Transaction.from_dict(raw_txn_dict).get_hash()) - return inner_txs, tx_ids + return inner_txs, transaction_ids diff --git a/xrpl/core/binarycodec/main.py b/xrpl/core/binarycodec/main.py index a1f4a35c0..c8cf0d8b0 100644 --- a/xrpl/core/binarycodec/main.py +++ b/xrpl/core/binarycodec/main.py @@ -83,11 +83,11 @@ def encode_for_signing_batch(json: Dict[str, Any]) -> str: """ prefix = _BATCH_PREFIX flags = UInt32.from_value(json["flags"]) - tx_ids = json["tx_ids"] - len_tx_ids = UInt32.from_value(len(tx_ids)) + transaction_ids = json["transaction_ids"] + len_transaction_ids = UInt32.from_value(len(transaction_ids)) - buffer = prefix + bytes(flags) + bytes(len_tx_ids) - for tx in tx_ids: + buffer = prefix + bytes(flags) + bytes(len_transaction_ids) + for tx in transaction_ids: buffer += bytes(Hash256.from_value(tx)) return buffer.hex().upper() diff --git a/xrpl/models/transactions/batch.py b/xrpl/models/transactions/batch.py index 17a71393b..538a8a006 100644 --- a/xrpl/models/transactions/batch.py +++ b/xrpl/models/transactions/batch.py @@ -63,7 +63,7 @@ class Batch(Transaction): """Represents a Batch transaction.""" raw_transactions: List[Transaction] = REQUIRED # type: ignore - tx_ids: Optional[List[str]] = None + transaction_ids: Optional[List[str]] = None batch_signers: Optional[List[BatchSigner]] = None transaction_type: TransactionType = field( diff --git a/xrpl/transaction/batch_signers.py b/xrpl/transaction/batch_signers.py index 0fa0b9f7b..aa5df1de0 100644 --- a/xrpl/transaction/batch_signers.py +++ b/xrpl/transaction/batch_signers.py @@ -35,7 +35,7 @@ def sign_multiaccount_batch( fields_to_sign: Dict[str, Any] = { "flags": transaction.flags, - "tx_ids": transaction.tx_ids, + "transaction_ids": transaction.transaction_ids, } if multisign: signer = Signer( @@ -110,7 +110,10 @@ def combine_batch_signers(transactions: List[Union[Batch, str]]) -> str: def _validate_batch_equivalence(transactions: List[Batch]) -> None: example_tx = transactions[0] for tx in transactions: - if tx.flags != example_tx.flags or tx.tx_ids != example_tx.tx_ids: + if ( + tx.flags != example_tx.flags + or tx.transaction_ids != example_tx.transaction_ids + ): raise XRPLException( "Flags and TransactionIDs are not the same for all provided " "transactions."