From c5b182d8963f139b1ba632eb413ae67eecf2fa25 Mon Sep 17 00:00:00 2001 From: Tlazypanda Date: Fri, 3 Oct 2025 14:14:15 +0530 Subject: [PATCH 1/2] add support for orderless txn --- CHANGELOG.md | 1 + aptos_sdk/async_client.py | 43 ++++++++++++++++++++++++++ aptos_sdk/transactions.py | 45 ++++++++++++++++++++++++++-- examples/orderless_txn.py | 63 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 examples/orderless_txn.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 91dd0a6..abeb67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to the Aptos Python SDK will be captured in this file. This ## Unreleased - Update dependencies for vulnerability fixes +- Add support for orderless txns ## 0.11.0 diff --git a/aptos_sdk/async_client.py b/aptos_sdk/async_client.py index 1ab40a2..c7a76da 100644 --- a/aptos_sdk/async_client.py +++ b/aptos_sdk/async_client.py @@ -22,6 +22,7 @@ SignedTransaction, TransactionArgument, TransactionPayload, + OrderlessPayload, ) from .type_tag import StructTag, TypeTag @@ -529,6 +530,48 @@ async def submit_and_wait_for_bcs_transaction( txn_hash = await self.submit_bcs_transaction(signed_transaction) await self.wait_for_transaction(txn_hash) return await self.transaction_by_hash(txn_hash) + + async def submit_orderless_transaction( + self, + sender: Account, + payload: TransactionPayload, + nonce: Optional[int] = None, + wait: bool = False + ) -> str: + + if nonce is None: + raise ValueError("Nonce required for orderless") + + if not isinstance(payload.value, EntryFunction): + raise ValueError("Only EntryFunction supported for orderless") + + orderless = OrderlessPayload(payload.value, nonce) + + chain_id = await self.chain_id() + + # Orderless transactions typically have shorter expiration windows (e.g., 30 seconds) + # Use a much shorter TTL than regular transactions + orderless_expiration_ttl = 30 # 30 seconds + + raw_txn = RawTransaction( + sender=sender.address(), + sequence_number=0xDEADBEEF, + payload=TransactionPayload(orderless), + max_gas_amount=self.client_config.max_gas_amount, + gas_unit_price=self.client_config.gas_unit_price, + expiration_timestamps_secs=int(time.time()) + orderless_expiration_ttl, + chain_id=chain_id, + ) + + authenticator = sender.sign_transaction(raw_txn) + signed_txn = SignedTransaction(raw_txn, authenticator) + + tx_hash = await self.submit_bcs_transaction(signed_txn) + + if wait: + await self.wait_for_transaction(tx_hash) + + return tx_hash async def transaction_pending(self, txn_hash: str) -> bool: response = await self._get(endpoint=f"transactions/by_hash/{txn_hash}") diff --git a/aptos_sdk/transactions.py b/aptos_sdk/transactions.py index a68d92e..da14ecc 100644 --- a/aptos_sdk/transactions.py +++ b/aptos_sdk/transactions.py @@ -253,6 +253,7 @@ class TransactionPayload: SCRIPT: int = 0 MODULE_BUNDLE: int = 1 SCRIPT_FUNCTION: int = 2 + INNER_PAYLOAD: int = 4 variant: int value: Any @@ -264,6 +265,8 @@ def __init__(self, payload: Any): self.variant = TransactionPayload.MODULE_BUNDLE elif isinstance(payload, EntryFunction): self.variant = TransactionPayload.SCRIPT_FUNCTION + elif isinstance(payload, OrderlessPayload): + self.variant = TransactionPayload.INNER_PAYLOAD else: raise Exception("Invalid type") self.value = payload @@ -295,7 +298,6 @@ def serialize(self, serializer: Serializer) -> None: serializer.uleb128(self.variant) self.value.serialize(serializer) - class ModuleBundle: def __init__(self): raise NotImplementedError @@ -477,6 +479,45 @@ def serialize(self, serializer: Serializer) -> None: serializer.sequence(self.ty_args, Serializer.struct) serializer.sequence(self.args, Serializer.to_bytes) +class OrderlessPayload: + """Orderless transaction payload wrapper""" + + def __init__(self, entry_function: EntryFunction, nonce: int): + self.entry_function = entry_function + self.nonce = nonce + + def serialize(self, serializer: Serializer): + """Serialize orderless payload WITHOUT the outer variant (TransactionPayload handles that)""" + + # OrderlessTransactionPayload::V1 variant + serializer.uleb128(0) + + # Executable::EntryFunction variant + serializer.uleb128(1) + + # Serialize the entry function + self.entry_function.serialize(serializer) + + # ExtraConfig::V1 variant + serializer.uleb128(0) + + # Option - None + serializer.bool(False) + + # Option nonce - Some + serializer.bool(True) + serializer.u64(self.nonce) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OrderlessPayload): + return NotImplemented + return ( + self.entry_function == other.entry_function + and self.nonce == other.nonce + ) + + def __str__(self): + return f"OrderlessPayload(nonce={self.nonce}, {self.entry_function})" class ModuleId: address: AccountAddress @@ -898,4 +939,4 @@ def test_deserialize_raw_transaction_multi_agent(self): raw_txn_with_data.serialize(ser) self.assertEqual(ser.output().hex(), input_ma) - self.assertTrue(isinstance(raw_txn_with_data, MultiAgentRawTransaction)) + self.assertTrue(isinstance(raw_txn_with_data, MultiAgentRawTransaction)) \ No newline at end of file diff --git a/examples/orderless_txn.py b/examples/orderless_txn.py new file mode 100644 index 0000000..6e89ddb --- /dev/null +++ b/examples/orderless_txn.py @@ -0,0 +1,63 @@ +import asyncio +import time +from aptos_sdk.account import Account +from aptos_sdk.async_client import RestClient, FaucetClient +from aptos_sdk.bcs import Serializer +from aptos_sdk.transactions import ( + TransactionPayload, + EntryFunction, + TransactionArgument, +) + +async def example_single_orderless(): + """Submit a single orderless transaction""" + print("=== Single Orderless Transaction ===\n") + + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + faucet_client = FaucetClient("https://faucet.devnet.aptoslabs.com", client) + + sender = Account.generate() + recipient = Account.generate() + + print(f"Sender: {sender.address()}") + print(f"Recipient: {recipient.address()}") + + print("\nFunding sender...") + await faucet_client.fund_account(sender.address(), 100_000_000) + + # Create transfer payload + payload = TransactionPayload(EntryFunction.natural( + "0x1::aptos_account", + "transfer", + [], + [ + TransactionArgument(recipient.address(), Serializer.struct), + TransactionArgument(1_000_000, Serializer.u64), + ] + )) + + nonce_orderless = 12345 + + print("Submitting orderless transaction...") + tx_hash = await client.submit_orderless_transaction( + sender, + payload, + nonce=nonce_orderless, + wait=True + ) + + print(f"✓ Transaction completed: {tx_hash}") + + balance = await client.account_balance(recipient.address()) + print(f"✓ Recipient balance: {balance} octas") + + await client.close() + +if __name__ == "__main__": + try: + asyncio.run(example_single_orderless()) + + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() \ No newline at end of file From ad0d3534207bd98366294e6b136a410dbe275f26 Mon Sep 17 00:00:00 2001 From: Tlazypanda Date: Tue, 7 Oct 2025 14:52:42 +0530 Subject: [PATCH 2/2] add script & multisig paths, fmt & constants --- aptos_sdk/async_client.py | 57 +++++---- aptos_sdk/transactions.py | 101 ++++++++++++---- examples/orderless_txn.py | 247 ++++++++++++++++++++++++++++++++------ 3 files changed, 320 insertions(+), 85 deletions(-) diff --git a/aptos_sdk/async_client.py b/aptos_sdk/async_client.py index c7a76da..94a2b7a 100644 --- a/aptos_sdk/async_client.py +++ b/aptos_sdk/async_client.py @@ -3,6 +3,7 @@ import asyncio import logging +import secrets import time from dataclasses import dataclass from typing import Any, Dict, List, Optional @@ -18,11 +19,12 @@ from .transactions import ( EntryFunction, MultiAgentRawTransaction, + OrderlessPayload, RawTransaction, + Script, SignedTransaction, TransactionArgument, TransactionPayload, - OrderlessPayload, ) from .type_tag import StructTag, TypeTag @@ -530,29 +532,40 @@ async def submit_and_wait_for_bcs_transaction( txn_hash = await self.submit_bcs_transaction(signed_transaction) await self.wait_for_transaction(txn_hash) return await self.transaction_by_hash(txn_hash) - + async def submit_orderless_transaction( - self, - sender: Account, - payload: TransactionPayload, - nonce: Optional[int] = None, - wait: bool = False - ) -> str: - + self, + sender: Account, + payload: TransactionPayload, + nonce: Optional[int] = None, + multisig_address: Optional[AccountAddress] = None, + wait: bool = False, + ) -> str: + if nonce is None: - raise ValueError("Nonce required for orderless") - - if not isinstance(payload.value, EntryFunction): - raise ValueError("Only EntryFunction supported for orderless") - + nonce = secrets.randbits(64) + + # Extract executable from payload (can be None for multisig voting) + executable = payload.value if payload.value else None + + if executable is not None and not isinstance( + executable, (EntryFunction, Script) + ): + raise ValueError( + "Orderless transactions only support EntryFunction and Script payloads" + ) + + # Create orderless payload + orderless = OrderlessPayload(executable, nonce, multisig_address) + orderless = OrderlessPayload(payload.value, nonce) - + chain_id = await self.chain_id() - # Orderless transactions typically have shorter expiration windows (e.g., 30 seconds) + # Orderless transactions typically have shorter expiration windows (60 seconds) # Use a much shorter TTL than regular transactions - orderless_expiration_ttl = 30 # 30 seconds - + orderless_expiration_ttl = 60 + raw_txn = RawTransaction( sender=sender.address(), sequence_number=0xDEADBEEF, @@ -562,15 +575,15 @@ async def submit_orderless_transaction( expiration_timestamps_secs=int(time.time()) + orderless_expiration_ttl, chain_id=chain_id, ) - + authenticator = sender.sign_transaction(raw_txn) signed_txn = SignedTransaction(raw_txn, authenticator) - + tx_hash = await self.submit_bcs_transaction(signed_txn) - + if wait: await self.wait_for_transaction(tx_hash) - + return tx_hash async def transaction_pending(self, txn_hash: str) -> bool: diff --git a/aptos_sdk/transactions.py b/aptos_sdk/transactions.py index da14ecc..835557c 100644 --- a/aptos_sdk/transactions.py +++ b/aptos_sdk/transactions.py @@ -253,7 +253,7 @@ class TransactionPayload: SCRIPT: int = 0 MODULE_BUNDLE: int = 1 SCRIPT_FUNCTION: int = 2 - INNER_PAYLOAD: int = 4 + INNER_PAYLOAD: int = 4 variant: int value: Any @@ -265,7 +265,7 @@ def __init__(self, payload: Any): self.variant = TransactionPayload.MODULE_BUNDLE elif isinstance(payload, EntryFunction): self.variant = TransactionPayload.SCRIPT_FUNCTION - elif isinstance(payload, OrderlessPayload): + elif isinstance(payload, OrderlessPayload): self.variant = TransactionPayload.INNER_PAYLOAD else: raise Exception("Invalid type") @@ -298,6 +298,7 @@ def serialize(self, serializer: Serializer) -> None: serializer.uleb128(self.variant) self.value.serialize(serializer) + class ModuleBundle: def __init__(self): raise NotImplementedError @@ -479,45 +480,95 @@ def serialize(self, serializer: Serializer) -> None: serializer.sequence(self.ty_args, Serializer.struct) serializer.sequence(self.args, Serializer.to_bytes) + class OrderlessPayload: """Orderless transaction payload wrapper""" - - def __init__(self, entry_function: EntryFunction, nonce: int): - self.entry_function = entry_function + + # OrderlessTransactionPayload variants + ORDERLESS_V1: int = 0 + + # Executable variants + EXECUTABLE_SCRIPT: int = 0 + EXECUTABLE_ENTRY_FUNCTION: int = 1 + EXECUTABLE_EMPTY: int = 2 # For multisig voting without execution + + # ExtraConfig variants + EXTRA_CONFIG_V1: int = 0 + + def __init__( + self, + executable: Optional[Union[Script, EntryFunction]], + nonce: int, + multisig_address: Optional[AccountAddress] = None, + ): + """ + Create an orderless transaction payload. + + :param executable: Script or EntryFunction to execute. None for multisig voting. + :param nonce: Unique nonce for replay protection + :param multisig_address: Address of multisig account (required for multisig transactions) + """ + if executable is not None and not isinstance( + executable, (Script, EntryFunction) + ): + raise ValueError("Executable must be either Script or EntryFunction") + + # If no executable, must be multisig voting + if executable is None and multisig_address is None: + raise ValueError("Either executable or multisig_address must be provided") + + self.executable = executable self.nonce = nonce - + self.multisig_address = multisig_address + def serialize(self, serializer: Serializer): """Serialize orderless payload WITHOUT the outer variant (TransactionPayload handles that)""" - + # OrderlessTransactionPayload::V1 variant - serializer.uleb128(0) - - # Executable::EntryFunction variant - serializer.uleb128(1) - - # Serialize the entry function - self.entry_function.serialize(serializer) - + serializer.uleb128(OrderlessPayload.ORDERLESS_V1) + + # Executable variant + if self.executable is None: + # Empty executable for multisig voting + serializer.uleb128(OrderlessPayload.EXECUTABLE_EMPTY) + elif isinstance(self.executable, Script): + serializer.uleb128(OrderlessPayload.EXECUTABLE_SCRIPT) + self.executable.serialize(serializer) + elif isinstance(self.executable, EntryFunction): + serializer.uleb128(OrderlessPayload.EXECUTABLE_ENTRY_FUNCTION) + self.executable.serialize(serializer) + else: + raise ValueError("Invalid executable type") + # ExtraConfig::V1 variant - serializer.uleb128(0) - - # Option - None - serializer.bool(False) - + serializer.uleb128(OrderlessPayload.EXTRA_CONFIG_V1) + + # Option + if self.multisig_address is not None: + serializer.bool(True) # Some + self.multisig_address.serialize(serializer) + else: + serializer.bool(False) # None + # Option nonce - Some serializer.bool(True) serializer.u64(self.nonce) - + def __eq__(self, other: object) -> bool: if not isinstance(other, OrderlessPayload): return NotImplemented return ( - self.entry_function == other.entry_function + self.executable == other.executable and self.nonce == other.nonce + and self.multisig_address == other.multisig_address ) - + def __str__(self): - return f"OrderlessPayload(nonce={self.nonce}, {self.entry_function})" + multisig_str = ( + f", multisig={self.multisig_address}" if self.multisig_address else "" + ) + return f"OrderlessPayload(nonce={self.nonce}, {self.executable}{multisig_str})" + class ModuleId: address: AccountAddress @@ -939,4 +990,4 @@ def test_deserialize_raw_transaction_multi_agent(self): raw_txn_with_data.serialize(ser) self.assertEqual(ser.output().hex(), input_ma) - self.assertTrue(isinstance(raw_txn_with_data, MultiAgentRawTransaction)) \ No newline at end of file + self.assertTrue(isinstance(raw_txn_with_data, MultiAgentRawTransaction)) diff --git a/examples/orderless_txn.py b/examples/orderless_txn.py index 6e89ddb..d5d0ed8 100644 --- a/examples/orderless_txn.py +++ b/examples/orderless_txn.py @@ -1,63 +1,234 @@ +""" +Complete examples for orderless transactions in Aptos Python SDK. + +Demonstrates: +1. Regular orderless transaction with EntryFunction +2. Orderless transaction with Script +3. Multisig orderless transaction with execution +4. Multisig voting-only transaction (no execution) +""" + import asyncio import time + from aptos_sdk.account import Account -from aptos_sdk.async_client import RestClient, FaucetClient +from aptos_sdk.account_address import AccountAddress +from aptos_sdk.async_client import ClientConfig, FaucetClient, RestClient from aptos_sdk.bcs import Serializer from aptos_sdk.transactions import ( - TransactionPayload, EntryFunction, + Script, TransactionArgument, + TransactionPayload, ) -async def example_single_orderless(): - """Submit a single orderless transaction""" - print("=== Single Orderless Transaction ===\n") - - client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") - faucet_client = FaucetClient("https://faucet.devnet.aptoslabs.com", client) - +from .common import API_KEY, FAUCET_URL, NODE_URL + + +async def example_regular_orderless(): + """Example 1: Regular orderless transaction with EntryFunction""" + print("Example 1: Regular Orderless Transaction (EntryFunction)") + sender = Account.generate() recipient = Account.generate() - + print(f"Sender: {sender.address()}") print(f"Recipient: {recipient.address()}") - + print("\nFunding sender...") await faucet_client.fund_account(sender.address(), 100_000_000) - + # Create transfer payload - payload = TransactionPayload(EntryFunction.natural( - "0x1::aptos_account", - "transfer", - [], - [ - TransactionArgument(recipient.address(), Serializer.struct), - TransactionArgument(1_000_000, Serializer.u64), - ] - )) - - nonce_orderless = 12345 - - print("Submitting orderless transaction...") + payload = TransactionPayload( + EntryFunction.natural( + "0x1::aptos_account", + "transfer", + [], + [ + TransactionArgument(recipient.address(), Serializer.struct), + TransactionArgument(1_000_000, Serializer.u64), + ], + ) + ) + + # Use timestamp-based nonce for uniqueness + nonce = int(time.time() * 1000) + + print(f"Submitting orderless transaction with nonce: {nonce}...") tx_hash = await client.submit_orderless_transaction( - sender, - payload, - nonce=nonce_orderless, - wait=True + sender, payload, nonce=nonce, wait=True ) - + print(f"✓ Transaction completed: {tx_hash}") - + balance = await client.account_balance(recipient.address()) print(f"✓ Recipient balance: {balance} octas") - - await client.close() -if __name__ == "__main__": + +async def example_script_orderless(): + """Example 2: Orderless transaction with Script""" + print("Example 2: Orderless Transaction with Script") + + sender = Account.generate() + + print(f"Sender: {sender.address()}") + + print("\nFunding sender...") + await faucet_client.fund_account(sender.address(), 100_000_000) + + # Create a script payload (example with empty bytecode - replace with actual script) + # In practice, you'd compile a Move script and use the bytecode + script = Script( + code=b"", ty_args=[], args=[] # Your compiled Move script bytecode here + ) + + payload = TransactionPayload(script) + nonce = int(time.time() * 1000) + + print(f"Submitting orderless script transaction with nonce: {nonce}...") + + # Note: This will fail without actual valid script bytecode + # This is just to show the API usage + try: + tx_hash = await client.submit_orderless_transaction( + sender, payload, nonce=nonce, wait=True + ) + print(f"✓ Transaction completed: {tx_hash}") + except Exception as e: + print(f"Note: Script example failed (expected without valid bytecode): {e}") + + +async def example_multisig_orderless_with_execution(): + """Example 3: Multisig orderless transaction with execution""" + print("Example 3: Multisig Orderless Transaction with Execution") + + # Create multisig participants + owner1 = Account.generate() + owner2 = Account.generate() + recipient = Account.generate() + + print(f"Owner 1: {owner1.address()}") + print(f"Owner 2: {owner2.address()}") + print(f"Recipient: {recipient.address()}") + + print("\nFunding owner 1...") + await faucet_client.fund_account(owner1.address(), 100_000_000) + + # In practice, you would: + # 1. Create the multisig account first + # 2. Get the multisig account address + # For this example, we'll use a placeholder address + multisig_address = AccountAddress.from_str( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ) + + print(f"\nMultisig Address (example): {multisig_address}") + + # Create transfer payload for multisig execution + payload = TransactionPayload( + EntryFunction.natural( + "0x1::aptos_account", + "transfer", + [], + [ + TransactionArgument(recipient.address(), Serializer.struct), + TransactionArgument(500_000, Serializer.u64), + ], + ) + ) + + nonce = int(time.time() * 1000) + + print(f"\nSubmitting multisig orderless transaction with nonce: {nonce}...") + + try: + tx_hash = await client.submit_orderless_transaction( + owner1, payload, nonce=nonce, multisig_address=multisig_address, wait=True + ) + print(f"✓ Transaction completed: {tx_hash}") + except Exception as e: + print( + f"Note: Multisig example failed (expected without actual multisig setup): {e}" + ) + + +async def example_replay_protection(): + """Example 5: Demonstrating replay protection with same nonce""" + print("Example 5: Replay Protection (Same Nonce)") + + sender = Account.generate() + recipient = Account.generate() + + print(f"Sender: {sender.address()}") + print(f"Recipient: {recipient.address()}") + + print("\nFunding sender...") + await faucet_client.fund_account(sender.address(), 100_000_000) + + # Create transfer payload + payload = TransactionPayload( + EntryFunction.natural( + "0x1::aptos_account", + "transfer", + [], + [ + TransactionArgument(recipient.address(), Serializer.struct), + TransactionArgument(1_000_000, Serializer.u64), + ], + ) + ) + + # Use fixed nonce for both transactions + nonce = 999999 + + print(f"\nSubmitting first transaction with nonce: {nonce}...") + tx1_hash = await client.submit_orderless_transaction( + sender, payload, nonce=nonce, wait=True + ) + print(f"✓ First transaction completed: {tx1_hash}") + + print(f"\nAttempting second transaction with SAME nonce: {nonce}...") + try: + tx2_hash = await client.submit_orderless_transaction( + sender, payload, nonce=nonce, wait=True + ) + print(f"⚠ Second transaction succeeded (unexpected): {tx2_hash}") + print("This might mean the first transaction wasn't fully indexed yet") + except Exception as e: + print(f"✓ Replay protection worked! Transaction rejected: {e}") + print(f" Error: {e}") + + +async def main(): + """Run all examples""" + print("APTOS ORDERLESS TRANSACTIONS - COMPLETE EXAMPLES") + try: - asyncio.run(example_single_orderless()) - + # Example 1: Regular orderless transaction + await example_regular_orderless() + + # Example 2: Script orderless (will fail without valid bytecode) + await example_script_orderless() + + # Example 3: Multisig with execution (requires multisig setup) + await example_multisig_orderless_with_execution() + + # Example 5: Replay protection demonstration + await example_replay_protection() + + await client.close() + await faucet_client.close() + + print("ALL EXAMPLES COMPLETED") + except Exception as e: - print(f"\nError: {e}") + print(f"\n❌ Error running examples: {e}") import traceback - traceback.print_exc() \ No newline at end of file + + traceback.print_exc() + + +if __name__ == "__main__": + client = RestClient(NODE_URL, client_config=ClientConfig(api_key=API_KEY)) + faucet_client = FaucetClient(FAUCET_URL, client) + asyncio.run(main())