Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
56 changes: 56 additions & 0 deletions aptos_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import asyncio
import logging
import secrets
import time
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
Expand All @@ -18,7 +19,9 @@
from .transactions import (
EntryFunction,
MultiAgentRawTransaction,
OrderlessPayload,
RawTransaction,
Script,
SignedTransaction,
TransactionArgument,
TransactionPayload,
Expand Down Expand Up @@ -530,6 +533,59 @@ async def submit_and_wait_for_bcs_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,
multisig_address: Optional[AccountAddress] = None,
wait: bool = False,
) -> str:

if nonce is None:
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)

Comment on lines +561 to +562
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orderless variable is created twice. Line 561 overwrites line 559, ignoring the multisig_address parameter. Remove line 561 and use the variable from line 559, or remove lines 559-560 if line 561 is the intended implementation (though this would break multisig support).

Suggested change
orderless = OrderlessPayload(payload.value, nonce)

Copilot uses AI. Check for mistakes.
chain_id = await self.chain_id()

# Orderless transactions typically have shorter expiration windows (60 seconds)
# Use a much shorter TTL than regular transactions
orderless_expiration_ttl = 60

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}")
# TODO(@davidiw): consider raising a different error here, since this is an ambiguous state
Expand Down
92 changes: 92 additions & 0 deletions aptos_sdk/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -478,6 +481,95 @@ def serialize(self, serializer: Serializer) -> None:
serializer.sequence(self.args, Serializer.to_bytes)


class OrderlessPayload:
"""Orderless transaction payload wrapper"""

# 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(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(OrderlessPayload.EXTRA_CONFIG_V1)

# Option<MultisigAddress>
if self.multisig_address is not None:
serializer.bool(True) # Some
self.multisig_address.serialize(serializer)
else:
serializer.bool(False) # None

# Option<u64> nonce - Some
serializer.bool(True)
serializer.u64(self.nonce)

def __eq__(self, other: object) -> bool:
if not isinstance(other, OrderlessPayload):
return NotImplemented
return (
self.executable == other.executable
and self.nonce == other.nonce
and self.multisig_address == other.multisig_address
)

def __str__(self):
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
name: str
Expand Down
Loading
Loading