Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Batch amendment #757

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8c1f795
update definitions.json
mvadari Oct 2, 2024
ea28bc4
update scripts after rippled refactor
mvadari Oct 2, 2024
dbedfdc
add LedgerStateFix
mvadari Oct 2, 2024
703f4c8
add basic batch models
mvadari Oct 2, 2024
429d6fc
add autofill
mvadari Oct 3, 2024
2677584
add autofill tests
mvadari Oct 3, 2024
b79c7f7
fix multisign issue, add test
mvadari Oct 3, 2024
2443f4c
rename some tests
mvadari Oct 10, 2024
3fa2294
fix multisign so tests pass
mvadari Oct 10, 2024
5a000f7
improve typing
mvadari Oct 10, 2024
76d3666
improve tests, fix more issues
mvadari Oct 10, 2024
5e7bc74
more cleanup
mvadari Oct 10, 2024
68ba657
more typing improvements
mvadari Oct 10, 2024
7aae9d8
update changelog
mvadari Oct 10, 2024
786e156
add binary codec batch encoding
mvadari Oct 10, 2024
1a8939d
add multi-account batch signing helper function
mvadari Oct 10, 2024
c5ea66c
add batch signer combine function
mvadari Oct 10, 2024
41fbe40
move to transaction
mvadari Oct 10, 2024
eb0d88b
add tests, fix issues
mvadari Oct 10, 2024
ca6caec
fix typing issue
mvadari Oct 10, 2024
d3da22a
fix tests
mvadari Oct 10, 2024
c441795
Update main.py
mvadari Oct 10, 2024
a2dca0e
Merge branch 'main' into batch
mvadari Oct 25, 2024
2378081
Merge branch 'main' into batch
mvadari Nov 4, 2024
27a822f
Merge branch 'main' into batch
mvadari Nov 7, 2024
d2119e1
remove BatchTxn field
mvadari Nov 7, 2024
f9cbea7
fix tests
mvadari Nov 7, 2024
7d842c6
fix unrelated TicketSequence bug
mvadari Nov 7, 2024
2a632cf
improve autofilling
mvadari Nov 7, 2024
6f1cbed
better flag handling
mvadari Nov 7, 2024
8ca1005
update changelog
mvadari Nov 7, 2024
77fc6e1
fix integration tests
mvadari Nov 7, 2024
0792b3e
handle batch in batch
mvadari Nov 7, 2024
a2890a9
get working integration test
mvadari Nov 8, 2024
6ff9c60
Merge branch 'main' into batch
mvadari Dec 11, 2024
f29bc15
Merge branch 'main' into batch
mvadari Dec 13, 2024
be36119
Merge branch 'main' into batch
mvadari Dec 20, 2024
4157762
Merge branch 'main' into batch
mvadari Jan 2, 2025
a637da3
rename field
mvadari Jan 2, 2025
8be504d
more renames
mvadari Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"keypair",
"keypairs",
"multisign",
"multisigned",
"nftoken",
"PATHSET",
"rippletest",
Expand Down
46 changes: 43 additions & 3 deletions tests/integration/sugar/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 17 additions & 5 deletions tests/unit/models/transactions/test_transaction.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -158,8 +159,19 @@ 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):
def test_payment_txn_api_no_deliver_max(self):
delivered_amount = "200000"
payment_tx_json = {
"Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e",
Expand All @@ -175,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",
Expand All @@ -191,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",
Expand All @@ -207,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",
Expand Down
9 changes: 6 additions & 3 deletions tools/generate_tx_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))?",
Expand All @@ -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]+, "
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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.\"\"\"

Expand Down Expand Up @@ -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)
74 changes: 60 additions & 14 deletions xrpl/asyncio/transaction/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""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
from typing_extensions import Final, TypeVar

from xrpl.asyncio.account import get_next_valid_seq_number
from xrpl.asyncio.clients import Client, XRPLRequestFailureException
Expand All @@ -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
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,
Expand Down Expand Up @@ -85,11 +87,12 @@ def sign(
Returns:
The signed transaction blob.
"""
transaction_json = _prepare_transaction(transaction)
if multisign:
signature = keypairs_sign(
bytes.fromhex(
encode_for_multisigning(
transaction.to_xrpl(),
transaction_json,
wallet.address,
)
),
Expand All @@ -105,7 +108,7 @@ def sign(
]
return Transaction.from_dict(tx_dict)

transaction_json = _prepare_transaction(transaction, wallet)
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)
Expand Down Expand Up @@ -174,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
Expand All @@ -192,10 +192,13 @@ 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

_validate_account_xaddress(transaction_json, "Account", "SourceTag")
if "Destination" in transaction_json:
Expand All @@ -212,9 +215,12 @@ def _prepare_transaction(
return transaction_json


T = TypeVar("T", bound=Transaction, default=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
Expand Down Expand Up @@ -244,7 +250,12 @@ 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
return Transaction.from_dict(transaction_json)
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 cast(T, Transaction.from_dict(transaction_json))


async def _get_network_id_and_build_version(client: Client) -> None:
Expand Down Expand Up @@ -375,7 +386,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]:
Expand Down Expand Up @@ -482,3 +493,38 @@ 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[Transaction], List[str]]:
account_sequences: Dict[str, int] = {}
batch_index = 0
tx_ids: List[str] = []
inner_txs: List[Transaction] = []

for raw_txn in transaction.raw_transactions:
if raw_txn.batch_txn is not None:
inner_txs.append(raw_txn)
continue

batch_txn: Dict[str, Any] = {"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["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
Loading
Loading