From 6b2fd031f4ac4e61fa7135918a5adcf0d7e154b2 Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Fri, 5 Sep 2025 11:21:19 -0400 Subject: [PATCH 1/4] docs: Add documentation for functions in Python SDK --- aptos_sdk/__init__.py | 205 ++++++ aptos_sdk/account.py | 731 ++++++++++++++++++- aptos_sdk/account_address.py | 623 ++++++++++++++-- aptos_sdk/account_sequence_number.py | 397 +++++++++- aptos_sdk/aptos_cli_wrapper.py | 969 ++++++++++++++++++++++++- aptos_sdk/aptos_token_client.py | 776 +++++++++++++++++++- aptos_sdk/aptos_tokenv1_client.py | 664 ++++++++++++++++- aptos_sdk/asymmetric_crypto.py | 499 ++++++++++++- aptos_sdk/asymmetric_crypto_wrapper.py | 594 +++++++++++++++ aptos_sdk/async_client.py | 805 +++++++++++++++++++- aptos_sdk/authenticator.py | 943 +++++++++++++++++++++++- aptos_sdk/bcs.py | 581 ++++++++++++++- aptos_sdk/cli.py | 300 +++++++- aptos_sdk/ed25519.py | 534 +++++++++++++- aptos_sdk/metadata.py | 153 +++- aptos_sdk/package_publisher.py | 535 +++++++++++++- aptos_sdk/secp256k1_ecdsa.py | 689 +++++++++++++++++- aptos_sdk/transaction_worker.py | 395 +++++++++- aptos_sdk/transactions.py | 564 +++++++++++++- aptos_sdk/type_tag.py | 457 +++++++++++- examples/__init__.py | 91 +++ examples/common.py | 98 +++ examples/hello_blockchain.py | 419 ++++++++++- examples/multikey.py | 159 ++++ examples/rotate_key.py | 162 +++++ examples/transfer_coin.py | 202 ++++++ 26 files changed, 12266 insertions(+), 279 deletions(-) diff --git a/aptos_sdk/__init__.py b/aptos_sdk/__init__.py index b61b180..05fa82d 100644 --- a/aptos_sdk/__init__.py +++ b/aptos_sdk/__init__.py @@ -1,2 +1,207 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 + +""" +Aptos Python SDK - A comprehensive Python client library for the Aptos blockchain. + +The Aptos Python SDK provides a complete toolkit for interacting with the Aptos +blockchain network, including transaction submission, account management, smart +contract deployment, and blockchain data querying. It supports both synchronous +and asynchronous programming patterns. + +Core Features: +- **Account Management**: Create, manage, and authenticate blockchain accounts +- **Transaction Processing**: Submit, sign, and track blockchain transactions +- **Smart Contracts**: Deploy and interact with Move smart contracts +- **REST API Client**: Full-featured REST API client with async support +- **Token Operations**: Native and custom token management (APT, NFTs) +- **Cryptographic Support**: Ed25519, Secp256k1, and multi-key authentication +- **BCS Serialization**: Binary Canonical Serialization for efficient data encoding +- **CLI Integration**: Python wrapper for the official Aptos CLI + +Supported Networks: +- **Mainnet**: Production Aptos blockchain network +- **Testnet**: Public testing environment +- **Devnet**: Development and experimental features +- **Local Testnet**: Local development environment + +Architecture: +- **Async-First**: Built with asyncio for high-performance applications +- **Type Safety**: Full type hints and mypy compatibility +- **Modular Design**: Import only the components you need +- **Standards Compliant**: Follows Aptos network protocols and standards +- **Cross-Platform**: Works on Linux, macOS, and Windows + +Quick Start: + Basic account and transaction operations:: + + import asyncio + from aptos_sdk.async_client import FaucetClient, RestClient + from aptos_sdk.account import Account + + async def main(): + # Connect to Aptos devnet + rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + faucet_client = FaucetClient( + "https://faucet.devnet.aptoslabs.com", + rest_client + ) + + # Create accounts + alice = Account.generate() + bob = Account.generate() + + # Fund accounts from faucet + await faucet_client.fund_account(alice.address(), 100_000_000) + await faucet_client.fund_account(bob.address(), 0) + + # Transfer APT tokens + transaction_hash = await rest_client.transfer( + alice, bob.address(), 1_000_000 + ) + + # Wait for transaction completion + result = await rest_client.wait_for_transaction(transaction_hash) + print(f"Transaction successful: {result['success']}") + + await rest_client.close() + + asyncio.run(main()) + + Smart contract deployment:: + + from aptos_sdk.package_publisher import PackagePublisher + from aptos_sdk.account_address import AccountAddress + + # Compile and publish a Move package + publisher = PackagePublisher(rest_client) + + package_metadata, package_code = publisher.compile_package( + package_dir="./my_move_package", + named_addresses={ + "my_module": alice.address() + } + ) + + # Deploy the package + txn_hash = await publisher.publish_package( + alice, package_metadata, package_code + ) + + Token operations:: + + from aptos_sdk.aptos_token_client import AptosTokenClient + + # Create NFT collection and tokens + token_client = AptosTokenClient(rest_client) + + # Create collection + collection_name = "My NFT Collection" + await token_client.create_collection( + alice, + collection_name, + "A collection of unique NFTs", + "https://example.com/collection.json" + ) + + # Mint NFT + await token_client.create_token( + alice, + collection_name, + "My First NFT", + "A unique digital asset", + 1, # supply + "https://example.com/token.json" + ) + +Module Organization: + Core Modules: + - **account**: Account creation, management, and key handling + - **async_client**: Async REST and Faucet clients for network communication + - **transactions**: Transaction building, signing, and submission + - **authenticator**: Multi-signature and authentication schemes + - **bcs**: Binary Canonical Serialization utilities + - **account_address**: Blockchain address handling and validation + + Cryptography: + - **ed25519**: Ed25519 digital signature implementation + - **secp256k1_ecdsa**: Secp256k1 ECDSA signature support + - **asymmetric_crypto**: Unified cryptographic interface + + High-Level Clients: + - **aptos_token_client**: NFT and token creation/management + - **package_publisher**: Move package compilation and deployment + - **transaction_worker**: High-throughput transaction processing + + Development Tools: + - **aptos_cli_wrapper**: Python wrapper for Aptos CLI + - **type_tag**: Move type system utilities + - **metadata**: SDK version and HTTP header management + +Configuration: + Environment Variables: + - **APTOS_CLI_PATH**: Custom path to Aptos CLI binary + - **APTOS_PROFILE**: Default network profile for CLI operations + + Network Endpoints: + - **Mainnet**: https://fullnode.mainnet.aptoslabs.com/v1 + - **Testnet**: https://fullnode.testnet.aptoslabs.com/v1 + - **Devnet**: https://fullnode.devnet.aptoslabs.com/v1 + +Development: + Running tests:: + + # Install development dependencies + pip install -e ".[dev]" + + # Run unit tests + python -m pytest tests/ + + # Run integration tests (requires network access) + python -m pytest tests/integration/ + + Local testnet:: + + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper + + # Start local testnet for development + testnet = AptosCLIWrapper.start_node() + is_ready = await testnet.wait_until_operational() + + if is_ready: + # Use local endpoints: + # REST: http://127.0.0.1:8080/v1 + # Faucet: http://127.0.0.1:8081 + pass + + # Cleanup when done + testnet.stop() + +Requirements: + - Python 3.8 or higher + - httpx for HTTP requests + - cryptography for Ed25519/Secp256k1 support + - pynacl for additional cryptographic operations + - Aptos CLI (for local development and Move compilation) + +Security Considerations: + - **Private Keys**: Never log or expose private keys in production + - **Network Validation**: Always verify transaction results on-chain + - **Rate Limiting**: Respect API rate limits and implement backoff strategies + - **Testnet Only**: Use testnet/devnet for development and testing + - **Package Verification**: Verify Move package bytecode before deployment + +Support: + - **Documentation**: https://aptos.dev/sdks/python-sdk/ + - **GitHub**: https://github.com/aptos-labs/aptos-python-sdk + - **Discord**: https://discord.gg/aptoslabs + - **Forum**: https://forum.aptoslabs.com/ + +License: + Apache License 2.0 + +Note: + This SDK is actively maintained by Aptos Labs and the community. + For production use, always use the latest stable version and follow + security best practices for key management and transaction handling. +""" diff --git a/aptos_sdk/account.py b/aptos_sdk/account.py index 090c11e..5a973d7 100644 --- a/aptos_sdk/account.py +++ b/aptos_sdk/account.py @@ -15,7 +15,99 @@ class Account: - """Represents an account as well as the private, public key-pair for the Aptos blockchain.""" + """Represents a complete Aptos blockchain account with cryptographic key management. + + The Account class encapsulates the fundamental components needed to interact with + the Aptos blockchain: an account address and its associated private key. It provides + comprehensive functionality for account creation, transaction signing, key management, + and persistent storage. + + Key Features: + - **Multiple Key Types**: Supports Ed25519 and Secp256k1 ECDSA cryptographic schemes + - **Random Generation**: Secure random account creation with proper entropy + - **Key Import/Export**: Load accounts from hex strings or JSON files + - **Transaction Signing**: Sign transactions and arbitrary data + - **Address Derivation**: Automatic address calculation from public keys + - **Persistence**: Save and load account data to/from files + - **Authentication**: Generate authentication keys and proof challenges + + Cryptographic Support: + - **Ed25519**: Default signature scheme, fast and secure + - **Secp256k1 ECDSA**: Ethereum-compatible signatures for interoperability + - **Multi-key**: Support for threshold and multi-signature schemes + + Examples: + Create a new account:: + + from aptos_sdk.account import Account + + # Generate new Ed25519 account + account = Account.generate() + print(f"Address: {account.address()}") + print(f"Private key: {account.private_key}") + + Create Secp256k1 account:: + + # Generate Secp256k1 ECDSA account (Ethereum-compatible) + secp_account = Account.generate_secp256k1_ecdsa() + print(f"Secp256k1 address: {secp_account.address()}") + + Load existing account:: + + # From hex private key + hex_key = "***1234567890abcdef..." + imported_account = Account.load_key(hex_key) + + # From JSON file + saved_account = Account.load("./my_account.json") + + Sign transactions:: + + from aptos_sdk.async_client import RestClient + + async def transfer_tokens(): + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + + # Create transfer transaction + recipient = Account.generate().address() + txn_hash = await client.transfer(account, recipient, 1000) + + # Wait for completion + result = await client.wait_for_transaction(txn_hash) + print(f"Transfer successful: {result['success']}") + + Persistent storage:: + + # Save account to file + account.store("./wallet.json") + + # Load account later + restored_account = Account.load("./wallet.json") + assert account == restored_account + + Sign arbitrary data:: + + # Sign custom message + message = b"Hello, Aptos!" + signature = account.sign(message) + + # Verify signature + public_key = account.public_key() + is_valid = public_key.verify(message, signature) + print(f"Signature valid: {is_valid}") + + Security Considerations: + - **Private Key Protection**: Never expose private keys in logs or UI + - **Secure Storage**: Use encrypted storage for production private keys + - **Key Rotation**: Consider implementing key rotation for long-lived accounts + - **Testnet First**: Always test on devnet/testnet before mainnet deployment + - **Entropy**: The random generation uses cryptographically secure random sources + + Note: + Account addresses are derived deterministically from public keys using + SHA3-256 hashing. The same private key will always generate the same + address across different SDK instances. + """ account_address: AccountAddress private_key: asymmetric_crypto.PrivateKey @@ -23,10 +115,45 @@ class Account: def __init__( self, account_address: AccountAddress, private_key: asymmetric_crypto.PrivateKey ): + """Initialize an Account with the given address and private key. + + This constructor creates an Account instance from an existing address and + private key pair. It's typically used internally by factory methods like + generate() or load_key() rather than being called directly. + + Args: + account_address: The blockchain address for this account. + private_key: The private key that controls this account. Must correspond + to the given address. + + Examples: + Direct construction (advanced usage):: + + from aptos_sdk.ed25519 import PrivateKey + from aptos_sdk.account_address import AccountAddress + + # Create components separately + private_key = PrivateKey.random() + address = AccountAddress.from_key(private_key.public_key()) + + # Construct account + account = Account(address, private_key) + + Note: + The constructor does not validate that the address corresponds to + the private key. Use the factory methods (generate, load_key) for + guaranteed consistency. + """ self.account_address = account_address self.private_key = private_key def __eq__(self, other: object) -> bool: + """ + Check equality between two Account instances. + + :param other: The other object to compare with + :return: True if accounts are equal, False otherwise + """ if not isinstance(other, Account): return NotImplemented return ( @@ -36,12 +163,120 @@ def __eq__(self, other: object) -> bool: @staticmethod def generate() -> Account: + """Generate a new Account with a cryptographically secure random Ed25519 private key. + + This method creates a completely new account with a randomly generated Ed25519 + private key and derives the corresponding account address. Ed25519 is the + default and recommended signature scheme for Aptos due to its security and + performance characteristics. + + Returns: + Account: A new account with randomly generated Ed25519 credentials. + + Examples: + Create new accounts:: + + # Generate single account + alice = Account.generate() + print(f"Alice's address: {alice.address()}") + + # Generate multiple accounts + accounts = [Account.generate() for _ in range(5)] + for i, account in enumerate(accounts): + print(f"Account {i}: {account.address()}") + + Use in async context:: + + import asyncio + from aptos_sdk.async_client import FaucetClient, RestClient + + async def setup_test_account(): + # Generate account + account = Account.generate() + + # Fund from faucet + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + faucet = FaucetClient("https://faucet.devnet.aptoslabs.com", client) + + await faucet.fund_account(account.address(), 100_000_000) + balance = await client.account_balance(account.address()) + print(f"Account funded with {balance} APT") + + return account + + Security: + - Uses cryptographically secure random number generation + - Each call produces a unique, unpredictable private key + - Private key entropy comes from system random sources + - No two generated accounts will have the same private key + + Note: + The generated account exists only in memory until explicitly saved + using the store() method. The address is deterministically derived + from the public key using SHA3-256 hashing. + """ private_key = ed25519.PrivateKey.random() account_address = AccountAddress.from_key(private_key.public_key()) return Account(account_address, private_key) @staticmethod def generate_secp256k1_ecdsa() -> Account: + """Generate a new Account with a cryptographically secure random Secp256k1 ECDSA private key. + + This method creates a new account using the Secp256k1 ECDSA signature scheme, + which is compatible with Ethereum and Bitcoin. This enables interoperability + with Ethereum tooling and allows users familiar with Ethereum to use the same + cryptographic primitives. + + Returns: + Account: A new account with randomly generated Secp256k1 ECDSA credentials. + + Examples: + Create Ethereum-compatible account:: + + # Generate Secp256k1 account + eth_compatible_account = Account.generate_secp256k1_ecdsa() + print(f"Secp256k1 address: {eth_compatible_account.address()}") + + # The private key can be used with Ethereum tooling + private_key_hex = str(eth_compatible_account.private_key) + print(f"Private key (Ethereum format): {private_key_hex}") + + Mixed signature schemes:: + + # Create accounts with different signature schemes + ed25519_account = Account.generate() # Default Ed25519 + secp256k1_account = Account.generate_secp256k1_ecdsa() + + print(f"Ed25519 account: {ed25519_account.address()}") + print(f"Secp256k1 account: {secp256k1_account.address()}") + + # Both can interact with Aptos equally + # but have different signature formats + + Use Cases: + - **Ethereum Migration**: Users migrating from Ethereum ecosystems + - **Cross-Chain Applications**: Applications spanning Ethereum and Aptos + - **Hardware Wallets**: Some hardware wallets prefer Secp256k1 + - **Enterprise Integration**: Systems already using Secp256k1 + + Performance Considerations: + - Secp256k1 signatures are larger than Ed25519 (64 vs 32 bytes) + - Ed25519 has faster verification times + - Secp256k1 has wider hardware support + - Both are equally secure when implemented correctly + + Security: + - Uses the same secure random generation as Ed25519 + - Follows Bitcoin/Ethereum security practices + - Compatible with standard Secp256k1 implementations + - Addresses are derived using Aptos's standard address scheme + + Note: + While Secp256k1 is supported, Ed25519 is recommended for new applications + due to its superior performance characteristics. Use Secp256k1 primarily + for compatibility with existing Ethereum-based systems. + """ private_key = secp256k1_ecdsa.PrivateKey.random() public_key = asymmetric_crypto_wrapper.PublicKey(private_key.public_key()) account_address = AccountAddress.from_key(public_key) @@ -49,12 +284,151 @@ def generate_secp256k1_ecdsa() -> Account: @staticmethod def load_key(key: str) -> Account: + """Create an Account from a hex-encoded Ed25519 private key string. + + This method reconstructs an Account from a previously exported private key. + It's commonly used to import accounts from external sources, CLI tools, + or when restoring accounts from backup storage. + + Args: + key: Hex-encoded Ed25519 private key string (64 characters, 32 bytes). + Can be with or without '***' prefix. + + Returns: + Account: An account instance created from the given private key. + + Raises: + ValueError: If the key format is invalid or cannot be parsed. + + Examples: + Import from hex string:: + + # Standard hex format (64 characters) + private_key_hex = "1a2b3c4d5e6f789..." # 64 hex chars + account = Account.load_key(private_key_hex) + + # With '***' prefix + prefixed_key = "***1a2b3c4d5e6f789..." + account = Account.load_key(prefixed_key) + + Restore from backup:: + + # Export account key for backup + original_account = Account.generate() + backup_key = str(original_account.private_key) + + # Later, restore from backup + restored_account = Account.load_key(backup_key) + + # Verify they're the same + assert original_account.address() == restored_account.address() + + CLI integration:: + + # Import from Aptos CLI output + # aptos init --profile my-account + # aptos account list --profile my-account + cli_private_key = "***a1b2c3d4e5f6..." + account = Account.load_key(cli_private_key) + + Security Considerations: + - **Never hardcode private keys** in source code + - Use environment variables or secure key management + - Validate key sources to prevent injection attacks + - Consider using encrypted storage for sensitive keys + + Note: + This method only supports Ed25519 private keys. For Secp256k1 keys, + you'll need to use the appropriate Secp256k1 import methods or + construct the account manually. + """ private_key = ed25519.PrivateKey.from_str(key) account_address = AccountAddress.from_key(private_key.public_key()) return Account(account_address, private_key) @staticmethod def load(path: str) -> Account: + """Load an Account from a JSON file containing account data. + + This method reads account information from a JSON file created by the + store() method or compatible external tools. It provides persistent + storage and retrieval of account credentials. + + Args: + path: Path to the JSON file containing account data. The file must + contain 'account_address' and 'private_key' fields. + + Returns: + Account: An account instance loaded from the file data. + + Raises: + FileNotFoundError: If the specified file doesn't exist. + json.JSONDecodeError: If the file contains invalid JSON. + KeyError: If required fields are missing from the JSON. + ValueError: If the account data is malformed. + + File Format: + Expected JSON structure:: + + { + "account_address": "***1234567890abcdef...", + "private_key": "***abcdef1234567890..." + } + + Examples: + Save and load account:: + + # Create and save account + original_account = Account.generate() + original_account.store("./wallet.json") + + # Load account later + loaded_account = Account.load("./wallet.json") + + # Verify integrity + assert original_account.address() == loaded_account.address() + assert original_account == loaded_account + + Load from CLI-generated file:: + + # If you used: aptos init --profile myaccount + # The profile data can be imported + try: + account = Account.load("./.aptos/config.yaml") + print(f"Loaded account: {account.address()}") + except Exception as e: + print(f"Failed to load account: {e}") + + Batch operations:: + + import os + + # Load multiple accounts from directory + accounts = [] + for filename in os.listdir("./wallets/"): + if filename.endswith(".json"): + filepath = os.path.join("./wallets/", filename) + account = Account.load(filepath) + accounts.append(account) + + print(f"Loaded {len(accounts)} accounts") + + Security Considerations: + - **File Permissions**: Ensure JSON files have restricted permissions + - **Encryption**: Consider encrypting files containing private keys + - **Backup**: Keep secure backups of account files + - **Access Control**: Limit access to account files in production + + Integration: + Compatible with files created by: + - The Account.store() method + - Aptos CLI account exports + - Custom wallet implementations using the same format + + Note: + The loaded account will have Ed25519 keys. The address format is + flexible and accepts both strict and relaxed address formats. + """ with open(path) as file: data = json.load(file) return Account( @@ -63,6 +437,70 @@ def load(path: str) -> Account: ) def store(self, path: str): + """Store the Account data to a JSON file for persistent storage. + + This method serializes the account's address and private key to a JSON + file that can be later loaded using the load() method. It provides a + simple way to persist account credentials across application sessions. + + Args: + path: File path where to save the account data. Will create or + overwrite the file at the specified location. + + Raises: + PermissionError: If the file cannot be written due to permissions. + OSError: If there are filesystem-related errors. + + Security Warning: + The JSON file will contain the private key in plaintext. Ensure + proper file permissions and consider encryption for sensitive data. + + Examples: + Basic storage and retrieval:: + + # Create account + account = Account.generate() + + # Store to file + account.store("./my_wallet.json") + + # Load later + loaded_account = Account.load("./my_wallet.json") + assert account == loaded_account + + Secure file permissions:: + + import os + import stat + + # Store account + account.store("./secure_wallet.json") + + # Set restrictive permissions (owner read/write only) + os.chmod("./secure_wallet.json", stat.S_IRUSR | stat.S_IWUSR) + + Backup multiple accounts:: + + accounts = [Account.generate() for _ in range(5)] + + for i, account in enumerate(accounts): + filename = f"./backups/account_{i}.json" + account.store(filename) + print(f"Saved account {account.address()} to {filename}") + + File Format: + Creates JSON with structure:: + + { + "account_address": "***", + "private_key": "***" + } + + Note: + - The file will be created or overwritten if it exists + - Only Ed25519 private keys are currently supported for storage + - Consider implementing encryption wrapper for production use + """ data = { "account_address": str(self.account_address), "private_key": str(self.private_key), @@ -71,35 +509,301 @@ def store(self, path: str): json.dump(data, file) def address(self) -> AccountAddress: - """Returns the address associated with the given account""" - + """Get the blockchain address associated with this account. + + The account address is a unique identifier derived from the account's + public key using SHA3-256 hashing. This address is used to identify + the account on the blockchain and in transactions. + + Returns: + AccountAddress: The unique address for this account on the blockchain. + + Examples: + Get account address:: + + account = Account.generate() + address = account.address() + print(f"Account address: {address}") + # Output: Account address: ***a1b2c3d4e5f67890... + + Use address in transactions:: + + from aptos_sdk.async_client import RestClient + + async def check_balance(): + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + + # Use account address for queries + balance = await client.account_balance(account.address()) + print(f"Balance: {balance} APT") + + # Use as transaction recipient + recipient_address = account.address() + + Address comparison:: + + account1 = Account.generate() + account2 = Account.generate() + + # Addresses are unique + assert account1.address() != account2.address() + + # Same account always has same address + assert account1.address() == account1.address() + + Properties: + - **Deterministic**: Same private key always produces same address + - **Unique**: Each private key produces a unique address + - **Immutable**: Address cannot change without changing the private key + - **Format**: 32-byte hex string with '***' prefix + + Note: + The address is computed from the public key, not stored separately. + This ensures consistency and reduces the risk of address/key mismatches. + """ return self.account_address def auth_key(self) -> str: - """Returns the auth_key for the associated account""" + """Get the authentication key for this account. + + The authentication key is derived from the account's public key and + represents the current key that can authenticate transactions for this + account. Initially, the auth key equals the account address, but it can + change through key rotation operations. + + Returns: + str: The authentication key as a hex string with '***' prefix. + + Examples: + Check initial auth key:: + + account = Account.generate() + address = str(account.address()) + auth_key = account.auth_key() + + # Initially, auth key equals address + assert address == auth_key + print(f"Address: {address}") + print(f"Auth key: {auth_key}") + + Use in authentication:: + + # Auth key is used for verifying transaction signatures + transaction_data = b"transaction_payload" + signature = account.sign(transaction_data) + + # The auth key identifies which public key to use for verification + public_key = account.public_key() + is_valid = public_key.verify(transaction_data, signature) + + Key rotation scenario:: + + # After key rotation, auth key would differ from original address + # but the account address remains the same for identification + original_address = account.address() + current_auth_key = account.auth_key() + + # Address is permanent, auth key can change + print(f"Permanent address: {original_address}") + print(f"Current auth key: {current_auth_key}") + + Key Concepts: + - **Account Address**: Permanent identifier, never changes + - **Authentication Key**: Current key for signing, can be rotated + - **Initial State**: Auth key == address for new accounts + - **After Rotation**: Auth key != address, but address stays same + + Use Cases: + - Verifying transaction signatures + - Key rotation operations + - Multi-signature account management + - Authentication in smart contracts + + Note: + For newly generated accounts, the authentication key will be identical + to the account address. They only differ after key rotation operations. + """ return str(AccountAddress.from_key(self.private_key.public_key())) def sign(self, data: bytes) -> asymmetric_crypto.Signature: + """Sign arbitrary data with the account's private key. + + This method creates a cryptographic signature over any data using the + account's private key. The signature can be verified using the corresponding + public key, providing proof of data authenticity and account ownership. + + Args: + data: The raw bytes to be signed. Can be any binary data including + transaction payloads, messages, or arbitrary content. + + Returns: + asymmetric_crypto.Signature: A signature object that can be verified + with the account's public key. + + Examples: + Sign custom message:: + + account = Account.generate() + message = b"Hello, Aptos blockchain!" + + # Create signature + signature = account.sign(message) + + # Verify signature + public_key = account.public_key() + is_valid = public_key.verify(message, signature) + print(f"Signature valid: {is_valid}") # True + + Sign structured data:: + + import json + + # Sign JSON data + data_dict = { + "action": "transfer", + "amount": 1000, + "recipient": "***abc123..." + } + data_bytes = json.dumps(data_dict, sort_keys=True).encode() + signature = account.sign(data_bytes) + + Authentication proof:: + + # Prove account ownership + challenge = b"prove_ownership_2023" + proof_signature = account.sign(challenge) + + # Others can verify you own the account + # without revealing your private key + + Transaction component:: + + # This is typically used internally by transaction signing + # but can be used for custom transaction construction + raw_transaction_bytes = serialize_transaction(...) + transaction_signature = account.sign(raw_transaction_bytes) + + Security Properties: + - **Non-repudiation**: Only the private key holder can create valid signatures + - **Data Integrity**: Signatures detect any modification to signed data + - **Authentication**: Proves the signer owns the private key + - **Unforgeable**: Cryptographically impossible to forge without the private key + + Signature Schemes: + - **Ed25519**: Default, fast verification, 64-byte signatures + - **Secp256k1 ECDSA**: Ethereum-compatible, variable-length signatures + + Note: + The signature is deterministic for Ed25519 but may be randomized for + Secp256k1, meaning the same data might produce different valid signatures + each time it's signed with Secp256k1. + """ return self.private_key.sign(data) def sign_simulated_transaction( self, transaction: RawTransactionInternal ) -> AccountAuthenticator: + """ + Sign a simulated transaction for testing purposes. + + :param transaction: The transaction to simulate signing + :return: An AccountAuthenticator for the simulated signature + """ return transaction.sign_simulated(self.private_key.public_key()) def sign_transaction( self, transaction: RawTransactionInternal ) -> AccountAuthenticator: + """ + Sign a transaction with this account's private key. + + :param transaction: The transaction to sign + :return: An AccountAuthenticator containing the signature + """ return transaction.sign(self.private_key) def public_key(self) -> asymmetric_crypto.PublicKey: - """Returns the public key for the associated account""" - + """Get the public key corresponding to this account's private key. + + The public key is the cryptographic counterpart to the private key and + is used for signature verification, address derivation, and sharing with + others who need to verify signatures or send transactions to this account. + + Returns: + asymmetric_crypto.PublicKey: The public key that corresponds to this + account's private key. + + Examples: + Get public key for verification:: + + account = Account.generate() + public_key = account.public_key() + + # Use for signature verification + message = b"test message" + signature = account.sign(message) + is_valid = public_key.verify(message, signature) + print(f"Signature valid: {is_valid}") # True + + Share public key safely:: + + # Public keys are safe to share + public_key_hex = str(account.public_key()) + print(f"My public key: {public_key_hex}") + + # Others can use it to: + # 1. Verify signatures from you + # 2. Derive your account address + # 3. Send you transactions + + Address derivation:: + + from aptos_sdk.account_address import AccountAddress + + # Address is derived from public key + derived_address = AccountAddress.from_key(account.public_key()) + account_address = account.address() + + assert derived_address == account_address + + Multi-signature setup:: + + # Collect public keys for multi-sig account + accounts = [Account.generate() for _ in range(3)] + public_keys = [acc.public_key() for acc in accounts] + + # Use public_keys to create multi-signature account + # (threshold signatures, etc.) + + Key Properties: + - **Safe to Share**: Public keys can be shared openly without risk + - **Deterministic**: Always the same for a given private key + - **Verification**: Used to verify signatures created by the private key + - **Address Derivation**: Account addresses are computed from public keys + + Common Uses: + - Signature verification by other parties + - Creating multi-signature accounts + - Address computation and validation + - Key rotation proofs and challenges + - Smart contract public key storage + + Note: + Unlike private keys, public keys are safe to store, transmit, and share. + They enable others to interact with your account without compromising security. + """ return self.private_key.public_key() class RotationProofChallenge: - type_info_account_address: AccountAddress = AccountAddress.from_str("0x1") + """ + Represents a rotation proof challenge for rotating authentication keys. + + This challenge is used to prove ownership when rotating an account's + authentication key to a new public key. + """ + + type_info_account_address: AccountAddress = AccountAddress.from_str("***") type_info_module_name: str = "account" type_info_struct_name: str = "RotationProofChallenge" sequence_number: int @@ -114,12 +818,25 @@ def __init__( current_auth_key: AccountAddress, new_public_key: asymmetric_crypto.PublicKey, ): + """ + Initialize a rotation proof challenge. + + :param sequence_number: The sequence number for this rotation + :param originator: The account address initiating the rotation + :param current_auth_key: The current authentication key + :param new_public_key: The new public key to rotate to + """ self.sequence_number = sequence_number self.originator = originator self.current_auth_key = current_auth_key self.new_public_key = new_public_key def serialize(self, serializer: Serializer): + """ + Serialize the rotation proof challenge using BCS serialization. + + :param serializer: The BCS serializer to use for serialization + """ self.type_info_account_address.serialize(serializer) serializer.str(self.type_info_module_name) serializer.str(self.type_info_struct_name) diff --git a/aptos_sdk/account_address.py b/aptos_sdk/account_address.py index 0215d7e..529eed2 100644 --- a/aptos_sdk/account_address.py +++ b/aptos_sdk/account_address.py @@ -1,6 +1,65 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Account address management for the Aptos blockchain. + +This module provides comprehensive support for managing account addresses on the Aptos +blockchain, including address parsing, validation, derivation, and formatting according +to the AIP-40 address standard. + +Key features: +- AIP-40 compliant address parsing and formatting +- Address derivation from public keys and seeds +- Support for special address formats +- Resource account and named object address generation +- Strict and relaxed parsing modes + +The module implements the Aptos address standard defined in AIP-40, which specifies +that addresses should be represented in either LONG form (64 hex characters) or +SHORT form for special addresses (addresses 0x0 through 0xf). + +Examples: + Basic address operations:: + + # Parse from string (strict) + addr = AccountAddress.from_str("0x1") + + # Parse from string (relaxed) + addr = AccountAddress.from_str_relaxed("1") + + # Derive from public key + addr = AccountAddress.from_key(public_key) + + Special address handling:: + + # Special addresses use SHORT form + special_addr = AccountAddress.from_str("0xa") + print(special_addr) # "0xa" + + # Non-special addresses use LONG form + regular_addr = AccountAddress.from_str( + "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + ) + + Resource and object addresses:: + + # Create resource account address + resource_addr = AccountAddress.for_resource_account( + creator_address, b"seed" + ) + + # Create named object address + object_addr = AccountAddress.for_named_object( + creator_address, b"object_name" + ) + + # Create token collection address + collection_addr = AccountAddress.for_named_collection( + creator_address, "My Collection" + ) +""" + from __future__ import annotations import hashlib @@ -12,6 +71,22 @@ class AuthKeyScheme: + """Authentication key schemes for address derivation. + + This class defines the byte constants used as suffixes when deriving addresses + from various sources. Each scheme represents a different method of address + derivation and ensures that addresses derived through different methods + cannot collide. + + Attributes: + Ed25519: Single Ed25519 key authentication (0x00) + MultiEd25519: Multi-signature Ed25519 authentication (0x01) + SingleKey: Single key authentication wrapper (0x02) + MultiKey: Multi-key authentication with threshold (0x03) + DeriveObjectAddressFromGuid: Object address from GUID (0xFD) + DeriveObjectAddressFromSeed: Object address from seed (0xFE) + DeriveResourceAccountAddress: Resource account address (0xFF) + """ Ed25519: bytes = b"\x00" MultiEd25519: bytes = b"\x01" SingleKey: bytes = b"\x02" @@ -22,43 +97,121 @@ class AuthKeyScheme: class ParseAddressError(Exception): - """ - There was an error parsing an address. + """Exception raised when there's an error parsing an account address. + + This exception is raised when an address string or byte sequence cannot + be parsed into a valid AccountAddress, typically due to invalid length, + format, or content. + + Examples: + Catching parse errors:: + + try: + addr = AccountAddress.from_str("invalid") + except ParseAddressError as e: + print(f"Failed to parse address: {e}") """ class AccountAddress: + """Represents an account address on the Aptos blockchain. + + An AccountAddress is a 32-byte identifier for accounts, objects, and resources + on the Aptos blockchain. It implements the AIP-40 address standard for parsing + and formatting addresses in both strict and relaxed modes. + + The address system supports: + - Special addresses (0x0 through 0xf) with SHORT representation + - Regular addresses with LONG representation (64 hex characters) + - Address derivation from public keys + - Resource account and named object address generation + + Attributes: + address: The raw 32-byte address data + LENGTH: The required byte length of all addresses (32) + + Examples: + Creating addresses:: + + # From hex string (strict parsing) + addr1 = AccountAddress.from_str("0x1") + + # From hex string (relaxed parsing) + addr2 = AccountAddress.from_str_relaxed("abc123") + + # From public key + addr3 = AccountAddress.from_key(public_key) + + # From raw bytes + addr4 = AccountAddress(b"\x00" * 32) + + Address formatting:: + + special_addr = AccountAddress.from_str("0xa") + print(special_addr) # "0xa" (SHORT form) + + regular_addr = AccountAddress.from_str( + "0x" + "1" * 64 + ) + print(regular_addr) # Long form with 0x prefix + """ address: bytes LENGTH: int = 32 def __init__(self, address: bytes): + """Initialize an AccountAddress with raw address bytes. + + Args: + address: The 32-byte address data. + + Raises: + ParseAddressError: If the address is not exactly 32 bytes. + """ self.address = address if len(address) != AccountAddress.LENGTH: raise ParseAddressError("Expected address of length 32") def __eq__(self, other: object) -> bool: + """Check equality with another AccountAddress. + + Args: + other: The object to compare with. + + Returns: + True if both addresses have the same raw bytes. + """ if not isinstance(other, AccountAddress): return NotImplemented return self.address == other.address def __str__(self): - """ - Represent an account address in a way that is compliant with the v1 address - standard. The standard is defined as part of AIP-40, read more here: - https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md - - In short, all special addresses SHOULD be represented in SHORT form, e.g. - - 0x1 - - All other addresses MUST be represented in LONG form, e.g. - - 0x002098630cfad4734812fa37dc18d9b8d59242feabe49259e26318d468a99584 - - For an explanation of what defines a "special" address, see `is_special`. - - All string representations of addresses MUST be prefixed with 0x. + """Get the AIP-40 compliant string representation of this address. + + Represents an account address according to the v1 address standard + defined in AIP-40. Special addresses (0x0 through 0xf) are shown in + SHORT form, while all other addresses are shown in LONG form. + + The formatting rules are: + - Special addresses: "0x0" through "0xf" (SHORT form) + - Regular addresses: "0x" + 64 hex characters (LONG form) + + Returns: + AIP-40 compliant string representation with "0x" prefix. + + Examples: + Special address formatting:: + + addr = AccountAddress(b"\x00" * 32) + str(addr) # "0x0" + + Regular address formatting:: + + addr = AccountAddress(b"\x10" + b"\x00" * 31) + str(addr) # "0x1000000000000000000000000000000000000000000000000000000000000000" + + See Also: + AIP-40 standard: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md """ suffix = self.address.hex() if self.is_special(): @@ -66,51 +219,95 @@ def __str__(self): return f"0x{suffix}" def __repr__(self): + """Get the string representation for debugging. + + Returns: + Same as __str__ for consistency. + """ return self.__str__() def is_special(self): - """ - Returns whether the address is a "special" address. Addresses are considered - special if the first 63 characters of the hex string are zero. In other words, - an address is special if the first 31 bytes are zero and the last byte is - smaller than `0b10000` (16). In other words, special is defined as an address - that matches the following regex: `^0x0{63}[0-9a-f]$`. In short form this means - the addresses in the range from `0x0` to `0xf` (inclusive) are special. - - For more details see the v1 address standard defined as part of AIP-40: - https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md + """Check if this address qualifies as a "special" address. + + Special addresses are those in the range 0x0 to 0xf (inclusive) that + can be represented in SHORT form according to AIP-40. An address is + considered special if: + - The first 31 bytes are all zero + - The last byte is less than 16 (0x10) + + This corresponds to addresses that match the regex pattern: + ^0{63}[0-9a-f]$ in hexadecimal representation. + + Returns: + True if this is a special address that can use SHORT form. + + Examples: + Special addresses:: + + AccountAddress(b"\x00" * 32).is_special() # True (0x0) + AccountAddress(b"\x00" * 31 + b"\x0f").is_special() # True (0xf) + + Non-special addresses:: + + AccountAddress(b"\x00" * 31 + b"\x10").is_special() # False (0x10) + AccountAddress(b"\x01" + b"\x00" * 31).is_special() # False + + See Also: + AIP-40 standard: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md """ return all(b == 0 for b in self.address[:-1]) and self.address[-1] < 0b10000 @staticmethod def from_str(address: str) -> AccountAddress: - """ - NOTE: This function has strict parsing behavior. For relaxed behavior, please use - `from_string_relaxed` function. - - Creates an instance of AccountAddress from a hex string. - - This function allows only the strictest formats defined by AIP-40. In short this - means only the following formats are accepted: - - LONG - - SHORT for special addresses - - Where: - - LONG is defined as 0x + 64 hex characters. - - SHORT for special addresses is 0x0 to 0xf inclusive without padding zeroes. - - This means the following are not accepted: - - SHORT for non-special addresses. - - Any address without a leading 0x. - - Learn more about the different address formats by reading AIP-40: - https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md. - - Parameters: - - address (str): A hex string representing an account address. - + """Create an AccountAddress from a hex string with strict AIP-40 validation. + + This function enforces the strictest address format requirements defined + by AIP-40. It only accepts properly formatted addresses with appropriate + prefixes and length requirements. + + Accepted formats: + - LONG form: "0x" + exactly 64 hex characters + - SHORT form: "0x" + single hex character (0-f) for special addresses only + + Args: + address: A hex string representing the account address. + Returns: - - AccountAddress: An instance of AccountAddress. + A new AccountAddress instance. + + Raises: + RuntimeError: If the address format doesn't meet strict AIP-40 requirements: + - Missing "0x" prefix + - Wrong length for address type + - Padding zeroes in special addresses + - Short form used for non-special addresses + + Examples: + Valid strict format usage:: + + # Special addresses in SHORT form + addr1 = AccountAddress.from_str("0x0") + addr2 = AccountAddress.from_str("0xf") + + # Regular addresses in LONG form + addr3 = AccountAddress.from_str( + "0x" + "1" * 64 + ) + + Invalid formats (will raise RuntimeError):: + + # Missing 0x prefix + AccountAddress.from_str("123abc...") + + # Padded special address + AccountAddress.from_str("0x0f") + + # Short form for non-special address + AccountAddress.from_str("0x10") + + See Also: + - from_str_relaxed: For lenient parsing + - AIP-40: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md """ # Assert the string starts with 0x. if not address.startswith("0x"): @@ -145,31 +342,51 @@ def from_str(address: str) -> AccountAddress: @staticmethod def from_str_relaxed(address: str) -> AccountAddress: - """ - NOTE: This function has relaxed parsing behavior. For strict behavior, please use - the `from_string` function. Where possible, use `from_string` rather than this - function. `from_string_relaxed` is only provided for backwards compatibility. - - Creates an instance of AccountAddress from a hex string. - - This function allows all formats defined by AIP-40. In short, this means the - following formats are accepted: - - LONG, with or without leading 0x - - SHORT, with or without leading 0x - - Where: - - LONG is 64 hex characters. - - SHORT is 1 to 63 hex characters inclusive. - - Padding zeroes are allowed, e.g., 0x0123 is valid. - - Learn more about the different address formats by reading AIP-40: - https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md. - - Parameters: - - address (str): A hex string representing an account address. - + """Create an AccountAddress from a hex string with relaxed validation. + + This function provides backward compatibility by accepting various address + formats beyond the strict AIP-40 requirements. It's more permissive than + from_str() and handles padding and missing prefixes automatically. + + Accepted formats: + - LONG form: 64 hex characters (with or without "0x" prefix) + - SHORT form: 1-63 hex characters (with or without "0x" prefix) + - Padding zeroes are automatically added as needed + + Args: + address: A hex string representing the account address. + Returns: - - AccountAddress: An instance of AccountAddress. + A new AccountAddress instance. + + Raises: + RuntimeError: If the hex string is invalid: + - Empty or too long (>64 characters after removing "0x") + - Contains non-hexadecimal characters + + Examples: + Flexible format handling:: + + # With or without 0x prefix + addr1 = AccountAddress.from_str_relaxed("0x1") + addr2 = AccountAddress.from_str_relaxed("1") + + # Padding handled automatically + addr3 = AccountAddress.from_str_relaxed("abc123") + addr4 = AccountAddress.from_str_relaxed("0x00abc123") + + # Long addresses + addr5 = AccountAddress.from_str_relaxed( + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ) + + Note: + Use from_str() instead when possible for strict AIP-40 compliance. + This method is primarily for backward compatibility. + + See Also: + - from_str: For strict AIP-40 compliant parsing + - AIP-40: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md """ addr = address @@ -199,6 +416,49 @@ def from_str_relaxed(address: str) -> AccountAddress: @staticmethod def from_key(key: asymmetric_crypto.PublicKey) -> AccountAddress: + """Derive an account address from a public key. + + Creates an account address by hashing the public key bytes along with + the appropriate authentication scheme identifier. This ensures that + different key types produce different addresses even with identical + key material. + + The derivation process: + 1. Hash the public key's cryptographic bytes + 2. Append the appropriate AuthKeyScheme suffix + 3. Take the SHA3-256 hash to produce the 32-byte address + + Args: + key: A public key implementing the asymmetric_crypto.PublicKey interface. + Supported types: + - ed25519.PublicKey (single Ed25519 key) + - ed25519.MultiPublicKey (multi-signature Ed25519) + - asymmetric_crypto_wrapper.PublicKey (single key wrapper) + - asymmetric_crypto_wrapper.MultiPublicKey (multi-key wrapper) + + Returns: + The derived AccountAddress for the given public key. + + Raises: + Exception: If the key type is not supported. + + Examples: + Deriving addresses from different key types:: + + # From Ed25519 public key + ed25519_key = ed25519.PrivateKey.random().public_key() + addr1 = AccountAddress.from_key(ed25519_key) + + # From multi-signature key + keys = [ed25519.PrivateKey.random().public_key() for _ in range(3)] + multisig_key = ed25519.MultiPublicKey(keys, threshold=2) + addr2 = AccountAddress.from_key(multisig_key) + + Note: + The same public key will always produce the same address, but + different key types (even with identical cryptographic material) + will produce different addresses due to the scheme suffixes. + """ hasher = hashlib.sha3_256() hasher.update(key.to_crypto_bytes()) @@ -217,6 +477,43 @@ def from_key(key: asymmetric_crypto.PublicKey) -> AccountAddress: @staticmethod def for_resource_account(creator: AccountAddress, seed: bytes) -> AccountAddress: + """Generate a resource account address. + + Resource accounts are special accounts that don't have corresponding private + keys and are used to hold resources on behalf of other accounts. They are + created deterministically from a creator address and seed. + + Args: + creator: The address of the account creating the resource account. + seed: Arbitrary bytes used to ensure uniqueness. + + Returns: + The deterministic address for the resource account. + + Examples: + Creating resource account addresses:: + + creator_addr = AccountAddress.from_str("0x1") + + # Different seeds produce different addresses + resource1 = AccountAddress.for_resource_account( + creator_addr, b"my_resource_1" + ) + resource2 = AccountAddress.for_resource_account( + creator_addr, b"my_resource_2" + ) + + # Same creator + seed = same address (deterministic) + resource3 = AccountAddress.for_resource_account( + creator_addr, b"my_resource_1" + ) + assert resource1 == resource3 + + Note: + Resource accounts are commonly used for storing program resources + and don't have associated private keys, making them secure for + holding assets controlled by smart contracts. + """ hasher = hashlib.sha3_256() hasher.update(creator.address) hasher.update(seed) @@ -225,6 +522,38 @@ def for_resource_account(creator: AccountAddress, seed: bytes) -> AccountAddress @staticmethod def for_guid_object(creator: AccountAddress, creation_num: int) -> AccountAddress: + """Generate an object address from a GUID (Globally Unique Identifier). + + Creates a deterministic object address using the creator's address and + a creation number. This is used for objects that are created sequentially + and need unique addresses. + + Args: + creator: The address of the account creating the object. + creation_num: A sequential number used for uniqueness (typically + incremented for each object created by this account). + + Returns: + The deterministic address for the object. + + Examples: + Creating sequential object addresses:: + + creator = AccountAddress.from_str("0x123abc...") + + # Sequential object creation + obj1 = AccountAddress.for_guid_object(creator, 0) + obj2 = AccountAddress.for_guid_object(creator, 1) + obj3 = AccountAddress.for_guid_object(creator, 2) + + # Same parameters = same address + obj1_duplicate = AccountAddress.for_guid_object(creator, 0) + assert obj1 == obj1_duplicate + + Note: + The creation_num is typically managed by the blockchain to ensure + uniqueness. Each creator maintains their own sequence counter. + """ hasher = hashlib.sha3_256() serializer = Serializer() serializer.u64(creation_num) @@ -235,6 +564,44 @@ def for_guid_object(creator: AccountAddress, creation_num: int) -> AccountAddres @staticmethod def for_named_object(creator: AccountAddress, seed: bytes) -> AccountAddress: + """Generate a named object address from a seed. + + Creates a deterministic object address using the creator's address and + an arbitrary seed. This allows for creating objects with predictable + addresses based on meaningful names or identifiers. + + Args: + creator: The address of the account creating the object. + seed: Arbitrary bytes that uniquely identify this object. + Often derived from human-readable names. + + Returns: + The deterministic address for the named object. + + Examples: + Creating named object addresses:: + + creator = AccountAddress.from_str("0x123abc...") + + # Objects named with meaningful identifiers + config_obj = AccountAddress.for_named_object( + creator, b"global_config" + ) + + metadata_obj = AccountAddress.for_named_object( + creator, b"metadata_store" + ) + + # Same name = same address (deterministic) + config_duplicate = AccountAddress.for_named_object( + creator, b"global_config" + ) + assert config_obj == config_duplicate + + Note: + This is commonly used for singleton objects or well-known resources + that need predictable addresses for easy reference. + """ hasher = hashlib.sha3_256() hasher.update(creator.address) hasher.update(seed) @@ -245,6 +612,45 @@ def for_named_object(creator: AccountAddress, seed: bytes) -> AccountAddress: def for_named_token( creator: AccountAddress, collection_name: str, token_name: str ) -> AccountAddress: + """Generate a token address from collection and token names. + + Creates a deterministic address for a specific token within a collection. + The address is derived from the creator address and a combination of + the collection name and token name. + + Args: + creator: The address of the account that created the collection. + collection_name: The name of the token collection. + token_name: The name of the specific token within the collection. + + Returns: + The deterministic address for the named token. + + Examples: + Creating token addresses:: + + creator = AccountAddress.from_str("0x123abc...") + + # Tokens in different collections + token1 = AccountAddress.for_named_token( + creator, "My NFT Collection", "Token #1" + ) + + token2 = AccountAddress.for_named_token( + creator, "My NFT Collection", "Token #2" + ) + + # Same collection + token name = same address + token1_duplicate = AccountAddress.for_named_token( + creator, "My NFT Collection", "Token #1" + ) + assert token1 == token1_duplicate + + Note: + The seed format is: collection_name + "::" + token_name + This ensures tokens in different collections have different addresses + even if they share the same token name. + """ collection_bytes = collection_name.encode() token_bytes = token_name.encode() return AccountAddress.for_named_object( @@ -255,13 +661,65 @@ def for_named_token( def for_named_collection( creator: AccountAddress, collection_name: str ) -> AccountAddress: + """Generate a collection address from a collection name. + + Creates a deterministic address for a token collection based on the + creator address and collection name. + + Args: + creator: The address of the account creating the collection. + collection_name: The human-readable name of the collection. + + Returns: + The deterministic address for the named collection. + + Examples: + Creating collection addresses:: + + creator = AccountAddress.from_str("0x123abc...") + + # Collections with different names + collection1 = AccountAddress.for_named_collection( + creator, "My First Collection" + ) + + collection2 = AccountAddress.for_named_collection( + creator, "My Second Collection" + ) + + # Same name = same address (deterministic) + collection1_duplicate = AccountAddress.for_named_collection( + creator, "My First Collection" + ) + assert collection1 == collection1_duplicate + + Note: + This is commonly used for NFT collections and other grouped assets + where a predictable address is needed for the collection metadata. + """ return AccountAddress.for_named_object(creator, collection_name.encode()) @staticmethod def deserialize(deserializer: Deserializer) -> AccountAddress: + """Deserialize an AccountAddress from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized AccountAddress instance. + + Raises: + Exception: If there are insufficient bytes in the stream. + """ return AccountAddress(deserializer.fixed_bytes(AccountAddress.LENGTH)) def serialize(self, serializer: Serializer): + """Serialize this AccountAddress to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.fixed_bytes(self.address) @@ -357,6 +815,15 @@ class TestAddresses: class Test(unittest.TestCase): + """Comprehensive test suite for AccountAddress functionality. + + Tests all aspects of address handling including: + - Address derivation from various sources + - String parsing in strict and relaxed modes + - AIP-40 compliance validation + - Special address handling + - Resource and object address generation + """ def test_multi_ed25519(self): private_key_1 = ed25519.PrivateKey.from_str( "4e5e3be60f4bbd5e98d086d932f3ce779ff4b58da99bf9e5241ae1212a29e5fe" diff --git a/aptos_sdk/account_sequence_number.py b/aptos_sdk/account_sequence_number.py index e0e31af..3159823 100644 --- a/aptos_sdk/account_sequence_number.py +++ b/aptos_sdk/account_sequence_number.py @@ -1,6 +1,70 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Account sequence number management for the Aptos Python SDK. + +This module provides thread-safe, asynchronous management of account sequence numbers +for transaction submission. It implements flow control mechanisms to prevent +mempool overflow and ensures proper transaction ordering. + +Key features: +- Concurrent sequence number allocation with proper synchronization +- Automatic mempool flow control (max 100 transactions in flight per account) +- Timeout-based recovery from stuck transactions +- Thread-safe operations using asyncio locks +- Network state synchronization + +The sequence number management follows the flow control pattern used by the +Aptos faucet to handle high-throughput transaction submission while respecting +mempool limits. + +Examples: + Basic sequence number management:: + + from aptos_sdk.async_client import RestClient + from aptos_sdk.account_address import AccountAddress + from aptos_sdk.account_sequence_number import AccountSequenceNumber + + # Create client and sequence manager + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + account_addr = AccountAddress.from_str("0x123...") + seq_manager = AccountSequenceNumber(client, account_addr) + + # Get next sequence number for transaction + seq_num = await seq_manager.next_sequence_number() + + # Submit transaction with seq_num... + + # Wait for all pending transactions to complete + await seq_manager.synchronize() + + High-throughput transaction submission:: + + # Submit multiple transactions concurrently + tasks = [] + for i in range(50): + seq_num = await seq_manager.next_sequence_number() + task = submit_transaction_with_sequence(seq_num) + tasks.append(task) + + # Wait for all transactions + await asyncio.gather(*tasks) + await seq_manager.synchronize() + + Custom configuration:: + + from aptos_sdk.account_sequence_number import AccountSequenceNumberConfig + + # Custom flow control settings + config = AccountSequenceNumberConfig() + config.maximum_in_flight = 50 # Lower concurrency + config.maximum_wait_time = 60 # Longer timeout + config.sleep_time = 0.05 # Less aggressive polling + + seq_manager = AccountSequenceNumber(client, account_addr, config) +""" + from __future__ import annotations import asyncio @@ -14,7 +78,37 @@ class AccountSequenceNumberConfig: - """Common configuration for account number generation""" + """Configuration parameters for account sequence number management. + + This class defines the flow control parameters used by AccountSequenceNumber + to manage transaction submission rates and handle network congestion. + + Attributes: + maximum_in_flight: Maximum number of unconfirmed transactions allowed + per account (default: 100). This matches Aptos mempool limits. + maximum_wait_time: Maximum seconds to wait for transaction confirmation + before forcing a resync (default: 30). + sleep_time: Seconds to sleep between network polls when waiting + (default: 0.01). + + Examples: + Custom configuration:: + + config = AccountSequenceNumberConfig() + config.maximum_in_flight = 50 # More conservative + config.maximum_wait_time = 60 # Longer timeout + config.sleep_time = 0.05 # Less aggressive polling + + Low-latency configuration:: + + config = AccountSequenceNumberConfig() + config.maximum_in_flight = 10 # Fewer concurrent txns + config.sleep_time = 0.001 # Very frequent polling + + Note: + The default values are optimized for the Aptos mainnet and testnet + environments. Adjust based on network conditions and requirements. + """ maximum_in_flight: int = 100 maximum_wait_time: int = 30 @@ -22,29 +116,93 @@ class AccountSequenceNumberConfig: class AccountSequenceNumber: - """ - A managed wrapper around sequence numbers that implements the trivial flow control used by the - Aptos faucet: - * Submit up to 100 transactions per account in parallel with a timeout of 20 seconds - * If local assumes 100 are in flight, determine the actual committed state from the network - * If there are less than 100 due to some being committed, adjust the window - * If 100 are in flight Wait .1 seconds before re-evaluating - * If ever waiting more than 30 seconds restart the sequence number to the current on-chain state - Assumptions: - * Accounts are expected to be managed by a single AccountSequenceNumber and not used otherwise. - * They are initialized to the current on-chain state, so if there are already transactions in - flight, they may take some time to reset. - * Accounts are automatically initialized if not explicitly - - Notes: - * This is co-routine safe, that is many async tasks can be reading from this concurrently. - * The state of an account cannot be used across multiple AccountSequenceNumber services. - * The synchronize method will create a barrier that prevents additional next_sequence_number - calls until it is complete. - * This only manages the distribution of sequence numbers it does not help handle transaction - failures. - * If a transaction fails, you should call synchronize and wait for timeouts. - * Mempool limits the number of transactions per account to 100, hence why we chose 100. + """Thread-safe sequence number manager for high-throughput transaction submission. + + This class manages sequence number allocation for an Aptos account with built-in + flow control to prevent mempool overflow. It implements the same strategy used + by the Aptos faucet for reliable high-volume transaction processing. + + Flow Control Strategy: + - Allows up to 100 transactions in flight simultaneously (configurable) + - Monitors network state to track transaction confirmations + - Implements automatic backoff when mempool capacity is reached + - Provides timeout-based recovery for stuck transactions + - Ensures FIFO ordering of sequence number allocation + + Key Features: + - **Concurrency Safe**: Multiple async tasks can safely request sequence numbers + - **Automatic Initialization**: Syncs with on-chain state on first use + - **Flow Control**: Respects mempool limits to prevent rejection + - **Recovery Mechanisms**: Handles network issues and stuck transactions + - **Ordering Guarantees**: FIFO sequence number allocation via async locks + + Attributes: + _client: REST client for network communication + _account: The account address being managed + _lock: Async lock ensuring thread safety + _maximum_in_flight: Max unconfirmed transactions (default 100) + _maximum_wait_time: Timeout for transaction confirmation (default 30s) + _sleep_time: Polling interval during waits (default 0.01s) + _last_committed_number: Last confirmed on-chain sequence number + _current_number: Next sequence number to allocate + _initialized: Whether the manager has been initialized + + Important Assumptions: + - Each account should be managed by exactly one AccountSequenceNumber instance + - The account should not be used for manual transaction submission while managed + - Network connectivity is generally stable (handles temporary failures) + - Transactions eventually confirm or fail (not permanently stuck) + + Usage Guidelines: + - Call synchronize() after transaction failures to reset state + - Use non-blocking mode (block=False) to check availability without waiting + - Monitor logs for timeout warnings indicating potential issues + - Configure parameters based on network conditions and requirements + + Examples: + Basic usage:: + + seq_manager = AccountSequenceNumber(client, account_address) + + # Get next sequence number + seq_num = await seq_manager.next_sequence_number() + + # Submit transaction... + + # Wait for completion + await seq_manager.synchronize() + + High-throughput submission:: + + # Submit 50 transactions concurrently + tasks = [] + for i in range(50): + seq_num = await seq_manager.next_sequence_number() + task = submit_transaction(seq_num) + tasks.append(task) + + await asyncio.gather(*tasks) + await seq_manager.synchronize() + + Error handling:: + + try: + seq_num = await seq_manager.next_sequence_number(block=False) + if seq_num is None: + print("Too many transactions in flight, try later") + return + + # Submit transaction... + + except Exception as e: + # Reset state after errors + await seq_manager.synchronize() + raise + + Warning: + Do not use the same account with multiple AccountSequenceNumber instances + simultaneously, as this will lead to sequence number conflicts and transaction + failures. """ _client: RestClient @@ -65,6 +223,33 @@ def __init__( account: AccountAddress, config: AccountSequenceNumberConfig = AccountSequenceNumberConfig(), ): + """Initialize a sequence number manager for the given account. + + Args: + client: REST client for communicating with the Aptos network. + account: The account address to manage sequence numbers for. + config: Configuration parameters for flow control behavior. + Defaults to standard settings optimized for Aptos networks. + + Examples: + Standard initialization:: + + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + account = AccountAddress.from_str("0x123...") + seq_manager = AccountSequenceNumber(client, account) + + Custom configuration:: + + config = AccountSequenceNumberConfig() + config.maximum_in_flight = 50 + config.maximum_wait_time = 60 + + seq_manager = AccountSequenceNumber(client, account, config) + + Note: + The sequence manager starts uninitialized and will automatically + sync with the on-chain state on first use. + """ self._client = client self._account = account self._lock = asyncio.Lock() @@ -74,9 +259,55 @@ def __init__( self._sleep_time = config.sleep_time async def next_sequence_number(self, block: bool = True) -> Optional[int]: - """ - Returns the next sequence number available on this account. This leverages a lock to - guarantee first-in, first-out ordering of requests. + """Get the next available sequence number for transaction submission. + + This method provides thread-safe allocation of sequence numbers with built-in + flow control. It ensures FIFO ordering through an async lock and respects + mempool limits to prevent transaction rejection. + + Args: + block: If True (default), wait for an available sequence number when + the maximum number of transactions are in flight. If False, return + None immediately when no sequence numbers are available. + + Returns: + The next sequence number to use for transaction submission, or None + if block=False and the maximum number of transactions are in flight. + + Raises: + Exception: Network communication errors or other failures during + synchronization with the blockchain state. + + Examples: + Blocking mode (default):: + + # This will wait if necessary + seq_num = await seq_manager.next_sequence_number() + transaction.sequence_number = seq_num + + Non-blocking mode:: + + # Check availability without waiting + seq_num = await seq_manager.next_sequence_number(block=False) + if seq_num is None: + print("Account busy, try again later") + return + + Batch processing:: + + batch_size = 10 + sequence_numbers = [] + + for i in range(batch_size): + seq_num = await seq_manager.next_sequence_number() + sequence_numbers.append(seq_num) + + # Submit all transactions... + + Note: + This method automatically initializes the sequence manager on first use + by querying the current on-chain sequence number. Subsequent calls use + the cached state with periodic network updates for flow control. """ async with self._lock: if not self._initialized: @@ -104,16 +335,66 @@ async def next_sequence_number(self, block: bool = True) -> Optional[int]: return next_number async def _initialize(self): - """Optional initializer. called by next_sequence_number if not called prior.""" + """Initialize the sequence manager with current on-chain state. + + This method is automatically called on first use of next_sequence_number. + It queries the network to get the current sequence number for the account + and sets up the internal state tracking. + + Note: + This is an internal method. Users should not call it directly as it's + automatically invoked when needed. + """ self._initialized = True self._current_number = await self._current_sequence_number() self._last_uncommitted_number = self._current_number async def synchronize(self): - """ - Poll the network until all submitted transactions have either been committed or until - the maximum wait time has elapsed. This will prevent any calls to next_sequence_number - until this called has returned. + """Wait for all pending transactions to complete or timeout. + + This method creates a synchronization barrier that blocks all other + operations until either: + 1. All pending transactions are confirmed on-chain, or + 2. The maximum wait time is exceeded + + During synchronization, no new sequence numbers can be allocated, + ensuring a consistent view of the account state. + + Use Cases: + - After transaction submission to ensure completion + - Before critical operations requiring known account state + - After errors to reset and resync with network state + - At application shutdown to wait for pending operations + + Examples: + Wait for transaction batch completion:: + + # Submit transactions + for data in transaction_batch: + seq_num = await seq_manager.next_sequence_number() + await submit_transaction(data, seq_num) + + # Wait for all to complete + await seq_manager.synchronize() + print("All transactions processed") + + Error recovery:: + + try: + # Transaction operations... + pass + except Exception as e: + logging.error(f"Transaction failed: {e}") + # Reset state + await seq_manager.synchronize() + + Raises: + Exception: Network communication errors or other failures during + the synchronization process. + + Warning: + This method may take significant time to complete if transactions + are slow to confirm. Monitor the logs for timeout warnings. """ async with self._lock: await self._update() @@ -122,7 +403,20 @@ async def synchronize(self): ) async def _resync(self, check: Callable[[AccountSequenceNumber], bool]): - """Forces a resync with the upstream, this should be called within the lock""" + """Force resynchronization with the blockchain state. + + This internal method implements the timeout and recovery logic when + transactions are not confirming as expected. It polls the network + state and attempts to determine which transactions have confirmed. + + Args: + check: A callable that returns True while resync should continue. + Used to implement different resync conditions. + + Note: + This is an internal method called within the async lock context. + It should not be called directly by users. + """ start_time = await self._client.current_timestamp() failed = False while check(self): @@ -155,21 +449,46 @@ async def _resync(self, check: Callable[[AccountSequenceNumber], bool]): await self._initialize() async def _update(self): + """Update the last committed sequence number from the network. + + Returns: + The current sequence number from the blockchain. + + Note: + This is an internal method for network state synchronization. + """ self._last_uncommitted_number = await self._current_sequence_number() return self._last_uncommitted_number async def _current_sequence_number(self) -> int: + """Get the current sequence number for the account from the network. + + Returns: + The current sequence number as reported by the blockchain. + + Note: + This is an internal method that queries the network directly. + """ return await self._client.account_sequence_number(self._account) class Test(unittest.IsolatedAsyncioTestCase): + """Test suite for AccountSequenceNumber functionality. + + Tests the sequence number management including: + - Sequential number allocation + - Flow control when at capacity + - Network state synchronization + - Blocking vs non-blocking behavior + """ async def test_common_path(self): - """ - Verifies that: - * AccountSequenceNumber returns sequential numbers starting from 0 - * When the account has been updated on-chain include that in computations 100 -> 105 - * Ensure that none is returned if the call for next_sequence_number would block - * Ensure that synchronize completes if the value matches on-chain + """Test the common usage patterns of AccountSequenceNumber. + + This test verifies: + - Sequential number allocation starting from the current on-chain state + - Proper handling of on-chain state updates (e.g., 0 -> 5 -> 100+) + - Non-blocking behavior returns None when at capacity + - Synchronization completes when network state matches expectations """ patcher = unittest.mock.patch( "aptos_sdk.async_client.RestClient.account_sequence_number", return_value=0 diff --git a/aptos_sdk/aptos_cli_wrapper.py b/aptos_sdk/aptos_cli_wrapper.py index b3d0202..65ed548 100644 --- a/aptos_sdk/aptos_cli_wrapper.py +++ b/aptos_sdk/aptos_cli_wrapper.py @@ -1,6 +1,110 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Wrapper utilities for integrating with the official Aptos CLI. + +This module provides Python interfaces to the Aptos command-line interface (CLI), +enabling seamless integration between Python applications and the official Aptos +tooling. It supports Move package compilation, testing, and local testnet management. + +Key Features: +- **Move Compilation**: Compile Move packages with named address resolution +- **Package Testing**: Run Move unit tests with proper environment setup +- **Local Testnet**: Start and manage local Aptos testnets for development +- **CLI Integration**: Automatic detection and execution of Aptos CLI commands +- **Error Handling**: Comprehensive error reporting for CLI operations +- **Process Management**: Robust subprocess management with output capture + +Supported Operations: +- Move package compilation with metadata generation +- Move unit test execution with coverage reporting +- Local testnet startup with faucet integration +- Named address parameter resolution +- CLI availability detection and validation + +Use Cases: +- Development workflow automation +- CI/CD pipeline integration +- Local testing environment setup +- Move package build systems +- Automated deployment scripts +- Developer tooling and IDE integration + +Examples: + Compile a Move package:: + + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper + from aptos_sdk.account_address import AccountAddress + + # Define named addresses + named_addresses = { + "MyModule": AccountAddress.from_str("***1234..."), + "Treasury": AccountAddress.from_str("***5678...") + } + + # Compile the package + AptosCLIWrapper.compile_package( + package_dir="./my-move-package", + named_addresses=named_addresses + ) + + Run Move unit tests:: + + # Test the package with same named addresses + AptosCLIWrapper.test_package( + package_dir="./my-move-package", + named_addresses=named_addresses + ) + + Start a local testnet:: + + # Start local node with faucet + local_node = AptosCLIWrapper.start_node() + + # Wait for node to become operational + is_ready = await local_node.wait_until_operational() + + if is_ready: + print("Local testnet is ready!") + # Use LOCAL_NODE and LOCAL_FAUCET constants for connections + + # Clean up when done + local_node.stop() + + Check CLI availability:: + + if AptosCLIWrapper.does_cli_exist(): + print("Aptos CLI is available") + else: + print("Please install the Aptos CLI") + +Environment Variables: + APTOS_CLI_PATH: Path to the Aptos CLI executable if not in system PATH. + +Requirements: + - Aptos CLI installed and accessible + - Rust toolchain (for Move compilation) + - Network access (for dependency resolution) + - Sufficient disk space for compilation artifacts + +Error Handling: + The module provides specific exception types: + - MissingCLIError: CLI not found or not accessible + - CLIError: CLI command execution failed + +Security Considerations: + - Local testnets are for development only + - Private keys generated by local testnets are not secure + - Compilation artifacts may contain sensitive information + - Subprocess execution should be used carefully in production + +Note: + This wrapper requires the official Aptos CLI to be installed. The local + testnet functionality creates temporary directories and processes that + should be properly cleaned up after use. +""" + from __future__ import annotations import asyncio @@ -15,22 +119,108 @@ from .account_address import AccountAddress from .async_client import FaucetClient, RestClient -# Assume that the binary is in the global path if one is not provided. +# Default CLI binary path - can be overridden via environment variable DEFAULT_BINARY = os.getenv("APTOS_CLI_PATH", "aptos") -LOCAL_FAUCET = "http://127.0.0.1:8081" -LOCAL_NODE = "http://127.0.0.1:8080/v1" -# Assume that the node failed to start if it has been more than this time since the process started +# Local testnet endpoint URLs +LOCAL_FAUCET = "http://127.0.0.1:8081" # Default local faucet endpoint +LOCAL_NODE = "http://127.0.0.1:8080/v1" # Default local node REST API endpoint + +# Maximum time to wait for local testnet to become operational MAXIMUM_WAIT_TIME_SEC = 30 class AptosCLIWrapper: - """Tooling to make easy access to the Aptos CLI tool from within Python.""" + """Python wrapper for the official Aptos CLI with integrated tooling support. + + This class provides static methods for invoking Aptos CLI operations from Python, + including Move package compilation, testing, and local testnet management. It + handles subprocess management, error reporting, and named address resolution. + + Key Features: + - **Static Interface**: All methods are static for easy access + - **Error Handling**: Comprehensive error reporting with detailed output + - **Named Addresses**: Automatic formatting of named address parameters + - **Process Management**: Robust subprocess execution with output capture + - **Validation**: CLI availability checking before execution + + Examples: + Basic package operations:: + + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper + + # Check if CLI is available + if not AptosCLIWrapper.does_cli_exist(): + raise Exception("Aptos CLI not found") + + # Compile a Move package + AptosCLIWrapper.compile_package("./my-package", {}) + + # Run tests + AptosCLIWrapper.test_package("./my-package", {}) + + With named addresses:: + + from aptos_sdk.account_address import AccountAddress + + named_addresses = { + "admin": AccountAddress.from_str("***1"), + "user": AccountAddress.from_str("***2") + } + + AptosCLIWrapper.compile_package( + package_dir="./complex-package", + named_addresses=named_addresses + ) + + Note: + All methods require the Aptos CLI to be installed and accessible. + Use does_cli_exist() to verify availability before calling other methods. + """ @staticmethod def prepare_named_addresses( named_addresses: Dict[str, AccountAddress] ) -> List[str]: + """Convert named addresses dictionary to CLI argument format. + + This method transforms a Python dictionary of named addresses into the + command-line argument format expected by the Aptos CLI. It handles the + proper formatting and comma separation required by the CLI. + + Args: + named_addresses: Dictionary mapping address names to AccountAddress objects. + + Returns: + List of CLI arguments for named addresses, empty if no addresses provided. + + Examples: + Empty addresses:: + + >>> AptosCLIWrapper.prepare_named_addresses({}) + [] + + Single address:: + + >>> addresses = {"admin": AccountAddress.from_str("***1")} + >>> AptosCLIWrapper.prepare_named_addresses(addresses) + ['--named-addresses', 'admin=***1'] + + Multiple addresses:: + + >>> addresses = { + ... "admin": AccountAddress.from_str("***1"), + ... "user": AccountAddress.from_str("***2") + ... } + >>> args = AptosCLIWrapper.prepare_named_addresses(addresses) + >>> args + ['--named-addresses', 'admin=***1,user=***2'] + + Note: + The CLI expects named addresses in a comma-separated format after + the --named-addresses flag. This method handles the formatting + automatically. + """ total_names = len(named_addresses) args: List[str] = [] if total_names == 0: @@ -46,6 +236,57 @@ def prepare_named_addresses( @staticmethod def compile_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): + """Compile a Move package using the Aptos CLI. + + This method compiles a Move package with the specified named addresses, + generating bytecode and metadata required for package publishing. The + compilation process validates Move code, resolves dependencies, and + produces deployable artifacts. + + Args: + package_dir: Path to the directory containing the Move package + (must contain a Move.toml file). + named_addresses: Dictionary mapping named address identifiers + to their resolved AccountAddress values. + + Raises: + MissingCLIError: If the Aptos CLI is not found or accessible. + CLIError: If the compilation process fails with detailed error output. + + Examples: + Compile basic package:: + + AptosCLIWrapper.compile_package( + package_dir="./my-move-package", + named_addresses={} + ) + + Compile with named addresses:: + + from aptos_sdk.account_address import AccountAddress + + named_addresses = { + "deployer": AccountAddress.from_str("***1234..."), + "resource_account": AccountAddress.from_str("***5678...") + } + + AptosCLIWrapper.compile_package( + package_dir="./complex-package", + named_addresses=named_addresses + ) + + Compilation Output: + - Generated bytecode in build/ directory + - Package metadata for publishing + - Dependency resolution artifacts + - ABI files for integration + + Note: + - Requires Move.toml configuration file in package directory + - Named addresses must match those declared in Move.toml + - Compilation artifacts are saved to build/ subdirectory + - The --save-metadata flag ensures metadata is generated for publishing + """ AptosCLIWrapper.assert_cli_exists() args = [ DEFAULT_BINARY, @@ -63,11 +304,133 @@ def compile_package(package_dir: str, named_addresses: Dict[str, AccountAddress] @staticmethod def start_node() -> AptosInstance: + """Start a local Aptos testnet for development and testing. + + This method launches a complete local Aptos testnet including: + - A single validator node + - Built-in faucet service + - REST API endpoint + - Pre-funded test accounts + + Returns: + AptosInstance object representing the running testnet. + + Raises: + MissingCLIError: If the Aptos CLI is not found or accessible. + + Examples: + Basic local testnet:: + + # Start the testnet + testnet = AptosCLIWrapper.start_node() + + # Wait for it to become operational + is_ready = await testnet.wait_until_operational() + + if is_ready: + # Use LOCAL_NODE and LOCAL_FAUCET for connections + from aptos_sdk.async_client import RestClient, FaucetClient + + client = RestClient(LOCAL_NODE) + faucet = FaucetClient(LOCAL_FAUCET, client) + + # Perform operations... + + # Clean up + testnet.stop() + + Context manager pattern:: + + async def with_testnet(): + testnet = AptosCLIWrapper.start_node() + try: + await testnet.wait_until_operational() + yield testnet + finally: + testnet.stop() + + Testnet Features: + - Fresh blockchain state for each run + - Pre-funded accounts for testing + - Fast block times for rapid iteration + - Full Move VM and transaction processing + - REST API compatible with mainnet/testnet + + Note: + - Creates temporary directories that are cleaned up on stop + - Uses default ports (8080 for REST API, 8081 for faucet) + - Only one testnet should be running per machine + - Testnet state is ephemeral and lost when stopped + """ AptosCLIWrapper.assert_cli_exists() return AptosInstance.start() @staticmethod def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): + """Run Move unit tests for a package using the Aptos CLI. + + This method executes all unit tests defined in the Move package, + providing comprehensive test coverage and validation. Tests run in + an isolated environment with proper address resolution. + + Args: + package_dir: Path to the directory containing the Move package + (must contain a Move.toml file). + named_addresses: Dictionary mapping named address identifiers + to their resolved AccountAddress values. + + Raises: + MissingCLIError: If the Aptos CLI is not found or accessible. + CLIError: If any tests fail or the test process encounters errors. + + Examples: + Run basic tests:: + + AptosCLIWrapper.test_package( + package_dir="./my-move-package", + named_addresses={} + ) + + Run tests with named addresses:: + + from aptos_sdk.account_address import AccountAddress + + test_addresses = { + "test_admin": AccountAddress.from_str("***cafe"), + "test_user": AccountAddress.from_str("***beef") + } + + AptosCLIWrapper.test_package( + package_dir="./my-package", + named_addresses=test_addresses + ) + + Test Features: + - Isolated test execution environment + - Access to Move testing framework + - Proper address and resource simulation + - Detailed test result reporting + - Coverage analysis capabilities + + Test Structure: + Tests should be defined using the Move testing framework:: + + #[test] + public fun test_basic_functionality() { + // Test logic here + } + + #[test(admin = @***1, user = @***2)] + public fun test_with_addresses(admin: &signer, user: &signer) { + // Test with specific signers + } + + Note: + - Tests run in a simulated blockchain environment + - Named addresses are resolved during test execution + - Failed tests will cause the method to raise CLIError + - Test output includes detailed failure information + """ AptosCLIWrapper.assert_cli_exists() args = [ DEFAULT_BINARY, @@ -84,38 +447,268 @@ def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): @staticmethod def assert_cli_exists(): + """Assert that the Aptos CLI is available and accessible. + + Raises: + MissingCLIError: If the Aptos CLI cannot be found in the system PATH + or at the location specified by APTOS_CLI_PATH environment variable. + + Example: + >>> AptosCLIWrapper.assert_cli_exists() + # Raises MissingCLIError if CLI not found + + Note: + This method is called internally by other CLI operations to ensure + the CLI is available before attempting to execute commands. + """ if not AptosCLIWrapper.does_cli_exist(): raise MissingCLIError() @staticmethod def does_cli_exist(): + """Check if the Aptos CLI is available and accessible. + + This method verifies that the Aptos CLI binary can be found and executed + from the current environment. It checks both the system PATH and any + custom path specified via the APTOS_CLI_PATH environment variable. + + Returns: + True if the CLI is available, False otherwise. + + Examples: + Check CLI availability:: + + if AptosCLIWrapper.does_cli_exist(): + print("Aptos CLI is ready to use") + AptosCLIWrapper.compile_package("./package", {}) + else: + print("Please install the Aptos CLI") + print("Visit: https://aptos.dev/tools/aptos-cli/") + + Conditional operations:: + + def safe_compile(package_dir, named_addresses): + if not AptosCLIWrapper.does_cli_exist(): + raise RuntimeError("Aptos CLI not available") + return AptosCLIWrapper.compile_package(package_dir, named_addresses) + + Environment Variables: + APTOS_CLI_PATH: Custom path to the Aptos CLI binary if not in PATH. + + Note: + This method uses shutil.which() to locate the CLI binary, which + respects the system PATH and executable permissions. + """ return shutil.which(DEFAULT_BINARY) is not None class MissingCLIError(Exception): - """The CLI was not found in the expected path.""" + """Exception raised when the Aptos CLI cannot be found or accessed. + + This error indicates that the Aptos CLI binary is not available in the + system PATH or at the location specified by the APTOS_CLI_PATH environment + variable. This prevents any CLI operations from being executed. + + Common Causes: + - Aptos CLI is not installed + - CLI binary is not in system PATH + - APTOS_CLI_PATH points to incorrect location + - Insufficient permissions to execute the CLI + - CLI binary is corrupted or incompatible + + Resolution: + 1. Install the Aptos CLI from https://aptos.dev/tools/aptos-cli/ + 2. Ensure the binary is in your system PATH + 3. Set APTOS_CLI_PATH environment variable if using custom location + 4. Verify executable permissions on the CLI binary + + Examples: + Handling the error:: + + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper, MissingCLIError + + try: + AptosCLIWrapper.compile_package("./package", {}) + except MissingCLIError as e: + print(f"CLI Error: {e}") + print("Please install the Aptos CLI") + + Pre-emptive checking:: + + if not AptosCLIWrapper.does_cli_exist(): + raise MissingCLIError() + + # Safe to use CLI operations + AptosCLIWrapper.compile_package("./package", {}) + + Attributes: + message: Detailed error message indicating the expected CLI path. + """ def __init__(self): - super().__init__("The CLI was not found in the expected path, {DEFAULT_BINARY}") + """Initialize MissingCLIError with information about the expected CLI path.""" + super().__init__(f"The CLI was not found in the expected path, {DEFAULT_BINARY}") class CLIError(Exception): - """The CLI failed execution of a command.""" + """Exception raised when an Aptos CLI command execution fails. + + This error captures the details of a failed CLI operation, including the + command that was executed, its output, and any error messages. It provides + comprehensive information for debugging CLI integration issues. + + Common Causes: + - Compilation errors in Move code + - Missing dependencies or configuration + - Invalid command parameters + - Network issues during dependency resolution + - File system permission problems + - Invalid Move.toml configuration + - Conflicting named addresses + + Examples: + Handling compilation errors:: + + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper, CLIError + + try: + AptosCLIWrapper.compile_package("./faulty-package", {}) + except CLIError as e: + print(f"Compilation failed: {e}") + # Error includes command, output, and stderr for debugging + + Handling test failures:: + + try: + AptosCLIWrapper.test_package("./package-with-failing-tests", {}) + except CLIError as e: + print("Tests failed with details:") + print(e) + # Includes test failure details and error output + + Attributes: + command: List of command arguments that were executed. + output: Standard output from the failed command. + error: Standard error output from the failed command. + message: Formatted error message with command details. + """ def __init__(self, command, output, error): + """Initialize CLIError with command execution details. + + Args: + command: List of command arguments that were executed. + output: Standard output bytes from the command. + error: Standard error bytes from the command. + """ + self.command = command + self.output = output + self.error = error + super().__init__( f"The CLI operation failed:\n\tCommand: {' '.join(command)}\n\tOutput: {output}\n\tError: {error}" ) class AptosInstance: - """ - A standalone Aptos node running by itself. This still needs a bit of work: - * a test instance should be loaded into its own port space. Currently they share ports as - those are not configurable without a config file. As a result, it is possible that two - test runs may share a single AptosInstance and both successfully complete. - * Probably need some means to monitor the process in case it stops, as we aren't actively - monitoring this. + """Manages a local Aptos testnet instance for development and testing. + + This class represents a complete local Aptos testnet running as a subprocess, + including validator node, faucet service, and REST API endpoints. It provides + lifecycle management with start, stop, and health monitoring capabilities. + + Architecture: + - Single validator node configuration + - Built-in faucet for test token distribution + - REST API endpoint for transaction submission + - WebAPI for blockchain queries + - Temporary storage that's cleaned up on stop + + Lifecycle Management: + - Automatic subprocess management + - Background output/error capture + - Health monitoring with REST client + - Graceful shutdown and cleanup + - Temporary directory management + + Current Limitations: + - Fixed port configuration (8080 for REST, 8081 for faucet) + - Single instance per machine due to port conflicts + - No persistent storage across restarts + - Limited configuration customization + + Examples: + Basic usage:: + + # Start local testnet + testnet = AptosInstance.start() + + try: + # Wait for testnet to be ready + is_ready = await testnet.wait_until_operational() + + if is_ready: + # Use testnet for operations + from aptos_sdk.async_client import RestClient + client = RestClient(LOCAL_NODE) + chain_id = await client.chain_id() + print(f"Testnet ready, chain ID: {chain_id}") + else: + print("Testnet failed to start") + finally: + # Always clean up + testnet.stop() + + Monitoring testnet health:: + + testnet = AptosInstance.start() + + # Check if testnet is operational + if await testnet.is_operational(): + print("Testnet is healthy") + else: + print("Testnet is not responding") + + # Check for early termination + if testnet.is_stopped(): + print("Testnet process has stopped") + errors = testnet.errors() + if errors: + print(f"Error output: {errors}") + + Context management pattern:: + + async def with_local_testnet(): + testnet = AptosInstance.start() + try: + await testnet.wait_until_operational() + return testnet + except Exception: + testnet.stop() + raise + + Network Configuration: + - REST API: http://127.0.0.1:8080/v1 + - Faucet API: http://127.0.0.1:8081 + - Chain ID: Dynamically generated for each instance + + Resource Management: + - Creates temporary directory for blockchain data + - Spawns subprocess for node execution + - Background threads for output capture + - Automatic cleanup on stop() or destruction + + Security Notes: + - Intended for development and testing only + - Private keys are generated for convenience, not security + - No persistent storage or backup mechanisms + - Local network access only + + Future Improvements: + - Configurable port assignments for multiple instances + - Enhanced process monitoring and restart capabilities + - Configurable blockchain parameters + - Persistent storage options """ _node_runner: subprocess.Popen @@ -124,11 +717,35 @@ class AptosInstance: _error: List[str] def __del__(self): + """Destructor that ensures testnet cleanup on object destruction. + + This method is called automatically when the AptosInstance object + is garbage collected, ensuring that the testnet process is properly + terminated and resources are cleaned up. + + Note: + While this provides a safety net, it's better to explicitly + call stop() to ensure timely resource cleanup. + """ self.stop() def __init__( self, node_runner: subprocess.Popen, temp_dir: tempfile.TemporaryDirectory ): + """Initialize AptosInstance with subprocess and temporary directory. + + This constructor sets up the testnet instance with process management + and output capture. It starts background threads to continuously + capture stdout and stderr from the node process. + + Args: + node_runner: Subprocess running the Aptos node. + temp_dir: Temporary directory for blockchain data storage. + + Note: + This constructor is typically called by the start() class method + rather than directly by user code. + """ self._node_runner = node_runner self._temp_dir = temp_dir @@ -136,18 +753,26 @@ def __init__( self._error = [] def queue_lines(pipe, target): + """Background thread function to capture process output. + + Args: + pipe: Process pipe (stdout or stderr). + target: List to store captured lines. + """ for line in iter(pipe.readline, b""): if line == "": continue target.append(line) pipe.close() + # Start background thread for stderr capture err_thread = threading.Thread( target=queue_lines, args=(node_runner.stderr, self._error) ) err_thread.daemon = True err_thread.start() + # Start background thread for stdout capture out_thread = threading.Thread( target=queue_lines, args=(node_runner.stdout, self._output) ) @@ -156,13 +781,71 @@ def queue_lines(pipe, target): @staticmethod def start() -> AptosInstance: + """Start a new local Aptos testnet instance. + + This factory method creates and starts a complete local Aptos testnet + with validator node, faucet service, and REST API. The testnet runs + in an isolated temporary directory and is ready for development use. + + Returns: + AptosInstance object representing the running testnet. + + Raises: + subprocess.SubprocessError: If the testnet process fails to start. + OSError: If temporary directory creation fails. + + CLI Arguments Used: + - run-local-testnet: Main command to start local testnet + - --test-dir: Directory for blockchain data storage + - --with-faucet: Enable built-in faucet service + - --force-restart: Clean start, removing existing data + - --assume-yes: Skip interactive confirmations + + Examples: + Basic startup:: + + testnet = AptosInstance.start() + + # Wait for testnet to become ready + if await testnet.wait_until_operational(): + print("Testnet is ready for use!") + + # Clean up when done + testnet.stop() + + Error handling:: + + try: + testnet = AptosInstance.start() + await testnet.wait_until_operational() + except subprocess.SubprocessError as e: + print(f"Failed to start testnet: {e}") + except OSError as e: + print(f"Filesystem error: {e}") + + Network Endpoints: + After successful startup, the testnet provides: + - REST API: http://127.0.0.1:8080/v1 + - Faucet: http://127.0.0.1:8081 + + Resource Allocation: + - Temporary directory for blockchain storage + - Subprocess for node execution + - Background threads for output capture + - Network ports (8080, 8081) + + Note: + The testnet starts immediately but may take a few seconds to become + fully operational. Use wait_until_operational() to ensure readiness + before performing operations. + """ temp_dir = tempfile.TemporaryDirectory() args = [ DEFAULT_BINARY, "node", "run-local-testnet", "--test-dir", - str(temp_dir), + str(temp_dir.name), "--with-faucet", "--force-restart", "--assume-yes", @@ -173,17 +856,177 @@ def start() -> AptosInstance: return AptosInstance(node_runner, temp_dir) def stop(self): - self._node_runner.terminate() - self._node_runner.wait() - self._temp_dir.cleanup() + """Stop the local testnet and clean up all associated resources. + + This method gracefully terminates the testnet process, waits for it + to exit, and cleans up the temporary directory. It ensures complete + resource cleanup and prevents resource leaks. + + Process: + 1. Send termination signal to the node process + 2. Wait for process to exit cleanly + 3. Clean up temporary directory and files + 4. Release any held resources + + Examples: + Basic cleanup:: + + testnet = AptosInstance.start() + # ... use testnet ... + testnet.stop() # Always clean up + + Exception-safe cleanup:: + + testnet = AptosInstance.start() + try: + # Testnet operations + await testnet.wait_until_operational() + # ... perform tests ... + finally: + testnet.stop() # Ensure cleanup even if tests fail + + Note: + - This method is idempotent - safe to call multiple times + - Automatically called by destructor as safety net + - Blocks until process termination is complete + - All blockchain data is permanently lost after cleanup + + Warning: + After calling stop(), the AptosInstance should not be used for + any further operations. Create a new instance if needed. + """ + if hasattr(self, '_node_runner') and self._node_runner: + self._node_runner.terminate() + self._node_runner.wait() + if hasattr(self, '_temp_dir') and self._temp_dir: + self._temp_dir.cleanup() def errors(self) -> List[str]: + """Get error output lines from the testnet process. + + Returns: + List of error messages from the node's stderr stream. + + Examples: + Checking for errors:: + + testnet = AptosInstance.start() + + if testnet.is_stopped(): + errors = testnet.errors() + if errors: + print("Testnet failed with errors:") + for error in errors: + print(f" {error.strip()}") + + Debugging startup issues:: + + testnet = AptosInstance.start() + await asyncio.sleep(5) # Wait a bit + + if not await testnet.is_operational(): + print("Testnet not ready, checking errors:") + for error in testnet.errors(): + print(error) + + Note: + - Errors are captured continuously in background thread + - List grows over time as more errors occur + - Useful for debugging testnet startup or runtime issues + """ return self._error def output(self) -> List[str]: + """Get standard output lines from the testnet process. + + Returns: + List of output messages from the node's stdout stream. + + Examples: + Monitoring testnet output:: + + testnet = AptosInstance.start() + await testnet.wait_until_operational() + + # Check what the testnet has logged + output = testnet.output() + print("Testnet output:") + for line in output[-10:]: # Last 10 lines + print(f" {line.strip()}") + + Debugging connectivity:: + + testnet = AptosInstance.start() + + # Look for specific startup messages + output = testnet.output() + if any("REST API" in line for line in output): + print("REST API is ready") + + Note: + - Output is captured continuously in background thread + - Includes all informational and debug messages + - Useful for understanding testnet behavior and status + """ return self._output async def wait_until_operational(self) -> bool: + """Wait for the testnet to become fully operational. + + This method polls the testnet's health endpoints until either the + testnet becomes operational or the maximum wait time is exceeded. + It checks both the REST API and faucet service for readiness. + + Returns: + True if testnet becomes operational within timeout, False otherwise. + + Timeout: + Maximum wait time is controlled by MAXIMUM_WAIT_TIME_SEC (30 seconds). + + Examples: + Basic startup waiting:: + + testnet = AptosInstance.start() + + if await testnet.wait_until_operational(): + print("Testnet is ready for use!") + # Safe to make API calls + else: + print("Testnet failed to start within timeout") + testnet.stop() + + With custom timeout handling:: + + testnet = AptosInstance.start() + + start_time = time.time() + is_ready = await testnet.wait_until_operational() + elapsed = time.time() - start_time + + if is_ready: + print(f"Testnet ready in {elapsed:.2f} seconds") + else: + print(f"Testnet timeout after {elapsed:.2f} seconds") + errors = testnet.errors() + if errors: + print("Error output:", errors[-5:]) # Last 5 errors + + Health Checks: + - REST API chain_id() call must succeed + - Faucet healthy() check must return True + - Process must still be running + + Polling Behavior: + - Checks health every 100ms (0.1 seconds) + - Exits early if testnet becomes operational + - Exits early if testnet process stops + - Times out after MAXIMUM_WAIT_TIME_SEC seconds + + Note: + This method is essential for ensuring testnet readiness before + performing operations. Using the testnet before it's operational + will result in connection errors. + """ operational = await self.is_operational() start = time.time() last = start @@ -199,16 +1042,102 @@ async def wait_until_operational(self) -> bool: return not self.is_stopped() async def is_operational(self) -> bool: + """Check if the testnet is currently operational. + + This method performs health checks on both the REST API and faucet + service to determine if the testnet is ready to handle requests. + + Returns: + True if both REST API and faucet are responding correctly. + + Health Checks: + 1. REST API chain_id() call - validates core node functionality + 2. Faucet healthy() check - validates faucet service availability + + Examples: + Simple health check:: + + testnet = AptosInstance.start() + await asyncio.sleep(2) # Give it time to start + + if await testnet.is_operational(): + print("Testnet is healthy") + else: + print("Testnet is not responding") + + Continuous monitoring:: + + testnet = AptosInstance.start() + + for i in range(10): + if await testnet.is_operational(): + print(f"Healthy at attempt {i+1}") + break + await asyncio.sleep(1) + else: + print("Testnet failed to become healthy") + + Error Handling: + - Any exception during health checks returns False + - Network timeouts are treated as non-operational + - HTTP errors indicate non-operational state + - Client connections are properly cleaned up + + Note: + This method creates temporary client connections that are + automatically closed after the health check completes. + """ rest_client = RestClient(LOCAL_NODE) - faucet_client = FaucetClient(LOCAL_NODE, rest_client) + faucet_client = FaucetClient(LOCAL_FAUCET, rest_client) try: + # Test REST API functionality await rest_client.chain_id() + # Test faucet service health return await faucet_client.healthy() except Exception: + # Any exception means not operational return False finally: + # Always clean up client connection await rest_client.close() def is_stopped(self) -> bool: + """Check if the testnet process has stopped running. + + Returns: + True if the node process has terminated, False if still running. + + Examples: + Check for early termination:: + + testnet = AptosInstance.start() + await asyncio.sleep(1) + + if testnet.is_stopped(): + print("Testnet stopped unexpectedly") + errors = testnet.errors() + if errors: + print("Last error:", errors[-1]) + + Monitor during operations:: + + testnet = AptosInstance.start() + await testnet.wait_until_operational() + + # Perform operations... + + if testnet.is_stopped(): + print("Testnet crashed during operation") + # Handle the failure + + Return Code: + - None: Process is still running + - 0: Process exited successfully + - Non-zero: Process exited with error + + Note: + This method checks the subprocess return code without blocking. + It's useful for detecting unexpected termination during operations. + """ return self._node_runner.returncode is not None diff --git a/aptos_sdk/aptos_token_client.py b/aptos_sdk/aptos_token_client.py index 96e8c52..3d44551 100644 --- a/aptos_sdk/aptos_token_client.py +++ b/aptos_sdk/aptos_token_client.py @@ -1,6 +1,210 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Aptos Token (Digital Assets) client for NFT and digital asset management. + +This module provides comprehensive tools for creating, managing, and interacting with +digital assets (also known as Token Objects or NFTs) on the Aptos blockchain. It supports +the latest Aptos token standard which is built on the object model for better flexibility +and composability. + +Key Features: +- **Collection Management**: Create and manage NFT collections with configurable properties +- **Token Minting**: Mint regular and soul-bound tokens with custom properties +- **Token Operations**: Transfer, burn, freeze/unfreeze tokens +- **Property Management**: Add, remove, and update token properties dynamically +- **Royalty Support**: Built-in royalty mechanisms for creators +- **Object Model**: Leverages Aptos' object model for improved token architecture + +Digital Asset Architecture: + The Aptos token standard uses the object model where each token is represented + as an independent object on-chain. This provides several benefits: + + - **Composability**: Tokens can be extended with additional resources + - **Flexibility**: Properties can be modified after creation (if permitted) + - **Efficiency**: Direct object addressing without complex lookups + - **Interoperability**: Standard interface for all digital assets + +Token Lifecycle: + 1. **Collection Creation**: Create a collection to hold related tokens + 2. **Token Minting**: Mint individual tokens within the collection + 3. **Property Management**: Add, update, or remove token properties + 4. **Transfer/Trade**: Transfer ownership between accounts + 5. **Lifecycle Management**: Freeze, unfreeze, or burn tokens as needed + +Collection Features: + - **Mutable Metadata**: Configure which aspects can be changed after creation + - **Supply Management**: Set maximum supply limits + - **Creator Controls**: Configure burn and freeze permissions + - **Royalty System**: Built-in royalty distribution for secondary sales + +Property System: + Tokens support rich property systems with typed values: + - **Basic Types**: bool, u8, u16, u32, u64, u128, u256 + - **Complex Types**: address, string, byte vectors + - **Dynamic Updates**: Properties can be modified post-creation (if allowed) + - **Type Safety**: Property values are strongly typed and serialized safely + +Examples: + Create a basic NFT collection and mint tokens:: + + from aptos_sdk.aptos_token_client import AptosTokenClient, PropertyMap, Property + from aptos_sdk.async_client import RestClient + from aptos_sdk.account import Account + + # Setup + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + token_client = AptosTokenClient(client) + creator = Account.load("./creator_account.json") + + # Create collection + collection_txn = await token_client.create_collection( + creator=creator, + description="My amazing NFT collection", + max_supply=1000, + name="Amazing NFTs", + uri="https://example.com/collection.json", + mutable_description=True, + mutable_royalty=False, + mutable_uri=True, + mutable_token_description=True, + mutable_token_name=False, + mutable_token_properties=True, + mutable_token_uri=True, + tokens_burnable_by_creator=True, + tokens_freezable_by_creator=False, + royalty_numerator=5, # 5% royalty + royalty_denominator=100 + ) + + await client.wait_for_transaction(collection_txn) + print(f"Collection created: {collection_txn}") + + Mint a token with custom properties:: + + # Create properties for the token + properties = PropertyMap([ + Property.string("rarity", "legendary"), + Property.u64("level", 42), + Property.bool("is_special", True), + Property.bytes("metadata", b"custom_data") + ]) + + # Mint the token + mint_txn = await token_client.mint_token( + creator=creator, + collection="Amazing NFTs", + description="A legendary item with special powers", + name="Legendary Sword #1", + uri="https://example.com/tokens/sword1.json", + properties=properties + ) + + await client.wait_for_transaction(mint_txn) + + # Get the minted token addresses + token_addresses = await token_client.tokens_minted_from_transaction(mint_txn) + print(f"Minted token at: {token_addresses[0]}") + + Create a soul-bound token (non-transferable):: + + from aptos_sdk.account_address import AccountAddress + + # Soul-bound tokens cannot be transferred + recipient = AccountAddress.from_str("***abc123...") + + soul_bound_txn = await token_client.mint_soul_bound_token( + creator=creator, + collection="Amazing NFTs", + description="Achievement badge for completing quest", + name="Quest Master Badge", + uri="https://example.com/badges/quest_master.json", + properties=PropertyMap([Property.string("achievement", "quest_master")]), + soul_bound_to=recipient + ) + + Read token information:: + + # Read token details from blockchain + token_address = AccountAddress.from_str("***token_address...") + token_data = await token_client.read_object(token_address) + + print(f"Token data: {token_data}") + + # Access specific resources + if Token in token_data.resources: + token = token_data.resources[Token] + print(f"Token name: {token.name}") + print(f"Description: {token.description}") + + if PropertyMap in token_data.resources: + props = token_data.resources[PropertyMap] + print(f"Properties: {props}") + + Transfer and manage tokens:: + + from aptos_sdk.account_address import AccountAddress + + # Transfer token to another account + recipient = AccountAddress.from_str("***recipient_address...") + owner = Account.load("./token_owner.json") + + transfer_txn = await token_client.transfer_token( + owner=owner, + token=token_addresses[0], + to=recipient + ) + + # Freeze token (prevent transfers) + freeze_txn = await token_client.freeze_token( + creator=creator, + token=token_addresses[0] + ) + + # Update token properties (if allowed) + new_property = Property.u64("level", 50) # Level up! + update_txn = await token_client.update_token_property( + creator=creator, + token=token_addresses[0], + prop=new_property + ) + +Gas Considerations: + - Collection creation: ~200,000 gas units + - Token minting: ~150,000 gas units + - Property updates: ~50,000 gas units + - Transfers: ~20,000 gas units + +Security Best Practices: + - **Mutable Permissions**: Carefully configure what aspects can be changed + - **Royalty Settings**: Set reasonable royalty percentages (typically 2.5-10%) + - **Property Validation**: Validate property values before setting + - **Creator Controls**: Use burn and freeze permissions judiciously + - **Testing**: Test collection and token creation on devnet first + +Common Use Cases: + - **Art NFTs**: Digital art with metadata and provenance + - **Gaming Assets**: In-game items with stats and properties + - **Certificates**: Soul-bound tokens for achievements and credentials + - **Collectibles**: Trading cards, sports memorabilia, etc. + - **Utility Tokens**: Access passes, membership tokens + - **Music/Media**: Audio, video, and multimedia NFTs + +Error Handling: + Common errors and solutions: + - **Insufficient Permissions**: Ensure creator has rights to modify tokens + - **Collection Not Found**: Verify collection name matches exactly + - **Property Type Mismatch**: Ensure property types are compatible + - **Transfer Restrictions**: Check if token is frozen or soul-bound + - **Supply Limits**: Verify collection hasn't reached max supply + +Note: + This implementation uses the latest Aptos token standard (Token Objects) + which is different from the legacy Token v1 standard. For Token v1 support, + use the AptosTokenV1Client instead. +""" + from __future__ import annotations from typing import Any, List, Tuple @@ -14,10 +218,37 @@ class Object: + """Represents an Aptos object with ownership and transfer permissions. + + The Object class encapsulates the core object metadata including ownership + and transfer restrictions. This is the base resource for all objects on + Aptos, including digital assets (tokens). + + Attributes: + allow_ungated_transfer (bool): Whether the object can be transferred + without explicit permission from the owner. + owner (AccountAddress): The current owner of the object. + struct_tag (str): The Move struct identifier for object resources. + + Examples: + Parse object data from blockchain response:: + + resource_data = { + "allow_ungated_transfer": True, + "owner": "***abc123..." + } + obj = Object.parse(resource_data) + print(f"Object owner: {obj.owner}") + print(f"Transferable: {obj.allow_ungated_transfer}") + + Note: + Objects with allow_ungated_transfer=False require explicit approval + from the owner or authorized parties for transfers. + """ allow_ungated_transfer: bool owner: AccountAddress - struct_tag: str = "0x1::object::ObjectCore" + struct_tag: str = "***::object::ObjectCore" def __init__(self, allow_ungated_transfer, owner): self.allow_ungated_transfer = allow_ungated_transfer @@ -25,6 +256,12 @@ def __init__(self, allow_ungated_transfer, owner): @staticmethod def parse(resource: dict[str, Any]) -> Object: + """ + Parse an Object from a resource dictionary. + + :param resource: Resource data from the blockchain + :return: Parsed Object instance + """ return Object( resource["allow_ungated_transfer"], AccountAddress.from_str_relaxed(resource["owner"]), @@ -35,12 +272,41 @@ def __str__(self) -> str: class Collection: + """Represents a token collection on the Aptos blockchain. + + A collection is a container for related tokens (NFTs) that share common + properties and governance. Collections define the rules and metadata + for all tokens within them. + + Attributes: + creator (AccountAddress): The address of the account that created the collection. + description (str): Human-readable description of the collection. + name (str): Unique name of the collection. + uri (str): URI pointing to collection metadata (JSON). + struct_tag (str): The Move struct identifier for collection resources. + + Examples: + Parse collection data from blockchain:: + + resource_data = { + "creator": "***abc123...", + "description": "A collection of unique digital art pieces", + "name": "Art Collection", + "uri": "https://example.com/collection.json" + } + collection = Collection.parse(resource_data) + print(f"Collection: {collection.name} by {collection.creator}") + + Note: + The collection URI should point to a JSON file following the standard + collection metadata schema for proper marketplace compatibility. + """ creator: AccountAddress description: str name: str uri: str - struct_tag: str = "0x4::collection::Collection" + struct_tag: str = "***::collection::Collection" def __init__(self, creator, description, name, uri): self.creator = creator @@ -53,6 +319,12 @@ def __str__(self) -> str: @staticmethod def parse(resource: dict[str, Any]) -> Collection: + """ + Parse a Collection from a resource dictionary. + + :param resource: Resource data from the blockchain + :return: Parsed Collection instance + """ return Collection( AccountAddress.from_str_relaxed(resource["creator"]), resource["description"], @@ -62,11 +334,44 @@ def parse(resource: dict[str, Any]) -> Collection: class Royalty: + """Represents royalty information for token collections and secondary sales. + + Royalties enable creators to earn a percentage of secondary sales of their + tokens on marketplaces and other platforms. The royalty is represented as + a fraction (numerator/denominator) and paid to a specific address. + + Attributes: + numerator (int): The numerator of the royalty fraction. + denominator (int): The denominator of the royalty fraction. + payee_address (AccountAddress): The address that receives royalty payments. + struct_tag (str): The Move struct identifier for royalty resources. + + Examples: + Calculate royalty percentage:: + + royalty = Royalty(250, 10000, payee_address) # 2.5% royalty + percentage = (royalty.numerator / royalty.denominator) * 100 + print(f"Royalty: {percentage}% to {royalty.payee_address}") + + Parse royalty from blockchain data:: + + resource_data = { + "numerator": 500, + "denominator": 10000, + "payee_address": "***abc123..." + } + royalty = Royalty.parse(resource_data) + print(f"Royalty: {royalty}") # 5% royalty + + Note: + Common royalty percentages range from 2.5% to 10%. The fraction should + be simplified to avoid unnecessary precision (e.g., use 1/40 instead of 25/1000). + """ numerator: int denominator: int payee_address: AccountAddress - struct_tag: str = "0x4::royalty::Royalty" + struct_tag: str = "***::royalty::Royalty" def __init__(self, numerator, denominator, payee_address): self.numerator = numerator @@ -78,6 +383,12 @@ def __str__(self) -> str: @staticmethod def parse(resource: dict[str, Any]) -> Royalty: + """ + Parse a Royalty from a resource dictionary. + + :param resource: Resource data from the blockchain + :return: Parsed Royalty instance + """ return Royalty( resource["numerator"], resource["denominator"], @@ -86,13 +397,44 @@ def parse(resource: dict[str, Any]) -> Royalty: class Token: + """Represents an individual token (NFT) on the Aptos blockchain. + + A token is a unique digital asset within a collection. Each token has + its own metadata, properties, and can be individually owned and transferred. + + Attributes: + collection (AccountAddress): Address of the collection this token belongs to. + index (int): Unique index of the token within its collection. + description (str): Human-readable description of the token. + name (str): Name of the token. + uri (str): URI pointing to token metadata (typically JSON). + struct_tag (str): The Move struct identifier for token resources. + + Examples: + Parse token data from blockchain:: + + resource_data = { + "collection": {"inner": "***collection_address..."}, + "index": 42, + "description": "A legendary sword with special powers", + "name": "Legendary Sword #42", + "uri": "https://example.com/tokens/42.json" + } + token = Token.parse(resource_data) + print(f"Token: {token.name} in collection {token.collection}") + + Note: + The token URI should point to a JSON file following the standard + token metadata schema (similar to ERC-721 metadata) for marketplace + compatibility. + """ collection: AccountAddress index: int description: str name: str uri: str - struct_tag: str = "0x4::token::Token" + struct_tag: str = "***::token::Token" def __init__( self, @@ -113,6 +455,12 @@ def __str__(self) -> str: @staticmethod def parse(resource: dict[str, Any]): + """ + Parse a Token from a resource dictionary. + + :param resource: Resource data from the blockchain + :return: Parsed Token instance + """ return Token( AccountAddress.from_str_relaxed(resource["collection"]["inner"]), int(resource["index"]), @@ -134,6 +482,70 @@ def __init__(self, property_type: Any): class Property: + """Represents a typed property for tokens with serialization capabilities. + + Properties are key-value pairs that can be attached to tokens to store + additional metadata and attributes. Each property has a name, type, and + value, and supports various primitive and complex types. + + Attributes: + name (str): The name/key of the property. + property_type (str): The Move type of the property value. + value (Any): The actual value of the property. + + Type Constants: + BOOL (int): Boolean type identifier (0) + U8 (int): 8-bit unsigned integer type identifier (1) + U16 (int): 16-bit unsigned integer type identifier (2) + U32 (int): 32-bit unsigned integer type identifier (3) + U64 (int): 64-bit unsigned integer type identifier (4) + U128 (int): 128-bit unsigned integer type identifier (5) + U256 (int): 256-bit unsigned integer type identifier (6) + ADDRESS (int): Account address type identifier (7) + BYTE_VECTOR (int): Byte vector type identifier (8) + STRING (int): String type identifier (9) + + Examples: + Create different types of properties:: + + # Boolean property + is_rare = Property.bool("is_rare", True) + + # Numeric properties + level = Property.u64("level", 25) + damage = Property.u32("damage", 150) + + # String property + category = Property.string("category", "weapon") + + # Address property + creator = Property("creator", "address", creator_address) + + # Byte data property + metadata = Property.bytes("metadata", b"custom_data") + + Use in transactions:: + + # Convert to transaction arguments for on-chain calls + tx_args = property.to_transaction_arguments() + + Parse from blockchain data:: + + # Parse property from resource data + prop = Property.parse("level", Property.U64, serialized_value) + print(f"Property: {prop.name} = {prop.value}") + + Supported Types: + - **bool**: Boolean values (true/false) + - **u8, u16, u32, u64, u128, u256**: Unsigned integers of various sizes + - **address**: Aptos account addresses + - **string**: UTF-8 encoded strings + - **vector**: Arbitrary byte arrays + + Note: + Properties are strongly typed and values must match the specified type. + BCS serialization is used for efficient on-chain storage and transmission. + """ name: str property_type: str value: Any @@ -254,9 +666,80 @@ def bytes(name: str, value: bytes) -> Property: class PropertyMap: + """Container for multiple token properties with serialization support. + + PropertyMap manages a collection of Property objects and provides utilities + for converting them to formats suitable for blockchain transactions and + parsing them from on-chain data. + + Attributes: + properties (List[Property]): List of properties contained in this map. + struct_tag (str): The Move struct identifier for property map resources. + + Examples: + Create a property map with various property types:: + + properties = PropertyMap([ + Property.string("name", "Legendary Sword"), + Property.u64("level", 50), + Property.bool("is_rare", True), + Property.bytes("metadata", b"custom_data"), + Property.u32("damage", 200) + ]) + + print(f"Property map: {properties}") + + Convert to transaction format:: + + # Get tuple format for transaction arguments + names, types, values = properties.to_tuple() + + # These can be used directly in transaction calls + # names = ["name", "level", "is_rare", "metadata", "damage"] + # types = ["***::string::String", "u64", "bool", "vector", "u32"] + # values = [b"...", b"...", b"...", b"...", b"..."] # BCS serialized + + Parse from blockchain data:: + + # Parse from resource data retrieved from blockchain + resource_data = { + "inner": { + "data": [ + {"key": "level", "value": {"type": 4, "value": "***64..."}}, + {"key": "rarity", "value": {"type": 9, "value": "***legendary"}} + ] + } + } + + parsed_map = PropertyMap.parse(resource_data) + print(f"Parsed properties: {parsed_map}") + + Usage in Token Operations: + Property maps are essential for token minting and property management:: + + # Create property map + props = PropertyMap([ + Property.string("category", "weapon"), + Property.u64("attack_power", 150) + ]) + + # Use in token minting + await token_client.mint_token( + creator=creator, + collection="Game Items", + description="A powerful weapon", + name="Magic Sword", + uri="https://example.com/sword.json", + properties=props + ) + + Note: + The to_tuple method returns data in the format expected by Move entry + functions for property operations on tokens. + """ properties: List[Property] - struct_tag: str = "0x4::property_map::PropertyMap" + struct_tag: str = "***::property_map::PropertyMap" def __init__(self, properties: List[Property]): self.properties = properties @@ -299,6 +782,109 @@ def parse(resource: dict[str, Any]) -> PropertyMap: class ReadObject: + """Aggregated view of parsed blockchain resources for token objects. + + ReadObject provides a structured interface for accessing multiple resource + types associated with a token object address. It automatically parses + known resource types and makes them available through a unified interface. + + Attributes: + resource_map (dict): Mapping of Move struct identifiers to Python classes + for automatic resource parsing. + resources (dict): Dictionary mapping resource classes to parsed instances. + + Supported Resource Types: + - **Collection**: Collection metadata and configuration + - **Object**: Core object ownership and transfer permissions + - **PropertyMap**: Token properties and custom attributes + - **Royalty**: Royalty information for secondary sales + - **Token**: Token metadata and collection reference + + Examples: + Read and parse token object resources:: + + from aptos_sdk.account_address import AccountAddress + + # Read object from blockchain + token_address = AccountAddress.from_str("***token_address...") + read_object = await token_client.read_object(token_address) + + # Access different resource types + if Token in read_object.resources: + token = read_object.resources[Token] + print(f"Token name: {token.name}") + print(f"Description: {token.description}") + print(f"Collection: {token.collection}") + + if PropertyMap in read_object.resources: + properties = read_object.resources[PropertyMap] + print(f"Properties: {properties}") + for prop in properties.properties: + print(f" {prop.name}: {prop.value}") + + if Object in read_object.resources: + obj = read_object.resources[Object] + print(f"Owner: {obj.owner}") + print(f"Transferable: {obj.allow_ungated_transfer}") + + if Royalty in read_object.resources: + royalty = read_object.resources[Royalty] + percentage = (royalty.numerator / royalty.denominator) * 100 + print(f"Royalty: {percentage}% to {royalty.payee_address}") + + Check for specific resource types:: + + # Check what resources are available + print(f"Available resources: {list(read_object.resources.keys())}") + + # Safely access optional resources + token = read_object.resources.get(Token) + if token: + print(f"Found token: {token.name}") + else: + print("No token resource found") + + Full object inspection:: + + # Print all resources (uses __str__ method) + print(read_object) + + # This will show something like: + # ReadObject + # ***::token::Token: Token[collection: ***abc..., name: Sword #1, ...] + # ***::property_map::PropertyMap: PropertyMap[Property[level, u64, 42], ...] + # ***::object::ObjectCore: Object[allow_ungated_transfer: True, owner: ***def...] + + Usage Patterns: + Conditional resource access:: + + def analyze_token_object(read_object: ReadObject): + analysis = {} + + # Basic token info + if Token in read_object.resources: + token = read_object.resources[Token] + analysis["name"] = token.name + analysis["description"] = token.description + + # Properties analysis + if PropertyMap in read_object.resources: + prop_map = read_object.resources[PropertyMap] + analysis["property_count"] = len(prop_map.properties) + analysis["properties"] = {p.name: p.value for p in prop_map.properties} + + # Ownership info + if Object in read_object.resources: + obj = read_object.resources[Object] + analysis["owner"] = str(obj.owner) + analysis["transferable"] = obj.allow_ungated_transfer + + return analysis + + Note: + Only resources that match known struct tags in resource_map will be + parsed and included. Unknown resource types are ignored during parsing. + """ resource_map: dict[str, Any] = { Collection.struct_tag: Collection, Object.struct_tag: Object, @@ -329,6 +915,12 @@ def __init__(self, client: RestClient): self.client = client async def read_object(self, address: AccountAddress) -> ReadObject: + """ + Read an object from the blockchain and parse its resources. + + :param address: The address of the object to read + :return: ReadObject containing parsed resources + """ resources = {} read_resources = await self.client.account_resources(address) @@ -356,6 +948,26 @@ def create_collection_payload( royalty_numerator: int, royalty_denominator: int, ) -> TransactionPayload: + """ + Create a transaction payload for creating a new token collection. + + :param description: Description of the collection + :param max_supply: Maximum number of tokens that can be minted in this collection + :param name: Name of the collection + :param uri: URI for collection metadata + :param mutable_description: Whether the collection description can be changed + :param mutable_royalty: Whether the collection royalty can be changed + :param mutable_uri: Whether the collection URI can be changed + :param mutable_token_description: Whether token descriptions can be changed + :param mutable_token_name: Whether token names can be changed + :param mutable_token_properties: Whether token properties can be changed + :param mutable_token_uri: Whether token URIs can be changed + :param tokens_burnable_by_creator: Whether tokens can be burned by the creator + :param tokens_freezable_by_creator: Whether tokens can be frozen by the creator + :param royalty_numerator: Numerator for royalty percentage calculation + :param royalty_denominator: Denominator for royalty percentage calculation + :return: Transaction payload for collection creation + """ transaction_arguments = [ TransactionArgument(description, Serializer.str), TransactionArgument(max_supply, Serializer.u64), @@ -403,6 +1015,28 @@ async def create_collection( royalty_numerator: int, royalty_denominator: int, ) -> str: # <:!:create_collection + """ + Create a new token collection on the blockchain. + + :param creator: The account that will create and own the collection + :param description: Description of the collection + :param max_supply: Maximum number of tokens that can be minted in this collection + :param name: Name of the collection + :param uri: URI for collection metadata + :param mutable_description: Whether the collection description can be changed + :param mutable_royalty: Whether the collection royalty can be changed + :param mutable_uri: Whether the collection URI can be changed + :param mutable_token_description: Whether token descriptions can be changed + :param mutable_token_name: Whether token names can be changed + :param mutable_token_properties: Whether token properties can be changed + :param mutable_token_uri: Whether token URIs can be changed + :param tokens_burnable_by_creator: Whether tokens can be burned by the creator + :param tokens_freezable_by_creator: Whether tokens can be frozen by the creator + :param royalty_numerator: Numerator for royalty percentage calculation + :param royalty_denominator: Denominator for royalty percentage calculation + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ payload = AptosTokenClient.create_collection_payload( description, max_supply, @@ -433,6 +1067,16 @@ def mint_token_payload( uri: str, properties: PropertyMap, ) -> TransactionPayload: + """ + Create a transaction payload for minting a new token. + + :param collection: Name of the collection to mint the token in + :param description: Description of the token + :param name: Name of the token + :param uri: URI for token metadata + :param properties: PropertyMap containing token properties + :return: Transaction payload for token minting + """ (property_names, property_types, property_values) = properties.to_tuple() transaction_arguments = [ TransactionArgument(collection, Serializer.str), @@ -469,6 +1113,18 @@ async def mint_token( uri: str, properties: PropertyMap, ) -> str: # <:!:mint_token + """ + Mint a new token in the specified collection. + + :param creator: The account that will mint the token + :param collection: Name of the collection to mint the token in + :param description: Description of the token + :param name: Name of the token + :param uri: URI for token metadata + :param properties: PropertyMap containing token properties + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ payload = AptosTokenClient.mint_token_payload( collection, description, name, uri, properties ) @@ -486,7 +1142,20 @@ async def mint_soul_bound_token( uri: str, properties: PropertyMap, soul_bound_to: AccountAddress, - ): + ) -> str: + """ + Mint a new soul-bound token that cannot be transferred. + + :param creator: The account that will mint the token + :param collection: Name of the collection to mint the token in + :param description: Description of the token + :param name: Name of the token + :param uri: URI for token metadata + :param properties: PropertyMap containing token properties + :param soul_bound_to: Address of the account the token will be soul-bound to + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ (property_names, property_types, property_values) = properties.to_tuple() transaction_arguments = [ TransactionArgument(collection, Serializer.str), @@ -521,13 +1190,30 @@ async def mint_soul_bound_token( async def transfer_token( self, owner: Account, token: AccountAddress, to: AccountAddress ) -> str: + """ + Transfer ownership of a token to another account. + + :param owner: The current owner of the token + :param token: The address of the token to transfer + :param to: The address of the new owner + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ return await self.client.transfer_object(owner, token, to) # <:!:transfer_token async def burn_token(self, creator: Account, token: AccountAddress) -> str: + """ + Burn (permanently destroy) a token. + + :param creator: The creator account that has permission to burn the token + :param token: The address of the token to burn + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ payload = EntryFunction.natural( - "0x4::aptos_token", + "***::aptos_token", "burn", - [TypeTag(StructTag.from_str("0x4::token::Token"))], + [TypeTag(StructTag.from_str("***::token::Token"))], [TransactionArgument(token, Serializer.struct)], ) @@ -537,10 +1223,18 @@ async def burn_token(self, creator: Account, token: AccountAddress) -> str: return await self.client.submit_bcs_transaction(signed_transaction) async def freeze_token(self, creator: Account, token: AccountAddress) -> str: + """ + Freeze a token, preventing its transfer. + + :param creator: The creator account that has permission to freeze the token + :param token: The address of the token to freeze + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ payload = EntryFunction.natural( - "0x4::aptos_token", + "***::aptos_token", "freeze_transfer", - [TypeTag(StructTag.from_str("0x4::token::Token"))], + [TypeTag(StructTag.from_str("***::token::Token"))], [TransactionArgument(token, Serializer.struct)], ) @@ -550,10 +1244,18 @@ async def freeze_token(self, creator: Account, token: AccountAddress) -> str: return await self.client.submit_bcs_transaction(signed_transaction) async def unfreeze_token(self, creator: Account, token: AccountAddress) -> str: + """ + Unfreeze a previously frozen token, allowing transfers again. + + :param creator: The creator account that has permission to unfreeze the token + :param token: The address of the token to unfreeze + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ payload = EntryFunction.natural( - "0x4::aptos_token", + "***::aptos_token", "unfreeze_transfer", - [TypeTag(StructTag.from_str("0x4::token::Token"))], + [TypeTag(StructTag.from_str("***::token::Token"))], [TransactionArgument(token, Serializer.struct)], ) @@ -565,13 +1267,22 @@ async def unfreeze_token(self, creator: Account, token: AccountAddress) -> str: async def add_token_property( self, creator: Account, token: AccountAddress, prop: Property ) -> str: + """ + Add a new property to an existing token. + + :param creator: The creator account that has permission to modify the token + :param token: The address of the token to modify + :param prop: The property to add to the token + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ transaction_arguments = [TransactionArgument(token, Serializer.struct)] transaction_arguments.extend(prop.to_transaction_arguments()) payload = EntryFunction.natural( - "0x4::aptos_token", + "***::aptos_token", "add_property", - [TypeTag(StructTag.from_str("0x4::token::Token"))], + [TypeTag(StructTag.from_str("***::token::Token"))], transaction_arguments, ) @@ -583,15 +1294,24 @@ async def add_token_property( async def remove_token_property( self, creator: Account, token: AccountAddress, name: str ) -> str: + """ + Remove a property from an existing token. + + :param creator: The creator account that has permission to modify the token + :param token: The address of the token to modify + :param name: The name of the property to remove + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ transaction_arguments = [ TransactionArgument(token, Serializer.struct), TransactionArgument(name, Serializer.str), ] payload = EntryFunction.natural( - "0x4::aptos_token", + "***::aptos_token", "remove_property", - [TypeTag(StructTag.from_str("0x4::token::Token"))], + [TypeTag(StructTag.from_str("***::token::Token"))], transaction_arguments, ) @@ -603,13 +1323,22 @@ async def remove_token_property( async def update_token_property( self, creator: Account, token: AccountAddress, prop: Property ) -> str: + """ + Update an existing property on a token. + + :param creator: The creator account that has permission to modify the token + :param token: The address of the token to modify + :param prop: The property with updated values + :return: Transaction hash as a hex string + :raises ApiError: If transaction submission fails + """ transaction_arguments = [TransactionArgument(token, Serializer.struct)] transaction_arguments.extend(prop.to_transaction_arguments()) payload = EntryFunction.natural( - "0x4::aptos_token", + "***::aptos_token", "update_property", - [TypeTag(StructTag.from_str("0x4::token::Token"))], + [TypeTag(StructTag.from_str("***::token::Token"))], transaction_arguments, ) @@ -621,12 +1350,19 @@ async def update_token_property( async def tokens_minted_from_transaction( self, txn_hash: str ) -> List[AccountAddress]: + """ + Get a list of token addresses that were minted in a specific transaction. + + :param txn_hash: The transaction hash to analyze for minted tokens + :return: List of addresses of tokens that were minted in the transaction + :raises ApiError: If transaction lookup fails + """ output = await self.client.transaction_by_hash(txn_hash) mints = [] for event in output["events"]: if event["type"] not in ( - "0x4::collection::MintEvent", - "0x4::collection::Mint", + "***::collection::MintEvent", + "***::collection::Mint", ): continue mints.append(AccountAddress.from_str_relaxed(event["data"]["token"])) diff --git a/aptos_sdk/aptos_tokenv1_client.py b/aptos_sdk/aptos_tokenv1_client.py index 6200b4c..9fdc528 100644 --- a/aptos_sdk/aptos_tokenv1_client.py +++ b/aptos_sdk/aptos_tokenv1_client.py @@ -1,6 +1,173 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Aptos Token V1 client for legacy NFT and token management. + +This module provides client functionality for interacting with the legacy Aptos Token V1 standard, +which was the original NFT implementation on Aptos before the introduction of Token Objects +(the current standard). While Token V1 is still supported for backward compatibility, new +projects should consider using the Token Objects standard via AptosTokenClient. + +Legacy Token V1 Features: +- **Collection Creation**: Create named collections with metadata +- **Token Minting**: Create tokens within collections with supply management +- **Transfer Mechanisms**: Both direct transfers and offer/claim workflows +- **Property Management**: Basic token properties and metadata +- **Royalty Support**: Built-in creator royalty system + +Key Differences from Token Objects: +- Uses table-based storage instead of object model +- Limited property system compared to modern Token Objects +- More complex transfer mechanisms (offer/claim vs direct transfer) +- Different address derivation for token identification +- Legacy BCS serialization patterns + +Token V1 Architecture: + Token V1 uses a table-based approach where: + - Collections are stored in creator's Collections resource + - Tokens are identified by (creator, collection, name, property_version) tuple + - Token ownership tracked in recipient's TokenStore resource + - Properties stored as separate key-value mappings + +Migration Note: + For new projects, consider using AptosTokenClient (Token Objects) which provides: + - Better composability and extensibility + - Improved property system with type safety + - Simplified transfer mechanisms + - Object-based architecture for better on-chain interactions + +Examples: + Basic Token V1 workflow:: + + from aptos_sdk.aptos_tokenv1_client import AptosTokenV1Client + from aptos_sdk.async_client import RestClient + from aptos_sdk.account import Account + + # Setup + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + token_client = AptosTokenV1Client(client) + creator = Account.load("./creator.json") + + # Create a collection + collection_txn = await token_client.create_collection( + account=creator, + name="My Legacy Collection", + description="A collection using Token V1 standard", + uri="https://example.com/collection.json" + ) + await client.wait_for_transaction(collection_txn) + + # Create a token in the collection + token_txn = await token_client.create_token( + account=creator, + collection_name="My Legacy Collection", + name="Token #1", + description="First token in legacy format", + supply=1, # NFT with supply of 1 + uri="https://example.com/token1.json", + royalty_points_per_million=50000 # 5% royalty + ) + await client.wait_for_transaction(token_txn) + + Token transfer using offer/claim pattern:: + + from aptos_sdk.account_address import AccountAddress + + recipient_address = AccountAddress.from_str("***recipient...") + recipient = Account.load("./recipient.json") + + # Offer token to recipient + offer_txn = await token_client.offer_token( + account=creator, + receiver=recipient_address, + creator=creator.address(), + collection_name="My Legacy Collection", + token_name="Token #1", + property_version=0, + amount=1 + ) + await client.wait_for_transaction(offer_txn) + + # Recipient claims the token + claim_txn = await token_client.claim_token( + account=recipient, + sender=creator.address(), + creator=creator.address(), + collection_name="My Legacy Collection", + token_name="Token #1", + property_version=0 + ) + await client.wait_for_transaction(claim_txn) + + Direct token transfer (requires both accounts):: + + # Direct transfer between accounts + transfer_txn = await token_client.direct_transfer_token( + sender=creator, + receiver=recipient, + creators_address=creator.address(), + collection_name="My Legacy Collection", + token_name="Token #1", + property_version=0, + amount=1 + ) + await client.wait_for_transaction(transfer_txn) + + Reading token information:: + + # Get token data (metadata) + token_data = await token_client.get_token_data( + creator=creator.address(), + collection_name="My Legacy Collection", + token_name="Token #1", + property_version=0 + ) + print(f"Token: {token_data['name']}") + print(f"Description: {token_data['description']}") + print(f"Supply: {token_data['supply']}") + + # Get token balance for an account + balance = await token_client.get_token_balance( + owner=recipient_address, + creator=creator.address(), + collection_name="My Legacy Collection", + token_name="Token #1", + property_version=0 + ) + print(f"Balance: {balance}") + + # Get collection information + collection_data = await token_client.get_collection( + creator=creator.address(), + collection_name="My Legacy Collection" + ) + print(f"Collection: {collection_data['name']}") + print(f"Max supply: {collection_data['maximum']}") + +Limitations of Token V1: + - Complex token identification system + - Limited property types and extensibility + - Table lookups required for token information + - More gas-intensive operations + - Less composable than object-based tokens + +Security Considerations: + - Always verify token authenticity by checking creator + - Be cautious with property_version when transferring + - Validate collection and token names to prevent spoofing + - Consider supply limits when minting fungible tokens + +Compatibility: + This client maintains compatibility with existing Token V1 deployments + and provides migration utilities for upgrading to Token Objects when + appropriate. + +See Also: + - AptosTokenClient: For new Token Objects standard + - Token migration guides: For upgrading from V1 to Token Objects +""" + from typing import Any from .account import Account @@ -13,17 +180,144 @@ class AptosTokenV1Client: - """A wrapper around reading and mutating AptosTokens also known as Token Objects""" + """Client for interacting with legacy Aptos Token V1 standard. + + AptosTokenV1Client provides a high-level interface for working with the original + Aptos token implementation (Token V1). While this standard is still supported + for backward compatibility, new projects should consider using Token Objects + via AptosTokenClient for better functionality and composability. + + Token V1 uses a table-based storage model where tokens are identified by + a combination of (creator, collection_name, token_name, property_version) + and stored in various on-chain tables rather than as independent objects. + + Key Features: + - **Legacy Compatibility**: Supports existing Token V1 deployments + - **Collection Management**: Create and manage token collections + - **Token Lifecycle**: Create, transfer, and query tokens + - **Offer/Claim Transfers**: Asynchronous token transfer mechanism + - **Direct Transfers**: Synchronous multi-agent transfers + - **Royalty System**: Built-in creator royalty support + + Attributes: + _client (RestClient): The underlying REST client for blockchain communication. + + Examples: + Initialize and create a basic NFT:: + + from aptos_sdk.aptos_tokenv1_client import AptosTokenV1Client + from aptos_sdk.async_client import RestClient + from aptos_sdk.account import Account + + # Setup client + rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + token_client = AptosTokenV1Client(rest_client) + creator = Account.load("./creator_key.json") + + # Create collection + await token_client.create_collection( + account=creator, + name="Art Collection", + description="Digital art pieces", + uri="https://example.com/collection.json" + ) + + # Create NFT + await token_client.create_token( + account=creator, + collection_name="Art Collection", + name="Artwork #1", + description="Beautiful digital art", + supply=1, + uri="https://example.com/art1.json", + royalty_points_per_million=25000 # 2.5% + ) + + Transfer tokens using offer/claim:: + + # Offer token to recipient + await token_client.offer_token( + account=current_owner, + receiver=recipient_address, + creator=creator.address(), + collection_name="Art Collection", + token_name="Artwork #1", + property_version=0, + amount=1 + ) + + # Recipient claims the token + await token_client.claim_token( + account=recipient, + sender=current_owner.address(), + creator=creator.address(), + collection_name="Art Collection", + token_name="Artwork #1", + property_version=0 + ) + + Note: + This client is for Token V1 compatibility. For new projects, consider + using AptosTokenClient which implements the modern Token Objects standard + with improved functionality and composability. + """ _client: RestClient def __init__(self, client: RestClient): + """Initialize the Token V1 client with a REST client. + + Args: + client: The RestClient instance to use for blockchain communication. + Must be configured for the appropriate Aptos network. + + Examples: + Create client for devnet:: + + from aptos_sdk.async_client import RestClient + + rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + token_client = AptosTokenV1Client(rest_client) + """ self._client = client async def create_collection( self, account: Account, name: str, description: str, uri: str ) -> str: - """Creates a new collection within the specified account""" + """Create a new token collection using the Token V1 standard. + + Creates a collection that can hold multiple tokens. In Token V1, + collections are stored in the creator's account and have a maximum + supply limit (set to U64_MAX by default for unlimited). + + Args: + account: The account that will own the collection and pay transaction fees. + name: Unique name for the collection within the creator's account. + Must be unique per creator. + description: Human-readable description of the collection. + uri: URI pointing to collection metadata JSON file. + + Returns: + str: Transaction hash of the collection creation transaction. + + Raises: + ApiError: If the transaction fails or collection name already exists. + + Examples: + Create a basic art collection:: + + tx_hash = await token_client.create_collection( + account=creator, + name="Digital Art Collection", + description="Unique digital artworks by Artist Name", + uri="https://example.com/collection-metadata.json" + ) + await client.wait_for_transaction(tx_hash) + + Note: + Collection names must be unique per creator. The collection is created + with unlimited maximum supply and default mutability settings (all false). + """ transaction_arguments = [ TransactionArgument(name, Serializer.str), @@ -57,6 +351,59 @@ async def create_token( uri: str, royalty_points_per_million: int, ) -> str: + """Create a new token within an existing collection. + + Creates a token with the specified supply and metadata. In Token V1, + tokens have both initial and maximum supply values, with royalties + specified as points per million (e.g., 25000 = 2.5%). + + Args: + account: The account creating the token (must be collection owner). + collection_name: Name of the collection to create token in. + name: Unique name for the token within the collection. + description: Human-readable description of the token. + supply: Initial and maximum supply of the token. + Use 1 for NFTs, higher values for fungible tokens. + uri: URI pointing to token metadata JSON file. + royalty_points_per_million: Royalty percentage as points per million. + 25000 = 2.5%, 50000 = 5%, etc. + + Returns: + str: Transaction hash of the token creation transaction. + + Raises: + ApiError: If the transaction fails, collection doesn't exist, + or token name already exists. + + Examples: + Create an NFT (supply = 1):: + + tx_hash = await token_client.create_token( + account=creator, + collection_name="Art Collection", + name="Masterpiece #1", + description="A unique digital artwork", + supply=1, # NFT + uri="https://example.com/token1.json", + royalty_points_per_million=25000 # 2.5% royalty + ) + + Create a fungible token:: + + tx_hash = await token_client.create_token( + account=creator, + collection_name="Game Tokens", + name="Gold Coins", + description="In-game currency", + supply=1000000, # 1M tokens + uri="https://example.com/gold-coins.json", + royalty_points_per_million=10000 # 1% royalty + ) + + Note: + Token names must be unique within the collection. The royalty + recipient is set to the token creator's address by default. + """ transaction_arguments = [ TransactionArgument(collection_name, Serializer.str), TransactionArgument(name, Serializer.str), @@ -100,6 +447,57 @@ async def offer_token( property_version: int, amount: int, ) -> str: + """Offer tokens to another account using the async transfer mechanism. + + Creates a pending token offer that the recipient can claim. This is the + first step of the two-phase Token V1 transfer process (offer -> claim). + The tokens remain in the sender's account until claimed. + + Args: + account: The account offering the tokens (current owner). + receiver: Address of the account to receive the token offer. + creator: Address of the account that created the token. + collection_name: Name of the collection containing the token. + token_name: Name of the specific token being offered. + property_version: Property version of the token (usually 0). + amount: Number of tokens to offer. + + Returns: + str: Transaction hash of the offer transaction. + + Raises: + ApiError: If the transaction fails, token doesn't exist, + or insufficient token balance. + + Examples: + Offer an NFT:: + + tx_hash = await token_client.offer_token( + account=current_owner, + receiver=recipient_address, + creator=original_creator.address(), + collection_name="Art Collection", + token_name="Masterpiece #1", + property_version=0, + amount=1 + ) + + Offer fungible tokens:: + + tx_hash = await token_client.offer_token( + account=token_holder, + receiver=buyer_address, + creator=token_creator.address(), + collection_name="Game Tokens", + token_name="Gold Coins", + property_version=0, + amount=100 + ) + + Note: + The recipient must call claim_token() to complete the transfer. + Offers can potentially be revoked or expire based on implementation. + """ transaction_arguments = [ TransactionArgument(receiver, Serializer.struct), TransactionArgument(creator, Serializer.struct), @@ -129,6 +527,41 @@ async def claim_token( token_name: str, property_version: int, ) -> str: + """Claim tokens that were offered by another account. + + Completes the second step of the Token V1 async transfer process. + Claims all tokens that were offered for the specified token ID. + + Args: + account: The account claiming the tokens (recipient). + sender: Address of the account that offered the tokens. + creator: Address of the account that created the token. + collection_name: Name of the collection containing the token. + token_name: Name of the specific token being claimed. + property_version: Property version of the token (usually 0). + + Returns: + str: Transaction hash of the claim transaction. + + Raises: + ApiError: If the transaction fails or no pending offer exists. + + Examples: + Claim an offered NFT:: + + tx_hash = await token_client.claim_token( + account=recipient, + sender=previous_owner.address(), + creator=original_creator.address(), + collection_name="Art Collection", + token_name="Masterpiece #1", + property_version=0 + ) + + Note: + This claims all tokens that were offered for this token ID. + The amount is determined by the original offer transaction. + """ transaction_arguments = [ TransactionArgument(sender, Serializer.struct), TransactionArgument(creator, Serializer.struct), @@ -158,6 +591,46 @@ async def direct_transfer_token( property_version: int, amount: int, ) -> str: + """Transfer tokens directly between two accounts in a single transaction. + + Performs a synchronous token transfer that requires both sender and + receiver to sign the transaction. This is more efficient than the + offer/claim mechanism but requires coordination between both parties. + + Args: + sender: The account sending the tokens (must sign). + receiver: The account receiving the tokens (must sign). + creators_address: Address of the account that created the token. + collection_name: Name of the collection containing the token. + token_name: Name of the specific token being transferred. + property_version: Property version of the token (usually 0). + amount: Number of tokens to transfer. + + Returns: + str: Transaction hash of the direct transfer transaction. + + Raises: + ApiError: If the transaction fails, token doesn't exist, + insufficient balance, or either party fails to sign. + + Examples: + Direct transfer of an NFT:: + + tx_hash = await token_client.direct_transfer_token( + sender=current_owner, + receiver=new_owner, + creators_address=creator.address(), + collection_name="Art Collection", + token_name="Masterpiece #1", + property_version=0, + amount=1 + ) + + Note: + This creates a multi-agent transaction requiring both accounts + to sign. Both sender and receiver must be available to sign + simultaneously. + """ transaction_arguments = [ TransactionArgument(creators_address, Serializer.struct), TransactionArgument(collection_name, Serializer.str), @@ -192,6 +665,48 @@ async def get_token( token_name: str, property_version: int, ) -> Any: + """Retrieve token information for a specific owner and token ID. + + Queries the owner's TokenStore to get information about their + holdings of a specific token, including the amount owned. + + Args: + owner: Address of the account that owns the token. + creator: Address of the account that created the token. + collection_name: Name of the collection containing the token. + token_name: Name of the specific token. + property_version: Property version of the token (usually 0). + + Returns: + Dict containing token information including: + - 'id': Token identifier object + - 'amount': String representation of amount owned + Returns {'id': token_id, 'amount': '0'} if not found. + + Raises: + ApiError: If the query fails (except for 404 not found). + + Examples: + Get token ownership info:: + + token_info = await token_client.get_token( + owner=holder_address, + creator=creator.address(), + collection_name="Art Collection", + token_name="Masterpiece #1", + property_version=0 + ) + + amount = token_info['amount'] + if amount == '0': + print("Account does not own this token") + else: + print(f"Account owns {amount} of this token") + + Note: + Returns amount as '0' if the account has no TokenStore resource + or doesn't own the specified token. + """ resource = await self._client.account_resource(owner, "0x3::token::TokenStore") token_store_handle = resource["data"]["tokens"]["handle"] @@ -227,6 +742,48 @@ async def get_token_balance( token_name: str, property_version: int, ) -> str: + """Get the token balance for a specific owner and token ID. + + Convenience method that extracts just the amount from get_token(). + Returns the number of tokens of the specified type owned by the account. + + Args: + owner: Address of the account to check balance for. + creator: Address of the account that created the token. + collection_name: Name of the collection containing the token. + token_name: Name of the specific token. + property_version: Property version of the token (usually 0). + + Returns: + str: String representation of the token balance. + Returns '0' if the account doesn't own any of this token. + + Examples: + Check NFT ownership:: + + balance = await token_client.get_token_balance( + owner=user_address, + creator=creator.address(), + collection_name="Art Collection", + token_name="Masterpiece #1", + property_version=0 + ) + + owns_nft = balance != '0' + print(f"User owns NFT: {owns_nft}") + + Check fungible token balance:: + + balance = await token_client.get_token_balance( + owner=player_address, + creator=game_creator.address(), + collection_name="Game Tokens", + token_name="Gold Coins", + property_version=0 + ) + + print(f"Player has {balance} gold coins") + """ info = await self.get_token( owner, creator, collection_name, token_name, property_version ) @@ -239,6 +796,49 @@ async def get_token_data( token_name: str, property_version: int, ) -> Any: + """Retrieve metadata and configuration for a specific token. + + Queries the token creator's Collections resource to get the + canonical token data including metadata, supply, and properties. + + Args: + creator: Address of the account that created the token. + collection_name: Name of the collection containing the token. + token_name: Name of the specific token. + property_version: Property version of the token (usually 0). + + Returns: + Dict containing token metadata including: + - 'name': Token name + - 'description': Token description + - 'uri': Metadata URI + - 'supply': Current supply + - 'maximum': Maximum supply + - 'royalty': Royalty information + - Other token-specific fields + + Raises: + ApiError: If the token doesn't exist or query fails. + + Examples: + Get token metadata:: + + token_data = await token_client.get_token_data( + creator=creator.address(), + collection_name="Art Collection", + token_name="Masterpiece #1", + property_version=0 + ) + + print(f"Token: {token_data['name']}") + print(f"Description: {token_data['description']}") + print(f"URI: {token_data['uri']}") + print(f"Supply: {token_data['supply']}/{token_data['maximum']}") + + Note: + This returns the canonical token definition, not ownership + information. Use get_token() to check specific ownership. + """ resource = await self._client.account_resource( creator, "0x3::token::Collections" ) @@ -260,6 +860,44 @@ async def get_token_data( async def get_collection( self, creator: AccountAddress, collection_name: str ) -> Any: + """Retrieve metadata and configuration for a specific collection. + + Queries the collection creator's Collections resource to get + collection metadata and configuration settings. + + Args: + creator: Address of the account that created the collection. + collection_name: Name of the collection to query. + + Returns: + Dict containing collection information including: + - 'name': Collection name + - 'description': Collection description + - 'uri': Collection metadata URI + - 'maximum': Maximum number of tokens allowed + - 'supply': Current number of tokens created + - Mutability settings for various fields + + Raises: + ApiError: If the collection doesn't exist or query fails. + + Examples: + Get collection info:: + + collection_data = await token_client.get_collection( + creator=creator.address(), + collection_name="Art Collection" + ) + + print(f"Collection: {collection_data['name']}") + print(f"Description: {collection_data['description']}") + print(f"URI: {collection_data['uri']}") + print(f"Supply: {collection_data['supply']}/{collection_data['maximum']}") + + Note: + This provides collection-level metadata. Use get_token_data() + to get information about specific tokens within the collection. + """ resource = await self._client.account_resource( creator, "0x3::token::Collections" ) @@ -275,6 +913,28 @@ async def get_collection( async def transfer_object( self, owner: Account, object: AccountAddress, to: AccountAddress ) -> str: + """Transfer an object-based resource to another account. + + This method is for transferring object-based resources and may be + used for hybrid Token V1/Object scenarios. Not typically used for + standard Token V1 transfers. + + Args: + owner: The current owner of the object. + object: Address of the object to transfer. + to: Address of the account to receive the object. + + Returns: + str: Transaction hash of the transfer transaction. + + Raises: + ApiError: If the transaction fails or object doesn't exist. + + Note: + This method is primarily for object-based transfers and may not + be applicable to standard Token V1 tokens. Use direct_transfer_token + or the offer/claim pattern for regular Token V1 transfers. + """ transaction_arguments = [ TransactionArgument(object, Serializer.struct), TransactionArgument(to, Serializer.struct), diff --git a/aptos_sdk/asymmetric_crypto.py b/aptos_sdk/asymmetric_crypto.py index f2ee235..abe96aa 100644 --- a/aptos_sdk/asymmetric_crypto.py +++ b/aptos_sdk/asymmetric_crypto.py @@ -1,6 +1,59 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Asymmetric cryptographic interfaces and protocols for the Aptos Python SDK. + +This module defines the foundational cryptographic interfaces used throughout +the Aptos ecosystem, including protocols for private keys, public keys, and +signatures. It provides standardized interfaces that concrete implementations +must follow, ensuring consistency across different cryptographic schemes. + +Key Components: +- **Protocol Definitions**: Abstract interfaces for cryptographic primitives +- **AIP-80 Compliance**: Standard formatting for private key serialization +- **Multi-Algorithm Support**: Extensible framework for Ed25519, secp256k1, etc. +- **BCS Integration**: Seamless serialization/deserialization support + +Supported Key Types: +- Ed25519: Primary signature scheme used by Aptos +- secp256k1: ECDSA signature scheme for Ethereum compatibility + +AIP-80 Standard: +The Aptos Improvement Proposal 80 (AIP-80) defines standard string formats +for private keys to improve interoperability and user experience. This module +provides utilities for parsing and formatting keys according to this standard. + +Examples: + Working with private key formatting:: + + from aptos_sdk.asymmetric_crypto import PrivateKey, PrivateKeyVariant + + # Format a raw hex key as AIP-80 compliant + raw_key = "0x1234abcd..." + formatted = PrivateKey.format_private_key(raw_key, PrivateKeyVariant.Ed25519) + # Returns: "ed25519-priv-0x1234abcd..." + + # Parse various formats to bytes + key_bytes = PrivateKey.parse_hex_input( + "ed25519-priv-0x1234abcd...", + PrivateKeyVariant.Ed25519 + ) + + Using protocol interfaces:: + + # All concrete key implementations follow these protocols + def sign_data(private_key: PrivateKey, message: bytes) -> Signature: + return private_key.sign(message) + + def verify_signature(public_key: PublicKey, message: bytes, sig: Signature) -> bool: + return public_key.verify(message, sig) + +Note: + This module defines protocols and interfaces only. Concrete implementations + are provided in separate modules (e.g., ed25519.py, secp256k1_ecdsa.py). +""" + from __future__ import annotations from enum import Enum @@ -11,22 +64,168 @@ class PrivateKeyVariant(Enum): + """Enumeration of supported private key cryptographic algorithms. + + This enum defines the cryptographic signature schemes supported by the + Aptos blockchain and their corresponding string identifiers used in + AIP-80 compliant formatting. + + Attributes: + Ed25519: The Ed25519 signature scheme (primary for Aptos). + Secp256k1: The secp256k1 ECDSA signature scheme (Ethereum compatibility). + + Examples: + Using the enum values:: + + # Check key type + if key_type == PrivateKeyVariant.Ed25519: + print("Using Ed25519 cryptography") + + # Get string representation + scheme_name = PrivateKeyVariant.Secp256k1.value # "secp256k1" + + Iterating over supported schemes:: + + for scheme in PrivateKeyVariant: + print(f"Supported: {scheme.value}") + + Note: + These values are used internally for key type identification and + correspond to the prefixes defined in the AIP-80 standard. + """ Ed25519 = "ed25519" Secp256k1 = "secp256k1" class PrivateKey(Deserializable, Serializable, Protocol): - def hex(self) -> str: ... + """Protocol defining the interface for asymmetric cryptographic private keys. + + This protocol establishes the standard interface that all private key + implementations must follow in the Aptos SDK. It combines cryptographic + operations with serialization capabilities and AIP-80 compliance utilities. + + The protocol ensures that all private key types can: + - Generate corresponding public keys + - Sign arbitrary data + - Serialize/deserialize for network transmission + - Format according to AIP-80 standards + + Key Management Standards: + - Implements AIP-80 compliant string formatting + - Supports multiple input formats (hex, bytes, AIP-80 strings) + - Provides type-safe parsing and validation + - Maintains backward compatibility with legacy formats + + Methods: + hex() -> str: Get hexadecimal representation of the private key + public_key() -> PublicKey: Derive the corresponding public key + sign(data: bytes) -> Signature: Sign data and return signature + + Static Methods: + format_private_key(): Format keys as AIP-80 compliant strings + parse_hex_input(): Parse various input formats to bytes + + Examples: + Implementing a private key class:: + + class MyPrivateKey(PrivateKey): + def __init__(self, key_bytes: bytes): + self._key_bytes = key_bytes + + def hex(self) -> str: + return self._key_bytes.hex() + + def public_key(self) -> PublicKey: + # Derive public key from private key + return MyPublicKey.from_private(self) + + def sign(self, data: bytes) -> Signature: + # Implementation-specific signing + return MySignature(self._sign_bytes(data)) + + Using the formatting utilities:: + + # Format existing key + formatted = PrivateKey.format_private_key( + "0xabcd1234...", + PrivateKeyVariant.Ed25519 + ) + + # Parse different input formats + key_bytes = PrivateKey.parse_hex_input( + "ed25519-priv-0xabcd1234...", + PrivateKeyVariant.Ed25519 + ) + + Note: + This is a Protocol (structural typing), not a base class. Concrete + implementations don't need to explicitly inherit from this protocol, + they just need to implement the required methods. + """ + def hex(self) -> str: + """Return the hexadecimal string representation of the private key. + + Returns: + Hexadecimal string of the private key bytes, typically prefixed with '0x'. + + Example: + >>> private_key.hex() + '0x1234abcd...' + """ + ... - def public_key(self) -> PublicKey: ... + def public_key(self) -> PublicKey: + """Derive the corresponding public key from this private key. + + Returns: + The public key derived from this private key using the appropriate + cryptographic algorithm. + + Example: + >>> pub_key = private_key.public_key() + >>> isinstance(pub_key, PublicKey) + True + """ + ... - def sign(self, data: bytes) -> Signature: ... + def sign(self, data: bytes) -> Signature: + """Sign the given data using this private key. + + Args: + data: The raw bytes to be signed. + + Returns: + A signature object that can be used to verify the data was signed + by the holder of this private key. + + Example: + >>> message = b"Hello, Aptos!" + >>> signature = private_key.sign(message) + >>> public_key.verify(message, signature) + True + """ + ... """ - The AIP-80 compliant prefixes for each private key type. Append this to a private key's hex representation - to get an AIP-80 compliant string. - - [Read about AIP-80](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md) + AIP-80 compliant prefixes for private key serialization. + + The Aptos Improvement Proposal 80 (AIP-80) defines standardized string + formats for private keys to improve interoperability and user experience. + Each supported cryptographic scheme has a unique prefix that identifies + the key type. + + Format: "{algorithm}-priv-{hex_value}" + + Supported Prefixes: + - "ed25519-priv-": For Ed25519 private keys + - "secp256k1-priv-": For secp256k1 ECDSA private keys + + Examples: + Ed25519 key: "ed25519-priv-0x1234abcd..." + secp256k1 key: "secp256k1-priv-0xabcd1234..." + + References: + [AIP-80 Specification](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md) """ AIP80_PREFIXES: dict[PrivateKeyVariant, str] = { PrivateKeyVariant.Ed25519: "ed25519-priv-", @@ -37,12 +236,55 @@ def sign(self, data: bytes) -> Signature: ... def format_private_key( private_key: bytes | str, key_type: PrivateKeyVariant ) -> str: - """ - Format a HexInput to an AIP-80 compliant string. - - :param private_key: The hex string or bytes format of the private key. - :param key_type: The private key type. - :return: AIP-80 compliant string. + """Format a private key as an AIP-80 compliant string. + + This method converts various private key input formats into the standardized + AIP-80 string format, which includes the algorithm prefix and ensures + consistent representation across the Aptos ecosystem. + + Args: + private_key: The private key in hex string or bytes format. + Can be a raw hex string (with or without '0x' prefix), + bytes object, or already AIP-80 formatted string. + key_type: The cryptographic algorithm type for this key. + + Returns: + AIP-80 compliant string in the format: + "{algorithm}-priv-{hex_value}" + + Raises: + ValueError: If the key_type is not supported. + TypeError: If the private_key is not string or bytes. + + Examples: + Format a raw hex string:: + + key = "0x1234abcd..." + formatted = PrivateKey.format_private_key( + key, PrivateKeyVariant.Ed25519 + ) + # Returns: "ed25519-priv-0x1234abcd..." + + Format bytes:: + + key_bytes = bytes.fromhex("1234abcd") + formatted = PrivateKey.format_private_key( + key_bytes, PrivateKeyVariant.Secp256k1 + ) + # Returns: "secp256k1-priv-0x1234abcd" + + Handle already formatted keys:: + + formatted_key = "ed25519-priv-0x1234abcd..." + result = PrivateKey.format_private_key( + formatted_key, PrivateKeyVariant.Ed25519 + ) + # Returns: "ed25519-priv-0x1234abcd..." (unchanged) + + Note: + If the input is already AIP-80 compliant for the specified key type, + the method will extract and reformat the hex portion to ensure + consistency. """ if key_type not in PrivateKey.AIP80_PREFIXES: raise ValueError(f"Unknown private key type: {key_type}") @@ -65,13 +307,68 @@ def format_private_key( def parse_hex_input( value: str | bytes, key_type: PrivateKeyVariant, strict: bool | None = None ) -> bytes: - """ - Parse a HexInput that may be a hex string, bytes, or an AIP-80 compliant string to a byte array. - - :param value: A hex string, byte array, or AIP-80 compliant string. - :param key_type: The private key type. - :param strict: If true, the value MUST be compliant with AIP-80. - :return: Parsed private key as bytes. + """Parse various private key input formats to standardized bytes. + + This method handles multiple input formats for private keys and converts + them to a consistent bytes representation. It supports legacy hex strings, + AIP-80 compliant strings, and raw bytes. + + Args: + value: The private key in various formats: + - Raw hex string ("1234abcd" or "0x1234abcd") + - AIP-80 compliant string ("ed25519-priv-0x1234abcd") + - Raw bytes object + key_type: The expected cryptographic algorithm type. + strict: AIP-80 compliance mode: + - True: Only accept AIP-80 compliant strings + - False: Accept legacy hex formats without warning + - None (default): Accept legacy formats with deprecation warning + + Returns: + The private key as a bytes object, ready for cryptographic operations. + + Raises: + ValueError: If key_type is unsupported, or if strict=True and input + is not AIP-80 compliant, or if input format is invalid. + TypeError: If value is not string or bytes. + + Examples: + Parse AIP-80 compliant string:: + + key_bytes = PrivateKey.parse_hex_input( + "ed25519-priv-0x1234abcd...", + PrivateKeyVariant.Ed25519 + ) + + Parse legacy hex string:: + + key_bytes = PrivateKey.parse_hex_input( + "0x1234abcd...", + PrivateKeyVariant.Ed25519, + strict=False # Suppress warning + ) + + Parse raw bytes:: + + key_bytes = PrivateKey.parse_hex_input( + bytes.fromhex("1234abcd"), + PrivateKeyVariant.Ed25519 + ) + + Strict mode (AIP-80 only):: + + try: + key_bytes = PrivateKey.parse_hex_input( + "0x1234abcd...", # Legacy format + PrivateKeyVariant.Ed25519, + strict=True + ) + except ValueError: + print("Must use AIP-80 format in strict mode") + + Note: + When strict=None (default), legacy hex formats trigger a deprecation + warning encouraging migration to AIP-80 compliant formats. """ if key_type not in PrivateKey.AIP80_PREFIXES: raise ValueError(f"Unknown private key type: {key_type}") @@ -106,14 +403,172 @@ def parse_hex_input( class PublicKey(Deserializable, Serializable, Protocol): + """Protocol defining the interface for asymmetric cryptographic public keys. + + This protocol establishes the standard interface that all public key + implementations must follow in the Aptos SDK. Public keys are used for + signature verification and address derivation in the Aptos blockchain. + + The protocol ensures that all public key types can: + - Verify signatures created by corresponding private keys + - Serialize for network transmission and storage + - Generate specialized byte representations for different contexts + + Key Features: + - **Signature Verification**: Cryptographic validation of signed data + - **Flexible Encoding**: Support for both BCS and specialized encodings + - **Multi-Algorithm Support**: Compatible with Ed25519, secp256k1, etc. + - **Address Derivation**: Foundation for generating blockchain addresses + + Methods: + to_crypto_bytes() -> bytes: Get specialized cryptographic byte encoding + verify(data: bytes, signature: Signature) -> bool: Verify a signature + + Examples: + Implementing a public key class:: + + class MyPublicKey(PublicKey): + def __init__(self, key_bytes: bytes): + self._key_bytes = key_bytes + + def to_crypto_bytes(self) -> bytes: + # Return algorithm-specific encoding + return self._key_bytes + + def verify(self, data: bytes, signature: Signature) -> bool: + # Implementation-specific verification + return self._verify_signature(data, signature) + + def serialize(self, serializer) -> None: + # BCS serialization + serializer.bytes(self._key_bytes) + + Using public key verification:: + + message = b"Hello, Aptos!" + signature = private_key.sign(message) + + # Verify the signature + if public_key.verify(message, signature): + print("Signature is valid!") + else: + print("Invalid signature!") + + Note: + The to_crypto_bytes() method exists for historical reasons where + some key types (like MultiEd25519) require special encoding beyond + standard BCS serialization. + """ def to_crypto_bytes(self) -> bytes: + """Get the specialized cryptographic byte representation. + + This method provides an algorithm-specific byte encoding that may + differ from the standard BCS serialization. It exists primarily + for compatibility with legacy systems and specialized key types + like MultiEd25519. + + Returns: + The public key in its specialized cryptographic byte format. + + Note: + For most single-signature schemes, this typically returns the + same bytes as BCS serialization. Multi-signature schemes may + use different encodings. + + Example: + >>> crypto_bytes = public_key.to_crypto_bytes() + >>> len(crypto_bytes) # Length depends on algorithm + 32 # Ed25519 public keys are 32 bytes + """ """ A long time ago, someone decided that we should have both bcs and a special representation for MultiEd25519, so we use this to let keys self-define a special encoding. """ ... - def verify(self, data: bytes, signature: Signature) -> bool: ... + def verify(self, data: bytes, signature: Signature) -> bool: + """Verify that a signature was created by the corresponding private key. + + This method performs cryptographic verification to ensure that the + given signature was created by signing the provided data with the + private key corresponding to this public key. + + Args: + data: The original data that was signed. + signature: The signature to verify. + + Returns: + True if the signature is valid for the given data and this public key, + False otherwise. + + Example: + >>> message = b"transaction data" + >>> signature = private_key.sign(message) + >>> public_key.verify(message, signature) + True + >>> public_key.verify(b"different data", signature) + False + + Note: + This method should be constant-time to prevent timing attacks + in security-critical applications. + """ + ... -class Signature(Deserializable, Serializable, Protocol): ... +class Signature(Deserializable, Serializable, Protocol): + """Protocol defining the interface for cryptographic signatures. + + This protocol establishes the standard interface that all signature + implementations must follow in the Aptos SDK. Signatures are the + cryptographic proofs that verify the authenticity and integrity of + signed data. + + The protocol ensures that all signature types can: + - Serialize for network transmission and storage + - Deserialize from various input formats + - Integrate seamlessly with the BCS serialization system + + Key Properties: + - **Immutable**: Signatures should be treated as immutable once created + - **Verifiable**: Can be verified using corresponding public keys + - **Serializable**: Compatible with Aptos network protocols + - **Type-Safe**: Maintains algorithm-specific signature formats + + Examples: + Implementing a signature class:: + + class MySignature(Signature): + def __init__(self, signature_bytes: bytes): + self._signature_bytes = signature_bytes + + def serialize(self, serializer) -> None: + # BCS serialization + serializer.bytes(self._signature_bytes) + + @classmethod + def deserialize(cls, deserializer) -> 'MySignature': + # BCS deserialization + signature_bytes = deserializer.bytes() + return cls(signature_bytes) + + Using signatures in verification:: + + # Create signature + message = b"Hello, Aptos!" + signature = private_key.sign(message) + + # Verify signature + is_valid = public_key.verify(message, signature) + + # Serialize for transmission + serializer = Serializer() + signature.serialize(serializer) + signature_bytes = serializer.output() + + Note: + This is a Protocol (structural typing), not a base class. Concrete + signature implementations don't need to explicitly inherit from this + protocol, they just need to implement the required serialization methods. + """ + ... diff --git a/aptos_sdk/asymmetric_crypto_wrapper.py b/aptos_sdk/asymmetric_crypto_wrapper.py index 663d976..d272153 100644 --- a/aptos_sdk/asymmetric_crypto_wrapper.py +++ b/aptos_sdk/asymmetric_crypto_wrapper.py @@ -1,6 +1,85 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Cryptographic wrapper classes for unified signature scheme handling in Aptos. + +This module provides wrapper classes that unify different cryptographic signature +schemes (Ed25519, secp256k1) under a common interface. These wrappers enable +polymorphic handling of different key types and support multi-signature scenarios. + +Key Features: +- **Algorithm Abstraction**: Unified interface for different signature schemes +- **Multi-Signature Support**: Threshold-based multi-signature authentication +- **Type-Safe Variants**: Compile-time and runtime type safety for crypto operations +- **BCS Serialization**: Full integration with Aptos Binary Canonical Serialization +- **Backward Compatibility**: Support for existing single and multi-key scenarios + +Wrapper Classes: +- PublicKey: Wraps Ed25519 and secp256k1 public keys with variant tagging +- Signature: Wraps signatures from different algorithms with type identification +- MultiPublicKey: Implements threshold-based multi-signature public keys +- MultiSignature: Handles collections of signatures with bitmap-based indexing + +Use Cases: +- Polymorphic signature verification across algorithms +- Multi-signature wallet implementations +- Key rotation and migration scenarios +- Account abstraction with flexible authentication + +Examples: + Single key usage:: + + from aptos_sdk import ed25519 + from aptos_sdk.asymmetric_crypto_wrapper import PublicKey, Signature + + # Wrap an Ed25519 key + ed25519_key = ed25519.PrivateKey.generate() + wrapped_public = PublicKey(ed25519_key.public_key()) + + # Sign and verify + message = b"Hello, Aptos!" + ed25519_sig = ed25519_key.sign(message) + wrapped_sig = Signature(ed25519_sig) + + # Verify through wrapper + is_valid = wrapped_public.verify(message, wrapped_sig) + + Multi-signature setup:: + + from aptos_sdk.asymmetric_crypto_wrapper import MultiPublicKey, MultiSignature + + # Create multi-sig with 2-of-3 threshold + keys = [key1.public_key(), key2.public_key(), key3.public_key()] + multi_key = MultiPublicKey(keys, threshold=2) + + # Create multi-signature (keys 0 and 2 sign) + sig1 = key1.sign(message) + sig3 = key3.sign(message) + multi_sig = MultiSignature([(0, sig1), (2, sig3)]) + + # Verify multi-signature + is_valid = multi_key.verify(message, multi_sig) + + Serialization example:: + + from aptos_sdk.bcs import Serializer, Deserializer + + # Serialize wrapped key + serializer = Serializer() + wrapped_public.serialize(serializer) + key_bytes = serializer.output() + + # Deserialize wrapped key + deserializer = Deserializer(key_bytes) + restored_key = PublicKey.deserialize(deserializer) + +Note: + These wrappers add a small overhead for type tagging and dispatching, + but enable powerful polymorphic cryptographic operations essential + for flexible blockchain authentication schemes. +""" + from __future__ import annotations from typing import List, Tuple, cast @@ -10,6 +89,43 @@ class PublicKey(asymmetric_crypto.PublicKey): + """Unified wrapper for different cryptographic public key types. + + This class provides a common interface for Ed25519 and secp256k1 public keys, + enabling polymorphic handling of different signature schemes within the Aptos + ecosystem. The wrapper maintains type information through variant tagging. + + Type Variants: + ED25519 (0): Ed25519 elliptic curve signature scheme + SECP256K1_ECDSA (1): secp256k1 ECDSA signature scheme + + Attributes: + variant: Integer identifier for the wrapped key type + public_key: The underlying concrete public key implementation + + Examples: + Wrapping different key types:: + + # Ed25519 key + ed25519_private = ed25519.PrivateKey.generate() + wrapped_ed25519 = PublicKey(ed25519_private.public_key()) + assert wrapped_ed25519.variant == PublicKey.ED25519 + + # secp256k1 key + secp256k1_private = secp256k1_ecdsa.PrivateKey.generate() + wrapped_secp256k1 = PublicKey(secp256k1_private.public_key()) + assert wrapped_secp256k1.variant == PublicKey.SECP256K1_ECDSA + + Polymorphic verification:: + + def verify_message(public_key: PublicKey, message: bytes, signature: Signature) -> bool: + # Works regardless of underlying algorithm + return public_key.verify(message, signature) + + Note: + The wrapper automatically detects the key type during construction + and sets the appropriate variant identifier for serialization. + """ ED25519: int = 0 SECP256K1_ECDSA: int = 1 @@ -17,6 +133,20 @@ class PublicKey(asymmetric_crypto.PublicKey): public_key: asymmetric_crypto.PublicKey def __init__(self, public_key: asymmetric_crypto.PublicKey): + """Initialize a public key wrapper for the given concrete key. + + Args: + public_key: An Ed25519 or secp256k1 public key to be wrapped. + + Raises: + NotImplementedError: If the public key type is not supported. + + Example: + >>> ed25519_key = ed25519.PrivateKey.generate().public_key() + >>> wrapped = PublicKey(ed25519_key) + >>> wrapped.variant + 0 + """ if isinstance(public_key, ed25519.PublicKey): self.variant = PublicKey.ED25519 elif isinstance(public_key, secp256k1_ecdsa.PublicKey): @@ -26,11 +156,45 @@ def __init__(self, public_key: asymmetric_crypto.PublicKey): self.public_key = public_key def to_crypto_bytes(self) -> bytes: + """Get the specialized cryptographic byte representation. + + Returns the public key in BCS-serialized format including the + variant tag, suitable for cryptographic operations and storage. + + Returns: + BCS-serialized bytes including variant tag and key data. + + Example: + >>> wrapped_key = PublicKey(ed25519_key) + >>> crypto_bytes = wrapped_key.to_crypto_bytes() + >>> len(crypto_bytes) # Includes variant byte + key bytes + 33 # 1 byte variant + 32 bytes Ed25519 key + """ ser = Serializer() self.serialize(ser) return ser.output() def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: + """Verify a signature against this public key. + + Unwraps the signature and delegates verification to the underlying + concrete public key implementation. Handles type coercion to ensure + the signature wrapper matches the key type. + + Args: + data: The original data that was signed. + signature: A wrapped signature to verify. + + Returns: + True if the signature is valid, False otherwise. + + Example: + >>> message = b"Hello, Aptos!" + >>> signature = private_key.sign(message) + >>> wrapped_signature = Signature(signature) + >>> wrapped_public.verify(message, wrapped_signature) + True + """ # Convert signature to the original signature sig = cast(Signature, signature) @@ -38,6 +202,27 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: @staticmethod def deserialize(deserializer: Deserializer) -> PublicKey: + """Deserialize a public key wrapper from BCS-encoded data. + + Reads the variant tag and delegates deserialization to the appropriate + concrete key implementation based on the detected type. + + Args: + deserializer: BCS deserializer containing the key data. + + Returns: + A new PublicKey wrapper containing the deserialized key. + + Raises: + Exception: If the variant tag is not recognized. + + Example: + >>> serializer = Serializer() + >>> original_key.serialize(serializer) + >>> key_bytes = serializer.output() + >>> deserializer = Deserializer(key_bytes) + >>> restored_key = PublicKey.deserialize(deserializer) + """ variant = deserializer.uleb128() if variant == PublicKey.ED25519: @@ -52,11 +237,63 @@ def deserialize(deserializer: Deserializer) -> PublicKey: return PublicKey(public_key) def serialize(self, serializer: Serializer): + """Serialize the public key wrapper to BCS format. + + Writes the variant tag followed by the underlying public key data + in BCS format. The variant enables proper deserialization. + + Args: + serializer: BCS serializer to write the key data to. + + Example: + >>> serializer = Serializer() + >>> wrapped_key.serialize(serializer) + >>> key_bytes = serializer.output() + """ serializer.uleb128(self.variant) serializer.struct(self.public_key) class Signature(asymmetric_crypto.Signature): + """Unified wrapper for different cryptographic signature types. + + This class provides a common interface for Ed25519 and secp256k1 signatures, + enabling polymorphic handling and verification across different signature + schemes. Like PublicKey, it uses variant tagging for type identification. + + Type Variants: + ED25519 (0): Ed25519 signature + SECP256K1_ECDSA (1): secp256k1 ECDSA signature + + Attributes: + variant: Integer identifier for the wrapped signature type + signature: The underlying concrete signature implementation + + Examples: + Wrapping different signature types:: + + message = b"Hello, Aptos!" + + # Ed25519 signature + ed25519_sig = ed25519_private.sign(message) + wrapped_ed25519_sig = Signature(ed25519_sig) + assert wrapped_ed25519_sig.variant == Signature.ED25519 + + # secp256k1 signature + secp256k1_sig = secp256k1_private.sign(message) + wrapped_secp256k1_sig = Signature(secp256k1_sig) + assert wrapped_secp256k1_sig.variant == Signature.SECP256K1_ECDSA + + Polymorphic operations:: + + def verify_any_signature(key: PublicKey, msg: bytes, sig: Signature) -> bool: + # Works regardless of underlying algorithm + return key.verify(msg, sig) + + Note: + The signature wrapper automatically detects and tags the signature type, + ensuring compatibility with the corresponding public key wrapper. + """ ED25519: int = 0 SECP256K1_ECDSA: int = 1 @@ -64,6 +301,21 @@ class Signature(asymmetric_crypto.Signature): signature: asymmetric_crypto.Signature def __init__(self, signature: asymmetric_crypto.Signature): + """Initialize a signature wrapper for the given concrete signature. + + Args: + signature: An Ed25519 or secp256k1 signature to be wrapped. + + Raises: + NotImplementedError: If the signature type is not supported. + + Example: + >>> message = b"test message" + >>> ed25519_sig = ed25519_private.sign(message) + >>> wrapped = Signature(ed25519_sig) + >>> wrapped.variant + 0 + """ if isinstance(signature, ed25519.Signature): self.variant = Signature.ED25519 elif isinstance(signature, secp256k1_ecdsa.Signature): @@ -74,6 +326,27 @@ def __init__(self, signature: asymmetric_crypto.Signature): @staticmethod def deserialize(deserializer: Deserializer) -> Signature: + """Deserialize a signature wrapper from BCS-encoded data. + + Reads the variant tag and delegates deserialization to the appropriate + concrete signature implementation based on the detected type. + + Args: + deserializer: BCS deserializer containing the signature data. + + Returns: + A new Signature wrapper containing the deserialized signature. + + Raises: + Exception: If the variant tag is not recognized. + + Example: + >>> serializer = Serializer() + >>> original_sig.serialize(serializer) + >>> sig_bytes = serializer.output() + >>> deserializer = Deserializer(sig_bytes) + >>> restored_sig = Signature.deserialize(deserializer) + """ variant = deserializer.uleb128() if variant == Signature.ED25519: @@ -88,11 +361,79 @@ def deserialize(deserializer: Deserializer) -> Signature: return Signature(signature) def serialize(self, serializer: Serializer): + """Serialize the signature wrapper to BCS format. + + Writes the variant tag followed by the underlying signature data + in BCS format. The variant enables proper deserialization. + + Args: + serializer: BCS serializer to write the signature data to. + + Example: + >>> serializer = Serializer() + >>> wrapped_signature.serialize(serializer) + >>> sig_bytes = serializer.output() + """ serializer.uleb128(self.variant) serializer.struct(self.signature) class MultiPublicKey(asymmetric_crypto.PublicKey): + """Multi-signature public key implementing threshold-based authentication. + + This class represents a collection of public keys with a threshold requirement, + enabling N-of-M multi-signature schemes. It's commonly used for multi-signature + wallets, governance systems, and enhanced security scenarios. + + Threshold Mechanics: + - Requires at least `threshold` valid signatures from the key set + - Signatures are indexed by position in the key array + - Uses bitmap encoding to identify which keys provided signatures + - Supports heterogeneous key types (Ed25519, secp256k1) within the same set + + Constraints: + MIN_KEYS (2): Minimum number of keys required + MAX_KEYS (32): Maximum number of keys allowed + MIN_THRESHOLD (1): Minimum threshold value + + Attributes: + keys: List of wrapped public keys in the multi-signature scheme + threshold: Minimum number of signatures required for validity + + Examples: + Creating a 2-of-3 multi-signature key:: + + key1 = ed25519.PrivateKey.generate().public_key() + key2 = ed25519.PrivateKey.generate().public_key() + key3 = secp256k1_ecdsa.PrivateKey.generate().public_key() + + multi_key = MultiPublicKey([key1, key2, key3], threshold=2) + print(multi_key) # "2-of-3 Multi key" + + Verification with multi-signature:: + + message = b"Multi-sig transaction" + + # Keys 0 and 2 sign (satisfies threshold of 2) + sig1 = private_key1.sign(message) + sig3 = private_key3.sign(message) + multi_sig = MultiSignature([(0, sig1), (2, sig3)]) + + # Verify + is_valid = multi_key.verify(message, multi_sig) + + Creating governance multi-sig:: + + # 3-of-5 governance setup + governance_keys = [generate_key() for _ in range(5)] + governance_multi = MultiPublicKey(governance_keys, threshold=3) + + # Requires 3 signatures for any governance action + + Note: + All keys are automatically wrapped in PublicKey wrappers for consistency, + enabling mixed-algorithm multi-signature schemes. + """ keys: List[PublicKey] threshold: int @@ -101,6 +442,25 @@ class MultiPublicKey(asymmetric_crypto.PublicKey): MIN_THRESHOLD = 1 def __init__(self, keys: List[asymmetric_crypto.PublicKey], threshold: int): + """Initialize a multi-signature public key with the given parameters. + + Args: + keys: List of public keys that can participate in signing. + Must be between MIN_KEYS and MAX_KEYS in length. + threshold: Minimum number of signatures required for validity. + Must be between MIN_THRESHOLD and the number of keys. + + Raises: + AssertionError: If key count or threshold is outside valid ranges. + + Example: + >>> keys = [key1, key2, key3] + >>> multi_key = MultiPublicKey(keys, threshold=2) + >>> len(multi_key.keys) + 3 + >>> multi_key.threshold + 2 + """ assert ( self.MIN_KEYS <= len(keys) <= self.MAX_KEYS ), f"Must have between {self.MIN_KEYS} and {self.MAX_KEYS} keys." @@ -119,9 +479,47 @@ def __init__(self, keys: List[asymmetric_crypto.PublicKey], threshold: int): self.threshold = threshold def __str__(self) -> str: + """Return a human-readable string representation. + + Returns: + String in the format "{threshold}-of-{total} Multi key". + + Example: + >>> str(MultiPublicKey([key1, key2, key3], 2)) + '2-of-3 Multi key' + """ return f"{self.threshold}-of-{len(self.keys)} Multi key" def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: + """Verify a multi-signature against this multi-public-key. + + Validates that the provided multi-signature contains at least the + threshold number of valid signatures from keys in this multi-key set. + + Args: + data: The original data that was signed. + signature: A MultiSignature containing indexed signatures. + + Returns: + True if the multi-signature satisfies the threshold requirement + and all included signatures are valid, False otherwise. + + Verification Process: + 1. Ensures sufficient signatures are provided (>= threshold) + 2. Validates each signature index is within the key set bounds + 3. Verifies each signature against its corresponding public key + 4. Returns False if any validation step fails + + Example: + >>> message = b"transaction data" + >>> multi_sig = MultiSignature([(0, sig1), (2, sig3)]) + >>> multi_key.verify(message, multi_sig) # 2 sigs >= threshold + True + + Note: + This method uses exception handling for robustness, returning False + for any verification failure rather than propagating exceptions. + """ try: total_sig = cast(MultiSignature, signature) assert self.threshold <= len( @@ -142,30 +540,140 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: @staticmethod def from_crypto_bytes(indata: bytes) -> MultiPublicKey: + """Deserialize a MultiPublicKey from its byte representation. + + Args: + indata: BCS-serialized bytes of the multi-public-key. + + Returns: + A new MultiPublicKey instance. + + Example: + >>> original_bytes = multi_key.to_crypto_bytes() + >>> restored_key = MultiPublicKey.from_crypto_bytes(original_bytes) + """ deserializer = Deserializer(indata) return deserializer.struct(MultiPublicKey) def to_crypto_bytes(self) -> bytes: + """Serialize the MultiPublicKey to its byte representation. + + Returns: + BCS-serialized bytes suitable for storage or transmission. + + Example: + >>> multi_bytes = multi_key.to_crypto_bytes() + >>> len(multi_bytes) # Depends on number and types of keys + """ serializer = Serializer() serializer.struct(self) return serializer.output() @staticmethod def deserialize(deserializer: Deserializer) -> MultiPublicKey: + """Deserialize a MultiPublicKey from a BCS deserializer. + + Args: + deserializer: BCS deserializer containing the multi-key data. + + Returns: + A new MultiPublicKey instance with the deserialized keys and threshold. + + Example: + >>> deserializer = Deserializer(serialized_data) + >>> multi_key = MultiPublicKey.deserialize(deserializer) + """ keys = deserializer.sequence(PublicKey.deserialize) threshold = deserializer.u8() return MultiPublicKey(keys, threshold) def serialize(self, serializer: Serializer): + """Serialize the MultiPublicKey to a BCS serializer. + + Args: + serializer: BCS serializer to write the multi-key data to. + + Example: + >>> serializer = Serializer() + >>> multi_key.serialize(serializer) + >>> serialized_bytes = serializer.output() + """ serializer.sequence(self.keys, Serializer.struct) serializer.u8(self.threshold) class MultiSignature(asymmetric_crypto.Signature): + """Multi-signature implementation with bitmap-based key indexing. + + This class represents a collection of signatures created by different keys + within a multi-signature scheme. It uses bitmap encoding to efficiently + track which keys in the set provided signatures. + + Bitmap Encoding: + - Each signature is associated with an index (position in the key array) + - A bitmap efficiently encodes which positions have signatures + - Supports up to MAX_SIGNATURES concurrent signatures + - Optimizes storage and verification performance + + Constraints: + MAX_SIGNATURES (16): Maximum number of signatures in one multi-signature + + Attributes: + signatures: List of (index, signature) tuples where index refers + to the position in the corresponding MultiPublicKey's key array + + Examples: + Creating a multi-signature:: + + # Keys at positions 0 and 2 sign + sig1 = private_key1.sign(message) + sig3 = private_key3.sign(message) # Note: key3 is at index 2 + + multi_sig = MultiSignature([(0, sig1), (2, sig3)]) + + Verifying with corresponding multi-key:: + + # MultiPublicKey with keys [key1, key2, key3], threshold=2 + is_valid = multi_key.verify(message, multi_sig) + # True because we have 2 signatures >= threshold + + Mixed algorithm signatures:: + + # Ed25519 signature at index 0 + ed25519_sig = ed25519_private.sign(message) + + # secp256k1 signature at index 1 + secp256k1_sig = secp256k1_private.sign(message) + + mixed_multi_sig = MultiSignature([ + (0, ed25519_sig), + (1, secp256k1_sig) + ]) + + Note: + Signatures are automatically wrapped in Signature wrappers to ensure + type consistency and proper serialization. + """ signatures: List[Tuple[int, Signature]] MAX_SIGNATURES: int = 16 def __init__(self, signatures: List[Tuple[int, asymmetric_crypto.Signature]]): + """Initialize a multi-signature with indexed signatures. + + Args: + signatures: List of (index, signature) tuples where index + corresponds to the position in the MultiPublicKey's key array. + + Raises: + AssertionError: If any index exceeds MAX_SIGNATURES. + + Example: + >>> sig1 = private_key1.sign(message) + >>> sig2 = private_key2.sign(message) + >>> multi_sig = MultiSignature([(0, sig1), (1, sig2)]) + >>> len(multi_sig.signatures) + 2 + """ # Sort first to ensure no issues in order # signatures.sort(key=lambda x: x[0]) self.signatures = [] @@ -177,15 +685,56 @@ def __init__(self, signatures: List[Tuple[int, asymmetric_crypto.Signature]]): self.signatures.append((index, Signature(signature))) def __eq__(self, other: object): + """Check equality with another MultiSignature. + + Args: + other: Object to compare with. + + Returns: + True if both MultiSignatures have identical signatures and indices. + + Example: + >>> multi_sig1 == multi_sig2 + True # If they contain the same (index, signature) pairs + """ if not isinstance(other, MultiSignature): return NotImplemented return self.signatures == other.signatures def __str__(self) -> str: + """Return a string representation of the multi-signature. + + Returns: + String representation showing the (index, signature) pairs. + + Example: + >>> str(multi_sig) + '[(0, ), (2, )]' + """ return f"{self.signatures}" @staticmethod def deserialize(deserializer: Deserializer) -> MultiSignature: + """Deserialize a MultiSignature from BCS-encoded data. + + Reads the signature sequence and bitmap to reconstruct the indexed + signatures. The bitmap indicates which key positions have signatures. + + Args: + deserializer: BCS deserializer containing the multi-signature data. + + Returns: + A new MultiSignature with the deserialized indexed signatures. + + Deserialization Process: + 1. Read the sequence of signatures + 2. Read the bitmap indicating which keys signed + 3. Reconstruct (index, signature) pairs using bitmap + + Example: + >>> deserializer = Deserializer(serialized_data) + >>> multi_sig = MultiSignature.deserialize(deserializer) + """ signatures = deserializer.sequence(Signature.deserialize) bitmap_raw = deserializer.to_bytes() bitmap = int.from_bytes(bitmap_raw, "little") @@ -202,6 +751,24 @@ def deserialize(deserializer: Deserializer) -> MultiSignature: return MultiSignature(indexed_signatures) def serialize(self, serializer: Serializer): + """Serialize the MultiSignature to BCS format. + + Creates a compact representation using a signature sequence and bitmap. + The bitmap efficiently encodes which key indices have signatures. + + Args: + serializer: BCS serializer to write the multi-signature data to. + + Serialization Format: + 1. Sequence of signatures (without indices) + 2. Bitmap indicating which key positions signed + 3. Variable-length bitmap encoding (1 or 2 bytes) + + Example: + >>> serializer = Serializer() + >>> multi_sig.serialize(serializer) + >>> serialized_bytes = serializer.output() + """ actual_sigs = [] bitmap = 0 @@ -215,6 +782,33 @@ def serialize(self, serializer: Serializer): def index_to_bitmap_value(i: int) -> int: + """Convert a key index to its corresponding bitmap bit value. + + This function implements the bitmap encoding used in multi-signatures + to efficiently represent which keys in a set have provided signatures. + + Args: + i: The key index (0-based position in the key array). + + Returns: + The bitmap value with the appropriate bit set for the given index. + + Bitmap Layout: + - Bits are ordered with the most significant bit representing index 0 + - Multiple bytes are used for indices > 7 + - Little-endian byte ordering is used + + Examples: + >>> index_to_bitmap_value(0) # First key + 128 # Binary: 10000000 + >>> index_to_bitmap_value(7) # Eighth key + 1 # Binary: 00000001 + >>> index_to_bitmap_value(8) # Ninth key (second byte) + 32768 # Binary: 10000000 00000000 + + Note: + This encoding matches the Aptos blockchain's multi-signature bitmap format. + """ bit = i % 8 byte = i // 8 return (128 >> bit) << (byte * 8) diff --git a/aptos_sdk/async_client.py b/aptos_sdk/async_client.py index 1ab40a2..84ef056 100644 --- a/aptos_sdk/async_client.py +++ b/aptos_sdk/async_client.py @@ -1,6 +1,155 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Asynchronous client library for interacting with the Aptos blockchain. + +This module provides comprehensive async client implementations for connecting to and +interacting with Aptos full nodes, faucet services, and indexer services. It supports +the full range of Aptos blockchain operations including account management, transaction +submission, resource queries, and event monitoring. + +Key Features: +- **RestClient**: Full-featured async client for Aptos REST API +- **IndexerClient**: GraphQL client for querying indexed blockchain data +- **FaucetClient**: Client for test network coin funding operations +- **BCS Support**: Binary Canonical Serialization for efficient data handling +- **Multi-Agent Transactions**: Support for complex multi-signature scenarios +- **Transaction Simulation**: Gas estimation and execution preview +- **Event Monitoring**: Real-time blockchain event querying +- **Error Handling**: Comprehensive exception hierarchy for API errors + +Client Types: + RestClient: Primary interface to Aptos full nodes via REST API + IndexerClient: GraphQL interface to Aptos indexer services + FaucetClient: Test network funding and account creation + +Transaction Types: + - Single-agent transactions (standard transfers, function calls) + - Multi-agent transactions (requiring multiple signatures) + - View function calls (read-only operations) + - BCS-encoded transactions (efficient binary format) + +Query Capabilities: + - Account information (balance, sequence number, resources) + - Transaction history and status + - Blockchain events and logs + - Move module and resource data + - Table lookups and aggregator values + - Block and ledger information + +Examples: + Basic client setup and account query:: + + from aptos_sdk.async_client import RestClient, ClientConfig + from aptos_sdk.account_address import AccountAddress + + # Create client with custom configuration + config = ClientConfig( + max_gas_amount=200_000, + gas_unit_price=150, + transaction_wait_in_seconds=30 + ) + + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1", config) + + # Query account information + address = AccountAddress.from_str("***123...") + account_info = await client.account(address) + balance = await client.account_balance(address) + + print(f"Sequence number: {account_info['sequence_number']}") + print(f"Balance: {balance} octas") + + await client.close() + + Transaction submission:: + + from aptos_sdk.account import Account + + # Create sender account + sender = Account.generate() + recipient = AccountAddress.from_str("***456...") + + # Transfer 1 APT (1 * 10^8 octas) + txn_hash = await client.bcs_transfer( + sender=sender, + recipient=recipient, + amount=100_000_000 # 1 APT in octas + ) + + # Wait for transaction completion + await client.wait_for_transaction(txn_hash) + txn_info = await client.transaction_by_hash(txn_hash) + + Multi-agent transaction:: + + # Create multi-agent transaction requiring multiple signatures + signed_txn = await client.create_multi_agent_bcs_transaction( + sender=primary_account, + secondary_accounts=[account2, account3], + payload=transaction_payload + ) + + txn_hash = await client.submit_bcs_transaction(signed_txn) + + IndexerClient usage:: + + indexer = IndexerClient( + "https://indexer.devnet.aptoslabs.com/v1/graphql", + bearer_token="optional_token" + ) + + # GraphQL query example + query = """ + query GetTransactions($address: String!) { + account_transactions(where: {account_address: {_eq: $address}}) { + transaction_version + transaction_timestamp + success + } + } + """ + + result = await indexer.query(query, {"address": str(address)}) + + Faucet usage for testnet:: + + faucet = FaucetClient( + "https://faucet.devnet.aptoslabs.com", + rest_client=client + ) + + # Fund account with test coins + account = Account.generate() + txn_hash = await faucet.fund_account( + address=account.address(), + amount=500_000_000 # 5 APT + ) + + await faucet.close() + +Error Handling: + The module provides specific exception types for different error scenarios: + + - ApiError: General API request failures (HTTP 4xx/5xx) + - AccountNotFound: Requested account doesn't exist + - ResourceNotFound: Requested resource not found in account + +Best Practices: + - Always call client.close() when done to clean up connections + - Use context managers or try/finally blocks for resource cleanup + - Configure appropriate timeouts for your use case + - Handle specific exceptions (AccountNotFound, ResourceNotFound) when expected + - Use BCS transactions for better performance and lower fees + - Implement retry logic for transient network errors + - Cache chain ID and other static values when making many requests + +Note: + All client operations are async and must be awaited. The clients use httpx + for HTTP/2 support and connection pooling for optimal performance. +""" + import asyncio import logging import time @@ -30,7 +179,59 @@ @dataclass class ClientConfig: - """Common configuration for clients, particularly for submitting transactions""" + """Configuration parameters for Aptos REST API clients. + + This class encapsulates common settings used by REST clients for transaction + submission, gas management, and network communication. These parameters affect + transaction costs, execution timeouts, and API authentication. + + Transaction Parameters: + expiration_ttl: Time-to-live for transactions in seconds (default: 600) + gas_unit_price: Price per unit of gas in octas (default: 100) + max_gas_amount: Maximum gas units allowed per transaction (default: 100,000) + transaction_wait_in_seconds: Timeout for transaction confirmation (default: 20) + + Network Parameters: + http2: Enable HTTP/2 for better performance (default: True) + api_key: Optional API key for authenticated requests (default: None) + + Examples: + Default configuration:: + + config = ClientConfig() + client = RestClient(node_url, config) + + High-throughput configuration:: + + config = ClientConfig( + gas_unit_price=150, # Higher gas price for faster processing + max_gas_amount=200_000, # Higher gas limit for complex transactions + transaction_wait_in_seconds=60, # Longer wait for busy networks + expiration_ttl=300 # Shorter expiration for high-frequency ops + ) + + Authenticated requests:: + + config = ClientConfig( + api_key="your-api-key-here", + http2=True # Recommended for API services + ) + + Conservative settings:: + + config = ClientConfig( + gas_unit_price=100, # Standard gas price + max_gas_amount=50_000, # Lower gas limit to prevent runaway + expiration_ttl=1200 # Longer expiration for manual workflows + ) + + Notes: + - Gas prices may need adjustment based on network congestion + - Higher gas limits allow more complex transactions but cost more + - HTTP/2 is recommended for better connection reuse and performance + - API keys are required for some premium or rate-limited services + - Transaction expiration prevents stale transactions from executing + """ expiration_ttl: int = 600 gas_unit_price: int = 100 @@ -41,11 +242,131 @@ class ClientConfig: class IndexerClient: - """A wrapper around the Aptos Indexer Service on Hasura""" + """GraphQL client for querying indexed Aptos blockchain data. + + This client provides access to the Aptos Indexer Service, which indexes + blockchain data into a PostgreSQL database exposed via Hasura GraphQL API. + The indexer provides rich querying capabilities for transactions, accounts, + events, and other blockchain data. + + Key Features: + - **Rich Queries**: Complex filtering, sorting, and aggregation of blockchain data + - **Real-time Data**: Access to up-to-date indexed blockchain information + - **Flexible API**: GraphQL interface supporting custom query structures + - **Authentication**: Optional bearer token authentication for premium access + - **High Performance**: Optimized database queries for fast data retrieval + + Use Cases: + - Analytics and reporting on blockchain activity + - Transaction history and account analysis + - Event monitoring and notification systems + - DeFi protocol data aggregation + - NFT marketplace data queries + - Portfolio tracking applications + + Attributes: + client: The underlying GraphQL client for executing queries + + Examples: + Basic setup and query:: + + indexer = IndexerClient( + "https://indexer.mainnet.aptoslabs.com/v1/graphql", + bearer_token="optional-auth-token" + ) + + # Query account transactions + query = """ + query GetAccountTransactions($address: String!, $limit: Int!) { + account_transactions( + where: {account_address: {_eq: $address}}, + limit: $limit, + order_by: {transaction_version: desc} + ) { + transaction_version + success + gas_used + transaction_timestamp + } + } + """ + + result = await indexer.query(query, { + "address": "0x1", + "limit": 100 + }) + + Token transfer queries:: + + query = """ + query GetTokenTransfers($token_address: String!) { + token_activities( + where: { + token_data_id: {_eq: $token_address}, + type: {_eq: "0x3::token_transfers::TokenTransferEvent"} + }, + limit: 50, + order_by: {transaction_version: desc} + ) { + from_address + to_address + amount + transaction_version + transaction_timestamp + } + } + """ + + transfers = await indexer.query(query, { + "token_address": "0xabc123..." + }) + + Account resource tracking:: + + query = """ + query GetAccountResources($address: String!) { + account_resources( + where: {account_address: {_eq: $address}} + ) { + resource_type + resource_data + write_set_change_index + transaction_version + } + } + """ + + resources = await indexer.query(query, {"address": address}) + + Note: + The indexer service may have query limits and rate limiting. Some + advanced features may require authentication tokens. Check the specific + indexer service documentation for available schema and limitations. + """ client: python_graphql_client.GraphqlClient def __init__(self, indexer_url: str, bearer_token: Optional[str] = None): + """Initialize the IndexerClient with connection parameters. + + Args: + indexer_url: The GraphQL endpoint URL for the Aptos indexer service. + bearer_token: Optional authentication token for premium access. + + Examples: + Public access:: + + client = IndexerClient( + "https://indexer.devnet.aptoslabs.com/v1/graphql" + ) + + Authenticated access:: + + client = IndexerClient( + "https://indexer.mainnet.aptoslabs.com/v1/graphql", + bearer_token="your-token-here" + ) + """ headers = {} if bearer_token: headers["Authorization"] = f"Bearer {bearer_token}" @@ -54,11 +375,230 @@ def __init__(self, indexer_url: str, bearer_token: Optional[str] = None): ) async def query(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]: + """Execute a GraphQL query against the Aptos Indexer. + + This async method executes GraphQL queries with variable substitution + and returns the structured response data. + + Args: + query: GraphQL query string with proper syntax and structure. + variables: Dictionary of variables to substitute in the query. + + Returns: + Dictionary containing the GraphQL response data and metadata. + + Raises: + Exception: On GraphQL syntax errors, network issues, or server errors. + + Examples: + Simple account query:: + + query = """ + query GetAccount($address: String!) { + account_transactions( + where: {account_address: {_eq: $address}} + limit: 10 + ) { + transaction_version + success + } + } + """ + + result = await indexer.query(query, {"address": "0x1"}) + transactions = result["data"]["account_transactions"] + + Complex aggregation query:: + + query = """ + query GetDailyStats($date: timestamptz!) { + transactions_aggregate( + where: { + inserted_at: {_gte: $date} + } + ) { + aggregate { + count + sum { + gas_used + } + } + } + } + """ + + stats = await indexer.query(query, { + "date": "2024-01-01T00:00:00Z" + }) + + Note: + The query must follow GraphQL syntax. Use the indexer's schema + documentation to understand available fields and relationships. + """ return await self.client.execute_async(query, variables) class RestClient: - """A wrapper around the Aptos-core Rest API""" + """Comprehensive async client for the Aptos blockchain REST API. + + This client provides complete access to Aptos full node functionality through + the REST API, supporting all blockchain operations including account management, + transaction submission, resource queries, event monitoring, and more. + + Core Capabilities: + - **Account Operations**: Balance queries, resource access, transaction history + - **Transaction Management**: Submission, simulation, status tracking, waiting + - **Blockchain Queries**: Block data, ledger info, event streams + - **Move Integration**: View functions, resource inspection, module access + - **Advanced Features**: Multi-agent transactions, BCS encoding, gas estimation + + Performance Features: + - **HTTP/2 Support**: Efficient connection reuse and multiplexing + - **Connection Pooling**: Optimized for high-throughput applications + - **Async/Await**: Non-blocking operations for better concurrency + - **Configurable Timeouts**: Flexible timeout management for different use cases + - **Automatic Retries**: Built-in resilience for transient network issues + + Transaction Types: + - Single-signature transactions (most common) + - Multi-signature transactions (shared accounts, DAOs) + - Script transactions (custom Move code execution) + - Entry function calls (smart contract interactions) + + Attributes: + _chain_id: Cached network chain ID (mainnet=1, testnet=2, etc.) + client: Underlying HTTP client with connection pooling + client_config: Configuration for gas, timeouts, and other parameters + base_url: Base URL of the Aptos full node REST API + + Examples: + Basic client setup:: + + from aptos_sdk.async_client import RestClient, ClientConfig + + # Use default configuration + client = RestClient("https://fullnode.mainnet.aptoslabs.com/v1") + + # Custom configuration for high-throughput apps + config = ClientConfig( + max_gas_amount=200_000, + gas_unit_price=150, + transaction_wait_in_seconds=60 + ) + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1", config) + + Account operations:: + + from aptos_sdk.account_address import AccountAddress + + address = AccountAddress.from_str("0x1") + + # Get account information + account_data = await client.account(address) + sequence_number = account_data["sequence_number"] + + # Check balance + balance = await client.account_balance(address) + print(f"Balance: {balance / 10**8} APT") + + # Get all resources + resources = await client.account_resources(address) + for resource in resources: + print(f"Resource: {resource['type']}") + + Transaction submission:: + + from aptos_sdk.account import Account + + # Create accounts + sender = Account.generate() + recipient = AccountAddress.from_str("0x456...") + + # Simple transfer + txn_hash = await client.bcs_transfer( + sender=sender, + recipient=recipient, + amount=100_000_000 # 1 APT in octas + ) + + # Wait for confirmation + await client.wait_for_transaction(txn_hash) + txn_data = await client.transaction_by_hash(txn_hash) + + if txn_data["success"]: + print(f"Transfer successful! Gas used: {txn_data['gas_used']}") + + Transaction simulation:: + + # Create transaction without submitting + raw_txn = await client.create_bcs_transaction( + sender=sender_account, + payload=transaction_payload + ) + + # Simulate to estimate gas + simulation = await client.simulate_transaction( + transaction=raw_txn, + sender=sender_account, + estimate_gas_usage=True + ) + + print(f"Estimated gas: {simulation[0]['gas_used']}") + print(f"Success: {simulation[0]['success']}") + + Multi-agent transactions:: + + # Transactions requiring multiple signatures + signed_txn = await client.create_multi_agent_bcs_transaction( + sender=primary_account, + secondary_accounts=[account2, account3], + payload=shared_transaction_payload + ) + + txn_hash = await client.submit_bcs_transaction(signed_txn) + + View function calls:: + + # Read-only function calls (no gas cost) + result = await client.view( + function="0x1::coin::balance", + type_arguments=["0x1::aptos_coin::AptosCoin"], + arguments=[str(address)] + ) + balance = int(result[0]) + + Event monitoring:: + + # Get events by creation number + events = await client.event_by_creation_number( + account_address=contract_address, + creation_number=0, # First event stream + limit=100 + ) + + for event in events: + print(f"Event: {event['type']}, Data: {event['data']}") + + Error Handling: + The client raises specific exceptions for different failure modes: + - ApiError: HTTP errors (4xx, 5xx status codes) + - AccountNotFound: Account doesn't exist on-chain + - ResourceNotFound: Requested resource not found in account + + Best Practices: + - Always call await client.close() when done + - Use try/finally or async context managers for cleanup + - Cache chain_id() result for better performance + - Configure appropriate gas limits for your transactions + - Implement exponential backoff for retries on failures + - Use BCS transactions for better performance and fees + - Monitor gas usage and adjust pricing as needed + + Note: + This client is designed for production use with proper connection + management, timeout handling, and error recovery. It supports both + mainnet and testnet environments. + """ _chain_id: Optional[int] client: httpx.AsyncClient @@ -66,6 +606,40 @@ class RestClient: base_url: str def __init__(self, base_url: str, client_config: ClientConfig = ClientConfig()): + """Initialize the REST client with configuration parameters. + + Args: + base_url: Base URL of the Aptos full node REST API. + Examples: "https://fullnode.mainnet.aptoslabs.com/v1", + "https://fullnode.devnet.aptoslabs.com/v1" + client_config: Configuration for gas, timeouts, and networking. + Defaults to standard settings if not provided. + + Examples: + Mainnet client:: + + client = RestClient("https://fullnode.mainnet.aptoslabs.com/v1") + + Testnet with custom config:: + + config = ClientConfig( + gas_unit_price=200, + max_gas_amount=150_000, + api_key="your-api-key" + ) + client = RestClient( + "https://fullnode.testnet.aptoslabs.com/v1", + config + ) + + Local development node:: + + client = RestClient("http://localhost:8080/v1") + + Note: + The client automatically configures HTTP/2, connection pooling, + proper headers, and timeouts for optimal performance. + """ self.base_url = base_url # Default limits limits = httpx.Limits() @@ -86,9 +660,23 @@ def __init__(self, base_url: str, client_config: ClientConfig = ClientConfig()): self.client.headers["Authorization"] = f"Bearer {client_config.api_key}" async def close(self): + """ + Close the underlying HTTP client connection. + + This is a coroutine that should be called when done with the client + to properly clean up resources. + """ await self.client.aclose() async def chain_id(self): + """ + Get the chain ID of the network. + + This is a coroutine that fetches and caches the chain ID from the node. + + :return: The numeric chain ID (e.g., 1 for mainnet, 2 for testnet) + :raises ApiError: If the node info request fails + """ if not self._chain_id: info = await self.info() self._chain_id = int(info["chain_id"]) @@ -395,6 +983,15 @@ async def events_by_event_handle( return response.json() async def current_timestamp(self) -> float: + """ + Get the current ledger timestamp in seconds. + + This is a coroutine that fetches the latest ledger info and + converts the timestamp from microseconds to seconds. + + :return: Current ledger timestamp as a float in seconds + :raises ApiError: If the node info request fails + """ info = await self.info() return float(info["ledger_timestamp"]) / 1_000_000 @@ -406,6 +1003,20 @@ async def get_table_item( key: Any, ledger_version: Optional[int] = None, ) -> Any: + """ + Retrieve an item from a Move table by its key. + + This is a coroutine that queries a table item using the table handle + and key information. + + :param handle: The table handle identifying the table + :param key_type: The Move type of the key (e.g., "address", "u64") + :param value_type: The Move type of the value (e.g., "u128", "vector") + :param key: The key value to look up + :param ledger_version: Ledger version to query. If not provided, uses the latest version + :return: The value stored at the given key in the table + :raises ApiError: If the request fails or the key is not found + """ response = await self._post( endpoint=f"tables/{handle}/item", data={ @@ -425,6 +1036,18 @@ async def aggregator_value( resource_type: str, aggregator_path: List[str], ) -> int: + """ + Retrieve the current value of an aggregator. + + This is a coroutine that follows a path through a resource to find + an aggregator and returns its current value. + + :param account_address: Address of the account containing the resource + :param resource_type: The Move type of the resource containing the aggregator + :param aggregator_path: Path through the resource structure to the aggregator + :return: Current value of the aggregator as an integer + :raises ApiError: If the resource is not found or the aggregator path is invalid + """ source = await self.account_resource(account_address, resource_type) source_data = data = source["data"] @@ -464,6 +1087,15 @@ async def aggregator_value( # async def info(self) -> Dict[str, str]: + """ + Get information about the Aptos node. + + This is a coroutine that retrieves general information about the node + including chain ID, ledger version, and timestamps. + + :return: Dictionary containing node information + :raises ApiError: If the request fails + """ response = await self.client.get(self.base_url) if response.status_code >= 400: raise ApiError(response.text, response.status_code) @@ -478,6 +1110,17 @@ async def simulate_bcs_transaction( signed_transaction: SignedTransaction, estimate_gas_usage: bool = False, ) -> Dict[str, Any]: + """ + Simulate a BCS-encoded signed transaction without executing it. + + This is a coroutine that submits a transaction for simulation to estimate + gas usage and validate execution without making on-chain changes. + + :param signed_transaction: The signed transaction to simulate + :param estimate_gas_usage: If True, estimate gas unit price and max gas amount + :return: Simulation result containing execution information + :raises ApiError: If the simulation request fails + """ headers = {"Content-Type": "application/x.aptos.signed_transaction+bcs"} params = {} if estimate_gas_usage: @@ -503,6 +1146,18 @@ async def simulate_transaction( sender: Account, estimate_gas_usage: bool = False, ) -> Dict[str, Any]: + """ + Simulate a raw transaction without executing it on-chain. + + This is a coroutine that signs a transaction with a simulated signature + (all zeros) and submits it for simulation. + + :param transaction: The raw transaction to simulate + :param sender: The account that would send the transaction + :param estimate_gas_usage: If True, estimate gas unit price and max gas amount + :return: Simulation result containing execution information + :raises ApiError: If the simulation request fails + """ # Note that simulated transactions are not signed and have all 0 signatures! authenticator = sender.sign_simulated_transaction(transaction) return await self.simulate_bcs_transaction( @@ -513,6 +1168,16 @@ async def simulate_transaction( async def submit_bcs_transaction( self, signed_transaction: SignedTransaction ) -> str: + """ + Submit a BCS-encoded signed transaction to the blockchain. + + This is a coroutine that submits a transaction for execution. + The transaction will be added to the mempool and eventually executed. + + :param signed_transaction: The signed transaction to submit + :return: The transaction hash as a hex string + :raises ApiError: If the submission fails + """ headers = {"Content-Type": "application/x.aptos.signed_transaction+bcs"} response = await self.client.post( f"{self.base_url}/transactions", @@ -526,11 +1191,32 @@ async def submit_bcs_transaction( async def submit_and_wait_for_bcs_transaction( self, signed_transaction: SignedTransaction ) -> Dict[str, Any]: + """ + Submit a BCS-encoded signed transaction and wait for it to complete. + + This is a coroutine that submits a transaction and polls until it's + no longer pending, then returns the transaction details. + + :param signed_transaction: The signed transaction to submit + :return: The completed transaction details + :raises ApiError: If submission fails or transaction times out + :raises AssertionError: If transaction fails or times out + """ 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 transaction_pending(self, txn_hash: str) -> bool: + """ + Check if a transaction is still pending. + + This is a coroutine that queries the transaction status to determine + if it's still pending execution. + + :param txn_hash: The transaction hash to check + :return: True if the transaction is still pending, False otherwise + :raises ApiError: If the status check request fails + """ 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 if response.status_code == 404: @@ -576,12 +1262,32 @@ async def account_transaction_sequence_number_status( return len(data) == 1 and data[0]["type"] != "pending_transaction" async def transaction_by_hash(self, txn_hash: str) -> Dict[str, Any]: + """ + Retrieve a transaction by its hash. + + This is a coroutine that fetches transaction details using the + transaction hash. + + :param txn_hash: The transaction hash to look up + :return: Transaction details as a dictionary + :raises ApiError: If the transaction is not found or request fails + """ response = await self._get(endpoint=f"transactions/by_hash/{txn_hash}") if response.status_code >= 400: raise ApiError(response.text, response.status_code) return response.json() async def transaction_by_version(self, version: int) -> Dict[str, Any]: + """ + Retrieve a transaction by its ledger version. + + This is a coroutine that fetches transaction details using the + ledger version number. + + :param version: The ledger version of the transaction to retrieve + :return: Transaction details as a dictionary + :raises ApiError: If the transaction is not found or request fails + """ response = await self._get(endpoint=f"transactions/by_version/{version}") if response.status_code >= 400: raise ApiError(response.text, response.status_code) @@ -655,6 +1361,18 @@ async def create_multi_agent_bcs_transaction( secondary_accounts: List[Account], payload: TransactionPayload, ) -> SignedTransaction: + """ + Create a multi-agent BCS transaction with multiple signers. + + This is a coroutine that creates a transaction requiring signatures + from multiple accounts (sender and secondary accounts). + + :param sender: The primary account sending the transaction + :param secondary_accounts: Additional accounts that must sign the transaction + :param payload: The transaction payload to execute + :return: A signed multi-agent transaction + :raises ApiError: If account sequence number lookup fails + """ raw_transaction = MultiAgentRawTransaction( RawTransaction( sender.address(), @@ -689,6 +1407,18 @@ async def create_bcs_transaction( payload: TransactionPayload, sequence_number: Optional[int] = None, ) -> RawTransaction: + """ + Create a raw BCS transaction ready for signing. + + This is a coroutine that builds a raw transaction with the specified + payload and transaction parameters. + + :param sender: The sender account or address + :param payload: The transaction payload to execute + :param sequence_number: Specific sequence number, or None to fetch from chain + :return: An unsigned raw transaction + :raises ApiError: If sequence number lookup or chain ID fetch fails + """ if isinstance(sender, Account): sender_address = sender.address() else: @@ -715,6 +1445,18 @@ async def create_bcs_signed_transaction( payload: TransactionPayload, sequence_number: Optional[int] = None, ) -> SignedTransaction: + """ + Create and sign a BCS transaction ready for submission. + + This is a coroutine that creates a raw transaction and signs it + with the sender's private key. + + :param sender: The account that will send and sign the transaction + :param payload: The transaction payload to execute + :param sequence_number: Specific sequence number, or None to fetch from chain + :return: A fully signed transaction ready for submission + :raises ApiError: If sequence number lookup or chain ID fetch fails + """ raw_transaction = await self.create_bcs_transaction( sender, payload, sequence_number ) @@ -733,13 +1475,25 @@ async def bcs_transfer( amount: int, sequence_number: Optional[int] = None, ) -> str: + """ + Transfer Aptos coins from sender to recipient. + + This is a coroutine that creates, signs, and submits a transfer transaction. + + :param sender: The account sending the coins + :param recipient: Address of the account to receive the coins + :param amount: Amount of coins to transfer in octas (1 APT = 10^8 octas) + :param sequence_number: Specific sequence number, or None to fetch from chain + :return: The transaction hash as a hex string + :raises ApiError: If transaction creation or submission fails + """ transaction_arguments = [ TransactionArgument(recipient, Serializer.struct), TransactionArgument(amount, Serializer.u64), ] payload = EntryFunction.natural( - "0x1::aptos_account", + "***::aptos_account", "transfer", [], transaction_arguments, @@ -758,13 +1512,27 @@ async def transfer_coins( amount: int, sequence_number: Optional[int] = None, ) -> str: + """ + Transfer coins of a specific type from sender to recipient. + + This is a coroutine that creates, signs, and submits a transfer transaction + for any coin type (not just Aptos coins). + + :param sender: The account sending the coins + :param recipient: Address of the account to receive the coins + :param coin_type: The fully qualified coin type (e.g., "***::usdc::USDC") + :param amount: Amount of coins to transfer (in the coin's base units) + :param sequence_number: Specific sequence number, or None to fetch from chain + :return: The transaction hash as a hex string + :raises ApiError: If transaction creation or submission fails + """ transaction_arguments = [ TransactionArgument(recipient, Serializer.struct), TransactionArgument(amount, Serializer.u64), ] payload = EntryFunction.natural( - "0x1::aptos_account", + "***::aptos_account", "transfer_coins", [TypeTag(StructTag.from_str(coin_type))], transaction_arguments, @@ -778,13 +1546,25 @@ async def transfer_coins( async def transfer_object( self, owner: Account, object: AccountAddress, to: AccountAddress ) -> str: + """ + Transfer ownership of an object to another account. + + This is a coroutine that creates, signs, and submits a transaction to + transfer ownership of an Aptos object. + + :param owner: The current owner of the object + :param object: The address of the object to transfer + :param to: The address of the new owner + :return: The transaction hash as a hex string + :raises ApiError: If transaction creation or submission fails + """ transaction_arguments = [ TransactionArgument(object, Serializer.struct), TransactionArgument(to, Serializer.struct), ] payload = EntryFunction.natural( - "0x1::object", + "***::object", "transfer_call", [], transaction_arguments, @@ -919,6 +1699,12 @@ def __init__( self.headers["Authorization"] = f"Bearer {auth_token}" async def close(self): + """ + Close the underlying REST client connection. + + This is a coroutine that should be called when done with the faucet client + to properly clean up resources. + """ await self.rest_client.close() async def fund_account( @@ -936,6 +1722,13 @@ async def fund_account( return txn_hash async def healthy(self) -> bool: + """ + Check if the faucet service is healthy and responding. + + This is a coroutine that performs a health check on the faucet service. + + :return: True if the faucet is healthy, False otherwise + """ response = await self.rest_client.client.get(self.base_url) return "tap:ok" == response.text diff --git a/aptos_sdk/authenticator.py b/aptos_sdk/authenticator.py index 4922063..4f56946 100644 --- a/aptos_sdk/authenticator.py +++ b/aptos_sdk/authenticator.py @@ -1,6 +1,148 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Transaction authentication infrastructure for the Aptos blockchain. + +This module provides the authentication framework that validates transaction signatures +and ensures proper authorization for blockchain operations. It supports multiple signature +schemes and complex transaction types including multi-agent and fee payer scenarios. + +Authentication Flow: + 1. **Transaction Creation**: Transactions are built with specific payloads + 2. **Signing**: Accounts sign the transaction hash using their private keys + 3. **Authenticator Packaging**: Signatures are wrapped in appropriate authenticators + 4. **Verification**: The blockchain validates signatures against stored authentication keys + 5. **Execution**: Validated transactions are processed on-chain + +Supported Authentication Types: +- **Single Signature**: Ed25519 and other single-key schemes +- **Multi-Signature**: Threshold-based multi-signature authentication +- **Multi-Agent**: Transactions requiring multiple distinct signers +- **Fee Payer**: Transactions with sponsored gas fees +- **Single Sender**: Modern unified single signature format + +Key Features: +- **Algorithm Flexibility**: Support for Ed25519, secp256k1, and other schemes +- **Multi-Party Transactions**: Complex transaction patterns with multiple signers +- **Threshold Security**: N-of-M multi-signature requirements +- **Fee Sponsorship**: Gas fee delegation to third parties +- **Backward Compatibility**: Support for legacy authentication formats +- **BCS Serialization**: Efficient binary encoding for blockchain storage + +Authentication Hierarchy: + TransactionAuthenticator (top level) + │ + ├── SingleSenderAuthenticator (modern single signature) + │ + ├── MultiAgentAuthenticator (multiple distinct signers) + │ └── Contains: sender + list of AccountAuthenticators + │ + └── FeePayerAuthenticator (fee sponsorship) + └── Contains: sender + fee payer + list of AccountAuthenticators + + AccountAuthenticator (account-level) + │ + ├── Ed25519Authenticator (legacy single Ed25519) + ├── MultiEd25519Authenticator (legacy multi-Ed25519) + ├── SingleKeyAuthenticator (modern single key) + └── MultiKeyAuthenticator (modern multi-key) + +Examples: + Basic single signature transaction:: + + from aptos_sdk.authenticator import Ed25519Authenticator, Authenticator + from aptos_sdk import ed25519 + + # Create Ed25519 authenticator + private_key = ed25519.PrivateKey.random() + public_key = private_key.public_key() + + # Sign transaction hash + tx_hash = b"transaction_hash_bytes" + signature = private_key.sign(tx_hash) + + # Create authenticator + account_auth = Ed25519Authenticator(public_key, signature) + tx_auth = Authenticator(account_auth) + + # Verify signature + is_valid = tx_auth.verify(tx_hash) + + Multi-signature authentication:: + + from aptos_sdk.authenticator import MultiEd25519Authenticator + from aptos_sdk import ed25519 + + # Create 2-of-3 multisig + private_keys = [ed25519.PrivateKey.random() for _ in range(3)] + public_keys = [pk.public_key() for pk in private_keys] + multi_pub_key = ed25519.MultiPublicKey(public_keys, threshold=2) + + # Sign with 2 keys (indices 0 and 2) + signatures = [ + (0, private_keys[0].sign(tx_hash)), + (2, private_keys[2].sign(tx_hash)) + ] + multi_signature = ed25519.MultiSignature(signatures) + + # Create multi-signature authenticator + multi_auth = MultiEd25519Authenticator(multi_pub_key, multi_signature) + tx_auth = Authenticator(multi_auth) + + Multi-agent transaction:: + + # Transaction requiring multiple distinct signers + sender_auth = Ed25519Authenticator(sender_public_key, sender_signature) + agent1_auth = Ed25519Authenticator(agent1_public_key, agent1_signature) + agent2_auth = Ed25519Authenticator(agent2_public_key, agent2_signature) + + # Create multi-agent authenticator + multi_agent_auth = MultiAgentAuthenticator( + sender=sender_auth, + secondary_signers=[agent1_auth, agent2_auth] + ) + tx_auth = Authenticator(multi_agent_auth) + + Fee payer transaction:: + + # Transaction where someone else pays the gas fees + sender_auth = Ed25519Authenticator(sender_public_key, sender_signature) + fee_payer_auth = Ed25519Authenticator(fee_payer_public_key, fee_payer_signature) + + fee_payer_tx_auth = FeePayerAuthenticator( + sender=sender_auth, + secondary_signers=[], + fee_payer=fee_payer_auth + ) + tx_auth = Authenticator(fee_payer_tx_auth) + +Security Considerations: +- **Signature Verification**: All signatures must be cryptographically valid +- **Key Authorization**: Public keys must match stored authentication keys +- **Replay Protection**: Transaction hashes include sequence numbers and timestamps +- **Multi-Signature Thresholds**: Ensure sufficient signatures meet threshold requirements +- **Agent Authorization**: Verify all required parties have signed multi-agent transactions + +Gas and Performance: +- Single signatures: ~20-50 gas units for verification +- Multi-signatures: ~50-200 gas units depending on threshold and key count +- Multi-agent: Additional gas per secondary signer +- Fee payer: Overhead for fee delegation logic + +Best Practices: +- Use Ed25519 for new applications (faster, smaller signatures) +- Implement proper key rotation for long-term security +- Set reasonable multi-signature thresholds (not too high) +- Validate all signatures before transaction submission +- Use appropriate authenticator types for each use case + +See Also: + - ed25519: Ed25519 cryptographic primitives + - asymmetric_crypto_wrapper: Unified cryptographic interfaces + - transactions: Transaction construction and management +""" + from __future__ import annotations import typing @@ -13,11 +155,58 @@ class Authenticator: - """ - Each transaction submitted to the Aptos blockchain contains a `TransactionAuthenticator`. - During transaction execution, the executor will check if every `AccountAuthenticator`'s - signature on the transaction hash is well-formed and whether `AccountAuthenticator`'s matches - the `AuthenticationKey` stored under the participating signer's account address. + """Top-level transaction authenticator for the Aptos blockchain. + + Each transaction submitted to the Aptos blockchain contains a TransactionAuthenticator + that proves the transaction was authorized by the appropriate accounts. During + transaction execution, the executor validates that every signature is well-formed + and matches the AuthenticationKey stored under each participating account. + + The Authenticator class serves as a wrapper that can contain different types of + authentication schemes, from simple single signatures to complex multi-party + transactions with fee delegation. + + Supported Authentication Types: + ED25519 (0): Legacy single Ed25519 signature + MULTI_ED25519 (1): Legacy multi-Ed25519 signatures + MULTI_AGENT (2): Multiple distinct signers for complex transactions + FEE_PAYER (3): Transactions with fee sponsorship + SINGLE_SENDER (4): Modern unified single signature format + + Attributes: + variant (int): Integer identifier for the authentication type + authenticator (typing.Any): The underlying concrete authenticator implementation + + Examples: + Simple single signature:: + + private_key = ed25519.PrivateKey.random() + signature = private_key.sign(transaction_hash) + + ed25519_auth = Ed25519Authenticator(private_key.public_key(), signature) + tx_auth = Authenticator(ed25519_auth) + + Multi-agent transaction:: + + sender_auth = Ed25519Authenticator(sender_key.public_key(), sender_sig) + agent_auth = Ed25519Authenticator(agent_key.public_key(), agent_sig) + + multi_agent = MultiAgentAuthenticator(sender_auth, [agent_auth]) + tx_auth = Authenticator(multi_agent) + + Serialization and verification:: + + # Serialize for blockchain submission + serializer = Serializer() + tx_auth.serialize(serializer) + auth_bytes = serializer.output() + + # Verify signatures + is_valid = tx_auth.verify(transaction_hash) + + Note: + The authenticator type is automatically determined from the wrapped + authenticator implementation and cannot be changed after construction. """ ED25519: int = 0 @@ -30,6 +219,12 @@ class Authenticator: authenticator: typing.Any def __init__(self, authenticator: typing.Any): + """ + Initialize an Authenticator with the appropriate variant. + + :param authenticator: The specific authenticator implementation + :raises Exception: If authenticator type is not recognized + """ if isinstance(authenticator, Ed25519Authenticator): self.variant = Authenticator.ED25519 elif isinstance(authenticator, MultiEd25519Authenticator): @@ -45,6 +240,13 @@ def __init__(self, authenticator: typing.Any): self.authenticator = authenticator def from_key(key: asymmetric_crypto.PublicKey) -> int: + """ + Determine the appropriate authenticator variant for a given public key type. + + :param key: The public key to determine the variant for + :return: The authenticator variant constant + :raises NotImplementedError: If key type is not supported + """ if isinstance(key, ed25519.PublicKey): return Authenticator.ED25519 elif isinstance(key, ed25519.MultiPublicKey): @@ -63,6 +265,12 @@ def __str__(self) -> str: return self.authenticator.__str__() def verify(self, data: bytes) -> bool: + """ + Verify the signature against the provided data. + + :param data: The data that was signed + :return: True if the signature is valid, False otherwise + """ return self.authenticator.verify(data) @staticmethod @@ -85,11 +293,23 @@ def deserialize(deserializer: Deserializer) -> Authenticator: return Authenticator(authenticator) def serialize(self, serializer: Serializer): + """ + Serialize this authenticator using BCS serialization. + + :param serializer: The BCS serializer to use + """ serializer.uleb128(self.variant) serializer.struct(self.authenticator) class AccountAuthenticator: + """ + An authenticator for a single account signature. + + This wraps different types of signature schemes that can be used + to authenticate an account's authorization of a transaction. + """ + ED25519: int = 0 MULTI_ED25519: int = 1 SINGLE_KEY: int = 2 @@ -99,6 +319,12 @@ class AccountAuthenticator: authenticator: typing.Any def __init__(self, authenticator: typing.Any): + """ + Initialize an AccountAuthenticator with the appropriate variant. + + :param authenticator: The specific authenticator implementation + :raises Exception: If authenticator type is not recognized + """ if isinstance(authenticator, Ed25519Authenticator): self.variant = AccountAuthenticator.ED25519 elif isinstance(authenticator, MultiEd25519Authenticator): @@ -125,6 +351,12 @@ def __str__(self) -> str: return self.authenticator.__str__() def verify(self, data: bytes) -> bool: + """ + Verify the signature against the provided data. + + :param data: The data that was signed + :return: True if the signature is valid, False otherwise + """ return self.authenticator.verify(data) @staticmethod @@ -145,15 +377,32 @@ def deserialize(deserializer: Deserializer) -> AccountAuthenticator: return AccountAuthenticator(authenticator) def serialize(self, serializer: Serializer): + """ + Serialize this account authenticator using BCS serialization. + + :param serializer: The BCS serializer to use + """ serializer.uleb128(self.variant) serializer.struct(self.authenticator) class Ed25519Authenticator: + """ + An authenticator that uses Ed25519 signature scheme. + + This is the most common signature scheme used in Aptos. + """ + public_key: ed25519.PublicKey signature: ed25519.Signature def __init__(self, public_key: ed25519.PublicKey, signature: ed25519.Signature): + """ + Initialize an Ed25519 authenticator. + + :param public_key: The Ed25519 public key + :param signature: The Ed25519 signature + """ self.public_key = public_key self.signature = signature @@ -167,6 +416,12 @@ def __str__(self) -> str: return f"PublicKey: {self.public_key}, Signature: {self.signature}" def verify(self, data: bytes) -> bool: + """ + Verify the Ed25519 signature against the provided data. + + :param data: The data that was signed + :return: True if the signature is valid, False otherwise + """ return self.public_key.verify(data, self.signature) @staticmethod @@ -176,11 +431,23 @@ def deserialize(deserializer: Deserializer) -> Ed25519Authenticator: return Ed25519Authenticator(key, signature) def serialize(self, serializer: Serializer): + """ + Serialize this Ed25519 authenticator using BCS serialization. + + :param serializer: The BCS serializer to use + """ serializer.struct(self.public_key) serializer.struct(self.signature) class FeePayerAuthenticator: + """ + An authenticator for fee-payer transactions. + + This allows a different account to pay the transaction fees + while still requiring signatures from all participants. + """ + sender: AccountAuthenticator secondary_signers: List[typing.Tuple[AccountAddress, AccountAuthenticator]] fee_payer: typing.Tuple[AccountAddress, AccountAuthenticator] @@ -191,6 +458,13 @@ def __init__( secondary_signers: List[typing.Tuple[AccountAddress, AccountAuthenticator]], fee_payer: typing.Tuple[AccountAddress, AccountAuthenticator], ): + """ + Initialize a fee payer authenticator. + + :param sender: The sender's authenticator + :param secondary_signers: List of (address, authenticator) pairs for secondary signers + :param fee_payer: Tuple of (address, authenticator) for the fee payer + """ self.sender = sender self.secondary_signers = secondary_signers self.fee_payer = fee_payer @@ -208,12 +482,28 @@ def __str__(self) -> str: return f"FeePayer: \n\tSender: {self.sender}\n\tSecondary Signers: {self.secondary_signers}\n\t{self.fee_payer}" def fee_payer_address(self) -> AccountAddress: + """ + Get the address of the fee payer. + + :return: The fee payer's account address + """ return self.fee_payer[0] def secondary_addresses(self) -> List[AccountAddress]: + """ + Get the addresses of all secondary signers. + + :return: List of secondary signer addresses + """ return [x[0] for x in self.secondary_signers] def verify(self, data: bytes) -> bool: + """ + Verify all signatures against the provided data. + + :param data: The data that was signed + :return: True if all signatures are valid, False otherwise + """ if not self.sender.verify(data): return False if not self.fee_payer[1].verify(data): @@ -236,6 +526,11 @@ def deserialize(deserializer: Deserializer) -> FeePayerAuthenticator: ) def serialize(self, serializer: Serializer): + """ + Serialize this fee payer authenticator using BCS serialization. + + :param serializer: The BCS serializer to use + """ serializer.struct(self.sender) serializer.sequence([x[0] for x in self.secondary_signers], Serializer.struct) serializer.sequence([x[1] for x in self.secondary_signers], Serializer.struct) @@ -244,6 +539,12 @@ def serialize(self, serializer: Serializer): class MultiAgentAuthenticator: + """ + An authenticator for multi-agent transactions. + + This requires signatures from multiple accounts to authorize a transaction. + """ + sender: AccountAuthenticator secondary_signers: List[typing.Tuple[AccountAddress, AccountAuthenticator]] @@ -252,6 +553,12 @@ def __init__( sender: AccountAuthenticator, secondary_signers: List[typing.Tuple[AccountAddress, AccountAuthenticator]], ): + """ + Initialize a multi-agent authenticator. + + :param sender: The sender's authenticator + :param secondary_signers: List of (address, authenticator) pairs for secondary signers + """ self.sender = sender self.secondary_signers = secondary_signers @@ -264,9 +571,20 @@ def __eq__(self, other: object) -> bool: ) def secondary_addresses(self) -> List[AccountAddress]: + """ + Get the addresses of all secondary signers. + + :return: List of secondary signer addresses + """ return [x[0] for x in self.secondary_signers] def verify(self, data: bytes) -> bool: + """ + Verify all signatures against the provided data. + + :param data: The data that was signed + :return: True if all signatures are valid, False otherwise + """ if not self.sender.verify(data): return False return all([x[1].verify(data) for x in self.secondary_signers]) @@ -283,58 +601,393 @@ def deserialize(deserializer: Deserializer) -> MultiAgentAuthenticator: ) def serialize(self, serializer: Serializer): + """ + Serialize this multi-agent authenticator using BCS serialization. + + :param serializer: The BCS serializer to use + """ serializer.struct(self.sender) serializer.sequence([x[0] for x in self.secondary_signers], Serializer.struct) serializer.sequence([x[1] for x in self.secondary_signers], Serializer.struct) class MultiEd25519Authenticator: + """An authenticator that uses multi-signature Ed25519 scheme. + + This authenticator supports threshold signatures using multiple Ed25519 keys, + requiring a minimum number of signatures (threshold) from a set of public keys + to authorize a transaction. This is useful for shared accounts, multi-party + custody, and governance scenarios. + + Features: + - N-of-M threshold signatures (e.g., 2-of-3, 3-of-5) + - Efficient Ed25519 cryptography + - Legacy support for older multi-signature formats + - BCS serialization compatibility + + Security Properties: + - Requires threshold number of valid signatures + - Each signature must be from a different key in the set + - Provides non-repudiation and authenticity + - Resistant to single key compromise + + Examples: + Create a 2-of-3 multi-signature:: + + import ed25519 + + # Generate 3 key pairs + private_keys = [ed25519.PrivateKey.random() for _ in range(3)] + public_keys = [pk.public_key() for pk in private_keys] + + # Create multi-public key with threshold 2 + multi_pub_key = ed25519.MultiPublicKey(public_keys, threshold=2) + + # Sign with keys 0 and 2 (meeting threshold) + tx_hash = b"transaction_hash" + signatures = [ + (0, private_keys[0].sign(tx_hash)), + (2, private_keys[2].sign(tx_hash)) + ] + multi_signature = ed25519.MultiSignature(signatures) + + # Create authenticator + auth = MultiEd25519Authenticator(multi_pub_key, multi_signature) + + Verify the multi-signature:: + + is_valid = auth.verify(tx_hash) # Should return True + + Attributes: + public_key (ed25519.MultiPublicKey): The multi-public key containing all keys and threshold + signature (ed25519.MultiSignature): The multi-signature containing threshold signatures + + Note: + This is a legacy format. New applications should consider using + MultiKeyAuthenticator for better algorithm flexibility. + """ + public_key: ed25519.MultiPublicKey signature: ed25519.MultiSignature - def __init__(self, public_key, signature): + def __init__(self, public_key: ed25519.MultiPublicKey, signature: ed25519.MultiSignature): + """Initialize a multi-Ed25519 authenticator. + + Args: + public_key: The multi-public key containing all keys and threshold requirements + signature: The multi-signature with the required threshold signatures + + Examples: + Basic initialization:: + + multi_pub_key = ed25519.MultiPublicKey([pk1, pk2, pk3], threshold=2) + multi_sig = ed25519.MultiSignature([(0, sig1), (2, sig3)]) + auth = MultiEd25519Authenticator(multi_pub_key, multi_sig) + """ self.public_key = public_key self.signature = signature + + def __eq__(self, other: object) -> bool: + """Check equality with another MultiEd25519Authenticator. + + Args: + other: Object to compare with + + Returns: + True if public keys and signatures are equal, False otherwise + """ + if not isinstance(other, MultiEd25519Authenticator): + return NotImplemented + return self.public_key == other.public_key and self.signature == other.signature + + def __str__(self) -> str: + """String representation of the multi-Ed25519 authenticator. + + Returns: + Human-readable string showing public key and signature details + """ + return f"MultiPublicKey: {self.public_key}, MultiSignature: {self.signature}" def verify(self, data: bytes) -> bool: - raise NotImplementedError + """Verify the multi-signature against the provided data. + + This method validates that: + 1. The threshold number of signatures is provided + 2. Each signature is from a different key in the multi-public key + 3. Each signature is cryptographically valid + + Args: + data: The data that was signed (typically a transaction hash) + + Returns: + True if the multi-signature is valid, False otherwise + + Note: + This method is currently not implemented in the base class. + Implementations should delegate to the underlying ed25519.MultiPublicKey.verify method. + """ + return self.public_key.verify(data, self.signature) @staticmethod def deserialize(deserializer: Deserializer) -> MultiEd25519Authenticator: - raise NotImplementedError + """Deserialize a MultiEd25519Authenticator from BCS bytes. + + Args: + deserializer: The BCS deserializer containing the authenticator data + + Returns: + A MultiEd25519Authenticator instance + + Raises: + DeserializationError: If the data is malformed or incomplete + """ + public_key = deserializer.struct(ed25519.MultiPublicKey) + signature = deserializer.struct(ed25519.MultiSignature) + return MultiEd25519Authenticator(public_key, signature) def serialize(self, serializer: Serializer): + """Serialize this multi-Ed25519 authenticator using BCS serialization. + + This serializes both the multi-public key (including all public keys + and the threshold) and the multi-signature (including signature indices + and the actual signature bytes). + + Args: + serializer: The BCS serializer to write to + """ serializer.struct(self.public_key) serializer.struct(self.signature) class SingleSenderAuthenticator: + """Modern unified single signature authenticator for the Aptos blockchain. + + This is the preferred authenticator format for simple single-signature transactions + in newer versions of Aptos. It provides a clean, unified interface that can wrap + different types of single-key authentication schemes. + + The SingleSenderAuthenticator is part of the Transaction Authenticator V2 format + and is designed to be more extensible and consistent than the legacy authenticator + formats. + + Features: + - Modern unified interface for single signatures + - Supports multiple signature algorithms through AccountAuthenticator + - Consistent with Transaction Authenticator V2 specification + - Efficient serialization and verification + - Forward compatibility with future signature schemes + + Use Cases: + - Standard single-account transactions + - Modern applications preferring the unified format + - Systems requiring forward compatibility + - Clean integration with newer Aptos features + + Examples: + Create with Ed25519 signature:: + + from aptos_sdk import ed25519 + from aptos_sdk.authenticator import ( + Ed25519Authenticator, + AccountAuthenticator, + SingleSenderAuthenticator, + Authenticator + ) + + # Generate key and sign + private_key = ed25519.PrivateKey.random() + public_key = private_key.public_key() + signature = private_key.sign(transaction_hash) + + # Create authenticator chain + ed25519_auth = Ed25519Authenticator(public_key, signature) + account_auth = AccountAuthenticator(ed25519_auth) + single_sender = SingleSenderAuthenticator(account_auth) + tx_auth = Authenticator(single_sender) + + Create with modern single key:: + + from aptos_sdk.authenticator import SingleKeyAuthenticator + + # Using the modern single key format + single_key_auth = SingleKeyAuthenticator(public_key, signature) + account_auth = AccountAuthenticator(single_key_auth) + single_sender = SingleSenderAuthenticator(account_auth) + + Verification:: + + # Verify the signature + is_valid = single_sender.verify(transaction_hash) + print(f"Signature valid: {is_valid}") + + Attributes: + sender (AccountAuthenticator): The account authenticator for the sender + + Note: + While this format is more modern, existing applications using Ed25519Authenticator + directly can continue to work. This format provides better extensibility for + future signature scheme additions. + """ + sender: AccountAuthenticator def __init__( self, sender: AccountAuthenticator, ): + """Initialize a single sender authenticator. + + Args: + sender: The account authenticator for the transaction sender + + Examples: + Basic initialization:: + + ed25519_auth = Ed25519Authenticator(public_key, signature) + account_auth = AccountAuthenticator(ed25519_auth) + single_sender = SingleSenderAuthenticator(account_auth) + """ self.sender = sender def __eq__(self, other: object) -> bool: + """Check equality with another SingleSenderAuthenticator. + + Args: + other: Object to compare with + + Returns: + True if sender authenticators are equal, False otherwise + """ if not isinstance(other, SingleSenderAuthenticator): return NotImplemented return self.sender == other.sender + + def __str__(self) -> str: + """String representation of the single sender authenticator. + + Returns: + Human-readable string showing sender details + """ + return f"SingleSender: {self.sender}" def verify(self, data: bytes) -> bool: + """Verify the sender's signature against the provided data. + + This delegates verification to the underlying account authenticator, + which in turn delegates to the specific signature implementation. + + Args: + data: The data that was signed (typically a transaction hash) + + Returns: + True if the signature is valid, False otherwise + """ return self.sender.verify(data) @staticmethod def deserialize(deserializer: Deserializer) -> SingleSenderAuthenticator: + """Deserialize a SingleSenderAuthenticator from BCS bytes. + + Args: + deserializer: The BCS deserializer containing the authenticator data + + Returns: + A SingleSenderAuthenticator instance + + Raises: + DeserializationError: If the data is malformed or incomplete + """ sender = deserializer.struct(AccountAuthenticator) return SingleSenderAuthenticator(sender) def serialize(self, serializer: Serializer): + """Serialize this single sender authenticator using BCS serialization. + + This serializes the underlying account authenticator which contains + the specific signature scheme and signature data. + + Args: + serializer: The BCS serializer to write to + """ serializer.struct(self.sender) class SingleKeyAuthenticator: + """Modern single-key authenticator with algorithm flexibility. + + This is the preferred single-key authentication format in newer Aptos versions. + Unlike Ed25519Authenticator which is tied to a specific algorithm, SingleKeyAuthenticator + can work with multiple signature algorithms through the asymmetric_crypto_wrapper. + + The authenticator uses the AIP-80 compliant key format, providing a unified interface + for different cryptographic algorithms while maintaining compatibility with existing + Aptos authentication infrastructure. + + Supported Algorithms: + - Ed25519: Fast and secure elliptic curve signatures + - Secp256k1: Bitcoin-compatible signatures + - Future algorithms: Extensible through the wrapper interface + + Features: + - Algorithm-agnostic interface + - AIP-80 compliant key formatting + - Efficient serialization and verification + - Forward compatibility with new signature schemes + - Consistent API across different algorithms + + Examples: + Create with Ed25519:: + + from aptos_sdk import ed25519 + from aptos_sdk.authenticator import SingleKeyAuthenticator + + # Generate Ed25519 key pair + private_key = ed25519.PrivateKey.random() + public_key = private_key.public_key() + + # Sign transaction hash + tx_hash = b"transaction_hash_bytes" + signature = private_key.sign(tx_hash) + + # Create single key authenticator + auth = SingleKeyAuthenticator(public_key, signature) + + # Verify signature + is_valid = auth.verify(tx_hash) + + Create with secp256k1:: + + from aptos_sdk import secp256k1_ecdsa + + # Generate secp256k1 key pair + private_key = secp256k1_ecdsa.PrivateKey.random() + public_key = private_key.public_key() + signature = private_key.sign(tx_hash) + + # Create authenticator (same interface) + auth = SingleKeyAuthenticator(public_key, signature) + + Serialization:: + + # Serialize for blockchain submission + serializer = Serializer() + auth.serialize(serializer) + auth_bytes = serializer.output() + + # Deserialize from bytes + deserializer = Deserializer(auth_bytes) + restored_auth = SingleKeyAuthenticator.deserialize(deserializer) + + Attributes: + public_key (asymmetric_crypto_wrapper.PublicKey): Wrapped public key with algorithm info + signature (asymmetric_crypto_wrapper.Signature): Wrapped signature with algorithm info + + Note: + The wrapper classes automatically handle algorithm detection and provide + a unified interface for verification. This authenticator is preferred over + algorithm-specific authenticators for new applications. + """ + public_key: asymmetric_crypto_wrapper.PublicKey signature: asymmetric_crypto_wrapper.Signature @@ -343,6 +996,28 @@ def __init__( public_key: asymmetric_crypto.PublicKey, signature: asymmetric_crypto.Signature, ): + """Initialize a single key authenticator with algorithm detection. + + The constructor automatically wraps the provided public key and signature + with the appropriate wrapper classes that handle algorithm-specific details. + + Args: + public_key: The public key (Ed25519, secp256k1, etc.) + signature: The signature corresponding to the public key + + Examples: + With raw Ed25519 objects:: + + ed25519_key = ed25519.PublicKey.from_str("...") + ed25519_sig = ed25519.Signature.from_str("...") + auth = SingleKeyAuthenticator(ed25519_key, ed25519_sig) + + With pre-wrapped objects:: + + wrapped_key = asymmetric_crypto_wrapper.PublicKey(ed25519_key) + wrapped_sig = asymmetric_crypto_wrapper.Signature(ed25519_sig) + auth = SingleKeyAuthenticator(wrapped_key, wrapped_sig) + """ if isinstance(public_key, asymmetric_crypto_wrapper.PublicKey): self.public_key = public_key else: @@ -352,22 +1027,194 @@ def __init__( self.signature = signature else: self.signature = asymmetric_crypto_wrapper.Signature(signature) + + def __eq__(self, other: object) -> bool: + """Check equality with another SingleKeyAuthenticator. + + Args: + other: Object to compare with + + Returns: + True if public keys and signatures are equal, False otherwise + """ + if not isinstance(other, SingleKeyAuthenticator): + return NotImplemented + return self.public_key == other.public_key and self.signature == other.signature + + def __str__(self) -> str: + """String representation of the single key authenticator. + + Returns: + Human-readable string showing key and signature details + """ + return f"SingleKey - PublicKey: {self.public_key}, Signature: {self.signature}" def verify(self, data: bytes) -> bool: + """Verify the signature against the provided data. + + This method delegates to the wrapped public key's verification method, + which automatically handles the algorithm-specific verification logic. + + Args: + data: The data that was signed (typically a transaction hash) + + Returns: + True if the signature is valid, False otherwise + """ return self.public_key.verify(data, self.signature.signature) @staticmethod def deserialize(deserializer: Deserializer) -> SingleKeyAuthenticator: + """Deserialize a SingleKeyAuthenticator from BCS bytes. + + Args: + deserializer: The BCS deserializer containing the authenticator data + + Returns: + A SingleKeyAuthenticator instance with the deserialized key and signature + + Raises: + DeserializationError: If the data is malformed or incomplete + """ public_key = deserializer.struct(asymmetric_crypto_wrapper.PublicKey) signature = deserializer.struct(asymmetric_crypto_wrapper.Signature) return SingleKeyAuthenticator(public_key, signature) def serialize(self, serializer: Serializer): + """Serialize this single key authenticator using BCS serialization. + + This serializes the wrapped public key and signature, including their + algorithm identifiers as specified in AIP-80. + + Args: + serializer: The BCS serializer to write to + """ serializer.struct(self.public_key) serializer.struct(self.signature) class MultiKeyAuthenticator: + """Modern multi-key authenticator with algorithm flexibility and threshold signatures. + + This is the preferred multi-signature authentication format in newer Aptos versions. + Unlike MultiEd25519Authenticator which is tied to Ed25519, MultiKeyAuthenticator + can work with mixed signature algorithms through the asymmetric_crypto_wrapper, + allowing for heterogeneous multi-signature schemes. + + The authenticator uses the AIP-80 compliant key format and supports threshold + signatures where N-of-M keys must sign to authorize a transaction. This provides + flexible multi-party authentication with algorithm diversity. + + Supported Algorithm Combinations: + - Mixed Ed25519 and secp256k1 keys in the same multi-signature + - Pure Ed25519 multi-signatures (recommended for performance) + - Pure secp256k1 multi-signatures (for Bitcoin compatibility) + - Future algorithm combinations through the wrapper interface + + Features: + - Algorithm-agnostic multi-signature interface + - Heterogeneous key mixing (Ed25519 + secp256k1 + future algorithms) + - N-of-M threshold signatures with configurable thresholds + - AIP-80 compliant key formatting + - Efficient serialization and verification + - Forward compatibility with new signature schemes + - Superior to legacy MultiEd25519Authenticator + + Use Cases: + - Multi-party custody with different cryptographic preferences + - Governance scenarios requiring diverse signature algorithms + - Cross-chain compatibility requiring secp256k1 support + - Organizations with mixed cryptographic infrastructure + - Future-proofing against algorithm deprecation + + Examples: + Mixed Ed25519 and secp256k1 2-of-3:: + + from aptos_sdk import ed25519, secp256k1_ecdsa + from aptos_sdk import asymmetric_crypto_wrapper + from aptos_sdk.authenticator import MultiKeyAuthenticator + + # Generate mixed key pairs + ed25519_key1 = ed25519.PrivateKey.random() + ed25519_key2 = ed25519.PrivateKey.random() + secp256k1_key = secp256k1_ecdsa.PrivateKey.random() + + # Create public key list + public_keys = [ + ed25519_key1.public_key(), + ed25519_key2.public_key(), + secp256k1_key.public_key() + ] + + # Create multi-public key with threshold 2 + multi_pub_key = asymmetric_crypto_wrapper.MultiPublicKey(public_keys, threshold=2) + + # Sign with keys 0 and 2 (Ed25519 + secp256k1) + tx_hash = b"transaction_hash" + signatures = [ + (0, ed25519_key1.sign(tx_hash)), + (2, secp256k1_key.sign(tx_hash)) + ] + multi_signature = asymmetric_crypto_wrapper.MultiSignature(signatures) + + # Create authenticator + auth = MultiKeyAuthenticator(multi_pub_key, multi_signature) + + # Verify the mixed multi-signature + is_valid = auth.verify(tx_hash) + + Pure Ed25519 3-of-5 (recommended for performance):: + + # Generate Ed25519 keys only + ed25519_keys = [ed25519.PrivateKey.random() for _ in range(5)] + public_keys = [key.public_key() for key in ed25519_keys] + + # Create 3-of-5 threshold + multi_pub_key = asymmetric_crypto_wrapper.MultiPublicKey(public_keys, threshold=3) + + # Sign with keys 1, 2, and 4 + signatures = [ + (1, ed25519_keys[1].sign(tx_hash)), + (2, ed25519_keys[2].sign(tx_hash)), + (4, ed25519_keys[4].sign(tx_hash)) + ] + multi_signature = asymmetric_crypto_wrapper.MultiSignature(signatures) + auth = MultiKeyAuthenticator(multi_pub_key, multi_signature) + + Integration with SingleSenderAuthenticator:: + + # Wrap in account and transaction authenticators + account_auth = AccountAuthenticator(multi_key_auth) + single_sender = SingleSenderAuthenticator(account_auth) + tx_auth = Authenticator(single_sender) + + # Submit to blockchain + serializer = Serializer() + tx_auth.serialize(serializer) + auth_bytes = serializer.output() + + Attributes: + public_key (asymmetric_crypto_wrapper.MultiPublicKey): Multi-public key with mixed algorithms + signature (asymmetric_crypto_wrapper.MultiSignature): Multi-signature with threshold validation + + Security Considerations: + - Mixed algorithms provide defense against algorithm-specific attacks + - Threshold must be set appropriately (not too low, not too high) + - Each signature algorithm contributes its own security properties + - Key management complexity increases with algorithm diversity + + Performance Notes: + - Pure Ed25519 multi-signatures are fastest + - Mixed algorithms have slight verification overhead + - Serialization size increases with algorithm diversity + - Network latency impact depends on signature sizes + + Note: + This is the modern replacement for MultiEd25519Authenticator. + New applications should prefer this format for its flexibility + and future compatibility. + """ + public_key: asymmetric_crypto_wrapper.MultiPublicKey signature: asymmetric_crypto_wrapper.MultiSignature @@ -376,24 +1223,104 @@ def __init__( public_key: asymmetric_crypto_wrapper.MultiPublicKey, signature: asymmetric_crypto_wrapper.MultiSignature, ): + """Initialize a multi-key authenticator with mixed algorithms. + + Args: + public_key: Multi-public key containing keys from different algorithms + signature: Multi-signature with the required threshold signatures + + Examples: + Basic mixed algorithm initialization:: + + # Assume we have ed25519 and secp256k1 keys + mixed_keys = [ed25519_pub, secp256k1_pub, another_ed25519_pub] + multi_pub_key = asymmetric_crypto_wrapper.MultiPublicKey(mixed_keys, threshold=2) + + # Signatures from threshold keys (indices 0 and 2) + signatures = [(0, ed25519_sig), (2, another_ed25519_sig)] + multi_sig = asymmetric_crypto_wrapper.MultiSignature(signatures) + + auth = MultiKeyAuthenticator(multi_pub_key, multi_sig) + """ self.public_key = public_key self.signature = signature + + def __eq__(self, other: object) -> bool: + """Check equality with another MultiKeyAuthenticator. + + Args: + other: Object to compare with + + Returns: + True if public keys and signatures are equal, False otherwise + """ + if not isinstance(other, MultiKeyAuthenticator): + return NotImplemented + return self.public_key == other.public_key and self.signature == other.signature + + def __str__(self) -> str: + """String representation of the multi-key authenticator. + + Returns: + Human-readable string showing multi-key details + """ + return f"MultiKey - PublicKey: {self.public_key}, Signature: {self.signature}" def verify(self, data: bytes) -> bool: + """Verify the multi-signature against the provided data. + + This method validates that: + 1. The threshold number of signatures is provided + 2. Each signature is from a different key in the multi-public key + 3. Each signature is cryptographically valid for its algorithm + 4. Mixed algorithm signatures are handled correctly + + Args: + data: The data that was signed (typically a transaction hash) + + Returns: + True if the multi-signature meets the threshold and all signatures are valid + """ return self.public_key.verify(data, self.signature) @staticmethod def deserialize(deserializer: Deserializer) -> MultiKeyAuthenticator: + """Deserialize a MultiKeyAuthenticator from BCS bytes. + + Args: + deserializer: The BCS deserializer containing the authenticator data + + Returns: + A MultiKeyAuthenticator instance with mixed algorithm support + + Raises: + DeserializationError: If the data is malformed or incomplete + """ public_key = deserializer.struct(asymmetric_crypto_wrapper.MultiPublicKey) signature = deserializer.struct(asymmetric_crypto_wrapper.MultiSignature) return MultiKeyAuthenticator(public_key, signature) def serialize(self, serializer: Serializer): + """Serialize this multi-key authenticator using BCS serialization. + + This serializes the multi-public key (including all public keys with + their algorithm identifiers and the threshold) and the multi-signature + (including signature indices and algorithm-specific signature bytes). + + Args: + serializer: The BCS serializer to write to + """ serializer.struct(self.public_key) serializer.struct(self.signature) class Test(unittest.TestCase): + """Unit tests for authenticator functionality. + + Tests serialization, deserialization, and verification of various authenticator types, + including mixed-algorithm multi-key authentication scenarios. + """ + def test_multi_key_auth(self): expected_output = bytes.fromhex( "040303002020fdbac9b10b7587bba7b5bc163bce69e796d71e4ed44c10fcb4488689f7a1440141049b8327d929a0e45285c04d19c9fffbee065c266b701972922d807228120e43f34ad68ac77f6ec0205fe39f7c5b6055dad973a03464a3a743302de0feaf6ec6d90141049b8327d929a0e45285c04d19c9fffbee065c266b701972922d807228120e43f34ad68ac77f6ec0205fe39f7c5b6055dad973a03464a3a743302de0feaf6ec6d902020040a9839b56be99b48c285ec252cf9bf779e42d3b62eb8664c31b18c1fdb29b574b1bfde0b89aedddb9fb8304ca5913c9feefea75d332d8f72ac3ab4598a884ea0801402bd50683abe6332a496121f8ec7db7be351f49b0087fa0dfb258c469822bd52e59fc9344944a1f338b0f0a61c7173453e0cd09cf961e45cb9396808fa67eeef301c0" diff --git a/aptos_sdk/bcs.py b/aptos_sdk/bcs.py index b626c61..1d19ec1 100644 --- a/aptos_sdk/bcs.py +++ b/aptos_sdk/bcs.py @@ -2,7 +2,47 @@ # SPDX-License-Identifier: Apache-2.0 """ -This is a simple BCS serializer and deserializer. Learn more at https://github.com/diem/bcs +Binary Canonical Serialization (BCS) implementation for the Aptos Python SDK. + +This module provides a simple BCS serializer and deserializer for encoding and decoding +data in the Binary Canonical Serialization format used throughout the Aptos ecosystem. +BCS is a canonical encoding format that ensures deterministic serialization. + +Learn more at https://github.com/diem/bcs + +The module contains: +- Protocol interfaces for serializable and deserializable objects +- Deserializer class for reading BCS-encoded data +- Serializer class for writing BCS-encoded data +- Helper functions for encoding values +- Comprehensive test suite + +Examples: + Basic serialization:: + + from aptos_sdk.bcs import Serializer, Deserializer + + # Serialize a string + ser = Serializer() + ser.str("hello") + data = ser.output() + + # Deserialize back to string + der = Deserializer(data) + result = der.str() # "hello" + + Working with custom structures:: + + class MyStruct: + def serialize(self, serializer): + serializer.str(self.name) + serializer.u32(self.value) + + @staticmethod + def deserialize(deserializer): + name = deserializer.str() + value = deserializer.u32() + return MyStruct(name, value) """ from __future__ import annotations @@ -23,40 +63,176 @@ class Deserializable(Protocol): - # The following class can be deserialized from a bcs stream. + """Protocol for objects that can be deserialized from a BCS byte stream. + + This protocol defines the interface that classes must implement to support + BCS deserialization. Classes implementing this protocol can be automatically + deserialized from binary data. + + The protocol requires: + - A `from_bytes` class method that creates an instance from raw bytes + - A `deserialize` static method that reads from a Deserializer + + Examples: + Implementing a deserializable class:: + + class MyClass: + def __init__(self, value: str): + self.value = value + + @staticmethod + def deserialize(deserializer: Deserializer) -> 'MyClass': + value = deserializer.str() + return MyClass(value) + + # Usage + data = b'\x05hello' # BCS-encoded string + obj = MyClass.from_bytes(data) + """ @classmethod def from_bytes(cls, indata: bytes) -> Deserializable: + """Create an instance of this class from BCS-encoded bytes. + + Args: + indata: The BCS-encoded byte data to deserialize. + + Returns: + An instance of the implementing class. + + Raises: + Exception: If the data cannot be deserialized or is malformed. + """ der = Deserializer(indata) return der.struct(cls) @staticmethod - def deserialize(deserializer: Deserializer) -> Deserializable: ... + def deserialize(deserializer: Deserializer) -> Deserializable: + """Deserialize an instance from a Deserializer. + + Args: + deserializer: The Deserializer to read data from. + + Returns: + A deserialized instance of the implementing class. + + Note: + This is an abstract method that must be implemented by concrete classes. + """ + ... class Serializable(Protocol): - # The following class can be serialized into a bcs stream. + """Protocol for objects that can be serialized into a BCS byte stream. + + This protocol defines the interface that classes must implement to support + BCS serialization. Classes implementing this protocol can be automatically + serialized to binary data. + + The protocol requires: + - A `to_bytes` method that converts the instance to raw bytes + - A `serialize` method that writes data to a Serializer + + Examples: + Implementing a serializable class:: + + class MyClass: + def __init__(self, value: str): + self.value = value + + def serialize(self, serializer: Serializer): + serializer.str(self.value) + + # Usage + obj = MyClass("hello") + data = obj.to_bytes() # Returns BCS-encoded bytes + """ def to_bytes(self) -> bytes: + """Convert this object to BCS-encoded bytes. + + Returns: + The BCS-encoded representation of this object as bytes. + + Raises: + Exception: If the object cannot be serialized. + """ ser = Serializer() ser.struct(self) return ser.output() - def serialize(self, serializer: Serializer): ... + def serialize(self, serializer: Serializer): + """Serialize this object using the provided Serializer. + + Args: + serializer: The Serializer to write data to. + + Note: + This is an abstract method that must be implemented by concrete classes. + """ + ... class Deserializer: + """A BCS deserializer for reading data from a byte stream. + + The Deserializer class provides methods to read various data types from + BCS-encoded byte data. It maintains an internal position in the byte stream + and provides methods to read primitive types, collections, and custom structures. + + Attributes: + _input: Internal BytesIO stream for reading data. + _length: Total length of the input data. + + Examples: + Basic usage:: + + data = b'\x01\x05hello' # BCS-encoded bool and string + der = Deserializer(data) + + flag = der.bool() # True + text = der.str() # "hello" + + Reading collections:: + + # Deserialize a sequence of strings + values = der.sequence(Deserializer.str) + + # Deserialize a map + mapping = der.map(Deserializer.str, Deserializer.u32) + """ _input: io.BytesIO _length: int def __init__(self, data: bytes): + """Initialize the deserializer with byte data. + + Args: + data: The BCS-encoded bytes to deserialize from. + """ self._length = len(data) self._input = io.BytesIO(data) def remaining(self) -> int: + """Get the number of bytes remaining in the input stream. + + Returns: + The number of unread bytes remaining in the stream. + """ return self._length - self._input.tell() def bool(self) -> bool: + """Read a boolean value from the stream. + + BCS encodes booleans as a single byte: 0 for False, 1 for True. + + Returns: + The deserialized boolean value. + + Raises: + Exception: If the byte value is not 0 or 1, or if there's + insufficient data in the stream. + """ value = int.from_bytes(self._read(1), byteorder="little", signed=False) if value == 0: return False @@ -66,9 +242,31 @@ def bool(self) -> bool: raise Exception("Unexpected boolean value: ", value) def to_bytes(self) -> bytes: + """Read a byte array from the stream. + + BCS encodes byte arrays as a ULEB128 length followed by the raw bytes. + + Returns: + The deserialized byte array. + + Raises: + Exception: If there's insufficient data in the stream or if the + length encoding is invalid. + """ return self._read(self.uleb128()) def fixed_bytes(self, length: int) -> bytes: + """Read a fixed-length byte array from the stream. + + Args: + length: The exact number of bytes to read. + + Returns: + The deserialized byte array of the specified length. + + Raises: + Exception: If there are insufficient bytes remaining in the stream. + """ return self._read(length) def map( @@ -76,6 +274,26 @@ def map( key_decoder: typing.Callable[[Deserializer], typing.Any], value_decoder: typing.Callable[[Deserializer], typing.Any], ) -> Dict[typing.Any, typing.Any]: + """Read a map (dictionary) from the stream. + + BCS encodes maps as a ULEB128 length followed by key-value pairs. + The pairs are sorted by the BCS encoding of the keys. + + Args: + key_decoder: Function to decode each key from the stream. + value_decoder: Function to decode each value from the stream. + + Returns: + A dictionary containing the deserialized key-value pairs. + + Raises: + Exception: If there's insufficient data or if the decoders fail. + + Examples: + Reading a map of string keys to u32 values:: + + mapping = der.map(Deserializer.str, Deserializer.u32) + """ length = self.uleb128() values: Dict = {} while len(values) < length: @@ -88,6 +306,28 @@ def sequence( self, value_decoder: typing.Callable[[Deserializer], typing.Any], ) -> List[typing.Any]: + """Read a sequence (list) from the stream. + + BCS encodes sequences as a ULEB128 length followed by the elements. + + Args: + value_decoder: Function to decode each element from the stream. + + Returns: + A list containing the deserialized elements. + + Raises: + Exception: If there's insufficient data or if the decoder fails. + + Examples: + Reading a sequence of strings:: + + strings = der.sequence(Deserializer.str) + + Reading a sequence of u32 values:: + + numbers = der.sequence(Deserializer.u32) + """ length = self.uleb128() values: List = [] while len(values) < length: @@ -95,30 +335,118 @@ def sequence( return values def str(self) -> str: + """Read a UTF-8 string from the stream. + + BCS encodes strings as byte arrays (ULEB128 length + bytes) that + contain valid UTF-8 data. + + Returns: + The deserialized string. + + Raises: + Exception: If there's insufficient data in the stream. + UnicodeDecodeError: If the bytes don't form valid UTF-8. + """ return self.to_bytes().decode() def struct(self, struct: typing.Any) -> typing.Any: + """Deserialize a custom struct from the stream. + + This method delegates to the struct's `deserialize` method to handle + custom deserialization logic. + + Args: + struct: A class or type that implements the `deserialize` method. + + Returns: + The deserialized struct instance. + + Raises: + Exception: If the struct doesn't have a deserialize method or + if deserialization fails. + """ return struct.deserialize(self) def u8(self) -> int: + """Read an 8-bit unsigned integer from the stream. + + Returns: + The deserialized u8 value (0-255). + + Raises: + Exception: If there's insufficient data in the stream. + """ return self._read_int(1) def u16(self) -> int: + """Read a 16-bit unsigned integer from the stream. + + Returns: + The deserialized u16 value (0-65535). + + Raises: + Exception: If there's insufficient data in the stream. + """ return self._read_int(2) def u32(self) -> int: + """Read a 32-bit unsigned integer from the stream. + + Returns: + The deserialized u32 value (0-4294967295). + + Raises: + Exception: If there's insufficient data in the stream. + """ return self._read_int(4) def u64(self) -> int: + """Read a 64-bit unsigned integer from the stream. + + Returns: + The deserialized u64 value (0-18446744073709551615). + + Raises: + Exception: If there's insufficient data in the stream. + """ return self._read_int(8) def u128(self) -> int: + """Read a 128-bit unsigned integer from the stream. + + Returns: + The deserialized u128 value (0-340282366920938463463374607431768211455). + + Raises: + Exception: If there's insufficient data in the stream. + """ return self._read_int(16) def u256(self) -> int: + """Read a 256-bit unsigned integer from the stream. + + Returns: + The deserialized u256 value. + + Raises: + Exception: If there's insufficient data in the stream. + """ return self._read_int(32) def uleb128(self) -> int: + """Read a ULEB128 (unsigned little-endian base 128) encoded integer. + + ULEB128 is a variable-length encoding where each byte contains 7 bits + of data and a continuation bit. It's commonly used for encoding lengths + and small integers efficiently. + + Returns: + The decoded integer value (0-4294967295). + + Raises: + Exception: If the encoded value exceeds u32 range or if there's + insufficient data in the stream. + """ value = 0 shift = 0 @@ -135,6 +463,17 @@ def uleb128(self) -> int: return value def _read(self, length: int) -> bytes: + """Read a specified number of bytes from the input stream. + + Args: + length: Number of bytes to read. + + Returns: + The requested bytes. + + Raises: + Exception: If there are insufficient bytes remaining in the stream. + """ value = self._input.read(length) if value is None or len(value) < length: actual_length = 0 if value is None else len(value) @@ -145,26 +484,89 @@ def _read(self, length: int) -> bytes: return value def _read_int(self, length: int) -> int: + """Read an integer of specified byte length from the stream. + + Args: + length: Number of bytes representing the integer. + + Returns: + The integer value interpreted as little-endian unsigned. + + Raises: + Exception: If there are insufficient bytes in the stream. + """ return int.from_bytes(self._read(length), byteorder="little", signed=False) class Serializer: + """A BCS serializer for writing data to a byte stream. + + The Serializer class provides methods to write various data types to a + BCS-encoded byte stream. It maintains an internal output buffer and provides + methods to serialize primitive types, collections, and custom structures. + + Attributes: + _output: Internal BytesIO buffer for accumulating serialized data. + + Examples: + Basic usage:: + + ser = Serializer() + ser.bool(True) + ser.str("hello") + data = ser.output() # Get the serialized bytes + + Serializing collections:: + + # Serialize a sequence of strings + ser.sequence(["a", "b", "c"], Serializer.str) + + # Serialize a map + ser.map({"key": 42}, Serializer.str, Serializer.u32) + """ _output: io.BytesIO def __init__(self): + """Initialize a new serializer with an empty output buffer.""" self._output = io.BytesIO() def output(self) -> bytes: + """Get the accumulated serialized data as bytes. + + Returns: + The BCS-encoded bytes written to this serializer. + """ return self._output.getvalue() def bool(self, value: bool): + """Write a boolean value to the stream. + + BCS encodes booleans as a single byte: 0 for False, 1 for True. + + Args: + value: The boolean value to serialize. + """ self._write_int(int(value), 1) def to_bytes(self, value: bytes): + """Write a byte array to the stream. + + BCS encodes byte arrays as a ULEB128 length followed by the raw bytes. + + Args: + value: The byte array to serialize. + """ self.uleb128(len(value)) self._output.write(value) def fixed_bytes(self, value): + """Write a fixed-length byte array to the stream. + + This method writes raw bytes without any length prefix. + + Args: + value: The byte array to write directly to the stream. + """ self._output.write(value) def map( @@ -173,6 +575,23 @@ def map( key_encoder: typing.Callable[[Serializer, typing.Any], None], value_encoder: typing.Callable[[Serializer, typing.Any], None], ): + """Write a map (dictionary) to the stream. + + BCS encodes maps as a ULEB128 length followed by key-value pairs. + The pairs are sorted by the BCS encoding of the keys to ensure + canonical ordering. + + Args: + values: The dictionary to serialize. + key_encoder: Function to encode each key. + value_encoder: Function to encode each value. + + Examples: + Serializing a map of string keys to u32 values:: + + mapping = {"a": 1, "b": 2} + ser.map(mapping, Serializer.str, Serializer.u32) + """ encoded_values = [] for key, value in values.items(): encoded_values.append( @@ -189,6 +608,24 @@ def map( def sequence_serializer( value_encoder: typing.Callable[[Serializer, typing.Any], None], ): + """Create a reusable sequence serializer function. + + This is a helper method that returns a function that can be used + to serialize sequences with a specific encoder. + + Args: + value_encoder: Function to encode each element in sequences. + + Returns: + A function that takes a serializer and a list of values and + serializes the sequence. + + Examples: + Creating a string sequence serializer:: + + str_seq = Serializer.sequence_serializer(Serializer.str) + str_seq(ser, ["a", "b", "c"]) + """ return lambda self, values: self.sequence(values, value_encoder) def sequence( @@ -196,53 +633,153 @@ def sequence( values: typing.List[typing.Any], value_encoder: typing.Callable[[Serializer, typing.Any], None], ): + """Write a sequence (list) to the stream. + + BCS encodes sequences as a ULEB128 length followed by the elements. + + Args: + values: The list of values to serialize. + value_encoder: Function to encode each element. + + Examples: + Serializing a sequence of strings:: + + ser.sequence(["a", "b", "c"], Serializer.str) + + Serializing a sequence of u32 values:: + + ser.sequence([1, 2, 3], Serializer.u32) + """ self.uleb128(len(values)) for value in values: self.fixed_bytes(encoder(value, value_encoder)) def str(self, value: str): + """Write a UTF-8 string to the stream. + + BCS encodes strings as byte arrays (ULEB128 length + bytes) containing + valid UTF-8 data. + + Args: + value: The string to serialize. + + Raises: + UnicodeEncodeError: If the string cannot be encoded as UTF-8. + """ self.to_bytes(value.encode()) def struct(self, value: typing.Any): + """Serialize a custom struct to the stream. + + This method delegates to the struct's `serialize` method to handle + custom serialization logic. + + Args: + value: An object that implements the `serialize` method. + + Raises: + AttributeError: If the value doesn't have a serialize method. + Exception: If serialization fails. + """ value.serialize(self) def u8(self, value: int): + """Write an 8-bit unsigned integer to the stream. + + Args: + value: The u8 value to serialize (0-255). + + Raises: + Exception: If the value is outside the valid range. + """ if value > MAX_U8: raise Exception(f"Cannot encode {value} into u8") self._write_int(value, 1) def u16(self, value: int): + """Write a 16-bit unsigned integer to the stream. + + Args: + value: The u16 value to serialize (0-65535). + + Raises: + Exception: If the value is outside the valid range. + """ if value > MAX_U16: raise Exception(f"Cannot encode {value} into u16") self._write_int(value, 2) def u32(self, value: int): + """Write a 32-bit unsigned integer to the stream. + + Args: + value: The u32 value to serialize (0-4294967295). + + Raises: + Exception: If the value is outside the valid range. + """ if value > MAX_U32: raise Exception(f"Cannot encode {value} into u32") self._write_int(value, 4) def u64(self, value: int): + """Write a 64-bit unsigned integer to the stream. + + Args: + value: The u64 value to serialize (0-18446744073709551615). + + Raises: + Exception: If the value is outside the valid range. + """ if value > MAX_U64: raise Exception(f"Cannot encode {value} into u64") self._write_int(value, 8) def u128(self, value: int): + """Write a 128-bit unsigned integer to the stream. + + Args: + value: The u128 value to serialize (0-340282366920938463463374607431768211455). + + Raises: + Exception: If the value is outside the valid range. + """ if value > MAX_U128: raise Exception(f"Cannot encode {value} into u128") self._write_int(value, 16) def u256(self, value: int): + """Write a 256-bit unsigned integer to the stream. + + Args: + value: The u256 value to serialize. + + Raises: + Exception: If the value is outside the valid range. + """ if value > MAX_U256: raise Exception(f"Cannot encode {value} into u256") self._write_int(value, 32) def uleb128(self, value: int): + """Write a ULEB128 (unsigned little-endian base 128) encoded integer. + + ULEB128 is a variable-length encoding where each byte contains 7 bits + of data and a continuation bit. It's commonly used for encoding lengths + and small integers efficiently. + + Args: + value: The integer value to encode (0-4294967295). + + Raises: + Exception: If the value exceeds the u32 range. + """ if value > MAX_U32: raise Exception(f"Cannot encode {value} into uleb128") @@ -256,18 +793,52 @@ def uleb128(self, value: int): self.u8(value & 0x7F) def _write_int(self, value: int, length: int): + """Write an integer of specified byte length to the stream. + + Args: + value: The integer value to write. + length: Number of bytes to use for the integer representation. + """ self._output.write(value.to_bytes(length, "little", signed=False)) def encoder( value: typing.Any, encoder: typing.Callable[[Serializer, typing.Any], typing.Any] ) -> bytes: + """Encode a single value using the specified encoder function. + + This is a convenience function that creates a new Serializer, uses the + provided encoder function to serialize the value, and returns the bytes. + + Args: + value: The value to encode. + encoder: Function that takes a serializer and value and encodes the value. + + Returns: + The BCS-encoded bytes for the value. + + Examples: + Encoding a string:: + + data = encoder("hello", Serializer.str) + + Encoding an integer:: + + data = encoder(42, Serializer.u32) + """ ser = Serializer() encoder(ser, value) return ser.output() class Test(unittest.TestCase): + """Test suite for BCS serialization and deserialization. + + This test class contains comprehensive tests for all BCS data types and + operations to ensure correct serialization and deserialization behavior. + Each test follows the pattern of serializing a value, deserializing it, + and verifying the round-trip preserves the original value. + """ def test_bool_true(self): in_value = True diff --git a/aptos_sdk/cli.py b/aptos_sdk/cli.py index a93cda9..5e890d8 100644 --- a/aptos_sdk/cli.py +++ b/aptos_sdk/cli.py @@ -1,6 +1,111 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Command-line interface utilities for the Aptos Python SDK. + +This module provides a CLI framework for common Aptos blockchain operations, +particularly focused on Move package development and deployment. It integrates +with the Aptos CLI for compilation and provides high-level abstractions for +complex operations like package publishing. + +Key Features: +- **Package Publishing**: End-to-end Move package compilation and deployment +- **Named Address Resolution**: Support for parameterized Move modules +- **Account Management**: Private key loading and account initialization +- **Network Configuration**: Flexible REST API endpoint specification +- **Error Handling**: Comprehensive validation and error reporting +- **CLI Integration**: Seamless integration with the official Aptos CLI + +Supported Commands: +- publish-package: Compile and deploy Move packages to the blockchain + +Use Cases: +- Move smart contract deployment workflows +- Automated deployment scripts and CI/CD integration +- Development environment setup and testing +- Multi-network deployment (devnet, testnet, mainnet) +- Package publishing with complex address configurations + +Examples: + Basic package publishing:: + + python -m aptos_sdk.cli publish-package \ + --package-dir ./my-move-package \ + --account ***1234... \ + --private-key-path ./private_key.txt \ + --rest-api https://fullnode.devnet.aptoslabs.com/v1 + + Package with named addresses:: + + python -m aptos_sdk.cli publish-package \ + --package-dir ./my-move-package \ + --account ***1234... \ + --private-key-path ./private_key.txt \ + --rest-api https://fullnode.devnet.aptoslabs.com/v1 \ + --named-address my_addr=***5678... \ + --named-address other_addr=***9abc... + + Programmatic usage:: + + import asyncio + from aptos_sdk.cli import main + + # Run CLI command programmatically + await main([ + 'publish-package', + '--package-dir', './my-package', + '--account', '***1234...', + '--private-key-path', './key.txt', + '--rest-api', 'https://fullnode.devnet.aptoslabs.com/v1' + ]) + + Integration with scripts:: + + from aptos_sdk.cli import publish_package + from aptos_sdk.account import Account + from aptos_sdk.account_address import AccountAddress + from aptos_sdk.ed25519 import PrivateKey + + # Direct function call + private_key = PrivateKey.from_str("ed25519-priv-...") + account = Account(AccountAddress.from_str("***123..."), private_key) + + await publish_package( + package_dir="./my-package", + named_addresses={"MyModule": AccountAddress.from_str("***456...")}, + signer=account, + rest_api="https://fullnode.devnet.aptoslabs.com/v1" + ) + +Requirements: + - Aptos CLI installed and available in PATH or specified via APTOS_CLI_PATH + - Valid Move package with Move.toml configuration + - Private key file in supported format (Ed25519) + - Network connectivity to Aptos REST API endpoint + +File Format Requirements: + Private Key File: Should contain a single line with the private key in + Ed25519 format, either as raw hex or AIP-80 compliant string. + + Move Package: Must have proper Move.toml configuration file with + dependencies and named addresses properly specified. + +Error Handling: + The CLI provides comprehensive error checking for: + - Missing required arguments + - Invalid private key formats + - Missing Aptos CLI installation + - Network connectivity issues + - Move compilation errors + - Package publishing failures + +Note: + This CLI is designed for development and deployment workflows. + For production use, consider implementing additional security measures + for private key handling and validation. +""" + from __future__ import annotations import argparse @@ -22,6 +127,64 @@ async def publish_package( signer: Account, rest_api: str, ): + """Compile and publish a Move package to the Aptos blockchain. + + This function orchestrates the complete package publishing workflow: + 1. Compiles the Move package using the Aptos CLI + 2. Creates a REST client connection to the specified network + 3. Publishes the compiled package to the blockchain + + Args: + package_dir: Path to the Move package directory containing Move.toml. + named_addresses: Dictionary mapping named address identifiers to + their resolved AccountAddress values. + signer: Account that will sign and pay for the package publication. + rest_api: URL of the Aptos REST API endpoint to publish to. + + Raises: + Exception: If the Move package compilation fails. + ApiError: If the package publication transaction fails. + FileNotFoundError: If the package directory or files don't exist. + + Examples: + Basic package publishing:: + + from aptos_sdk.account import Account + from aptos_sdk.account_address import AccountAddress + from aptos_sdk.ed25519 import PrivateKey + + # Create account from private key + private_key = PrivateKey.from_str("ed25519-priv-...") + account = Account(AccountAddress.from_str("***123..."), private_key) + + # Publish package + await publish_package( + package_dir="./my-move-package", + named_addresses={}, + signer=account, + rest_api="https://fullnode.devnet.aptoslabs.com/v1" + ) + + Package with named addresses:: + + named_addresses = { + "MyContract": AccountAddress.from_str("***456..."), + "Treasury": AccountAddress.from_str("***789...") + } + + await publish_package( + package_dir="./complex-package", + named_addresses=named_addresses, + signer=deployer_account, + rest_api="https://fullnode.mainnet.aptoslabs.com/v1" + ) + + Note: + - Requires the Aptos CLI to be installed and available + - The signer account must have sufficient APT to pay for gas + - Package compilation output will be stored in the package directory + - Named addresses must match those declared in Move.toml + """ AptosCLIWrapper.compile_package(package_dir, named_addresses) rest_client = RestClient(rest_api) @@ -30,6 +193,51 @@ async def publish_package( def key_value(indata: str) -> Tuple[str, AccountAddress]: + """Parse a named address string into name and AccountAddress components. + + This function parses command-line named address arguments in the format + "name=address" and returns a tuple suitable for use in named address + dictionaries. + + Args: + indata: String in format "name=address" where address can be any + valid AccountAddress format (hex string, shortened address, etc.) + + Returns: + Tuple of (name, AccountAddress) where name is the identifier and + AccountAddress is the parsed address object. + + Raises: + ValueError: If the input string is not in the expected "name=address" format. + Exception: If the address portion cannot be parsed as a valid AccountAddress. + + Examples: + Parse named address:: + + >>> name, addr = key_value("MyContract=***1234...") + >>> print(f"Name: {name}, Address: {addr}") + Name: MyContract, Address: ***1234... + + Multiple named addresses:: + + named_pairs = [ + key_value("TokenContract=***1111..."), + key_value("Treasury=***2222..."), + key_value("Admin=***3333...") + ] + + # Convert to dictionary + named_addresses = dict(named_pairs) + + Command-line usage:: + + --named-address MyContract=***1234... \ + --named-address Treasury=***5678... + + Note: + This function is primarily used by the argument parser to convert + command-line string arguments into structured data for Move compilation. + """ split_indata = indata.split("=") if len(split_indata) != 2: raise ValueError("Invalid named-address, expected name=account address") @@ -39,50 +247,120 @@ def key_value(indata: str) -> Tuple[str, AccountAddress]: async def main(args: List[str]): - parser = argparse.ArgumentParser(description="Aptos Pyton CLI") + """Main entry point for the Aptos Python SDK CLI. + + This function sets up the argument parser, validates inputs, and dispatches + to the appropriate command handlers. It provides comprehensive error checking + and user-friendly error messages. + + Args: + args: List of command-line arguments (typically from sys.argv[1:]) + + Raises: + SystemExit: On invalid arguments, missing requirements, or command failure. + + Examples: + Run from command line:: + + python -m aptos_sdk.cli publish-package \ + --package-dir ./my-package \ + --account ***1234... \ + --private-key-path ./key.txt \ + --rest-api https://fullnode.devnet.aptoslabs.com/v1 + + Run programmatically:: + + import asyncio + from aptos_sdk.cli import main + + await main([ + 'publish-package', + '--package-dir', './package', + '--account', '***1234...', + '--private-key-path', './key.txt', + '--rest-api', 'https://fullnode.devnet.aptoslabs.com/v1', + '--named-address', 'MyAddr=***5678...' + ]) + + Supported Commands: + publish-package: Compile and deploy a Move package + + Required Arguments (for publish-package): + --account: Account address that will publish the package + --package-dir: Path to Move package directory + --private-key-path: Path to private key file + --rest-api: Aptos REST API endpoint URL + + Optional Arguments: + --named-address: Named address mappings (can be specified multiple times) + + Environment Variables: + APTOS_CLI_PATH: Path to Aptos CLI executable (if not in PATH) + + Note: + The function performs extensive validation before executing commands + to provide clear error messages for common configuration issues. + """ + parser = argparse.ArgumentParser(description="Aptos Python CLI") parser.add_argument( "command", type=str, help="The command to execute", choices=["publish-package"] ) parser.add_argument( "--account", - help="The account to query or the signer of a transaction", + help="The account address that will sign and publish the package", type=AccountAddress.from_str, ) parser.add_argument( "--named-address", - help="A single literal address name paired to an account address, e.g., name=0x1", + help="Named address mapping in format 'name=address' (can be specified multiple times)", nargs="*", type=key_value, + default=[], + ) + parser.add_argument( + "--package-dir", + help="Path to the Move package directory containing Move.toml", + type=str ) - parser.add_argument("--package-dir", help="The path to the Move package", type=str) parser.add_argument( - "--private-key-path", help="The path to the signer's private key", type=str + "--private-key-path", + help="Path to file containing the signer's private key", + type=str ) parser.add_argument( "--rest-api", - help="The REST API to send queries to, e.g., https://testnet.aptoslabs.com/v1", + help="Aptos REST API endpoint URL (e.g., https://fullnode.devnet.aptoslabs.com/v1)", type=str, ) parsed_args = parser.parse_args(args) if parsed_args.command == "publish-package": + # Validate required arguments if parsed_args.account is None: parser.error("Missing required argument '--account'") if parsed_args.package_dir is None: parser.error("Missing required argument '--package-dir'") if parsed_args.rest_api is None: parser.error("Missing required argument '--rest-api'") + if parsed_args.private_key_path is None: + parser.error("Missing required argument '--private-key-path'") + # Check for Aptos CLI availability if not AptosCLIWrapper.does_cli_exist(): parser.error( - "Missing Aptos CLI. Export its path to APTOS_CLI_PATH environmental variable." + "Missing Aptos CLI. Please install it or export its path to APTOS_CLI_PATH environment variable." ) - if parsed_args.private_key_path is None: - parser.error("Missing required argument '--private-key-path'") - with open(parsed_args.private_key_path) as f: - private_key = PrivateKey.from_str(f.read()) + # Load private key from file + try: + with open(parsed_args.private_key_path) as f: + private_key = PrivateKey.from_str(f.read().strip()) + except FileNotFoundError: + parser.error(f"Private key file not found: {parsed_args.private_key_path}") + except Exception as e: + parser.error(f"Failed to load private key: {e}") + # Create account and execute command account = Account(parsed_args.account, private_key) await publish_package( parsed_args.package_dir, diff --git a/aptos_sdk/ed25519.py b/aptos_sdk/ed25519.py index de5dde2..88ce51f 100644 --- a/aptos_sdk/ed25519.py +++ b/aptos_sdk/ed25519.py @@ -1,6 +1,66 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Ed25519 cryptographic primitives for the Aptos Python SDK. + +This module provides Ed25519 digital signature functionality for the Aptos blockchain, +including single and multi-signature support. Ed25519 is a high-performance elliptic +curve signature scheme that provides strong security guarantees. + +The module includes: +- PrivateKey: Ed25519 private keys for signing operations +- PublicKey: Ed25519 public keys for signature verification +- Signature: Ed25519 signatures +- MultiPublicKey: Multi-signature public key aggregation +- MultiSignature: Multi-signature support with threshold verification + +All classes support BCS serialization for blockchain transactions and provide +standard string representations for interoperability. + +Examples: + Basic key generation and signing:: + + # Generate a random private key + private_key = PrivateKey.random() + public_key = private_key.public_key() + + # Sign some data + message = b"Hello, Aptos!" + signature = private_key.sign(message) + + # Verify the signature + is_valid = public_key.verify(message, signature) + + Multi-signature operations:: + + # Create a 2-of-3 multisig + keys = [PrivateKey.random().public_key() for _ in range(3)] + multisig_key = MultiPublicKey(keys, threshold=2) + + # Create signatures from 2 signers + sig1 = private_key1.sign(message) + sig2 = private_key2.sign(message) + + # Combine into multisig + multisig = MultiSignature.from_key_map(multisig_key, [ + (keys[0], sig1), (keys[1], sig2) + ]) + + # Verify multisig + is_valid = multisig_key.verify(message, multisig) + + AIP-80 compliant key formats:: + + # Create from AIP-80 format + key = PrivateKey.from_str( + "ed25519-priv-0x123...", strict=True + ) + + # Export to AIP-80 format + aip80_string = key.aip80() +""" + from __future__ import annotations import unittest @@ -13,29 +73,109 @@ class PrivateKey(asymmetric_crypto.PrivateKey): + """Ed25519 private key for digital signatures on Aptos. + + A private key is used to create digital signatures and derive the corresponding + public key. This implementation uses the NaCl library for cryptographic operations + and supports AIP-80 compliant key formats for interoperability. + + The private key is exactly 32 bytes (256 bits) as specified by the Ed25519 + signature scheme. + + Attributes: + LENGTH: The byte length of Ed25519 private keys (32) + key: The underlying NaCl SigningKey instance + + Examples: + Creating and using private keys:: + + # Generate a random private key + private_key = PrivateKey.random() + + # Create from hex string + hex_key = PrivateKey.from_hex("0x123...") + + # Create from AIP-80 format + aip80_key = PrivateKey.from_str( + "ed25519-priv-0x123...", strict=True + ) + + # Sign data + signature = private_key.sign(b"message") + + # Get public key + public_key = private_key.public_key() + """ LENGTH: int = 32 key: SigningKey def __init__(self, key: SigningKey): + """Initialize a PrivateKey with a NaCl SigningKey. + + Args: + key: The NaCl SigningKey instance to wrap. + """ self.key = key def __eq__(self, other: object): + """Check equality with another PrivateKey. + + Args: + other: The object to compare with. + + Returns: + True if both private keys are identical. + """ if not isinstance(other, PrivateKey): return NotImplemented return self.key == other.key def __str__(self): + """Get the AIP-80 compliant string representation. + + Returns: + AIP-80 formatted private key string (e.g., "ed25519-priv-0x123..."). + """ return self.aip80() @staticmethod def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: - """ - Parse a HexInput that may be a hex string, bytes, or an AIP-80 compliant string to a private key. - - :param value: A hex string, byte array, or AIP-80 compliant string. - :param strict: If true, the value MUST be compliant with AIP-80. - :return: Parsed Ed25519 private key. + """Parse a hex input to create an Ed25519 private key. + + Supports multiple input formats including plain hex strings, byte arrays, + and AIP-80 compliant prefixed strings. This provides flexibility for + different key storage and transmission formats. + + Args: + value: A hex string (with or without "0x" prefix), byte array, + or AIP-80 compliant string ("ed25519-priv-0x..."). + strict: If True, the value MUST be AIP-80 compliant. If False, + accepts plain hex. If None, auto-detects format. + + Returns: + A new PrivateKey instance. + + Raises: + Exception: If the input format is invalid or the key data + has incorrect length. + + Examples: + Different input formats:: + + # Plain hex string + key1 = PrivateKey.from_hex("123abc...") + + # Hex with 0x prefix + key2 = PrivateKey.from_hex("0x123abc...") + + # AIP-80 format (strict mode) + key3 = PrivateKey.from_hex( + "ed25519-priv-0x123abc...", strict=True + ) + + # Raw bytes + key4 = PrivateKey.from_hex(b"\x12\x3a\xbc...") """ return PrivateKey( SigningKey( @@ -47,35 +187,92 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: @staticmethod def from_str(value: str, strict: bool | None = None) -> PrivateKey: - """ - Parse a HexInput that may be a hex string or an AIP-80 compliant string to a private key. - - :param value: A hex string or AIP-80 compliant string. - :param strict: If true, the value MUST be compliant with AIP-80. - :return: Parsed Ed25519 private key. + """Parse a string representation to create an Ed25519 private key. + + This is a convenience method that delegates to from_hex() for string inputs. + Supports both plain hex strings and AIP-80 compliant formats. + + Args: + value: A hex string (with or without "0x" prefix) or AIP-80 + compliant string ("ed25519-priv-0x..."). + strict: If True, the value MUST be AIP-80 compliant. If False, + accepts plain hex. If None, auto-detects format. + + Returns: + A new PrivateKey instance. + + Raises: + Exception: If the input format is invalid or the key data + has incorrect length. """ return PrivateKey.from_hex(value, strict) def hex(self) -> str: + """Get the hexadecimal representation of the private key. + + Returns: + Hex string with "0x" prefix representing the 32-byte private key. + """ return f"0x{self.key.encode().hex()}" def aip80(self) -> str: + """Get the AIP-80 compliant string representation. + + AIP-80 (Aptos Improvement Proposal 80) defines a standard format + for representing private keys with type prefixes for improved + safety and interoperability. + + Returns: + AIP-80 formatted string ("ed25519-priv-0x..."). + """ return PrivateKey.format_private_key( self.hex(), asymmetric_crypto.PrivateKeyVariant.Ed25519 ) def public_key(self) -> PublicKey: + """Derive the corresponding public key from this private key. + + Returns: + The PublicKey that corresponds to this private key. + """ return PublicKey(self.key.verify_key) @staticmethod def random() -> PrivateKey: + """Generate a cryptographically secure random private key. + + Uses the system's secure random number generator to create + a new Ed25519 private key. + + Returns: + A new randomly generated PrivateKey instance. + """ return PrivateKey(SigningKey.generate()) def sign(self, data: bytes) -> Signature: + """Create a digital signature for the given data. + + Args: + data: The raw bytes to sign. + + Returns: + An Ed25519 Signature for the input data. + """ return Signature(self.key.sign(data).signature) @staticmethod def deserialize(deserializer: Deserializer) -> PrivateKey: + """Deserialize a PrivateKey from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized PrivateKey instance. + + Raises: + Exception: If the key data is not exactly 32 bytes. + """ key = deserializer.to_bytes() if len(key) != PrivateKey.LENGTH: raise Exception("Length mismatch") @@ -83,32 +280,103 @@ def deserialize(deserializer: Deserializer) -> PrivateKey: return PrivateKey(SigningKey(key)) def serialize(self, serializer: Serializer): + """Serialize this PrivateKey to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.to_bytes(self.key.encode()) class PublicKey(asymmetric_crypto.PublicKey): + """Ed25519 public key for signature verification on Aptos. + + A public key is derived from a private key and used to verify digital + signatures. Ed25519 public keys are exactly 32 bytes and provide strong + security guarantees for signature verification. + + Attributes: + LENGTH: The byte length of Ed25519 public keys (32) + key: The underlying NaCl VerifyKey instance + + Examples: + Creating and using public keys:: + + # Derive from private key + private_key = PrivateKey.random() + public_key = private_key.public_key() + + # Create from hex string + hex_key = PublicKey.from_str("0x123abc...") + + # Verify a signature + is_valid = public_key.verify(message, signature) + """ LENGTH: int = 32 key: VerifyKey def __init__(self, key: VerifyKey): + """Initialize a PublicKey with a NaCl VerifyKey. + + Args: + key: The NaCl VerifyKey instance to wrap. + """ self.key = key def __eq__(self, other: object): + """Check equality with another PublicKey. + + Args: + other: The object to compare with. + + Returns: + True if both public keys are identical. + """ if not isinstance(other, PublicKey): return NotImplemented return self.key == other.key def __str__(self) -> str: + """Get the hexadecimal string representation. + + Returns: + Hex string with "0x" prefix representing the 32-byte public key. + """ return f"0x{self.key.encode().hex()}" @staticmethod def from_str(value: str) -> PublicKey: + """Create a PublicKey from its hexadecimal string representation. + + Args: + value: Hex string representing the public key, with or without + "0x" prefix. + + Returns: + A new PublicKey instance. + + Raises: + ValueError: If the hex string is invalid or has wrong length. + """ if value[0:2] == "0x": value = value[2:] return PublicKey(VerifyKey(bytes.fromhex(value))) def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: + """Verify a digital signature against the given data. + + Args: + data: The original data that was signed. + signature: The signature to verify (must be an Ed25519 Signature). + + Returns: + True if the signature is valid for the given data, False otherwise. + + Note: + This method safely handles verification failures and returns False + for any exception during verification. + """ try: signature = cast(Signature, signature) self.key.verify(data, signature.data()) @@ -117,10 +385,26 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: return True def to_crypto_bytes(self) -> bytes: + """Get the raw cryptographic bytes of the public key. + + Returns: + The 32-byte Ed25519 public key as raw bytes. + """ return self.key.encode() @staticmethod def deserialize(deserializer: Deserializer) -> PublicKey: + """Deserialize a PublicKey from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized PublicKey instance. + + Raises: + Exception: If the key data is not exactly 32 bytes. + """ key = deserializer.to_bytes() if len(key) != PublicKey.LENGTH: raise Exception("Length mismatch") @@ -128,10 +412,45 @@ def deserialize(deserializer: Deserializer) -> PublicKey: return PublicKey(VerifyKey(key)) def serialize(self, serializer: Serializer): + """Serialize this PublicKey to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.to_bytes(self.key.encode()) class MultiPublicKey(asymmetric_crypto.PublicKey): + """Multi-signature public key for threshold signature schemes. + + A MultiPublicKey represents a collection of Ed25519 public keys with a + threshold requirement. It enables M-of-N signature schemes where M signatures + from N possible signers are required to validate a transaction. + + This is useful for multi-party custody, governance, and other scenarios + requiring distributed authorization. + + Attributes: + keys: List of individual Ed25519 public keys. + threshold: Minimum number of signatures required for validation. + MIN_KEYS: Minimum number of keys allowed (2). + MAX_KEYS: Maximum number of keys allowed (32). + MIN_THRESHOLD: Minimum threshold value (1). + + Examples: + Creating a 2-of-3 multisig:: + + keys = [ + PrivateKey.random().public_key(), + PrivateKey.random().public_key(), + PrivateKey.random().public_key() + ] + multisig = MultiPublicKey(keys, threshold=2) + + Verifying a multisig signature:: + + is_valid = multisig.verify(message, multi_signature) + """ keys: List[PublicKey] threshold: int @@ -140,6 +459,15 @@ class MultiPublicKey(asymmetric_crypto.PublicKey): MIN_THRESHOLD = 1 def __init__(self, keys: List[PublicKey], threshold: int): + """Initialize a MultiPublicKey with keys and threshold. + + Args: + keys: List of Ed25519 public keys (2-32 keys). + threshold: Number of signatures required (1 to len(keys)). + + Raises: + AssertionError: If key count or threshold is outside valid ranges. + """ assert ( self.MIN_KEYS <= len(keys) <= self.MAX_KEYS ), f"Must have between {self.MIN_KEYS} and {self.MAX_KEYS} keys." @@ -151,6 +479,11 @@ def __init__(self, keys: List[PublicKey], threshold: int): self.threshold = threshold def __str__(self) -> str: + """Get string representation of the multisig configuration. + + Returns: + Human-readable description (e.g., "2-of-3 Multi-Ed25519 public key"). + """ return f"{self.threshold}-of-{len(self.keys)} Multi-Ed25519 public key" def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: @@ -199,26 +532,87 @@ def serialize(self, serializer: Serializer): class Signature(asymmetric_crypto.Signature): + """Ed25519 digital signature. + + Represents a 64-byte Ed25519 signature created by signing data with + an Ed25519 private key. Signatures can be verified using the corresponding + public key. + + Attributes: + LENGTH: The byte length of Ed25519 signatures (64). + signature: The raw signature bytes. + + Examples: + Creating and using signatures:: + + private_key = PrivateKey.random() + message = b"Hello, Aptos!" + + # Create signature + signature = private_key.sign(message) + + # Verify signature + public_key = private_key.public_key() + is_valid = public_key.verify(message, signature) + + # Convert to/from hex string + hex_sig = str(signature) + parsed_sig = Signature.from_str(hex_sig) + """ LENGTH: int = 64 signature: bytes def __init__(self, signature: bytes): + """Initialize a Signature with raw signature bytes. + + Args: + signature: The 64-byte Ed25519 signature data. + """ self.signature = signature def __eq__(self, other: object): + """Check equality with another Signature. + + Args: + other: The object to compare with. + + Returns: + True if both signatures are identical. + """ if not isinstance(other, Signature): return NotImplemented return self.signature == other.signature def __str__(self) -> str: + """Get hexadecimal string representation. + + Returns: + Hex string with "0x" prefix representing the 64-byte signature. + """ return f"0x{self.signature.hex()}" def data(self) -> bytes: + """Get the raw signature bytes. + + Returns: + The 64-byte signature as raw bytes. + """ return self.signature @staticmethod def deserialize(deserializer: Deserializer) -> Signature: + """Deserialize a Signature from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized Signature instance. + + Raises: + Exception: If the signature data is not exactly 64 bytes. + """ signature = deserializer.to_bytes() if len(signature) != Signature.LENGTH: raise Exception("Length mismatch") @@ -227,19 +621,75 @@ def deserialize(deserializer: Deserializer) -> Signature: @staticmethod def from_str(value: str) -> Signature: + """Create a Signature from its hexadecimal string representation. + + Args: + value: Hex string representing the signature, with or without + "0x" prefix. + + Returns: + A new Signature instance. + + Raises: + ValueError: If the hex string is invalid or has wrong length. + """ if value[0:2] == "0x": value = value[2:] return Signature(bytes.fromhex(value)) def serialize(self, serializer: Serializer): + """Serialize this Signature to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.to_bytes(self.signature) class MultiSignature(asymmetric_crypto.Signature): + """Multi-signature combining multiple Ed25519 signatures. + + A MultiSignature aggregates individual signatures from multiple signers + along with a bitmap indicating which signers participated. This enables + efficient threshold signature verification. + + The encoding uses a 4-byte bitmap to track which of the up to 32 possible + signers provided signatures, followed by the actual signature data. + + Attributes: + signatures: List of (signer_index, signature) tuples. + BITMAP_NUM_OF_BYTES: Size of the signer bitmap (4 bytes). + + Examples: + Creating a multisig from individual signatures:: + + # Create signatures from 2 of 3 signers + sig1 = private_key1.sign(message) + sig2 = private_key3.sign(message) # Skip signer 2 + + # Create multisig + multisig = MultiSignature.from_key_map( + multisig_public_key, + [(public_key1, sig1), (public_key3, sig2)] + ) + + Verifying a multisig:: + + is_valid = multisig_public_key.verify(message, multisig) + """ signatures: List[Tuple[int, Signature]] BITMAP_NUM_OF_BYTES: int = 4 def __init__(self, signatures: List[Tuple[int, Signature]]): + """Initialize a MultiSignature with signer indices and signatures. + + Args: + signatures: List of (signer_index, signature) tuples where + signer_index is the position in the MultiPublicKey. + + Raises: + AssertionError: If any signer index exceeds bitmap capacity (32). + """ for signature in signatures: assert ( signature[0] < self.BITMAP_NUM_OF_BYTES * 8 @@ -247,11 +697,24 @@ def __init__(self, signatures: List[Tuple[int, Signature]]): self.signatures = signatures def __eq__(self, other: object): + """Check equality with another MultiSignature. + + Args: + other: The object to compare with. + + Returns: + True if both multisigs have identical signatures. + """ if not isinstance(other, MultiSignature): return NotImplemented return self.signatures == other.signatures def __str__(self) -> str: + """Get string representation of the multisig. + + Returns: + String showing the list of (index, signature) pairs. + """ return f"{self.signatures}" @staticmethod @@ -259,6 +722,21 @@ def from_key_map( public_key: MultiPublicKey, signatures_map: List[Tuple[PublicKey, Signature]], ) -> MultiSignature: + """Create a MultiSignature from a key-signature mapping. + + This convenience method maps public keys to their indices in the + MultiPublicKey and creates the appropriate MultiSignature structure. + + Args: + public_key: The MultiPublicKey containing the signer keys. + signatures_map: List of (public_key, signature) pairs. + + Returns: + A new MultiSignature with the mapped indices. + + Raises: + ValueError: If a public key is not found in the MultiPublicKey. + """ signatures = [] for entry in signatures_map: @@ -267,6 +745,20 @@ def from_key_map( @staticmethod def deserialize(deserializer: Deserializer) -> MultiSignature: + """Deserialize a MultiSignature from a BCS byte stream. + + The format is: [signature_1][signature_2]...[4-byte bitmap] + The bitmap indicates which signer positions have signatures. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized MultiSignature instance. + + Raises: + AssertionError: If the byte length doesn't match expected format. + """ signature_bytes = deserializer.to_bytes() count = len(signature_bytes) // Signature.LENGTH assert count * Signature.LENGTH + MultiSignature.BITMAP_NUM_OF_BYTES == len( @@ -290,6 +782,14 @@ def deserialize(deserializer: Deserializer) -> MultiSignature: return MultiSignature(signatures) def serialize(self, serializer: Serializer): + """Serialize this MultiSignature to a BCS byte stream. + + The format is: [signature_1][signature_2]...[4-byte bitmap] + The bitmap has bits set for each signer position that has a signature. + + Args: + serializer: The BCS serializer to write to. + """ signature_bytes = bytearray() bitmap = 0 @@ -305,6 +805,16 @@ def serialize(self, serializer: Serializer): class Test(unittest.TestCase): + """Comprehensive test suite for Ed25519 cryptographic operations. + + Tests all aspects of Ed25519 functionality including: + - Key generation and parsing + - AIP-80 format compliance + - Digital signature creation and verification + - BCS serialization/deserialization + - Multi-signature operations and validation + - Range checking and error handling + """ def test_private_key_from_str(self): private_key_hex = PrivateKey.from_str( "0x4e5e3be60f4bbd5e98d086d932f3ce779ff4b58da99bf9e5241ae1212a29e5fe", False diff --git a/aptos_sdk/metadata.py b/aptos_sdk/metadata.py index 144cd19..b959089 100644 --- a/aptos_sdk/metadata.py +++ b/aptos_sdk/metadata.py @@ -1,13 +1,164 @@ +""" +Metadata utilities for the Aptos Python SDK. + +This module provides utilities for managing SDK metadata, version information, +and HTTP headers used in API requests to Aptos nodes and services. It ensures +proper identification of the Python SDK in network communications. + +Key Features: +- **Version Detection**: Automatic SDK version detection from package metadata +- **HTTP Headers**: Standard headers for Aptos REST API identification +- **User-Agent**: Proper client identification for analytics and debugging +- **Compliance**: Follows Aptos API client identification standards + +Use Cases: +- REST API client identification +- SDK version reporting and analytics +- Debugging and troubleshooting support +- API rate limiting and client tracking +- User-Agent construction for HTTP requests + +Examples: + Get SDK version header:: + + from aptos_sdk.metadata import Metadata + + # Get the header value for HTTP requests + header_value = Metadata.get_aptos_header_val() + print(f"Client identifier: {header_value}") + # Output: "aptos-python-sdk/1.2.3" + + Use in HTTP requests:: + + import httpx + from aptos_sdk.metadata import Metadata + + # Add SDK identification to HTTP headers + headers = { + Metadata.APTOS_HEADER: Metadata.get_aptos_header_val(), + "Content-Type": "application/json" + } + + # Make request with proper identification + response = httpx.get( + "https://fullnode.devnet.aptoslabs.com/v1", + headers=headers + ) + + Integration with REST clients:: + + # The RestClient automatically includes this header + from aptos_sdk.async_client import RestClient + + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + # Automatically includes x-aptos-client header + +Note: + Version information is automatically detected from the installed package + metadata. If the package is installed in development mode, it may show + a development version identifier. +""" + import importlib.metadata as metadata -# constants +# Package name constant for metadata lookup PACKAGE_NAME = "aptos-sdk" class Metadata: + """Utility class for managing Aptos SDK metadata and HTTP headers. + + This class provides static methods and constants for SDK identification + in HTTP requests to Aptos services. It ensures proper client identification + for analytics, debugging, and API compliance purposes. + + Constants: + APTOS_HEADER: The standard HTTP header name for Aptos client identification + + Examples: + Access header constants:: + + from aptos_sdk.metadata import Metadata + + # Get the header name + header_name = Metadata.APTOS_HEADER + print(f"Header name: {header_name}") + # Output: "x-aptos-client" + + Generate header values:: + + # Get the full header value with version + header_value = Metadata.get_aptos_header_val() + print(f"Header value: {header_value}") + # Output: "aptos-python-sdk/1.2.3" + + Use in custom HTTP clients:: + + import requests + + headers = { + Metadata.APTOS_HEADER: Metadata.get_aptos_header_val() + } + + response = requests.get( + "https://fullnode.mainnet.aptoslabs.com/v1", + headers=headers + ) + + Note: + The metadata class is designed to be used statically and does not + require instantiation. + """ + + # HTTP header name for Aptos client identification APTOS_HEADER = "x-aptos-client" @staticmethod def get_aptos_header_val(): + """Generate the Aptos client header value for HTTP requests. + + This method constructs a standardized client identification string + that includes the SDK name and version. This header is automatically + included in requests made by the Aptos REST clients. + + The header format follows the pattern: "aptos-python-sdk/{version}" + where version is automatically detected from the installed package. + + Returns: + str: Header value in the format "aptos-python-sdk/{version}" + + Examples: + Get version header:: + + >>> from aptos_sdk.metadata import Metadata + >>> header = Metadata.get_aptos_header_val() + >>> print(header) + 'aptos-python-sdk/1.2.3' + + Use in HTTP request:: + + import httpx + + headers = { + "x-aptos-client": Metadata.get_aptos_header_val(), + "Content-Type": "application/json" + } + + async with httpx.AsyncClient() as client: + response = await client.get( + "https://fullnode.devnet.aptoslabs.com/v1", + headers=headers + ) + + Raises: + PackageNotFoundError: If the aptos-sdk package is not properly installed + or metadata cannot be accessed. + + Note: + - Version is automatically detected from package installation + - Development installations may show version as "0.0.0" or similar + - This header is used by Aptos services for analytics and debugging + - The header helps identify Python SDK traffic in server logs + """ version = metadata.version(PACKAGE_NAME) return f"aptos-python-sdk/{version}" diff --git a/aptos_sdk/package_publisher.py b/aptos_sdk/package_publisher.py index ac7fcad..9fdc9d9 100644 --- a/aptos_sdk/package_publisher.py +++ b/aptos_sdk/package_publisher.py @@ -1,6 +1,170 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Move package publishing and deployment utilities for the Aptos blockchain. + +This module provides comprehensive tools for deploying Move smart contracts to the Aptos +blockchain. It handles package compilation, metadata generation, deployment strategies, +and large package management through automated chunking. + +Key Features: +- **Package Publishing**: Deploy Move packages to accounts or objects +- **Large Package Support**: Automatic chunking for packages exceeding transaction limits +- **Object-Based Deployment**: Support for the new object-based code deployment model +- **Package Upgrades**: Upgrading existing deployed packages with compatibility checks +- **Multiple Deployment Modes**: Account-based, object-based, and upgrade modes +- **Deterministic Addresses**: Calculate deployment addresses before publishing + +Deployment Models: + Account-Based Deployment: + - Traditional deployment model where code is stored in an account + - Code is published to the sender's account storage + - Suitable for simple contracts and legacy compatibility + + Object-Based Deployment: + - Modern deployment model using Aptos objects + - Code is stored in a dedicated object with its own address + - Better isolation and more flexible upgrade policies + - Recommended for new packages + + Package Upgrades: + - Update existing deployed packages with new code + - Supports compatibility policies and authorization checks + - Works with both account-based and object-based deployments + +Large Package Handling: + Aptos transactions have a size limit (currently 64KB). This module automatically + detects packages that exceed this limit and uses a chunked publishing strategy: + + 1. **Chunking**: Package data is split into manageable chunks + 2. **Staging**: Chunks are uploaded using the large_packages module + 3. **Assembly**: The final transaction triggers on-chain reassembly + 4. **Publishing**: The complete package is deployed atomically + +Examples: + Basic package deployment:: + + from aptos_sdk.package_publisher import PackagePublisher, PublishMode + from aptos_sdk.async_client import RestClient + from aptos_sdk.account import Account + + # Setup + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + publisher = PackagePublisher(client) + account = Account.load("./deployer_account.json") + + # Deploy package + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./my_move_package" + ) + + # Wait for completion + for txn_hash in txn_hashes: + await client.wait_for_transaction(txn_hash) + + print(f"Package deployed in {len(txn_hashes)} transactions") + + Object-based deployment with address prediction:: + + # Predict deployment address + object_address = await publisher.derive_object_address(account.address()) + print(f"Package will be deployed to: {object_address}") + + # Deploy to object + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./my_package", + publish_mode=PublishMode.OBJECT_DEPLOY + ) + + # Verify deployment + for txn_hash in txn_hashes: + await client.wait_for_transaction(txn_hash) + + Package upgrade workflow:: + + # Identify the object to upgrade + code_object = AccountAddress.from_str("***existing_object_address") + + # Deploy upgrade + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./updated_package", + publish_mode=PublishMode.OBJECT_UPGRADE, + code_object=code_object + ) + + Low-level publishing with custom data:: + + # Read compiled package data + with open("package-metadata.bcs", "rb") as f: + metadata = f.read() + + modules = [] + for module_file in os.listdir("bytecode_modules"): + with open(f"bytecode_modules/{module_file}", "rb") as f: + modules.append(f.read()) + + # Publish directly + txn_hash = await publisher.publish_package(account, metadata, modules) + +Workflow Requirements: + 1. **Compile Package**: Use Move compiler or Aptos CLI to compile source code + 2. **Directory Structure**: Ensure proper build directory layout + 3. **Account Setup**: Have a funded account for transaction fees + 4. **Network Configuration**: Connect to the appropriate Aptos network + +Directory Structure: + Expected package directory layout:: + + my_package/ + ├── Move.toml # Package manifest + ├── sources/ # Move source files + │ ├── module1.move + │ └── module2.move + └── build/ # Compiled artifacts (generated) + └── MyPackage/ + ├── bytecode_modules/ # Compiled .mv files + │ ├── module1.mv + │ └── module2.mv + └── package-metadata.bcs # Package metadata + +Gas Considerations: + - Small packages: ~100,000 gas units + - Large packages: 200,000+ gas units per chunk + - Object deployment: Slightly higher gas costs + - Upgrades: Variable based on compatibility checks + +Security Considerations: + - **Package Verification**: Review all Move code before deployment + - **Upgrade Policies**: Set appropriate upgrade policies in Move.toml + - **Access Control**: Ensure only authorized accounts can upgrade packages + - **Testing**: Thoroughly test packages on devnet/testnet before mainnet + +Error Handling: + Common deployment errors: + - **Compilation Errors**: Fix Move source code issues + - **Missing Files**: Ensure proper build directory structure + - **Insufficient Funds**: Account needs enough APT for gas fees + - **Permission Denied**: Check upgrade authorization for existing packages + - **Network Issues**: Verify connectivity to Aptos network + +Best Practices: + - Use object-based deployment for new packages + - Set conservative upgrade policies + - Test on devnet before mainnet deployment + - Monitor gas usage for cost optimization + - Use descriptive package names and versions + - Document package APIs and upgrade procedures + +Note: + This module requires pre-compiled Move packages. Use the Aptos CLI or Move + compiler to generate the necessary bytecode and metadata files before + attempting to publish. +""" + import os from enum import Enum from typing import List, Optional @@ -32,16 +196,172 @@ class PublishMode(Enum): class PackagePublisher: - """A wrapper around publishing packages.""" + """Move package compilation and deployment manager for Aptos blockchain. + + The PackagePublisher provides a comprehensive interface for compiling and publishing + Move smart contract packages to the Aptos blockchain. It supports various deployment + modes including traditional account-based deployment, object-based deployment, and + package upgrades. + + Key Features: + - **Package Compilation**: Compile Move source code to bytecode + - **Metadata Generation**: Create package metadata for deployment + - **Large Package Support**: Handles packages exceeding transaction size limits + - **Chunked Publishing**: Automatic splitting of large packages across transactions + - **Object Deployment**: Support for object-based code deployment model + - **Package Upgrades**: Upgrading existing deployed packages + + Deployment Modes: + - **Account Deploy**: Traditional deployment to an account (default) + - **Object Deploy**: Deployment to an object (newer model) + - **Object Upgrade**: Upgrading existing object-based packages + + Examples: + Basic package deployment:: + + from aptos_sdk.package_publisher import PackagePublisher + from aptos_sdk.async_client import RestClient + from aptos_sdk.account import Account + + # Create client and publisher + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + publisher = PackagePublisher(client) + + # Deploy a package from a local directory + account = Account.load("./my_account.json") + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./my_move_package" + ) + + # Wait for transactions to complete + for txn_hash in txn_hashes: + await client.wait_for_transaction(txn_hash) + + Object-based deployment:: + + # Deploy to an object instead of an account + from aptos_sdk.package_publisher import PublishMode + + # Deploy as a new object + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./my_package", + publish_mode=PublishMode.OBJECT_DEPLOY + ) + + # Get the deployed object address + object_address = await publisher.derive_object_address(account.address()) + print(f"Package deployed to object: {object_address}") + + Upgrading a package:: + + # Upgrade an existing object-based package + from aptos_sdk.account_address import AccountAddress + + # Address of the existing code object + code_object = AccountAddress.from_str("***abcdef...") + + # Publish the upgrade + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./updated_package", + publish_mode=PublishMode.OBJECT_UPGRADE, + code_object=code_object + ) + + Large package handling:: + + # For packages exceeding transaction size limits + # Chunked publishing happens automatically + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./large_package" + ) + + print(f"Package published in {len(txn_hashes)} transactions") + + Workflow: + 1. Compile Move package (using CLI or other tools) + 2. Create PackagePublisher with RestClient + 3. Call publish_package_in_path with appropriate sender and package path + 4. Monitor transaction hashes for completion + 5. (Optional) Derive object address for object deployments + + Technical Details: + - Packages over ~62KB are automatically chunked across multiple transactions + - Object deployment uses a deterministic address derived from publisher and sequence number + - Package metadata and compiled bytecode modules are read from the build directory + - BCS serialization is used for efficient binary encoding + + Note: + This class requires Move packages to be precompiled with the Move compiler. + The package directory must contain a build subdirectory with compiled artifacts. + """ client: RestClient def __init__(self, client: RestClient): + """Initialize a PackagePublisher with a REST client. + + Creates a new package publisher that uses the provided REST client for + blockchain interactions. The client must be properly configured for + the target network (mainnet, testnet, etc.). + + Args: + client: The RestClient instance to use for blockchain communication + and transaction submission. + + Examples: + Create with default client:: + + from aptos_sdk.async_client import RestClient + + # Create for devnet + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + publisher = PackagePublisher(client) + + Create with custom client configuration:: + + from aptos_sdk.async_client import RestClient, ClientConfig + + # Custom gas settings for large packages + config = ClientConfig( + max_gas_amount=300_000, # Higher gas limit + gas_unit_price=150, # Higher priority + transaction_wait_in_seconds=60 # Longer timeout + ) + + client = RestClient("https://fullnode.mainnet.aptoslabs.com/v1", config) + publisher = PackagePublisher(client) + """ self.client = client async def publish_package( self, sender: Account, package_metadata: bytes, modules: List[bytes] ) -> str: + """Publish a Move package to an account on the Aptos blockchain. + + This method submits a transaction to publish a Move package to the sender's + account. It requires pre-compiled package metadata and module bytecode. + + Args: + sender: The account that will sign and pay for the transaction. + package_metadata: The BCS-encoded package metadata bytes. + modules: List of BCS-encoded bytecode modules. + + Returns: + str: The transaction hash of the submitted transaction. + + Transaction Details: + This calls the 0x1::code::publish_package_txn entry function with the + package metadata and modules as arguments. + + Note: + This is the low-level publish method. Most users should use + publish_package_in_path instead, which handles reading files + and chunking large packages. + """ transaction_arguments = [ TransactionArgument(package_metadata, Serializer.to_bytes), TransactionArgument( @@ -64,6 +384,29 @@ async def publish_package( async def publish_package_to_object( self, sender: Account, package_metadata: bytes, modules: List[bytes] ) -> str: + """Publish a Move package to a new object on the Aptos blockchain. + + This method submits a transaction to publish a Move package to a new object + instead of an account. This uses the object-based code deployment model, + which is newer and provides better isolation. + + Args: + sender: The account that will sign and pay for the transaction. + package_metadata: The BCS-encoded package metadata bytes. + modules: List of BCS-encoded bytecode modules. + + Returns: + str: The transaction hash of the submitted transaction. + + Transaction Details: + This calls the 0x1::object_code_deployment::publish entry function with + the package metadata and modules as arguments. + + Note: + After publishing, you can derive the object address using the + derive_object_address method. Object-based deployment is the recommended + approach for new packages. + """ transaction_arguments = [ TransactionArgument(package_metadata, Serializer.to_bytes), TransactionArgument( @@ -90,6 +433,34 @@ async def upgrade_package_object( modules: List[bytes], object_address: AccountAddress, ) -> str: + """Upgrade an existing object-based Move package. + + This method submits a transaction to upgrade an existing object-based + Move package with new code. The sender must have the appropriate permissions + to upgrade the package (typically, must be the original publisher). + + Args: + sender: The account that will sign and pay for the transaction. + package_metadata: The BCS-encoded package metadata bytes for the upgrade. + modules: List of BCS-encoded bytecode modules for the upgrade. + object_address: The address of the object containing the code to upgrade. + + Returns: + str: The transaction hash of the submitted transaction. + + Transaction Details: + This calls the 0x1::object_code_deployment::upgrade entry function with + the package metadata, modules, and object address as arguments. + + Upgrade Rules: + - The upgrade policy in the original package must allow upgrades + - The sender must be authorized to perform the upgrade + - Module compatibility requirements must be satisfied based on policy + + Note: + This only works for packages deployed with the object-based model. + For traditional account-based packages, use a different upgrade mechanism. + """ transaction_arguments = [ TransactionArgument(package_metadata, Serializer.to_bytes), TransactionArgument( @@ -118,6 +489,73 @@ async def publish_package_in_path( publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY, code_object: Optional[AccountAddress] = None, ) -> List[str]: + """Publish a Move package from a local directory to the Aptos blockchain. + + This high-level method handles reading compiled Move package files from a + directory and publishing them to the blockchain. It automatically determines + if chunked publishing is needed for large packages and supports different + deployment modes. + + Args: + sender: The account that will sign and pay for the transaction(s). + package_dir: Path to the Move package directory containing the compiled build. + Must have a Move.toml file and a build subdirectory with compiled artifacts. + large_package_address: Address of the module that handles large package + publishing (default: predefined MODULE_ADDRESS). + publish_mode: The deployment mode to use (ACCOUNT_DEPLOY, OBJECT_DEPLOY, + or OBJECT_UPGRADE). + code_object: The address of the object to upgrade (required only for + OBJECT_UPGRADE mode). + + Returns: + List[str]: List of transaction hashes. For small packages, this will + contain a single hash. For large packages, it will contain multiple + hashes corresponding to the chunked transactions. + + Raises: + ValueError: If code_object is not provided for OBJECT_UPGRADE mode, + if the publish_mode is invalid, or if required files are missing. + FileNotFoundError: If the package directory or required files don't exist. + + Directory Structure: + The package directory must contain: + - Move.toml: Package manifest file + - build/{package_name}/bytecode_modules/: Compiled module bytecode (.mv files) + - build/{package_name}/package-metadata.bcs: Package metadata file + + Examples: + Publish a package to an account:: + + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./my_move_package" + ) + + Publish a package to an object:: + + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./my_package", + publish_mode=PublishMode.OBJECT_DEPLOY + ) + + # Get the deployed object address + object_address = await publisher.derive_object_address(account.address()) + + Upgrade an existing object-based package:: + + txn_hashes = await publisher.publish_package_in_path( + sender=account, + package_dir="./updated_package", + publish_mode=PublishMode.OBJECT_UPGRADE, + code_object=AccountAddress.from_str("***abcdef...") + ) + + Note: + This method requires the package to be already compiled. It does not + compile the Move source code itself, but reads the compiled artifacts. + Use the Aptos CLI or Move compiler to compile the package first. + """ with open(os.path.join(package_dir, "Move.toml"), "rb") as f: data = tomli.load(f) package = data["package"]["name"] @@ -163,6 +601,37 @@ async def publish_package_in_path( async def derive_object_address( self, publisher_address: AccountAddress ) -> AccountAddress: + """Derive the address of a newly deployed object-based package. + + This method calculates the address where a package will be deployed when + using OBJECT_DEPLOY mode. It uses the publisher's address and next sequence + number to deterministically derive the object address. + + Args: + publisher_address: The address of the account publishing the package. + + Returns: + AccountAddress: The derived address where the package object will be created. + + Examples: + Get the address before deployment:: + + # Calculate where the package will be deployed + object_address = await publisher.derive_object_address(account.address()) + print(f"Package will be deployed to: {object_address}") + + # Deploy the package + await publisher.publish_package_in_path( + sender=account, + package_dir="./my_package", + publish_mode=PublishMode.OBJECT_DEPLOY + ) + + Note: + This method gets the current sequence number from the blockchain and + adds 1 to calculate the next sequence number that will be used for + the deployment transaction. + """ sequence_number = await self.client.account_sequence_number(publisher_address) return self.create_object_deployment_address( publisher_address, sequence_number + 1 @@ -172,6 +641,31 @@ async def derive_object_address( def create_object_deployment_address( creator_address: AccountAddress, creator_sequence_number: int ) -> AccountAddress: + """Calculate the deterministic address for an object-based code deployment. + + This static method computes the address where a package will be deployed + when using object-based deployment. The address is deterministically derived + from the creator's address and sequence number. + + Args: + creator_address: The address of the account creating the object. + creator_sequence_number: The sequence number of the creator account + that will be used for the deployment transaction. + + Returns: + AccountAddress: The deterministic address where the object will be created. + + Technical Details: + The address is derived using a domain-specific seed combining: + - The domain separator "aptos_framework::object_code_deployment" + - The creator's sequence number + - The creator's address + + Note: + This is a low-level method used by derive_object_address. Most users + should use derive_object_address instead, which automatically fetches + the current sequence number from the blockchain. + """ ser = Serializer() ser.to_bytes(OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR) ser.u64(creator_sequence_number) @@ -187,12 +681,39 @@ async def chunked_package_publish( large_package_address: AccountAddress = MODULE_ADDRESS, publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY, ) -> List[str]: - """ - Chunks the package_metadata and modules across as many transactions as necessary. - Each transaction has a base cost and the maximum size is currently 64K, so this chunks - them into 62K + the base transaction size. This should be sufficient for reasonably - optimistic transaction batching. The batching tries to place as much data in a transaction - before moving to the chunk to the next transaction. + """Publish a large package by splitting it across multiple transactions. + + This method handles publishing packages that exceed the transaction size limit + (currently 64KB) by splitting the package data across multiple transactions. + It optimizes the chunking to use as few transactions as possible while staying + within size limits. + + Args: + sender: The account that will sign and pay for the transactions. + package_metadata: The BCS-encoded package metadata bytes. + modules: List of BCS-encoded bytecode modules. + large_package_address: Address of the module that handles large package + publishing (default: predefined MODULE_ADDRESS). + publish_mode: The deployment mode to use (ACCOUNT_DEPLOY, OBJECT_DEPLOY, + or OBJECT_UPGRADE). + + Returns: + List[str]: List of transaction hashes for all the chunked transactions. + + Transaction Batching: + - Each transaction has a conservative 62KB size limit (below the 64KB max) + - Metadata is chunked first, followed by module bytecode + - Data is packed efficiently to minimize the number of transactions + - Transactions are submitted in sequence to maintain ordering + + Technical Details: + The chunked publishing uses the large_package_publisher module to handle + reassembly of the chunks on-chain. This module stores the chunks temporarily + until all chunks are received, then performs the actual deployment. + + Note: + This method is automatically called by publish_package_in_path when needed. + Most users should not need to call this directly. """ # Chunk the metadata and insert it into payloads. The last chunk may be small enough diff --git a/aptos_sdk/secp256k1_ecdsa.py b/aptos_sdk/secp256k1_ecdsa.py index a5bf7b7..c3b754c 100644 --- a/aptos_sdk/secp256k1_ecdsa.py +++ b/aptos_sdk/secp256k1_ecdsa.py @@ -1,6 +1,117 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +secp256k1 ECDSA cryptographic implementation for Aptos blockchain. + +This module provides a complete secp256k1 ECDSA implementation for the Aptos Python SDK, +enabling Ethereum-compatible signature schemes within the Aptos ecosystem. It implements +the asymmetric cryptography interfaces defined in asymmetric_crypto.py with full +BCS serialization support. + +Key Features: +- **Ethereum Compatibility**: Uses the same secp256k1 curve as Ethereum +- **Deterministic Signatures**: RFC 6979 deterministic signing for reproducibility +- **Signature Normalization**: Ensures canonical signatures (s < n/2) +- **AIP-80 Support**: Standard private key formatting and parsing +- **BCS Integration**: Full serialization/deserialization support +- **Production Ready**: Comprehensive test coverage and security best practices + +Cryptographic Properties: +- Curve: secp256k1 (y² = x³ + 7 over finite field) +- Hash Function: Keccak-256 (SHA3-256) +- Key Sizes: 32-byte private keys, 64-byte public keys +- Signature Size: 64 bytes (r, s values) +- Security Level: ~128 bits + +Use Cases: +- Ethereum account migration to Aptos +- Cross-chain application compatibility +- Hardware wallet integration (secp256k1 support) +- Multi-signature schemes requiring secp256k1 +- Legacy system integration + +Comparison with Ed25519: +- Pros: Ethereum compatibility, widespread hardware support +- Cons: Larger signatures, slower verification than Ed25519 +- Usage: Choose Ed25519 for new Aptos-native applications + +Examples: + Basic key generation and signing:: + + from aptos_sdk.secp256k1_ecdsa import PrivateKey + + # Generate a new private key + private_key = PrivateKey.random() + public_key = private_key.public_key() + + # Sign a message + message = b"Hello, Aptos!" + signature = private_key.sign(message) + + # Verify the signature + is_valid = public_key.verify(message, signature) + print(f"Signature valid: {is_valid}") + + Working with hex strings:: + + # Create from hex string + hex_key = "***234abcd..." + private_key = PrivateKey.from_hex(hex_key) + + # Get hex representation + print(f"Private key: {private_key.hex()}") + print(f"Public key: {public_key.hex()}") + print(f"Signature: {signature.hex()}") + + AIP-80 compliant formatting:: + + # AIP-80 formatted private key + aip80_key = "secp256k1-priv-***234abcd..." + private_key = PrivateKey.from_str(aip80_key, strict=True) + + # Convert to AIP-80 format + formatted = private_key.aip80() + print(f"AIP-80 format: {formatted}") + + Serialization for storage/transmission:: + + from aptos_sdk.bcs import Serializer, Deserializer + + # Serialize private key + serializer = Serializer() + private_key.serialize(serializer) + key_bytes = serializer.output() + + # Deserialize private key + deserializer = Deserializer(key_bytes) + restored_key = PrivateKey.deserialize(deserializer) + + assert private_key == restored_key + + Cross-chain compatibility:: + + # Import Ethereum private key + ethereum_key = "***456789abcdef..." + aptos_key = PrivateKey.from_hex(ethereum_key) + + # Same key can be used on both chains + # (though with different address derivation) + eth_style_pubkey = aptos_key.public_key().hex() + +Security Considerations: + - Always use secure random number generation for key creation + - Store private keys securely (encrypted, hardware wallets) + - Verify signatures before trusting signed data + - Be aware of signature malleability (this implementation normalizes) + - Consider key rotation policies for long-term security + - Use deterministic signing to avoid nonce reuse vulnerabilities + +Note: + This implementation uses the ecdsa library for core cryptographic operations + and follows the same security practices as Ethereum's secp256k1 usage. +""" + from __future__ import annotations import hashlib @@ -14,29 +125,143 @@ class PrivateKey(asymmetric_crypto.PrivateKey): + """secp256k1 ECDSA private key implementation. + + This class implements secp256k1 private keys with deterministic signing, + signature normalization, and full compatibility with the Aptos asymmetric + cryptography interfaces. + + Key Properties: + - **Curve**: secp256k1 elliptic curve (same as Bitcoin/Ethereum) + - **Hash Function**: Keccak-256 for all cryptographic operations + - **Key Length**: 32 bytes (256 bits) + - **Deterministic**: Uses RFC 6979 for deterministic signing + - **Normalized**: Ensures canonical signatures with s < n/2 + + Attributes: + LENGTH: The byte length of secp256k1 private keys (32) + key: The underlying ECDSA signing key object + + Examples: + Generate a new private key:: + + private_key = PrivateKey.random() + print(f"New key: {private_key.hex()}") + + Create from existing key material:: + + hex_key = "***234567890abcdef..." + private_key = PrivateKey.from_hex(hex_key) + + Create from AIP-80 format:: + + aip80_key = "secp256k1-priv-***234567890abcdef..." + private_key = PrivateKey.from_str(aip80_key, strict=True) + + Sign and verify:: + + message = b"Important transaction data" + signature = private_key.sign(message) + public_key = private_key.public_key() + + assert public_key.verify(message, signature) + + Note: + Private keys should be generated using cryptographically secure + random number generators and stored securely. + """ LENGTH: int = 32 key: SigningKey def __init__(self, key: SigningKey): + """Initialize a private key with the given ECDSA signing key. + + Args: + key: The ECDSA SigningKey object for secp256k1 operations. + + Example: + This is typically not called directly. Use the factory methods: + >>> private_key = PrivateKey.random() + >>> private_key = PrivateKey.from_hex("***abc123...") + """ self.key = key def __eq__(self, other: object): + """Check equality with another PrivateKey. + + Args: + other: Object to compare with. + + Returns: + True if both private keys are cryptographically equivalent. + + Example: + >>> key1 = PrivateKey.from_hex("***abc123...") + >>> key2 = PrivateKey.from_hex("***abc123...") + >>> key1 == key2 + True + """ if not isinstance(other, PrivateKey): return NotImplemented return self.key == other.key def __str__(self): + """Return the AIP-80 formatted string representation. + + Returns: + AIP-80 compliant private key string with secp256k1-priv- prefix. + + Example: + >>> str(private_key) + 'secp256k1-priv-***234567890abcdef...' + """ return self.aip80() @staticmethod def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: - """ - Parse a HexInput that may be a hex string, bytes, or an AIP-80 compliant string to a private key. - - :param value: A hex string, byte array, or AIP-80 compliant string. - :param strict: If true, the value MUST be compliant with AIP-80. - :return: Parsed private key as bytes. + """Create a private key from hex string, bytes, or AIP-80 format. + + This method parses various input formats and creates a secp256k1 private + key. It handles legacy hex formats and AIP-80 compliant strings. + + Args: + value: Private key in various formats: + - Raw hex string: "***234567890abcdef..." + - Hex with prefix: "***234567890abcdef..." + - Raw bytes: bytes.fromhex("234567890abcdef...") + - AIP-80 format: "secp256k1-priv-***234567890abcdef..." + strict: AIP-80 compliance mode: + - True: Only accept AIP-80 compliant strings + - False: Accept legacy formats without warning + - None: Accept legacy formats with warning + + Returns: + A new secp256k1 PrivateKey instance. + + Raises: + Exception: If the key length is invalid (not 32 bytes). + ValueError: If strict=True and format is not AIP-80 compliant. + + Examples: + From raw hex:: + + key = PrivateKey.from_hex("***234567890abcdef...") + + From AIP-80 format:: + + key = PrivateKey.from_hex( + "secp256k1-priv-***234567890abcdef...", + strict=True + ) + + From bytes:: + + key_bytes = bytes.fromhex("234567890abcdef...") + key = PrivateKey.from_hex(key_bytes) + + Note: + The private key must be exactly 32 bytes (64 hex characters). """ parsed_value = PrivateKey.parse_hex_input( value, asymmetric_crypto.PrivateKeyVariant.Secp256k1, strict @@ -49,33 +274,111 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: @staticmethod def from_str(value: str, strict: bool | None = None) -> PrivateKey: - """ - Parse a HexInput that may be a hex string or an AIP-80 compliant string to a private key. - - :param value: A hex string or AIP-80 compliant string. - :param strict: If true, the value MUST be compliant with AIP-80. - :return: Parsed Secp256k1 private key. + """Create a private key from a hex or AIP-80 compliant string. + + Convenience method that delegates to from_hex() for string inputs. + + Args: + value: Hex string or AIP-80 compliant string. + strict: AIP-80 compliance mode (see from_hex() for details). + + Returns: + A new secp256k1 PrivateKey instance. + + Example: + >>> key = PrivateKey.from_str("secp256k1-priv-***abc123...") + >>> key = PrivateKey.from_str("***abc123...", strict=False) """ return PrivateKey.from_hex(value, strict) def hex(self) -> str: - return f"0x{self.key.to_string().hex()}" + """Get the hexadecimal representation of the private key. + + Returns: + Hex string with '0x' prefix representing the 32-byte private key. + + Example: + >>> private_key.hex() + '***abc123456789def...' + """ + return f"***{self.key.to_string().hex()}" def aip80(self) -> str: + """Get the AIP-80 compliant string representation. + + Returns: + AIP-80 formatted string with secp256k1-priv- prefix. + + Example: + >>> private_key.aip80() + 'secp256k1-priv-***abc123456789def...' + """ return PrivateKey.format_private_key( self.hex(), asymmetric_crypto.PrivateKeyVariant.Secp256k1 ) def public_key(self) -> PublicKey: + """Derive the corresponding public key. + + Returns: + The public key derived from this private key. + + Example: + >>> private_key = PrivateKey.random() + >>> public_key = private_key.public_key() + >>> isinstance(public_key, PublicKey) + True + """ return PublicKey(self.key.verifying_key) @staticmethod def random() -> PrivateKey: + """Generate a new random secp256k1 private key. + + Uses cryptographically secure random number generation to create + a new private key suitable for production use. + + Returns: + A new randomly generated PrivateKey instance. + + Example: + >>> private_key = PrivateKey.random() + >>> len(private_key.key.to_string()) + 32 + + Note: + This method uses the system's secure random number generator. + The generated key is suitable for production cryptographic use. + """ return PrivateKey( SigningKey.generate(curve=SECP256k1, hashfunc=hashlib.sha3_256) ) def sign(self, data: bytes) -> Signature: + """Sign data using this private key with deterministic ECDSA. + + Creates a deterministic signature using RFC 6979, ensuring the same + input always produces the same signature. The signature is normalized + to ensure canonical form (s < n/2) to prevent malleability. + + Args: + data: The data to sign (typically a hash of the actual message). + + Returns: + A normalized secp256k1 signature. + + Example: + >>> message = b"Hello, Aptos!" + >>> signature = private_key.sign(message) + >>> public_key.verify(message, signature) + True + + Note: + - Uses Keccak-256 as the hash function + - Implements RFC 6979 deterministic signing + - Normalizes signatures to prevent malleability attacks + - Same input always produces same signature (deterministic) + """ sig = self.key.sign_deterministic(data, hashfunc=hashlib.sha3_256) n = SECP256k1.generator.order() r, s = util.sigdecode_string(sig, n) @@ -87,6 +390,26 @@ def sign(self, data: bytes) -> Signature: @staticmethod def deserialize(deserializer: Deserializer) -> PrivateKey: + """Deserialize a private key from BCS-encoded bytes. + + Args: + deserializer: BCS deserializer containing the private key bytes. + + Returns: + A new PrivateKey instance from the deserialized data. + + Raises: + Exception: If the key length is not 32 bytes. + + Example: + >>> serializer = Serializer() + >>> original_key.serialize(serializer) + >>> key_bytes = serializer.output() + >>> deserializer = Deserializer(key_bytes) + >>> restored_key = PrivateKey.deserialize(deserializer) + >>> original_key == restored_key + True + """ key = deserializer.to_bytes() if len(key) != PrivateKey.LENGTH: raise Exception("Length mismatch") @@ -94,28 +417,139 @@ def deserialize(deserializer: Deserializer) -> PrivateKey: return PrivateKey(SigningKey.from_string(key, SECP256k1, hashlib.sha3_256)) def serialize(self, serializer: Serializer): + """Serialize the private key to BCS format. + + Args: + serializer: BCS serializer to write the private key bytes to. + + Example: + >>> serializer = Serializer() + >>> private_key.serialize(serializer) + >>> key_bytes = serializer.output() + >>> len(key_bytes) + 32 + """ serializer.to_bytes(self.key.to_string()) class PublicKey(asymmetric_crypto.PublicKey): + """secp256k1 ECDSA public key implementation. + + This class implements secp256k1 public keys for verification of signatures + and address derivation. It follows the common format for secp256k1 public keys + with support for both compressed and uncompressed formats. + + Key Properties: + - **Curve**: secp256k1 elliptic curve (same as Bitcoin/Ethereum) + - **Format**: Uncompressed format with 0x04 prefix + - **Key Length**: 64 bytes (uncompressed without prefix) + - **Serialized Length**: 65 bytes (with prefix) + + Attributes: + LENGTH: The byte length of uncompressed secp256k1 public keys (64) + LENGTH_WITH_PREFIX_LENGTH: Length including 0x04 prefix byte (65) + key: The underlying ECDSA verification key object + + Examples: + Derive from private key:: + + private_key = PrivateKey.random() + public_key = private_key.public_key() + + Create from hex string:: + + # With or without 0x04 prefix + hex_key = "***4..." # 65 bytes with prefix + public_key = PublicKey.from_str(hex_key) + + Verify a signature:: + + message = b"Important message" + signature = private_key.sign(message) + + is_valid = public_key.verify(message, signature) + assert is_valid == True + + Note: + This implementation uses the uncompressed format (65 bytes) for + compatibility with common Ethereum and Bitcoin libraries. + """ LENGTH: int = 64 LENGTH_WITH_PREFIX_LENGTH: int = 65 key: VerifyingKey def __init__(self, key: VerifyingKey): + """Initialize a public key with the given ECDSA verifying key. + + Args: + key: The ECDSA VerifyingKey object for secp256k1 operations. + + Example: + This is typically not called directly. Use factory methods + or derive from a private key: + >>> private_key = PrivateKey.random() + >>> public_key = private_key.public_key() + """ self.key = key def __eq__(self, other: object): + """Check equality with another PublicKey. + + Args: + other: Object to compare with. + + Returns: + True if both public keys are cryptographically equivalent. + + Example: + >>> pk1 = private_key1.public_key() + >>> pk2 = private_key2.public_key() # Different key + >>> pk1 == pk2 + False + """ if not isinstance(other, PublicKey): return NotImplemented return self.key == other.key def __str__(self) -> str: + """Return the hexadecimal string representation. + + Returns: + Hex string representing the public key. + + Example: + >>> str(public_key) + '***4...' # 65 bytes with 0x04 prefix + """ return self.hex() @staticmethod def from_str(value: str) -> PublicKey: + """Create a public key from a hex string. + + Args: + value: Hex string representing the public key. + Can be with or without '0x' prefix. + Can be 64 bytes (raw key) or 65 bytes (with 0x04 prefix). + + Returns: + A new PublicKey instance. + + Raises: + Exception: If the key length is invalid. + + Examples: + From uncompressed format with prefix:: + + # 130 hex chars (65 bytes) with 0x04 prefix + key = PublicKey.from_str("***4210c9129e...") + + From raw format:: + + # 128 hex chars (64 bytes) without prefix + key = PublicKey.from_str("210c9129e...") + """ if value[0:2] == "0x": value = value[2:] # We are measuring hex values which are twice the length of their binary counterpart. @@ -129,9 +563,45 @@ def from_str(value: str) -> PublicKey: ) def hex(self) -> str: - return f"0x04{self.key.to_string().hex()}" + """Get the hexadecimal representation of the public key. + + Returns: + Hex string with '0x04' prefix (uncompressed format). + + Example: + >>> public_key.hex() + '***4210c9129e35337ff5d6488f90f18d842cf...' # 65 bytes with prefix + + Note: + The '0x04' prefix indicates an uncompressed public key format. + """ + return f"***4{self.key.to_string().hex()}" def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: + """Verify a signature against this public key. + + Verifies that the signature was created by the private key + corresponding to this public key when signing the provided data. + + Args: + data: The original data that was signed. + signature: The signature to verify. + + Returns: + True if the signature is valid, False otherwise. + + Example: + >>> message = b"Hello, world!" + >>> signature = private_key.sign(message) + >>> public_key.verify(message, signature) + True + >>> public_key.verify(b"Different message", signature) + False + + Note: + Catches all exceptions during verification and returns False + for any failure, making it safe to use in validation code. + """ try: signature = cast(Signature, signature) self.key.verify(signature.data(), data) @@ -140,10 +610,47 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: return True def to_crypto_bytes(self) -> bytes: + """Get the raw byte representation with prefix for cryptographic use. + + Returns: + 65-byte representation with 0x04 prefix followed by the 64-byte key. + + Example: + >>> key_bytes = public_key.to_crypto_bytes() + >>> len(key_bytes) + 65 + >>> key_bytes[0] == 0x04 + True + + Note: + The 0x04 prefix indicates an uncompressed secp256k1 public key. + """ return b"\x04" + self.key.to_string() @staticmethod def deserialize(deserializer: Deserializer) -> PublicKey: + """Deserialize a public key from BCS-encoded bytes. + + Handles both raw 64-byte keys and 65-byte keys with prefix. + + Args: + deserializer: BCS deserializer containing the public key bytes. + + Returns: + A new PublicKey instance from the deserialized data. + + Raises: + Exception: If the key length is invalid (not 64 or 65 bytes). + + Example: + >>> serializer = Serializer() + >>> original_key.serialize(serializer) + >>> key_bytes = serializer.output() + >>> deserializer = Deserializer(key_bytes) + >>> restored_key = PublicKey.deserialize(deserializer) + >>> original_key == restored_key + True + """ key = deserializer.to_bytes() if len(key) != PublicKey.LENGTH: # Some standards apply an extra byte to represent that this is a 64-byte key @@ -155,30 +662,140 @@ def deserialize(deserializer: Deserializer) -> PublicKey: return PublicKey(VerifyingKey.from_string(key, SECP256k1, hashlib.sha3_256)) def serialize(self, serializer: Serializer): + """Serialize the public key to BCS format with prefix. + + Writes the 65-byte representation (0x04 prefix + 64-byte key). + + Args: + serializer: BCS serializer to write the public key bytes to. + + Example: + >>> serializer = Serializer() + >>> public_key.serialize(serializer) + >>> key_bytes = serializer.output() + >>> len(key_bytes) + 65 + """ serializer.to_bytes(self.to_crypto_bytes()) class Signature(asymmetric_crypto.Signature): + """secp256k1 ECDSA signature implementation. + + This class represents secp256k1 signatures in canonical form (s < n/2) + and provides methods for serialization, deserialization, and comparison. + + Key Properties: + - **Format**: Raw r, s values concatenated (64 bytes total) + - **Normalized**: Uses canonical form with s < n/2 + - **Length**: 64 bytes (32 bytes for r + 32 bytes for s) + + Attributes: + LENGTH: The byte length of secp256k1 signatures (64) + signature: The raw signature bytes + + Examples: + Create from signing:: + + private_key = PrivateKey.random() + message = b"Hello, Aptos!" + signature = private_key.sign(message) + + Create from hex string:: + + sig_hex = "***1234abcd..." + signature = Signature.from_str(sig_hex) + + Verify with public key:: + + public_key = private_key.public_key() + is_valid = public_key.verify(message, signature) + assert is_valid == True + + Note: + Unlike some other secp256k1 implementations, this class uses the + raw r,s format (64 bytes) rather than DER encoding. + """ LENGTH: int = 64 signature: bytes def __init__(self, signature: bytes): + """Initialize a signature with the given raw bytes. + + Args: + signature: The 64-byte signature data (r, s values concatenated). + + Example: + This is typically not called directly. Signatures are usually + created by signing with a private key: + >>> signature = private_key.sign(message) + """ self.signature = signature def __eq__(self, other: object): + """Check equality with another Signature. + + Args: + other: Object to compare with. + + Returns: + True if both signatures contain the same bytes. + + Example: + >>> sig1 = private_key.sign(message) + >>> sig2 = Signature(sig1.data()) # Same data + >>> sig1 == sig2 + True + """ if not isinstance(other, Signature): return NotImplemented return self.signature == other.signature def __str__(self) -> str: + """Return the hexadecimal string representation. + + Returns: + Hex string with '0x' prefix representing the signature. + + Example: + >>> str(signature) + '***c9a34d6...' # 64 bytes + """ return self.hex() def hex(self) -> str: - return f"0x{self.signature.hex()}" + """Get the hexadecimal representation of the signature. + + Returns: + Hex string with '0x' prefix representing the 64-byte signature. + + Example: + >>> signature.hex() + '***a1b2c3d4...' # 64 bytes as hex + """ + return f"***{self.signature.hex()}" @staticmethod def from_str(value: str) -> Signature: + """Create a signature from a hex string. + + Args: + value: Hex string representing the signature. + Can be with or without '0x' prefix. + Must be exactly 64 bytes (128 hex characters). + + Returns: + A new Signature instance. + + Raises: + Exception: If the signature length is invalid. + + Example: + >>> sig = Signature.from_str("***a1b2c3d4...") + >>> len(sig.data()) + 64 + """ if value[0:2] == "0x": value = value[2:] if len(value) != Signature.LENGTH * 2: @@ -186,10 +803,40 @@ def from_str(value: str) -> Signature: return Signature(bytes.fromhex(value)) def data(self) -> bytes: + """Get the raw signature bytes. + + Returns: + The 64-byte raw signature data. + + Example: + >>> raw_bytes = signature.data() + >>> len(raw_bytes) + 64 + """ return self.signature @staticmethod def deserialize(deserializer: Deserializer) -> Signature: + """Deserialize a signature from BCS-encoded bytes. + + Args: + deserializer: BCS deserializer containing the signature bytes. + + Returns: + A new Signature instance from the deserialized data. + + Raises: + Exception: If the signature length is not 64 bytes. + + Example: + >>> serializer = Serializer() + >>> original_sig.serialize(serializer) + >>> sig_bytes = serializer.output() + >>> deserializer = Deserializer(sig_bytes) + >>> restored_sig = Signature.deserialize(deserializer) + >>> original_sig == restored_sig + True + """ signature = deserializer.to_bytes() if len(signature) != Signature.LENGTH: raise Exception("Length mismatch") @@ -197,6 +844,18 @@ def deserialize(deserializer: Deserializer) -> Signature: return Signature(signature) def serialize(self, serializer: Serializer): + """Serialize the signature to BCS format. + + Args: + serializer: BCS serializer to write the signature bytes to. + + Example: + >>> serializer = Serializer() + >>> signature.serialize(serializer) + >>> sig_bytes = serializer.output() + >>> len(sig_bytes) + 64 + """ serializer.to_bytes(self.signature) diff --git a/aptos_sdk/transaction_worker.py b/aptos_sdk/transaction_worker.py index 50d46aa..bb47d9a 100644 --- a/aptos_sdk/transaction_worker.py +++ b/aptos_sdk/transaction_worker.py @@ -21,15 +21,123 @@ class TransactionWorker: - """ - The TransactionWorker provides a simple framework for receiving payloads to be processed. It - acquires new sequence numbers and calls into the callback to produce a signed transaction, and - then submits the transaction. In another task, it waits for resolution of the submission - process or get pre-execution validation error. - - Note: This is not a particularly robust solution, as it lacks any framework to handle failed - transactions with functionality like retries or checking whether the framework is online. - This is the responsibility of a higher-level framework. + """High-throughput transaction processing framework for Aptos blockchain. + + The TransactionWorker provides an asynchronous framework for processing large volumes + of transactions efficiently. It manages sequence number coordination, transaction + generation, submission, and result tracking through separate concurrent tasks. + + Architecture: + - **Sequence Management**: Automatically acquires sequential transaction numbers + - **Concurrent Submission**: Submits transactions asynchronously for high throughput + - **Batch Processing**: Processes transaction results in batches for efficiency + - **Error Handling**: Captures and reports transaction submission errors + - **Queue-Based**: Uses asyncio queues for task coordination + + Key Features: + - **High Performance**: Designed for bulk transaction processing + - **Sequence Safety**: Ensures proper transaction ordering + - **Non-blocking**: Asynchronous operation doesn't block the caller + - **Error Tracking**: Comprehensive error reporting and exception handling + - **Result Monitoring**: Track transaction outcomes and failures + + Workflow: + 1. **Start**: Initialize worker tasks for submission and processing + 2. **Generate**: Transaction generator creates signed transactions with sequence numbers + 3. **Submit**: Submit transactions to the blockchain via REST client + 4. **Track**: Monitor submission results and errors + 5. **Process**: Batch process results for efficiency + 6. **Stop**: Clean shutdown of worker tasks + + Examples: + Basic transaction worker:: + + import asyncio + from aptos_sdk.async_client import RestClient + from aptos_sdk.account import Account + from aptos_sdk.transaction_worker import TransactionWorker + + async def transaction_generator(account, sequence_number): + # Create transfer transaction + recipient = Account.generate().address() + return await client.create_bcs_signed_transaction( + account, transfer_payload, sequence_number=sequence_number + ) + + async def bulk_transfers(): + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + sender = Account.generate() + + # Create and start worker + worker = TransactionWorker(sender, client, transaction_generator) + worker.start() + + try: + # Process transaction results + for _ in range(100): # Process 100 transactions + seq_num, tx_hash, error = await worker.next_processed_transaction() + if error: + print(f"Transaction {seq_num} failed: {error}") + else: + print(f"Transaction {seq_num} succeeded: {tx_hash}") + finally: + worker.stop() + + Token distribution example:: + + async def token_generator(account, sequence_number): + # Distribute tokens to random recipients + recipients = [Account.generate().address() for _ in range(10)] + recipient = random.choice(recipients) + + return await client.transfer_transaction( + account, recipient, 1000, sequence_number=sequence_number + ) + + # Process 1000 token distributions + worker = TransactionWorker(distributor_account, client, token_generator) + worker.start() + + success_count = 0 + for _ in range(1000): + seq_num, tx_hash, error = await worker.next_processed_transaction() + if not error: + success_count += 1 + + print(f"Successfully distributed tokens in {success_count} transactions") + worker.stop() + + Performance Considerations: + - **Batch Size**: Processes multiple transactions concurrently + - **Memory Usage**: Queues consume memory; monitor for large workloads + - **Network Limits**: Respects node rate limits and connection pooling + - **Sequence Coordination**: May wait for sequence numbers under high load + + Limitations: + - **No Retry Logic**: Failed transactions are not automatically retried + - **No Health Monitoring**: Doesn't check node health or connectivity + - **Basic Error Handling**: Errors are reported but not automatically resolved + - **Single Account**: Designed for single account use (sequence number coordination) + + Error Handling: + Transaction errors are captured and reported through the result queue. + Common error scenarios: + - Network connectivity issues + - Insufficient account balance + - Transaction validation failures + - Node overload or rate limiting + + Thread Safety: + The TransactionWorker is designed for single-threaded async use. + Don't share instances across multiple async contexts. + + Note: + This is a basic framework suitable for development and testing. + Production systems should implement additional features like: + - Retry logic with exponential backoff + - Health monitoring and circuit breakers + - Metrics collection and monitoring + - Graceful degradation strategies """ _account: Account @@ -53,6 +161,50 @@ def __init__( [Account, int], typing.Awaitable[SignedTransaction] ], ): + """Initialize a TransactionWorker for high-throughput transaction processing. + + Creates a transaction worker that will use the provided account for signing + transactions and submit them through the REST client. The transaction generator + function is called to create each transaction with the appropriate sequence number. + + Args: + account: The Account to use for signing transactions. Must have sufficient + balance for the transactions being generated. + rest_client: RestClient instance for submitting transactions to the blockchain. + transaction_generator: Async function that takes (Account, int) and returns + a SignedTransaction. This function is called for each transaction with + the next available sequence number. + + Examples: + Simple transfer generator:: + + async def transfer_generator(account, seq_num): + recipient = Account.generate().address() + return await client.create_bcs_signed_transaction( + account, + transfer_payload(recipient, 1000), + sequence_number=seq_num + ) + + worker = TransactionWorker(account, client, transfer_generator) + + Complex transaction generator:: + + async def complex_generator(account, seq_num): + # Randomly choose transaction type + if random.random() < 0.5: + # Token transfer + return create_transfer_txn(account, seq_num) + else: + # Smart contract interaction + return create_contract_txn(account, seq_num) + + worker = TransactionWorker(account, client, complex_generator) + + Note: + The worker is initialized but not started. Call start() to begin + processing transactions. + """ self._account = account self._account_sequence_number = AccountSequenceNumber( rest_client, account.address() @@ -69,6 +221,23 @@ def __init__( self._processed_transactions = asyncio.Queue() def address(self) -> AccountAddress: + """Get the address of the account used by this transaction worker. + + Returns: + AccountAddress: The address of the account that signs transactions. + + Examples: + Check worker account:: + + worker = TransactionWorker(account, client, generator) + print(f"Worker using account: {worker.address()}") + + Verify account balance:: + + worker_address = worker.address() + balance = await client.account_balance(worker_address) + print(f"Worker account balance: {balance} APT") + """ return self._account.address() async def _submit_transactions(self): @@ -132,10 +301,103 @@ async def _process_transactions(self): async def next_processed_transaction( self, ) -> typing.Tuple[int, typing.Optional[str], typing.Optional[Exception]]: + """Get the next processed transaction result from the worker. + + This method blocks until a transaction result is available. Results include + both successful submissions (with transaction hash) and failures (with error). + + Returns: + Tuple containing: + - int: The sequence number of the processed transaction + - Optional[str]: Transaction hash if successful, None if failed + - Optional[Exception]: Exception if failed, None if successful + + Examples: + Process results sequentially:: + + worker.start() + + while True: + seq_num, tx_hash, error = await worker.next_processed_transaction() + + if error: + print(f"Transaction {seq_num} failed: {error}") + else: + print(f"Transaction {seq_num} succeeded: {tx_hash}") + + Batch processing with timeout:: + + import asyncio + + results = [] + timeout_seconds = 30 + + try: + while len(results) < expected_count: + result = await asyncio.wait_for( + worker.next_processed_transaction(), + timeout=timeout_seconds + ) + results.append(result) + except asyncio.TimeoutError: + print(f"Timeout after {timeout_seconds}s, got {len(results)} results") + + Error handling:: + + seq_num, tx_hash, error = await worker.next_processed_transaction() + + if error: + if "insufficient balance" in str(error).lower(): + print("Account needs more funds") + elif "rate limit" in str(error).lower(): + print("Being rate limited, slow down") + else: + print(f"Unexpected error: {error}") + + Note: + This method will block indefinitely if no more transactions are being + processed. Make sure to call stop() when done to clean up resources. + """ return await self._processed_transactions.get() def stop(self): - """Stop the tasks for managing transactions""" + """Stop the transaction worker and cancel all background tasks. + + This method gracefully shuts down the transaction worker by canceling + the background tasks for transaction submission and processing. Any + pending transactions will be canceled. + + Raises: + Exception: If the worker hasn't been started yet or is already stopped. + + Examples: + Proper shutdown:: + + worker = TransactionWorker(account, client, generator) + worker.start() + + try: + # Process transactions... + pass + finally: + worker.stop() # Always clean up + + Context manager pattern:: + + async def process_with_worker(): + worker = TransactionWorker(account, client, generator) + worker.start() + + try: + # Do work... + yield worker + finally: + worker.stop() + + Note: + After calling stop(), the worker cannot be restarted. Create a new + TransactionWorker instance if you need to resume processing. + """ if not self._started: raise Exception("Start not yet called") if self._stopped: @@ -146,7 +408,49 @@ def stop(self): self._process_transactions_task.cancel() def start(self): - """Begin the tasks for managing transactions""" + """Start the transaction worker background tasks. + + This method begins the asynchronous tasks for transaction submission and + processing. The worker will start generating, submitting, and tracking + transactions immediately after this call. + + Raises: + Exception: If the worker has already been started. + + Examples: + Basic startup:: + + worker = TransactionWorker(account, client, generator) + worker.start() + + # Worker is now processing transactions + # Get results with next_processed_transaction() + + Startup with immediate processing:: + + worker = TransactionWorker(account, client, generator) + worker.start() + + # Start consuming results immediately + asyncio.create_task(process_results(worker)) + + Error handling:: + + try: + worker.start() + except Exception as e: + print(f"Failed to start worker: {e}") + # Handle startup failure + + Background Tasks: + Starting the worker creates two background tasks: + - **Submission Task**: Generates and submits transactions + - **Processing Task**: Processes submission results in batches + + Note: + The worker must be started before calling next_processed_transaction(). + Always pair start() with stop() for proper resource cleanup. + """ if self._started: raise Exception("Already started") self._started = True @@ -160,7 +464,74 @@ def start(self): class TransactionQueue: - """Provides a queue model for pushing transactions into the TransactionWorker.""" + """Queue-based transaction payload manager for TransactionWorker integration. + + The TransactionQueue provides a simple interface for feeding transaction payloads + to a TransactionWorker. It acts as a bridge between application logic that creates + transaction payloads and the worker that needs signed transactions. + + Key Features: + - **Async Queue**: Built on asyncio.Queue for efficient async operations + - **Payload Management**: Handles raw transaction payloads before signing + - **Worker Integration**: Designed to work seamlessly with TransactionWorker + - **Backpressure**: Built-in flow control through queue size limits + + Examples: + Basic queue usage:: + + from aptos_sdk.transaction_worker import TransactionQueue + from aptos_sdk.transactions import EntryFunction, TransactionArgument + + # Create queue and connect to worker + queue = TransactionQueue(rest_client) + worker = TransactionWorker(account, rest_client, queue.next) + + # Push transaction payloads + transfer_payload = EntryFunction.natural( + "***::aptos_account", + "transfer", + [], + [recipient_address, amount] + ) + + await queue.push(transfer_payload) + + Batch operations:: + + # Push multiple payloads + payloads = [ + create_transfer_payload(addr, 1000) + for addr in recipient_addresses + ] + + for payload in payloads: + await queue.push(payload) + + # Worker will process them automatically + + Custom payload generation:: + + async def generate_payloads(): + for i in range(1000): + payload = create_custom_payload(i) + await queue.push(payload) + + # Start payload generation and worker processing + asyncio.create_task(generate_payloads()) + worker.start() + + Integration Pattern: + The typical usage pattern is: + 1. Create TransactionQueue with REST client + 2. Create TransactionWorker with account and queue.next as generator + 3. Push payloads to queue with push() + 4. Worker automatically consumes payloads and creates signed transactions + + Note: + The queue uses unbounded storage by default. For high-volume applications, + consider implementing backpressure or queue size limits to prevent + memory exhaustion. + """ _client: RestClient _outstanding_transactions: asyncio.Queue diff --git a/aptos_sdk/transactions.py b/aptos_sdk/transactions.py index a68d92e..f83da5d 100644 --- a/aptos_sdk/transactions.py +++ b/aptos_sdk/transactions.py @@ -2,7 +2,193 @@ # SPDX-License-Identifier: Apache-2.0 """ -This translates Aptos transactions to and from BCS for signing and submitting to the REST API. +Aptos blockchain transaction construction, signing, and serialization. + +This module provides comprehensive functionality for creating, signing, and serializing +transactions on the Aptos blockchain. It handles the complete transaction lifecycle +from creation to submission-ready format with Binary Canonical Serialization (BCS). + +Key Features: +- **Transaction Construction**: Build raw transactions with various payload types +- **Multi-Signature Support**: Handle single-signer, multi-agent, and fee-payer transactions +- **Cryptographic Signing**: Support for Ed25519 and Secp256k1 ECDSA signatures +- **BCS Serialization**: Efficient binary encoding for network transmission +- **Payload Types**: Scripts, entry functions, and multi-signature operations +- **Transaction Validation**: Built-in verification and integrity checking +- **Gas Management**: Comprehensive gas pricing and limit handling + +Transaction Types: +- **RawTransaction**: Basic unsigned transaction with sender, payload, and gas parameters +- **SignedTransaction**: Fully signed transaction ready for blockchain submission +- **MultiAgentRawTransaction**: Transactions requiring multiple signers +- **FeePayerRawTransaction**: Transactions where a different account pays fees +- **Transaction Payloads**: Various execution types (scripts, entry functions) + +Architecture: + Transaction Flow:: + + 1. Create RawTransaction with payload and parameters + 2. Sign with appropriate private key(s) → SignedTransaction + 3. Serialize to BCS format for network transmission + 4. Submit to Aptos REST API + 5. Monitor execution and results + +Examples: + Basic transfer transaction:: + + from aptos_sdk.transactions import RawTransaction, EntryFunction + from aptos_sdk.account import Account + from aptos_sdk.account_address import AccountAddress + + # Create accounts + sender = Account.generate() + recipient = Account.generate().address() + + # Create transfer payload + transfer_payload = EntryFunction.natural( + "***::aptos_account", + "transfer", + [], + [recipient, 1_000_000] # 1 APT in octas + ) + + # Build raw transaction + raw_txn = RawTransaction( + sender=sender.address(), + sequence_number=0, + payload=transfer_payload, + max_gas_amount=100_000, + gas_unit_price=100, + expiration_timestamps_secs=int(time.time()) + 600, # 10 minutes + chain_id=1 # Mainnet + ) + + # Sign transaction + signed_txn = SignedTransaction(raw_txn, sender.sign_transaction(raw_txn)) + + Multi-agent transaction:: + + from aptos_sdk.transactions import MultiAgentRawTransaction + + # Create multi-agent transaction + multi_agent_txn = MultiAgentRawTransaction( + raw_transaction=raw_txn, + secondary_signer_addresses=[ + account2.address(), + account3.address() + ] + ) + + # Collect signatures from all signers + signatures = [ + account1.sign_transaction(multi_agent_txn), + account2.sign_transaction(multi_agent_txn), + account3.sign_transaction(multi_agent_txn) + ] + + # Create multi-agent signed transaction + signed_txn = SignedTransaction( + multi_agent_txn, + MultiAgentAuthenticator(signatures[0], signatures[1:]) + ) + + Fee-payer transaction:: + + from aptos_sdk.transactions import FeePayerRawTransaction + + # Transaction where fee_payer pays gas instead of sender + fee_payer_txn = FeePayerRawTransaction( + raw_transaction=raw_txn, + secondary_signer_addresses=[], + fee_payer_address=fee_payer.address() + ) + + # Both sender and fee payer must sign + sender_sig = sender.sign_transaction(fee_payer_txn) + fee_payer_sig = fee_payer.sign_transaction(fee_payer_txn) + + signed_txn = SignedTransaction( + fee_payer_txn, + FeePayerAuthenticator(sender_sig, [], fee_payer_sig) + ) + + Script execution:: + + from aptos_sdk.transactions import Script, TransactionArgument + + # Execute Move script with arguments + script_payload = Script( + code=compiled_script_bytes, + type_arguments=[], + arguments=[ + TransactionArgument(recipient, Serializer.struct), + TransactionArgument(amount, Serializer.u64) + ] + ) + + raw_txn = RawTransaction( + sender.address(), sequence_num, script_payload, + max_gas, gas_price, expiration, chain_id + ) + +Payload Types: + Entry Functions: + - Most common transaction type + - Call public functions in published Move modules + - Type-safe with automatic argument serialization + + Scripts: + - Execute arbitrary Move bytecode + - More flexible but requires compilation + - Deprecated in favor of entry functions + + Module Publishing: + - Deploy Move modules to the blockchain + - Requires compilation and metadata + +Gas Economics: + Transaction gas consists of: + - **Execution Cost**: Computation and storage operations + - **IO Cost**: Reading/writing blockchain state + - **Storage Cost**: Persistent data storage fees + + Gas Parameters: + - **max_gas_amount**: Maximum gas units willing to pay + - **gas_unit_price**: Price per gas unit in octas (1 APT = 10^8 octas) + - **Total Cost**: max_gas_amount × gas_unit_price (maximum) + +Security Considerations: + - **Sequence Numbers**: Must match account sequence to prevent replay + - **Expiration Times**: Prevent indefinite transaction validity + - **Chain ID**: Prevents cross-chain replay attacks + - **Signature Verification**: Cryptographic proof of authorization + - **Gas Limits**: Prevent infinite execution loops + +BCS Serialization: + Binary Canonical Serialization ensures: + - **Deterministic Encoding**: Same data always produces same bytes + - **Compact Format**: Efficient network transmission + - **Cross-Language**: Compatible across different SDK implementations + - **Integrity**: Hash-based verification of serialized data + +Error Handling: + Common transaction errors: + - **SEQUENCE_NUMBER_TOO_OLD**: Transaction sequence is behind account sequence + - **SEQUENCE_NUMBER_TOO_NEW**: Gap in sequence numbers + - **TRANSACTION_EXPIRED**: Past expiration timestamp + - **INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE**: Not enough funds for gas + - **INVALID_SIGNATURE**: Cryptographic signature verification failed + +Performance: + - **Batch Operations**: Use TransactionWorker for high throughput + - **Gas Optimization**: Right-size gas parameters to avoid overpayment + - **Payload Efficiency**: Prefer entry functions over scripts + - **Signature Schemes**: Ed25519 is faster than Secp256k1 for verification + +Note: + This module handles the low-level transaction mechanics. For higher-level + operations, consider using RestClient.transfer() or other convenience methods + that handle transaction construction automatically. """ from __future__ import annotations @@ -89,6 +275,136 @@ def deserialize(deserializer: Deserializer) -> RawTransactionWithData: class RawTransaction(Deserializable, RawTransactionInternal, Serializable): + """Represents a raw (unsigned) transaction on the Aptos blockchain. + + A RawTransaction contains all the essential information needed to execute a transaction + on the Aptos blockchain, except for cryptographic signatures. It serves as the foundation + for all transaction types and must be signed to become a valid SignedTransaction. + + Components: + - **Sender**: Account address initiating the transaction + - **Sequence Number**: Prevents replay attacks and ensures ordering + - **Payload**: The actual operation to execute (entry function, script, etc.) + - **Gas Parameters**: Maximum gas amount and price per unit + - **Expiration**: Timestamp after which transaction becomes invalid + - **Chain ID**: Network identifier preventing cross-chain attacks + + Transaction Lifecycle: + 1. **Construction**: Create RawTransaction with all parameters + 2. **Validation**: Verify parameters are valid and consistent + 3. **Signing**: Apply cryptographic signatures to create SignedTransaction + 4. **Serialization**: Convert to BCS format for network transmission + 5. **Submission**: Send to Aptos REST API for execution + + Examples: + Basic token transfer:: + + from aptos_sdk.transactions import RawTransaction, EntryFunction + from aptos_sdk.account import Account + import time + + # Setup + sender = Account.generate() + recipient = Account.generate().address() + + # Create transfer payload + payload = EntryFunction.natural( + "***::aptos_account", + "transfer", + [], + [recipient, 1_000_000] # 1 APT + ) + + # Build transaction + raw_txn = RawTransaction( + sender=sender.address(), + sequence_number=5, # Next sequence for sender account + payload=payload, + max_gas_amount=100_000, # Maximum gas units + gas_unit_price=100, # 100 octas per gas unit + expiration_timestamps_secs=int(time.time() + 600), # 10 min + chain_id=1 # Mainnet + ) + + # Sign and create final transaction + authenticator = sender.sign_transaction(raw_txn) + signed_txn = SignedTransaction(raw_txn, authenticator) + + Smart contract interaction:: + + # Call custom module function + contract_payload = EntryFunction.natural( + "***abc123::my_module", + "custom_function", + ["***::aptos_coin::AptosCoin"], # Type arguments + [1000, "hello world", True] # Function arguments + ) + + raw_txn = RawTransaction( + sender.address(), seq_num, contract_payload, + 200_000, 150, expiration, chain_id + ) + + Script execution (legacy):: + + from aptos_sdk.transactions import Script, TransactionArgument + + script_payload = Script( + code=compiled_move_bytecode, + type_arguments=[], + arguments=[ + TransactionArgument(recipient, Serializer.struct), + TransactionArgument(amount, Serializer.u64) + ] + ) + + raw_txn = RawTransaction( + sender.address(), seq_num, script_payload, + gas_limit, gas_price, expiration, chain_id + ) + + Gas Economics: + Gas calculation:: + + # Total maximum cost in octas + max_cost = max_gas_amount * gas_unit_price + + # Example: 100,000 gas units at 100 octas/unit = 10,000,000 octas + # That's 0.1 APT maximum (since 1 APT = 100,000,000 octas) + + Gas recommendations: + - **Simple transfers**: 100,000 gas units + - **Smart contracts**: 200,000-500,000 gas units + - **Complex operations**: 1,000,000+ gas units + - **Gas price**: 100-150 octas per unit (check network conditions) + + Security Considerations: + - **Sequence Number**: Must exactly match next expected sequence for sender + - **Expiration Time**: Should be reasonable (10-60 minutes) to prevent stale transactions + - **Chain ID**: Must match target network (1=mainnet, 2=testnet, etc.) + - **Gas Limits**: Set appropriately to avoid failed transactions or overpayment + - **Payload Validation**: Ensure payload parameters are correct and safe + + Validation Rules: + - Sender address must be valid and exist on-chain + - Sequence number must be >= current account sequence + - Gas parameters must be positive and reasonable + - Expiration must be in the future + - Chain ID must match target network + - Payload must be properly constructed and valid + + Performance Notes: + - Transaction size affects gas cost + - Complex payloads require more gas + - Ed25519 signatures are faster to verify than Secp256k1 + - BCS serialization is optimized for minimal size + + Note: + RawTransaction is immutable once created. Any modifications require + creating a new instance. This ensures transaction integrity and + prevents accidental modifications after signing. + """ + # Sender's address sender: AccountAddress # Sequence number of this transaction. This must match the sequence number in the sender's @@ -115,6 +431,123 @@ def __init__( expiration_timestamps_secs: int, chain_id: int, ): + """Initialize a RawTransaction with all required parameters. + + Creates a new raw transaction that can be signed and submitted to the Aptos + blockchain. All parameters are validated for basic correctness but full + validation occurs during signing and submission. + + Args: + sender: Account address that will sign and send this transaction. + Must be a valid 32-byte Aptos address. + sequence_number: Transaction sequence number for the sender account. + Must match the next expected sequence number for replay protection. + payload: The transaction payload defining what operation to execute. + Can be EntryFunction, Script, or ModuleBundle. + max_gas_amount: Maximum gas units willing to spend for execution. + Must be positive and sufficient for the operation. + gas_unit_price: Price to pay per gas unit in octas. + Current network rates typically range from 100-150 octas. + expiration_timestamps_secs: Unix timestamp when transaction expires. + Should be reasonable future time (10-60 minutes from now). + chain_id: Identifier for the target Aptos network. + 1=mainnet, 2=testnet, varies for custom networks. + + Raises: + ValueError: If any parameters are invalid or inconsistent. + TypeError: If parameters are not the expected types. + + Examples: + Simple transfer transaction:: + + import time + from aptos_sdk.transactions import RawTransaction, EntryFunction + + # Create transfer payload + payload = EntryFunction.natural( + "***::aptos_account", "transfer", [], + [recipient_address, 1_000_000] + ) + + # Build raw transaction + raw_txn = RawTransaction( + sender=alice.address(), + sequence_number=42, + payload=payload, + max_gas_amount=100_000, + gas_unit_price=100, + expiration_timestamps_secs=int(time.time()) + 600, + chain_id=1 + ) + + Smart contract call:: + + # Contract interaction with type arguments + contract_call = EntryFunction.natural( + "***deadbeef::defi_module", + "swap_tokens", + ["***::aptos_coin::AptosCoin", "***::test_coin::TestCoin"], + [input_amount, min_output_amount, slippage_tolerance] + ) + + raw_txn = RawTransaction( + sender=trader.address(), + sequence_number=get_next_sequence(trader), + payload=contract_call, + max_gas_amount=500_000, # Higher gas for complex operations + gas_unit_price=150, # Higher price for faster execution + expiration_timestamps_secs=int(time.time()) + 300, # 5 minutes + chain_id=1 + ) + + Batch operation setup:: + + # Create multiple transactions with sequential sequence numbers + base_sequence = await client.account_sequence_number(sender.address()) + + transactions = [] + for i, recipient in enumerate(recipients): + payload = create_transfer_payload(recipient, amounts[i]) + + raw_txn = RawTransaction( + sender=sender.address(), + sequence_number=base_sequence + i, + payload=payload, + max_gas_amount=100_000, + gas_unit_price=100, + expiration_timestamps_secs=int(time.time()) + 1800, # 30 min + chain_id=chain_id + ) + transactions.append(raw_txn) + + Parameter Guidelines: + Sequence Numbers: + - Must be exactly next expected sequence for sender + - Get current sequence from: client.account_sequence_number(sender) + - Increment by 1 for each subsequent transaction + + Gas Parameters: + - max_gas_amount: Start with 100,000 for simple operations + - Increase for complex smart contract interactions + - Monitor actual gas usage and adjust accordingly + - gas_unit_price: Check network congestion and adjust + + Expiration Times: + - Not too short: Allow time for network processing + - Not too long: Prevent stale transactions + - Typical range: 5-60 minutes from creation + - Consider network conditions and urgency + + Chain IDs: + - Mainnet: 1 + - Testnet: 2 + - Devnet: Varies + - Custom networks: Check with network operator + + Note: + Once created, RawTransaction instances are immutable. This prevents + accidental modification after creation and ensures signature validity. + """ self.sender = sender self.sequence_number = sequence_number self.payload = payload @@ -124,6 +557,12 @@ def __init__( self.chain_id = chain_id def __eq__(self, other: object) -> bool: + """ + Check equality between two RawTransaction instances. + + :param other: The other object to compare with + :return: True if transactions are equal, False otherwise + """ if not isinstance(other, RawTransaction): return NotImplemented return ( @@ -148,6 +587,11 @@ def __str__(self): """ def prehash(self) -> bytes: + """ + Generate the prehash for this transaction type. + + :return: SHA3-256 hash of the transaction type identifier + """ hasher = hashlib.sha3_256() hasher.update(b"APTOS::RawTransaction") return hasher.digest() @@ -175,11 +619,24 @@ def serialize(self, serializer: Serializer) -> None: class MultiAgentRawTransaction(RawTransactionWithData): + """ + A multi-agent transaction that requires signatures from multiple accounts. + + This is used when a transaction needs to be authorized by more than just + the sender account. + """ + secondary_signers: List[AccountAddress] def __init__( self, raw_transaction: RawTransaction, secondary_signers: List[AccountAddress] ): + """ + Initialize a multi-agent transaction. + + :param raw_transaction: The underlying raw transaction + :param secondary_signers: Additional accounts that must sign this transaction + """ self.raw_transaction = raw_transaction self.secondary_signers = secondary_signers @@ -206,6 +663,12 @@ def deserialize_inner(deserializer: Deserializer) -> MultiAgentRawTransaction: class FeePayerRawTransaction(RawTransactionWithData): + """ + A transaction where fees can be paid by a different account than the sender. + + This allows for sponsored transactions where a third party pays the gas fees. + """ + secondary_signers: List[AccountAddress] fee_payer: Optional[AccountAddress] @@ -215,6 +678,13 @@ def __init__( secondary_signers: List[AccountAddress], fee_payer: Optional[AccountAddress], ): + """ + Initialize a fee payer transaction. + + :param raw_transaction: The underlying raw transaction + :param secondary_signers: Additional accounts that must sign this transaction + :param fee_payer: The account that will pay the transaction fees (optional) + """ self.raw_transaction = raw_transaction self.secondary_signers = secondary_signers self.fee_payer = fee_payer @@ -250,6 +720,12 @@ def deserialize_inner(deserializer: Deserializer) -> FeePayerRawTransaction: class TransactionPayload: + """ + Represents the payload of a transaction - what the transaction will execute. + + Can be a Script, ModuleBundle, or EntryFunction (most common). + """ + SCRIPT: int = 0 MODULE_BUNDLE: int = 1 SCRIPT_FUNCTION: int = 2 @@ -258,6 +734,12 @@ class TransactionPayload: value: Any def __init__(self, payload: Any): + """ + Initialize a transaction payload. + + :param payload: The payload object (Script, ModuleBundle, or EntryFunction) + :raises Exception: If payload type is not supported + """ if isinstance(payload, Script): self.variant = TransactionPayload.SCRIPT elif isinstance(payload, ModuleBundle): @@ -422,6 +904,12 @@ def __str__(self): class EntryFunction: + """ + Represents a call to an entry function in a Move module. + + Entry functions are the most common way to execute code on Aptos. + """ + module: ModuleId function: str ty_args: List[TypeTag] @@ -430,6 +918,14 @@ class EntryFunction: def __init__( self, module: ModuleId, function: str, ty_args: List[TypeTag], args: List[bytes] ): + """ + Initialize an entry function. + + :param module: The module identifier containing the function + :param function: The name of the function to call + :param ty_args: Type arguments for the function call + :param args: Encoded arguments to pass to the function + """ self.module = module self.function = function self.ty_args = ty_args @@ -456,6 +952,15 @@ def natural( ty_args: List[TypeTag], args: List[TransactionArgument], ) -> EntryFunction: + """ + Create an EntryFunction from natural string representation. + + :param module: Module name in string format (e.g., "***::coin") + :param function: Function name + :param ty_args: Type arguments for the function + :param args: Transaction arguments to be encoded + :return: A new EntryFunction instance + """ module_id = ModuleId.from_str(module) byte_args = [] @@ -479,10 +984,22 @@ def serialize(self, serializer: Serializer) -> None: class ModuleId: + """ + Identifies a Move module by its address and name. + + Modules are the fundamental units of code organization in Move. + """ + address: AccountAddress name: str def __init__(self, address: AccountAddress, name: str): + """ + Initialize a ModuleId. + + :param address: The account address where the module is deployed + :param name: The name of the module + """ self.address = address self.name = name @@ -496,6 +1013,12 @@ def __str__(self) -> str: @staticmethod def from_str(module_id: str) -> ModuleId: + """ + Parse a ModuleId from a string representation. + + :param module_id: String in format "address::module_name" + :return: A new ModuleId instance + """ split = module_id.split("::") return ModuleId(AccountAddress.from_str(split[0]), split[1]) @@ -511,6 +1034,12 @@ def serialize(self, serializer: Serializer) -> None: class TransactionArgument: + """ + Represents an argument to pass to a transaction function. + + Encapsulates a value and its encoding function for BCS serialization. + """ + value: Any encoder: Callable[[Serializer, Any], None] @@ -519,16 +1048,33 @@ def __init__( value: Any, encoder: Callable[[Serializer, Any], None], ): + """ + Initialize a transaction argument. + + :param value: The value to be passed as an argument + :param encoder: Function to encode the value for BCS serialization + """ self.value = value self.encoder = encoder def encode(self) -> bytes: + """ + Encode this argument using BCS serialization. + + :return: The BCS-encoded bytes representation of the argument + """ ser = Serializer() self.encoder(ser, self.value) return ser.output() class SignedTransaction: + """ + A transaction that has been signed and is ready for submission to the blockchain. + + Contains both the raw transaction data and the cryptographic proof of authorization. + """ + transaction: RawTransaction authenticator: Authenticator @@ -537,6 +1083,12 @@ def __init__( transaction: RawTransaction, authenticator: Union[AccountAuthenticator, Authenticator], ): + """ + Initialize a signed transaction. + + :param transaction: The raw transaction to be signed + :param authenticator: The authenticator containing the signature(s) + """ self.transaction = transaction if isinstance(authenticator, AccountAuthenticator): if ( @@ -561,11 +1113,21 @@ def __str__(self) -> str: return f"Transaction: {self.transaction}Authenticator: {self.authenticator}" def bytes(self) -> bytes: + """ + Get the BCS serialized bytes of this signed transaction. + + :return: The serialized transaction bytes + """ ser = Serializer() ser.struct(self) return ser.output() def verify(self) -> bool: + """ + Verify that this transaction's signatures are valid. + + :return: True if all signatures are valid, False otherwise + """ auth = self.authenticator.authenticator if isinstance(auth, MultiAgentAuthenticator): transaction: RawTransactionInternal = MultiAgentRawTransaction( diff --git a/aptos_sdk/type_tag.py b/aptos_sdk/type_tag.py index b53ec5a..1c48f2f 100644 --- a/aptos_sdk/type_tag.py +++ b/aptos_sdk/type_tag.py @@ -1,6 +1,48 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Type tag definitions for Move types in the Aptos Python SDK. + +This module provides type representations for the Move language type system. +Move is Aptos' smart contract programming language, and this module contains +Python representations of Move's primitive and composite types. + +The module includes: +- TypeTag: Root class for all Move type representations +- Primitive type tags for basic Move types (bool, u8, u16, u32, u64, u128, u256, address) +- StructTag: Representation of custom Move structs with generic type parameters +- Serialization/deserialization support for all type tags + +Examples: + Creating and using primitive type tags:: + + # Create a u64 type tag + u64_tag = TypeTag(U64Tag(1234567890)) + + # Create a boolean type tag + bool_tag = TypeTag(BoolTag(True)) + + Creating struct type tags:: + + # Create a simple struct tag + struct_tag = StructTag( + AccountAddress.from_str("0x1"), + "coin", + "Coin", + [TypeTag(U64Tag(0))] # Generic type parameter + ) + + # Parse from string format + parsed = StructTag.from_str("0x1::coin::Coin") + + Serialization:: + + # All type tags support BCS serialization + serialized = struct_tag.to_bytes() + deserialized = StructTag.from_bytes(serialized) +""" + from __future__ import annotations import typing @@ -12,7 +54,45 @@ class TypeTag(Deserializable, Serializable): - """TypeTag represents a primitive in Move.""" + """Root class representing Move language types in Aptos. + + TypeTag is a discriminated union that can contain any Move type, including + primitive types (bool, integers, addresses) and complex types (structs, vectors). + Each TypeTag wraps a specific type tag implementation and provides a unified + interface for type operations. + + The discriminator values correspond to Move's type system: + + Attributes: + BOOL: Discriminator for boolean types (0) + U8: Discriminator for 8-bit unsigned integers (1) + U64: Discriminator for 64-bit unsigned integers (2) + U128: Discriminator for 128-bit unsigned integers (3) + ACCOUNT_ADDRESS: Discriminator for account addresses (4) + SIGNER: Discriminator for signer types (5) - not implemented + VECTOR: Discriminator for vector types (6) - not implemented + STRUCT: Discriminator for custom struct types (7) + U16: Discriminator for 16-bit unsigned integers (8) + U32: Discriminator for 32-bit unsigned integers (9) + U256: Discriminator for 256-bit unsigned integers (10) + value: The wrapped type tag implementation + + Examples: + Creating type tags for primitives:: + + bool_type = TypeTag(BoolTag(True)) + u64_type = TypeTag(U64Tag(12345)) + address_type = TypeTag(AccountAddressTag( + AccountAddress.from_str("0x1") + )) + + Creating struct type tags:: + + struct_type = TypeTag(StructTag( + AccountAddress.from_str("0x1"), + "coin", "Coin", [] + )) + """ BOOL: int = 0 U8: int = 1 @@ -29,9 +109,22 @@ class TypeTag(Deserializable, Serializable): value: typing.Any def __init__(self, value: typing.Any): + """Initialize a TypeTag with a specific type implementation. + + Args: + value: The type tag implementation (e.g., BoolTag, U64Tag, StructTag) + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another TypeTag. + + Args: + other: The object to compare with. + + Returns: + True if both TypeTags represent the same type and value. + """ if not isinstance(other, TypeTag): return NotImplemented return ( @@ -39,13 +132,35 @@ def __eq__(self, other: object) -> bool: ) def __str__(self): + """Get string representation of the type tag. + + Returns: + String representation of the underlying type. + """ return self.value.__str__() def __repr__(self): + """Get detailed string representation for debugging. + + Returns: + String representation of the type tag. + """ return self.__str__() @staticmethod def deserialize(deserializer: Deserializer) -> TypeTag: + """Deserialize a TypeTag from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized TypeTag instance. + + Raises: + NotImplementedError: If the type variant is not supported + (SIGNER, VECTOR) or unknown. + """ variant = deserializer.uleb128() if variant == TypeTag.BOOL: return TypeTag(BoolTag.deserialize(deserializer)) @@ -72,223 +187,503 @@ def deserialize(deserializer: Deserializer) -> TypeTag: raise NotImplementedError def serialize(self, serializer: Serializer): + """Serialize this TypeTag to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.uleb128(self.value.variant()) serializer.struct(self.value) class BoolTag(Deserializable, Serializable): + """Type tag for Move boolean values. + + Represents the Move `bool` primitive type, which can hold true or false values. + + Attributes: + value: The boolean value this tag represents. + + Examples: + Creating and using BoolTag:: + + true_tag = BoolTag(True) + false_tag = BoolTag(False) + + # Serialize/deserialize + serialized = true_tag.to_bytes() + deserialized = BoolTag.from_bytes(serialized) + """ value: bool def __init__(self, value: bool): + """Initialize a BoolTag with a boolean value. + + Args: + value: The boolean value to wrap. + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another BoolTag. + + Args: + other: The object to compare with. + + Returns: + True if both tags represent the same boolean value. + """ if not isinstance(other, BoolTag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the boolean value. + + Returns: + String representation ("True" or "False"). + """ return self.value.__str__() def variant(self): + """Get the type discriminator for this tag. + + Returns: + The BOOL type discriminator. + """ return TypeTag.BOOL @staticmethod def deserialize(deserializer: Deserializer) -> BoolTag: + """Deserialize a BoolTag from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized BoolTag instance. + """ return BoolTag(deserializer.bool()) def serialize(self, serializer: Serializer): + """Serialize this BoolTag to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.bool(self.value) class U8Tag(Deserializable, Serializable): + """Type tag for Move 8-bit unsigned integer values. + + Represents the Move `u8` primitive type, which holds unsigned 8-bit integers + in the range 0-255. + + Attributes: + value: The u8 integer value this tag represents. + + Examples: + Creating and using U8Tag:: + + tag = U8Tag(255) # Maximum u8 value + + # Serialize/deserialize + serialized = tag.to_bytes() + deserialized = U8Tag.from_bytes(serialized) + """ value: int def __init__(self, value: int): + """Initialize a U8Tag with an 8-bit unsigned integer. + + Args: + value: The u8 value to wrap (0-255). + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another U8Tag. + + Args: + other: The object to compare with. + + Returns: + True if both tags represent the same u8 value. + """ if not isinstance(other, U8Tag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the u8 value. + + Returns: + String representation of the integer value. + """ return self.value.__str__() def variant(self): + """Get the type discriminator for this tag. + + Returns: + The U8 type discriminator. + """ return TypeTag.U8 @staticmethod def deserialize(deserializer: Deserializer) -> U8Tag: + """Deserialize a U8Tag from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized U8Tag instance. + """ return U8Tag(deserializer.u8()) def serialize(self, serializer: Serializer): + """Serialize this U8Tag to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ serializer.u8(self.value) class U16Tag(Deserializable, Serializable): + """Type tag for Move 16-bit unsigned integer values. + + Represents the Move `u16` primitive type, which holds unsigned 16-bit integers + in the range 0-65535. + + Attributes: + value: The u16 integer value this tag represents. + """ value: int def __init__(self, value: int): + """Initialize a U16Tag with a 16-bit unsigned integer. + + Args: + value: The u16 value to wrap (0-65535). + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another U16Tag.""" if not isinstance(other, U16Tag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the u16 value.""" return self.value.__str__() def variant(self): + """Get the type discriminator for this tag.""" return TypeTag.U16 @staticmethod def deserialize(deserializer: Deserializer) -> U16Tag: + """Deserialize a U16Tag from a BCS byte stream.""" return U16Tag(deserializer.u16()) def serialize(self, serializer: Serializer): + """Serialize this U16Tag to a BCS byte stream.""" serializer.u16(self.value) class U32Tag(Deserializable, Serializable): + """Type tag for Move 32-bit unsigned integer values. + + Represents the Move `u32` primitive type, which holds unsigned 32-bit integers + in the range 0-4294967295. + + Attributes: + value: The u32 integer value this tag represents. + """ value: int def __init__(self, value: int): + """Initialize a U32Tag with a 32-bit unsigned integer. + + Args: + value: The u32 value to wrap (0-4294967295). + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another U32Tag.""" if not isinstance(other, U32Tag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the u32 value.""" return self.value.__str__() def variant(self): + """Get the type discriminator for this tag.""" return TypeTag.U32 @staticmethod def deserialize(deserializer: Deserializer) -> U32Tag: + """Deserialize a U32Tag from a BCS byte stream.""" return U32Tag(deserializer.u32()) def serialize(self, serializer: Serializer): + """Serialize this U32Tag to a BCS byte stream.""" serializer.u32(self.value) class U64Tag(Deserializable, Serializable): + """Type tag for Move 64-bit unsigned integer values. + + Represents the Move `u64` primitive type, which holds unsigned 64-bit integers + in the range 0-18446744073709551615. + + Attributes: + value: The u64 integer value this tag represents. + """ value: int def __init__(self, value: int): + """Initialize a U64Tag with a 64-bit unsigned integer. + + Args: + value: The u64 value to wrap (0-18446744073709551615). + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another U64Tag.""" if not isinstance(other, U64Tag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the u64 value.""" return self.value.__str__() def variant(self): + """Get the type discriminator for this tag.""" return TypeTag.U64 @staticmethod def deserialize(deserializer: Deserializer) -> U64Tag: + """Deserialize a U64Tag from a BCS byte stream.""" return U64Tag(deserializer.u64()) def serialize(self, serializer: Serializer): + """Serialize this U64Tag to a BCS byte stream.""" serializer.u64(self.value) class U128Tag(Deserializable, Serializable): + """Type tag for Move 128-bit unsigned integer values. + + Represents the Move `u128` primitive type, which holds unsigned 128-bit integers + in the range 0-340282366920938463463374607431768211455. + + Attributes: + value: The u128 integer value this tag represents. + """ value: int def __init__(self, value: int): + """Initialize a U128Tag with a 128-bit unsigned integer. + + Args: + value: The u128 value to wrap. + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another U128Tag.""" if not isinstance(other, U128Tag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the u128 value.""" return self.value.__str__() def variant(self): + """Get the type discriminator for this tag.""" return TypeTag.U128 @staticmethod def deserialize(deserializer: Deserializer) -> U128Tag: + """Deserialize a U128Tag from a BCS byte stream.""" return U128Tag(deserializer.u128()) def serialize(self, serializer: Serializer): + """Serialize this U128Tag to a BCS byte stream.""" serializer.u128(self.value) class U256Tag(Deserializable, Serializable): + """Type tag for Move 256-bit unsigned integer values. + + Represents the Move `u256` primitive type, which holds unsigned 256-bit integers. + + Attributes: + value: The u256 integer value this tag represents. + """ value: int def __init__(self, value: int): + """Initialize a U256Tag with a 256-bit unsigned integer. + + Args: + value: The u256 value to wrap. + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another U256Tag.""" if not isinstance(other, U256Tag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the u256 value.""" return self.value.__str__() def variant(self): + """Get the type discriminator for this tag.""" return TypeTag.U256 @staticmethod def deserialize(deserializer: Deserializer) -> U256Tag: + """Deserialize a U256Tag from a BCS byte stream.""" return U256Tag(deserializer.u256()) def serialize(self, serializer: Serializer): + """Serialize this U256Tag to a BCS byte stream.""" serializer.u256(self.value) class AccountAddressTag(Deserializable, Serializable): + """Type tag for Move address values. + + Represents the Move `address` primitive type, which holds account addresses + used to identify accounts and resources on the Aptos blockchain. + + Attributes: + value: The AccountAddress value this tag represents. + + Examples: + Creating and using AccountAddressTag:: + + addr = AccountAddress.from_str("0x1") + tag = AccountAddressTag(addr) + + # Serialize/deserialize + serialized = tag.to_bytes() + deserialized = AccountAddressTag.from_bytes(serialized) + """ value: AccountAddress def __init__(self, value: AccountAddress): + """Initialize an AccountAddressTag with an account address. + + Args: + value: The AccountAddress to wrap. + """ self.value = value def __eq__(self, other: object) -> bool: + """Check equality with another AccountAddressTag.""" if not isinstance(other, AccountAddressTag): return NotImplemented return self.value == other.value def __str__(self): + """Get string representation of the address value.""" return self.value.__str__() def variant(self): + """Get the type discriminator for this tag.""" return TypeTag.ACCOUNT_ADDRESS @staticmethod def deserialize(deserializer: Deserializer) -> AccountAddressTag: + """Deserialize an AccountAddressTag from a BCS byte stream.""" return AccountAddressTag(deserializer.struct(AccountAddress)) def serialize(self, serializer: Serializer): + """Serialize this AccountAddressTag to a BCS byte stream.""" serializer.struct(self.value) class StructTag(Deserializable, Serializable): + """Type tag for Move struct types. + + Represents custom Move struct types, which are user-defined composite types + that can have generic type parameters. StructTags fully specify a struct + type including its location (address and module), name, and type arguments. + + Attributes: + address: The account address where the module is published. + module: The name of the module containing the struct. + name: The name of the struct. + type_args: List of type arguments for generic structs. + + Examples: + Creating struct tags:: + + # Simple struct without generics + struct_tag = StructTag( + AccountAddress.from_str("0x1"), + "account", "Account", [] + ) + + # Generic struct with type parameters + coin_tag = StructTag( + AccountAddress.from_str("0x1"), + "coin", "Coin", + [TypeTag(StructTag( + AccountAddress.from_str("0x1"), + "aptos_coin", "AptosCoin", [] + ))] + ) + + Parsing from string:: + + tag = StructTag.from_str("0x1::coin::Coin<0x1::aptos_coin::AptosCoin>") + print(tag) # "0x1::coin::Coin<0x1::aptos_coin::AptosCoin>" + """ address: AccountAddress module: str name: str type_args: List[TypeTag] def __init__(self, address, module, name, type_args): + """Initialize a StructTag. + + Args: + address: The account address where the struct's module is published. + module: The name of the module containing the struct. + name: The name of the struct. + type_args: List of type arguments for generic type parameters. + """ self.address = address self.module = module self.name = name self.type_args = type_args def __eq__(self, other: object) -> bool: + """Check equality with another StructTag. + + Args: + other: The object to compare with. + + Returns: + True if both StructTags represent the same struct type. + """ if not isinstance(other, StructTag): return NotImplemented return ( @@ -299,6 +694,13 @@ def __eq__(self, other: object) -> bool: ) def __str__(self) -> str: + """Get the canonical string representation of this struct type. + + The format is: address::module::name + + Returns: + String representation of the struct type. + """ value = f"{self.address}::{self.module}::{self.name}" if len(self.type_args) > 0: value += f"<{self.type_args[0]}" @@ -309,10 +711,40 @@ def __str__(self) -> str: @staticmethod def from_str(type_tag: str) -> StructTag: + """Parse a StructTag from its string representation. + + Args: + type_tag: String representation of a struct type, e.g., + "0x1::coin::Coin<0x1::aptos_coin::AptosCoin>" + + Returns: + The parsed StructTag instance. + + Examples: + Parsing simple and complex struct types:: + + simple = StructTag.from_str("0x1::account::Account") + + nested = StructTag.from_str( + "0x1::coin::Coin<0x1::aptos_coin::AptosCoin>" + ) + """ return StructTag._from_str_internal(type_tag, 0)[0][0].value @staticmethod def _from_str_internal(type_tag: str, index: int) -> Tuple[List[TypeTag], int]: + """Internal recursive parser for struct type strings. + + This method handles the complex parsing of nested generic types, + including proper handling of angle brackets and comma separators. + + Args: + type_tag: The string to parse. + index: Current parsing position. + + Returns: + Tuple of (parsed type tags list, new index position). + """ name = "" tags = [] inner_tags: List[TypeTag] = [] @@ -357,10 +789,23 @@ def _from_str_internal(type_tag: str, index: int) -> Tuple[List[TypeTag], int]: return (tags, index) def variant(self): + """Get the type discriminator for this tag. + + Returns: + The STRUCT type discriminator. + """ return TypeTag.STRUCT @staticmethod def deserialize(deserializer: Deserializer) -> StructTag: + """Deserialize a StructTag from a BCS byte stream. + + Args: + deserializer: The BCS deserializer to read from. + + Returns: + The deserialized StructTag instance. + """ address = deserializer.struct(AccountAddress) module = deserializer.str() name = deserializer.str() @@ -368,6 +813,11 @@ def deserialize(deserializer: Deserializer) -> StructTag: return StructTag(address, module, name, type_args) def serialize(self, serializer: Serializer): + """Serialize this StructTag to a BCS byte stream. + + Args: + serializer: The BCS serializer to write to. + """ self.address.serialize(serializer) serializer.str(self.module) serializer.str(self.name) @@ -375,6 +825,11 @@ def serialize(self, serializer: Serializer): class Test(unittest.TestCase): + """Test suite for type tag functionality. + + Tests parsing, serialization, and string representation of complex + nested struct types with multiple levels of generic type parameters. + """ def test_nested_structs(self): l0 = "0x0::l0::L0" l10 = "0x1::l10::L10" diff --git a/examples/__init__.py b/examples/__init__.py index e69de29..0104349 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -0,0 +1,91 @@ +""" +Aptos Python SDK Examples - Comprehensive tutorials and sample code. + +This package contains example scripts and tutorials demonstrating how to use +the Aptos Python SDK for various blockchain operations. The examples are +designed to be educational and serve as starting points for developers +building applications on the Aptos blockchain. + +Example Categories: + + **Basic Operations**: + - hello_blockchain.py: Complete smart contract deployment and interaction + - fee_payer_transfer_coin.py: Sponsored transaction demonstrations + - common.py: Shared configuration and utilities + + **Token Management**: + - aptos_token.py: NFT creation and management using Token Objects + + **Advanced Features**: + - multisig.py: Multi-signature transaction handling + - multikey.py: Multi-key authentication examples + - large_package_publisher.py: Publishing large Move packages + + **Testing and Integration**: + - integration_test.py: Comprehensive SDK testing suite + +Quick Start: + Most examples can be run directly from the command line:: + + # Basic blockchain interaction + python -m examples.hello_blockchain ***contract_address*** + + # NFT operations + python -m examples.aptos_token + + # Multi-signature transactions + python -m examples.multisig + + Or imported and used programmatically:: + + import asyncio + from examples.hello_blockchain import main, publish_contract + + async def run_example(): + contract_addr = await publish_contract("./my_contract") + await main(contract_addr) + + asyncio.run(run_example()) + +Configuration: + All examples use environment variables for network configuration. + See examples.common for details on customizing endpoints:: + + import os + # Switch to testnet + os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" + os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" + + # Now run any example + from examples import hello_blockchain + +Prerequisites: + - Python 3.8+ with asyncio support + - Aptos CLI installed (for Move compilation) + - Network connectivity to Aptos nodes + - For mainnet: real APT tokens for transaction fees + +Learning Path: + 1. **Start with hello_blockchain.py** - covers the basics of account + management, contract deployment, and blockchain interaction + 2. **Explore aptos_token.py** - learn NFT creation and token operations + 3. **Try fee_payer_transfer_coin.py** - understand sponsored transactions + 4. **Advanced examples** - multisig, multikey for complex scenarios + +Safety: + - All examples default to devnet for safety + - Private keys are generated randomly and not persisted + - No real value transactions unless explicitly configured for mainnet + - Smart contracts are deployed to test networks only + +Support: + - Each example includes comprehensive documentation + - Error handling examples and troubleshooting guides + - Comments explain Aptos-specific concepts and patterns + - Links to relevant Aptos documentation and resources + +Note: + These examples are for educational purposes. Production applications + should implement additional security measures, error handling, and + testing appropriate for their specific use cases. +""" diff --git a/examples/common.py b/examples/common.py index 9d69f17..6c84733 100644 --- a/examples/common.py +++ b/examples/common.py @@ -1,22 +1,120 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Common configuration and utilities for Aptos Python SDK examples. + +This module provides shared constants and configuration settings used across +all example scripts in the Aptos Python SDK. It centralizes network endpoints, +authentication settings, and file paths to ensure consistency and easy +configuration management. + +Key Features: +- **Environment-Based Configuration**: All settings can be overridden via environment variables +- **Multi-Network Support**: Supports devnet, testnet, and mainnet configurations +- **Development Flexibility**: Easy switching between different Aptos networks +- **Authentication Management**: Centralized faucet authentication handling + +Environment Variables: + APTOS_CORE_PATH: Path to the aptos-core repository for development + APTOS_FAUCET_URL: URL of the Aptos faucet service for funding accounts + FAUCET_AUTH_TOKEN: Authentication token for faucet requests (if required) + APTOS_INDEXER_URL: URL of the Aptos GraphQL indexer service + APTOS_NODE_URL: URL of the Aptos REST API node endpoint + +Network Configurations: + Devnet (Default): + - Node: https://api.devnet.aptoslabs.com/v1 + - Faucet: https://faucet.devnet.aptoslabs.com + - Indexer: https://api.devnet.aptoslabs.com/v1/graphql + + Testnet: + - Node: https://api.testnet.aptoslabs.com/v1 + - Faucet: https://faucet.testnet.aptoslabs.com + - Indexer: https://api.testnet.aptoslabs.com/v1/graphql + + Mainnet: + - Node: https://api.mainnet.aptoslabs.com/v1 + - Faucet: N/A (no public faucet on mainnet) + - Indexer: https://api.mainnet.aptoslabs.com/v1/graphql + +Usage Examples: + Using default devnet configuration:: + + from examples.common import NODE_URL, FAUCET_URL + from aptos_sdk.async_client import RestClient, FaucetClient + + # Connect to devnet by default + rest_client = RestClient(NODE_URL) + faucet_client = FaucetClient(FAUCET_URL, rest_client) + + Switching to testnet:: + + import os + os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" + os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" + + # Now imports will use testnet URLs + from examples.common import NODE_URL, FAUCET_URL + + Using with authentication token:: + + import os + os.environ["FAUCET_AUTH_TOKEN"] = "your_faucet_token_here" + + from examples.common import FAUCET_URL, FAUCET_AUTH_TOKEN + from aptos_sdk.async_client import FaucetClient, RestClient + + rest_client = RestClient(NODE_URL) + faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) + + Development with local aptos-core:: + + import os + os.environ["APTOS_CORE_PATH"] = "/path/to/your/aptos-core" + + from examples.common import APTOS_CORE_PATH + # Use APTOS_CORE_PATH for local Move package compilation + +Note: + - All examples default to devnet for safety and ease of use + - Mainnet usage requires real APT tokens and careful consideration + - Faucet authentication tokens may be required for some networks + - The indexer URL is used for GraphQL queries and advanced data access +""" + import os import os.path +# Path to the aptos-core repository for local development +# Used for accessing Move examples and local blockchain setup APTOS_CORE_PATH = os.getenv( "APTOS_CORE_PATH", os.path.abspath("./aptos-core"), ) + # :!:>section_1 +# Network Configuration - All can be overridden via environment variables + +# Aptos faucet service URL for funding test accounts +# Default: Devnet faucet (provides free APT tokens for testing) FAUCET_URL = os.getenv( "APTOS_FAUCET_URL", "https://faucet.devnet.aptoslabs.com", ) + +# Optional authentication token for faucet requests +# Required for some faucet configurations or rate limit increases FAUCET_AUTH_TOKEN = os.getenv("FAUCET_AUTH_TOKEN") + +# Aptos GraphQL indexer service URL for advanced queries +# Provides indexed blockchain data with powerful query capabilities INDEXER_URL = os.getenv( "APTOS_INDEXER_URL", "https://api.devnet.aptoslabs.com/v1/graphql", ) + +# Aptos REST API node endpoint URL +# Primary interface for blockchain interactions (transactions, queries, etc.) NODE_URL = os.getenv("APTOS_NODE_URL", "https://api.devnet.aptoslabs.com/v1") # <:!:section_1 diff --git a/examples/hello_blockchain.py b/examples/hello_blockchain.py index a33b99a..ebe9cd1 100644 --- a/examples/hello_blockchain.py +++ b/examples/hello_blockchain.py @@ -2,15 +2,161 @@ # SPDX-License-Identifier: Apache-2.0 """ -This example depends on the hello_blockchain.move module having already been published to the destination blockchain. - -One method to do so is to use the CLI: - * Acquire the Aptos CLI - * `cd ~` - * `aptos init` - * `cd ~/aptos-core/aptos-move/move-examples/hello_blockchain` - * `aptos move publish --named-addresses hello_blockchain=${your_address_from_aptos_init}` - * `python -m examples.hello-blockchain ${your_address_from_aptos_init}` +Hello Blockchain - Complete example of smart contract deployment and interaction. + +This example demonstrates the complete workflow of deploying and interacting with +a Move smart contract on the Aptos blockchain using the Python SDK. It showcases +account management, contract deployment, transaction submission, and state querying. + +Features Demonstrated: +- **Account Creation**: Generate new accounts programmatically +- **Faucet Integration**: Fund accounts with test APT tokens +- **Smart Contract Deployment**: Compile and publish Move modules +- **Contract Interaction**: Call smart contract functions +- **State Management**: Read and update on-chain resources +- **Transaction Management**: Submit, wait for, and verify transactions +- **Custom Client**: Extend RestClient with domain-specific methods + +Smart Contract Overview: + The hello_blockchain Move module provides a simple message storage system: + - **Resource**: `MessageHolder` - stores a string message per account + - **Function**: `set_message(message: String)` - sets or updates the message + - **Access**: Messages are stored per account and publicly readable + +Workflow: + 1. **Setup Phase**: Create accounts and fund them from the faucet + 2. **Deployment Phase**: Compile and publish the Move smart contract + 3. **Interaction Phase**: Call contract functions to store and retrieve messages + 4. **Verification Phase**: Query blockchain state to verify changes + +Prerequisites: + Before running this example, deploy the hello_blockchain Move module: + + Using Aptos CLI:: + + # Install Aptos CLI if not already installed + curl -fsSL "https://aptos.dev/scripts/install_cli.py" | python3 + + # Initialize your account + aptos init + + # Navigate to the Move example + cd ~/aptos-core/aptos-move/move-examples/hello_blockchain + + # Publish the module (replace with your address) + aptos move publish --named-addresses hello_blockchain=***your_address*** + + Using this script:: + + # Option 1: Use the publish_contract function + contract_addr = await publish_contract("./path/to/hello_blockchain") + + # Option 2: Run with existing contract + python -m examples.hello_blockchain ***contract_address*** + +Usage Examples: + Run with existing contract:: + + python -m examples.hello_blockchain ***0x123abc...*** + + Programmatic usage:: + + import asyncio + from examples.hello_blockchain import main, publish_contract + from aptos_sdk.account_address import AccountAddress + + # Deploy and run + async def run_example(): + # Option 1: Deploy new contract + contract_addr = await publish_contract("./hello_blockchain") + await main(contract_addr) + + # Option 2: Use existing contract + existing_addr = AccountAddress.from_str("***0x123...***") + await main(existing_addr) + + asyncio.run(run_example()) + + Custom network configuration:: + + import os + # Switch to testnet + os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" + os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" + + # Run example on testnet + python -m examples.hello_blockchain ***contract_address*** + +Expected Output: + The script will display: + - Account addresses for Alice and Bob + - Initial account balances after funding + - Message storage and retrieval for both accounts + - Transaction hashes and confirmations + + Example output:: + + === Addresses === + Alice: ***0xabc123... + Bob: ***0xdef456... + + === Initial Balances === + Alice: 10000000 + Bob: 10000000 + + === Testing Alice === + Initial value: None + Setting the message to "Hello, Blockchain" + New value: {'message': 'Hello, Blockchain', 'message_change_events': {...}} + + === Testing Bob === + Initial value: None + Setting the message to "Hello, Blockchain" + New value: {'message': 'Hello, Blockchain', 'message_change_events': {...}} + +Move Smart Contract Structure: + The hello_blockchain.move file should contain:: + + module hello_blockchain::message { + use std::string::String; + use std::signer; + + struct MessageHolder has key { + message: String, + } + + public entry fun set_message(account: &signer, message: String) { + let account_addr = signer::address_of(account); + if (!exists(account_addr)) { + move_to(account, MessageHolder { message }); + } else { + let old_holder = borrow_global_mut(account_addr); + old_holder.message = message; + } + } + } + +Error Handling: + Common issues and solutions: + - **Missing Contract**: Ensure the Move module is deployed first + - **Network Issues**: Check NODE_URL and FAUCET_URL configuration + - **Insufficient Funds**: Verify faucet funding was successful + - **Transaction Failures**: Check gas fees and account sequence numbers + - **Compilation Errors**: Verify Aptos CLI installation and Move.toml + +Security Notes: + - This example uses devnet/testnet only (safe for experimentation) + - Private keys are generated randomly and not persisted + - All transactions are publicly visible on the blockchain + - Smart contracts are immutable once deployed + +Learning Objectives: + After running this example, you should understand: + 1. How to create and fund Aptos accounts programmatically + 2. How to deploy Move smart contracts from Python + 3. How to interact with deployed contracts using entry functions + 4. How to query on-chain resources and verify state changes + 5. How to extend RestClient for domain-specific functionality """ import asyncio @@ -34,10 +180,69 @@ class HelloBlockchainClient(RestClient): + """Extended REST client with domain-specific methods for hello_blockchain contract. + + This class demonstrates how to extend the base RestClient to add application-specific + functionality for interacting with a particular smart contract. It encapsulates + the details of resource queries and transaction construction for the hello_blockchain + Move module. + + Key Features: + - **Resource Queries**: Simplified access to MessageHolder resources + - **Transaction Construction**: Automated entry function payload creation + - **Error Handling**: Graceful handling of missing resources + - **Type Safety**: Proper typing for contract-specific operations + + Examples: + Basic usage:: + + client = HelloBlockchainClient("https://api.devnet.aptoslabs.com/v1") + + # Read message (returns None if not set) + message = await client.get_message(contract_addr, user_addr) + + # Set message (creates or updates MessageHolder resource) + txn_hash = await client.set_message(contract_addr, account, "Hello!") + await client.wait_for_transaction(txn_hash) + + Note: + This pattern of extending RestClient is recommended for applications that + interact with specific smart contracts frequently. It provides a clean + abstraction over raw resource queries and transaction construction. + """ + async def get_message( self, contract_address: AccountAddress, account_address: AccountAddress ) -> Optional[Dict[str, Any]]: - """Retrieve the resource message::MessageHolder::message""" + """Retrieve the MessageHolder resource for a specific account. + + This method queries the blockchain for the MessageHolder resource stored + under the given account address. The resource is created by the hello_blockchain + Move module when a user calls set_message for the first time. + + Args: + contract_address: The address where the hello_blockchain module is published. + account_address: The account address to query for the MessageHolder resource. + + Returns: + Dictionary containing the MessageHolder resource data if it exists, + including the 'message' field and any event handles. Returns None + if the account has never called set_message. + + Examples: + Query existing message:: + + message_data = await client.get_message(contract_addr, alice.address()) + if message_data: + print(f"Alice's message: {message_data['message']}") + else: + print("Alice hasn't set a message yet") + + Note: + This method handles the ResourceNotFound exception gracefully by + returning None, making it safe to call even for accounts that haven't + interacted with the contract yet. + """ try: return await self.account_resource( account_address, f"{contract_address}::message::MessageHolder" @@ -48,8 +253,51 @@ async def get_message( async def set_message( self, contract_address: AccountAddress, sender: Account, message: str ) -> str: - """Potentially initialize and set the resource message::MessageHolder::message""" - + """Set or update the message in the sender's MessageHolder resource. + + This method constructs and submits a transaction that calls the set_message + entry function in the hello_blockchain Move module. The function will either + create a new MessageHolder resource (if this is the first call) or update + the existing message. + + Args: + contract_address: The address where the hello_blockchain module is published. + sender: The account that will sign and send the transaction. + message: The string message to store in the MessageHolder resource. + + Returns: + The transaction hash as a string. Use wait_for_transaction() to + confirm the transaction was processed successfully. + + Raises: + ApiError: If the transaction submission fails due to network issues, + insufficient funds, or other blockchain-related errors. + + Examples: + Set a new message:: + + # Send transaction + txn_hash = await client.set_message( + contract_addr, + alice, + "Hello, Aptos blockchain!" + ) + + # Wait for confirmation + result = await client.wait_for_transaction(txn_hash) + print(f"Transaction successful: {result['success']}") + + Update existing message:: + + # This will update the existing MessageHolder resource + await client.set_message(contract_addr, alice, "Updated message!") + + Note: + The Move smart contract automatically handles whether to create a new + MessageHolder resource or update an existing one. The gas cost is + slightly higher for the first call (resource creation) compared to + subsequent updates. + """ payload = EntryFunction.natural( f"{contract_address}::message", "set_message", @@ -63,39 +311,184 @@ async def set_message( async def publish_contract(package_dir: str) -> AccountAddress: + """Deploy the hello_blockchain Move package to the Aptos blockchain. + + This function demonstrates the complete smart contract deployment workflow: + 1. Generate a new publisher account + 2. Fund the account from the faucet + 3. Compile the Move package using Aptos CLI + 4. Extract compiled bytecode and metadata + 5. Publish the package to the blockchain + + The deployment process creates a new account specifically for publishing + the contract, which becomes the address where the hello_blockchain module + is permanently stored on the blockchain. + + Args: + package_dir: Path to the Move package directory containing Move.toml + and the source files. Should contain the hello_blockchain module. + + Returns: + AccountAddress of the deployed contract (same as publisher address). + This address is used to interact with the contract functions. + + Raises: + Exception: If Move compilation fails due to syntax errors or missing files. + ApiError: If blockchain operations fail (funding, publishing, etc.). + FileNotFoundError: If compiled bytecode files are not found after compilation. + + Examples: + Deploy from local package:: + + contract_address = await publish_contract( + "./aptos-move/move-examples/hello_blockchain" + ) + print(f"Contract deployed at: {contract_address}") + + Deploy and interact:: + + # Deploy the contract + contract_addr = await publish_contract("./hello_blockchain") + + # Use the returned address for interactions + client = HelloBlockchainClient(NODE_URL) + txn = await client.set_message(contract_addr, account, "Hello!") + + Directory Structure Expected:: + + package_dir/ + ├── Move.toml # Package configuration + ├── sources/ + │ └── message.move # The hello_blockchain module + └── build/ # Generated after compilation + └── Examples/ + ├── package-metadata.bcs + └── bytecode_modules/ + └── message.mv + + Move.toml Configuration:: + + [package] + name = "Examples" + version = "1.0.0" + + [addresses] + hello_blockchain = "_" + + [dependencies] + AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", ... } + + Note: + - The function generates a fresh account for each deployment + - Named addresses are automatically resolved during compilation + - The deployment transaction is confirmed before returning + - The REST client is properly closed to prevent resource leaks + """ + # Generate a new account specifically for contract publishing contract_publisher = Account.generate() rest_client = HelloBlockchainClient(NODE_URL) faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) + + # Fund the publisher account with enough APT for deployment await faucet_client.fund_account(contract_publisher.address(), 10_000_000) + # Compile the Move package with the publisher address as hello_blockchain AptosCLIWrapper.compile_package( package_dir, {"hello_blockchain": contract_publisher.address()} ) + # Read the compiled bytecode module module_path = os.path.join( package_dir, "build", "Examples", "bytecode_modules", "message.mv" ) with open(module_path, "rb") as f: module = f.read() + # Read the package metadata metadata_path = os.path.join( package_dir, "build", "Examples", "package-metadata.bcs" ) with open(metadata_path, "rb") as f: metadata = f.read() + # Publish the package to the blockchain package_publisher = PackagePublisher(rest_client) txn_hash = await package_publisher.publish_package( contract_publisher, metadata, [module] ) + + # Wait for deployment transaction to be confirmed await rest_client.wait_for_transaction(txn_hash) + # Clean up resources await rest_client.close() return contract_publisher.address() async def main(contract_address: AccountAddress): + """Execute the hello_blockchain smart contract interaction demo. + + This function demonstrates a complete smart contract interaction workflow + by creating test accounts, funding them, and showing how multiple users + can interact with the deployed hello_blockchain contract independently. + + The demo showcases: + 1. **Account Generation**: Create Alice and Bob accounts programmatically + 2. **Faucet Funding**: Fund both accounts with test APT tokens + 3. **Balance Verification**: Check account balances after funding + 4. **Contract Interaction**: Each account sets their own message + 5. **State Queries**: Read back the stored messages to verify success + 6. **Resource Management**: Properly close network connections + + Args: + contract_address: The address where the hello_blockchain module is deployed. + This should be the address returned from publish_contract() or obtained + from a previous deployment. + + Workflow: + 1. Generate two test accounts (Alice and Bob) + 2. Fund both accounts with 10 APT each from the faucet + 3. Display account addresses and balances + 4. For each account: + - Query initial message state (should be None) + - Set a message using the smart contract + - Query the updated state to verify the message was stored + 5. Clean up network connections + + Examples: + Run with deployed contract:: + + from aptos_sdk.account_address import AccountAddress + + contract_addr = AccountAddress.from_str("******bc123...***") + await main(contract_addr) + + End-to-end deployment and interaction:: + + # Deploy first, then interact + contract_addr = await publish_contract("./hello_blockchain") + await main(contract_addr) + + Expected Behavior: + - Alice and Bob can each store independent messages + - Messages are persistent on the blockchain + - Each account's MessageHolder resource is separate + - All transactions should complete successfully + + Error Scenarios: + - Contract not deployed at the given address + - Faucet funding failures (network issues, rate limits) + - Transaction failures (insufficient gas, network problems) + - Resource query failures (node connectivity issues) + + Note: + This function uses the extended HelloBlockchainClient which provides + convenient methods for interacting with the specific smart contract. + The same operations could be performed using the base RestClient with + more manual transaction construction. + """ + # Generate two test accounts to demonstrate independent contract usage alice = Account.generate() bob = Account.generate() @@ -103,9 +496,11 @@ async def main(contract_address: AccountAddress): print(f"Alice: {alice.address()}") print(f"Bob: {bob.address()}") + # Set up clients for blockchain interaction rest_client = HelloBlockchainClient(NODE_URL) faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) + # Fund both accounts concurrently for efficiency alice_fund = faucet_client.fund_account(alice.address(), 10_000_000) bob_fund = faucet_client.fund_account(bob.address(), 10_000_000) await asyncio.gather(*[alice_fund, bob_fund]) diff --git a/examples/multikey.py b/examples/multikey.py index 577eb10..5cb8282 100644 --- a/examples/multikey.py +++ b/examples/multikey.py @@ -1,6 +1,94 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Multi-Key Authentication Example for Aptos Python SDK. + +This example demonstrates how to create and use multi-signature (multi-key) accounts +on the Aptos blockchain. Multi-signature accounts require multiple cryptographic keys +to sign transactions, providing enhanced security for high-value operations. + +Features Demonstrated: + - Creating a multi-key account with mixed key types (secp256k1 and Ed25519) + - Setting up a threshold signature scheme (2-of-3 in this example) + - Signing transactions with multiple keys + - Verifying multi-signatures before submission + - Transferring funds from a multi-key account + +Key Concepts: + - **Multi-Key Account**: An account controlled by multiple cryptographic keys + - **Threshold Signatures**: Requires a minimum number of signatures (threshold) + out of the total available keys to authorize transactions + - **Mixed Key Types**: Supports both secp256k1 (ECDSA) and Ed25519 keys in + the same multi-key setup + - **Account Address Derivation**: Multi-key accounts have addresses derived + from the combined public keys and threshold + +Security Benefits: + - **Distributed Control**: No single key can authorize transactions alone + - **Reduced Single Points of Failure**: Even if one key is compromised, + the account remains secure + - **Flexible Access Patterns**: Different combinations of signers can + authorize transactions + - **Key Type Diversity**: Mixing different signature schemes provides + cryptographic diversity + +Workflow: + 1. Generate multiple private keys of different types (secp256k1, Ed25519) + 2. Create a MultiPublicKey with a 2-of-3 threshold + 3. Derive the account address from the multi-key setup + 4. Fund the multi-key account using the faucet + 5. Create a transaction payload (APT transfer) + 6. Sign the transaction with 2 out of 3 keys (meeting the threshold) + 7. Combine signatures into a MultiSignature + 8. Verify all signatures before submission + 9. Submit the signed transaction to the network + 10. Wait for transaction confirmation + +Prerequisites: + - Access to an Aptos test network (devnet/testnet) + - Faucet access for funding accounts + - Network configuration in common.py + +Usage: + Run this script directly to see multi-key authentication in action: + python3 examples/multikey.py + +Expected Output: + - Display of Alice's multi-key address and Bob's single-key address + - Initial account balances after funding + - Transaction execution transferring 1,000 APT from Alice to Bob + - Final balances showing the transfer completion + - Verification of all signature operations + +Security Considerations: + - Store private keys securely in production environments + - Use hardware security modules (HSMs) for high-value multi-key setups + - Regularly audit key holder access and permissions + - Consider key rotation policies for long-term security + - Test signature verification thoroughly before mainnet deployment + +Error Handling: + - Network connectivity issues + - Insufficient account balances + - Invalid signature combinations + - Transaction simulation failures + - Faucet funding limitations + +Learning Objectives: + - Understand multi-signature account creation and management + - Learn threshold signature schemes and their security properties + - Practice mixed cryptographic key type usage + - Gain experience with complex transaction authorization patterns + - Explore advanced account security models on Aptos + +Related Examples: + - authenticate.py: Single-key authentication patterns + - hello_blockchain.py: Basic transaction patterns + - transfer_coin.py: Simple APT transfers + - multisig.py: Legacy multi-signature account patterns +""" + import asyncio from aptos_sdk import asymmetric_crypto_wrapper, ed25519, secp256k1_ecdsa @@ -21,6 +109,77 @@ async def main(): + """ + Demonstrate multi-key authentication and transaction signing on Aptos. + + This function showcases the complete workflow for creating a multi-signature + account, funding it, and executing a transfer transaction that requires + multiple signatures to authorize. + + The example creates a 2-of-3 multi-key account using mixed cryptographic + key types (secp256k1 and Ed25519) and demonstrates how to: + + 1. **Setup Phase**: + - Initialize REST and Faucet clients for network interaction + - Generate 3 private keys of different types (2 secp256k1, 1 Ed25519) + - Create a MultiPublicKey with threshold=2 (requires 2 signatures) + - Derive the multi-key account address + - Create a regular single-key account for Bob + + 2. **Funding Phase**: + - Fund both Alice's multi-key account and Bob's account using faucet + - Display initial balances for verification + + 3. **Transaction Phase**: + - Construct an APT transfer transaction from Alice to Bob + - Sign the transaction with 2 out of 3 available keys + - Combine individual signatures into a MultiSignature + - Create an AccountAuthenticator with MultiKeyAuthenticator + + 4. **Verification Phase**: + - Verify each individual signature against its corresponding key + - Verify the combined multi-signature against the multi-key + - Verify the complete authenticator + + 5. **Submission Phase**: + - Submit the signed transaction to the network + - Wait for transaction confirmation + - Display final balances to confirm the transfer + + Key Security Features: + - **Threshold Security**: Requires 2 signatures out of 3 possible + - **Cryptographic Diversity**: Uses both secp256k1 and Ed25519 keys + - **Signature Verification**: Validates all signatures before submission + - **Address Derivation**: Deterministically derives address from multi-key + + Network Requirements: + - Active Aptos devnet/testnet connection + - Faucet service availability for account funding + - Sufficient network tokens for transaction fees + + Error Scenarios Handled: + - Network connectivity issues during client operations + - Transaction failures during submission or confirmation + - Signature verification failures before submission + - Account balance insufficiency for transfers + + Raises: + Exception: If network operations fail, signature verification fails, + or transaction submission encounters errors. + + Example Output: + === Addresses === + Multikey Alice: ***bcd123... + Bob: ***456def... + + === Initial Balances === + Alice: 100000000 + Bob: 1 + + === Final Balances === + Alice: 99999000 # Reduced by transfer amount + fees + Bob: 1001 # Increased by transfer amount + """ # :!:>section_1 rest_client = RestClient(NODE_URL) faucet_client = FaucetClient( diff --git a/examples/rotate_key.py b/examples/rotate_key.py index 852572f..4b5dd0d 100644 --- a/examples/rotate_key.py +++ b/examples/rotate_key.py @@ -1,3 +1,125 @@ +""" +Authentication Key Rotation Example for Aptos Python SDK. + +This example demonstrates how to perform authentication key rotation on the Aptos +blockchain, showcasing both single-key and multi-key rotation scenarios. Authentication +key rotation allows changing the private key that controls an account while keeping +the same account address, providing crucial security and recovery capabilities. + +Key rotation is essential for: + - **Key Compromise Recovery**: When a private key is suspected to be compromised + - **Proactive Security**: Periodic key rotation as a security best practice + - **Key Management**: Transitioning from single keys to multi-signature setups + - **Access Transfer**: Transferring account control to different parties + - **Emergency Recovery**: Recovering access using backup keys + +Features Demonstrated: + - Single Ed25519 key rotation from one private key to another + - Multi-signature key rotation from single key to multi-key setup + - Rotation proof challenge generation and signing + - Authentication key validation after rotation + - Account reconstruction with new private keys + - On-chain verification of rotation completion + +Key Concepts: + - **Authentication Key**: The key that proves ownership of an account + - **Account Address**: Remains constant even after key rotation + - **Rotation Proof**: Cryptographic proof that the current key holder + authorizes the key change + - **Dual Signatures**: Both current and new keys must sign the rotation proof + - **Multi-Key Migration**: Transitioning from single to multi-signature control + +Security Model: + The rotation process requires signatures from both: + 1. **Current Key**: Proves current control of the account + 2. **New Key**: Proves possession of the new private key + + This dual-signature requirement prevents unauthorized key rotations even + if an attacker knows the new private key but not the current one. + +Workflow Overview: + 1. **Setup Phase**: + - Generate accounts (Alice as primary, Bob's key as rotation target) + - Fund Alice's account for transaction fees + - Display initial account states + + 2. **Single Key Rotation**: + - Create rotation proof challenge with sequence number and addresses + - Sign the challenge with both current (Alice) and new (Bob) keys + - Submit rotation transaction to the blockchain + - Verify authentication key change on-chain + - Reconstruct Alice's account object with new private key + + 3. **Multi-Key Migration**: + - Create multi-key setup combining multiple Ed25519 keys + - Generate rotation proof for single-to-multi transition + - Submit multi-key rotation transaction + - Validate the new multi-signature authentication key + +Rotation Proof Challenge Components: + - **Sequence Number**: Current account sequence to prevent replay attacks + - **Originator**: The account address being rotated + - **Current Auth Key**: The authentication key being replaced + - **New Public Key**: The public key portion of the replacement key + +Transaction Structure: + The rotation transaction calls `0x1::account::rotate_authentication_key` with: + - Authentication schemes for both keys (current and new) + - Public keys for both current and new authentication + - Signatures from both keys proving authorization + +Security Considerations: + - **Private Key Protection**: Store rotation keys securely + - **Replay Protection**: Each rotation uses current sequence number + - **Atomic Operations**: Rotation either succeeds completely or fails + - **Verification**: Always verify rotation completion on-chain + - **Key Material**: Securely dispose of old private keys after rotation + +Common Use Cases: + - **Suspected Compromise**: Immediate rotation to new secure keys + - **Operational Security**: Periodic key rotation policies + - **Multi-Sig Migration**: Moving high-value accounts to multi-signature + - **Recovery Operations**: Using backup keys to regain account access + - **Organizational Changes**: Transferring control between team members + +Error Scenarios: + - Invalid signatures in rotation proof + - Insufficient account balance for transaction fees + - Network connectivity issues during submission + - Sequence number mismatches (replay protection) + - Malformed rotation proof challenges + +Prerequisites: + - Active Aptos network connection (devnet/testnet) + - Faucet access for funding transaction fees + - Understanding of Ed25519 cryptographic signatures + - Knowledge of account authentication mechanisms + +Usage: + Run this script to see authentication key rotation in action: + python3 examples/rotate_key.py + +Expected Output: + - Formatted display of account information before rotation + - Progress indicators during rotation operations + - Updated account information after each rotation + - Verification of successful authentication key changes + - Final multi-signature authentication key confirmation + +Learning Objectives: + - Master authentication key rotation mechanics + - Understand dual-signature security requirements + - Practice single-to-multi-key migrations + - Learn rotation proof challenge construction + - Gain experience with advanced account security patterns + +Related Examples: + - multikey.py: Multi-signature account creation and usage + - hello_blockchain.py: Basic account and transaction patterns + - authenticate.py: Authentication and signature verification + - multisig.py: Legacy multi-signature account patterns +""" + import asyncio from typing import List, cast @@ -20,10 +142,50 @@ def truncate(address: str) -> str: + """ + Truncate a long address string for display purposes. + + Takes a long address string and returns a shortened version showing + only the first 6 and last 6 characters, with "..." in between. + This is useful for displaying addresses in formatted tables. + + Args: + address: The full address string to truncate. + + Returns: + A shortened string in the format "123abc...def456". + + Example: + >>> truncate("***23456789abcdef") + "***23...def" + """ return address[0:6] + "..." + address[-6:] def format_account_info(account: Account) -> str: + """ + Format account information for tabular display. + + Extracts key information from an Account object and formats it + into a fixed-width string suitable for table display. Each field + is truncated and left-justified to maintain consistent formatting. + + Args: + account: The Account object to format. + + Returns: + A formatted string containing truncated account information + with consistent spacing for table display. + + The formatted string contains: + - Account address (truncated) + - Authentication key (truncated) + - Private key hex representation (truncated) + - Public key string representation (truncated) + + Example Output: + "***bcd...456 ***def...789 abc123...xyz ed25519..." + """ vals = [ str(account.address()), account.auth_key(), diff --git a/examples/transfer_coin.py b/examples/transfer_coin.py index 975bb99..d5c7bca 100644 --- a/examples/transfer_coin.py +++ b/examples/transfer_coin.py @@ -1,6 +1,117 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +""" +Basic Coin Transfer Example - Fundamental APT token operations on Aptos. + +This example demonstrates the core functionality of the Aptos Python SDK by showing +how to perform basic APT coin transfers between accounts. It covers account creation, +funding from the faucet, balance checking, and executing transfers using the BCS +(Binary Canonical Serialization) format for optimal performance. + +Key Concepts Demonstrated: +- **Account Generation**: Create new Aptos accounts programmatically +- **Faucet Integration**: Fund test accounts with APT tokens +- **Balance Queries**: Check account balances before and after transactions +- **BCS Transfers**: Efficient binary-encoded transaction format +- **Transaction Confirmation**: Wait for transaction completion +- **Indexer Queries**: Optional GraphQL-based transaction history lookup + +Workflow: + 1. **Setup Phase**: Initialize clients and generate test accounts + 2. **Funding Phase**: Fund accounts from the devnet faucet + 3. **Transfer Phase**: Execute multiple APT transfers between accounts + 4. **Verification Phase**: Verify balance changes after each transfer + 5. **History Phase**: Query transaction history using the indexer (optional) + +Transaction Details: + - **Transfer Method**: BCS format for efficiency and lower gas costs + - **Gas Handling**: Automatic gas fee calculation and deduction + - **Confirmation**: Synchronous waiting for blockchain confirmation + - **Atomicity**: Transactions are atomic (all-or-nothing execution) + +Balance Tracking: + The example tracks balances at multiple points to show transaction effects: + - Initial balances after faucet funding + - Intermediate balances after first transfer + - Final balances after second transfer + +Examples: + Run the basic transfer example:: + + python -m examples.transfer_coin + + Expected output shows: + - Account addresses for Alice and Bob + - Initial balances (Alice: 100,000,000 octas, Bob: 1 octa) + - Balance changes after each 1,000 octa transfer + - Transaction history from indexer (if available) + + Programmatic usage:: + + import asyncio + from examples.transfer_coin import main + + # Run the transfer example + asyncio.run(main()) + + Custom network configuration:: + + import os + # Switch to testnet + os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" + os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" + + # Run on testnet + python -m examples.transfer_coin + +APT Token Details: + - **Unit**: APT tokens are measured in "octas" (1 APT = 100,000,000 octas) + - **Precision**: 8 decimal places (similar to Bitcoin's satoshis) + - **Gas**: Transaction fees are paid in APT and deducted automatically + - **Type**: APT is represented as "***::aptos_coin::AptosCoin" on-chain + +Indexer Integration: + If an indexer URL is configured, the example demonstrates: + - GraphQL query construction for transaction history + - Account-specific transaction filtering + - Coin activity tracking including amounts and timestamps + - Data structure navigation for complex query results + +Gas Economics: + - **Transfer Cost**: ~20-50 gas units for basic APT transfers + - **Gas Price**: Configurable, defaults to 100 octas per gas unit + - **Total Fee**: Typically 2,000-5,000 octas per transfer (~$0.001 USD) + - **Faucet Funding**: Devnet provides 100 APT free for testing + +Error Scenarios: + Common issues and solutions: + - **Insufficient Balance**: Ensure sender has enough APT for amount + gas + - **Network Issues**: Check NODE_URL connectivity and faucet availability + - **Invalid Addresses**: Verify account addresses are properly formatted + - **Sequence Numbers**: SDK handles sequence number management automatically + +Best Practices: + - Always close REST clients to prevent resource leaks + - Use BCS transfers for better performance vs JSON transactions + - Check balances before large transfers to avoid failures + - Handle network errors with appropriate retry logic + - Use testnet/devnet for development, never mainnet for examples + +Learning Objectives: + After running this example, you should understand: + 1. How to create and fund Aptos accounts for testing + 2. How to check account balances and track changes + 3. How to perform APT transfers using the Python SDK + 4. How transaction confirmation works on Aptos + 5. How to query transaction history using the indexer + 6. The relationship between APT tokens, octas, and gas fees + +Note: + This example uses devnet by default, which is safe for experimentation. + All accounts and transactions are on the test network with no real value. +""" + import asyncio from aptos_sdk.account import Account @@ -10,11 +121,102 @@ async def main(): + """Execute the basic APT coin transfer demonstration. + + This function demonstrates the fundamental workflow for transferring APT tokens + between accounts on the Aptos blockchain. It showcases account generation, + faucet funding, balance tracking, and transaction execution using the most + efficient BCS (Binary Canonical Serialization) format. + + The demo performs the following operations: + 1. **Client Setup**: Initialize REST, Faucet, and optional Indexer clients + 2. **Account Creation**: Generate Alice and Bob accounts with new key pairs + 3. **Funding**: Fund Alice with 100 APT and Bob with minimal balance (1 octa) + 4. **First Transfer**: Alice sends 1,000 octas to Bob + 5. **Second Transfer**: Alice sends another 1,000 octas to Bob + 6. **Balance Verification**: Track balance changes throughout the process + 7. **History Query**: Optional indexer query for transaction history + 8. **Cleanup**: Close all network connections properly + + Transaction Flow: + Initial State: + - Alice: 100,000,000 octas (100 APT from faucet) + - Bob: 1 octa (minimal funding from faucet) + + After First Transfer (1,000 octas): + - Alice: ~99,997,000 octas (100 APT - 1,000 - gas fees) + - Bob: 1,001 octas (1 + 1,000 received) + + After Second Transfer (1,000 octas): + - Alice: ~99,994,000 octas (previous - 1,000 - gas fees) + - Bob: 2,001 octas (previous + 1,000 received) + + Technical Details: + - **Transfer Method**: Uses `bcs_transfer()` for optimal performance + - **Gas Management**: Automatic gas calculation and payment from sender + - **Confirmation**: Synchronous waiting ensures transaction completion + - **Error Handling**: Network operations may raise ApiError exceptions + - **Balance Precision**: All amounts in octas (1 APT = 100,000,000 octas) + + Indexer Integration: + If INDEXER_URL is configured, the function demonstrates: + - GraphQL query construction for transaction history + - Account-specific filtering using Bob's address + - Coin activity data extraction (amounts, types, timestamps) + - Assertion validation that transactions were recorded + + Expected Output:: + + === Addresses === + Alice: ***abc123... + Bob: ***def456... + + === Initial Balances === + Alice: 100000000 + Bob: 1 + + === Intermediate Balances === + Alice: 99997000 # Approximate after gas fees + Bob: 1001 + + === Final Balances === + Alice: 99994000 # Approximate after second transfer + Bob: 2001 + + Error Scenarios: + - **Network Connectivity**: REST API or faucet unavailable + - **Insufficient Funds**: Alice doesn't have enough for transfer + gas + - **Invalid Configuration**: Malformed URLs in environment variables + - **Indexer Issues**: GraphQL queries may fail if indexer is down + + Performance Notes: + - **BCS Format**: More efficient than JSON transactions (~30% gas savings) + - **Concurrent Operations**: Uses asyncio.gather for parallel balance queries + - **Connection Pooling**: REST client reuses connections for efficiency + - **Minimal Funding**: Bob gets only 1 octa to show exact transfer amounts + + Network Requirements: + - Active internet connection + - Access to Aptos devnet endpoints + - Faucet service availability for account funding + - Optional: Indexer service for transaction history queries + + Raises: + ApiError: For network communication failures or blockchain errors + Exception: For general application errors or configuration issues + + Note: + This function is designed to be educational and uses devnet exclusively. + All transactions are on test networks with no real monetary value. + """ + # Initialize clients for blockchain interaction # :!:>section_1 rest_client = RestClient(NODE_URL) faucet_client = FaucetClient( FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN ) # <:!:section_1 + + # Optional indexer client for transaction history queries if INDEXER_URL and INDEXER_URL != "none": indexer_client = IndexerClient(INDEXER_URL) else: From 44d569a8e4eed004b647c6dcb94b41d138f77cad Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Mon, 8 Sep 2025 13:45:15 -0400 Subject: [PATCH 2/4] fix bad code change in docs change --- aptos_sdk/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aptos_sdk/account.py b/aptos_sdk/account.py index 5a973d7..54cc3dd 100644 --- a/aptos_sdk/account.py +++ b/aptos_sdk/account.py @@ -803,7 +803,7 @@ class RotationProofChallenge: authentication key to a new public key. """ - type_info_account_address: AccountAddress = AccountAddress.from_str("***") + type_info_account_address: AccountAddress = AccountAddress.from_str("0x1") type_info_module_name: str = "account" type_info_struct_name: str = "RotationProofChallenge" sequence_number: int From 016ccb974cb61d31dd791fa168129fa5a19e415b Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Mon, 8 Sep 2025 13:56:54 -0400 Subject: [PATCH 3/4] format documented files --- aptos_sdk/account.py | 352 ++++++++++----------- aptos_sdk/account_address.py | 263 ++++++++-------- aptos_sdk/account_sequence_number.py | 159 +++++----- aptos_sdk/aptos_cli_wrapper.py | 410 +++++++++++++------------ aptos_sdk/aptos_token_client.py | 211 +++++++------ aptos_sdk/aptos_tokenv1_client.py | 220 ++++++------- aptos_sdk/asymmetric_crypto.py | 205 +++++++------ aptos_sdk/asymmetric_crypto_wrapper.py | 272 ++++++++-------- aptos_sdk/async_client.py | 248 +++++++-------- aptos_sdk/authenticator.py | 372 +++++++++++----------- aptos_sdk/bcs.py | 299 +++++++++--------- aptos_sdk/cli.py | 108 +++---- aptos_sdk/ed25519.py | 258 ++++++++-------- aptos_sdk/metadata.py | 62 ++-- aptos_sdk/package_publisher.py | 202 ++++++------ aptos_sdk/secp256k1_ecdsa.py | 303 +++++++++--------- aptos_sdk/transaction_worker.py | 172 +++++------ aptos_sdk/transactions.py | 162 +++++----- aptos_sdk/type_tag.py | 206 +++++++------ examples/common.py | 28 +- examples/hello_blockchain.py | 166 +++++----- examples/multikey.py | 40 +-- examples/rotate_key.py | 24 +- examples/transfer_coin.py | 52 ++-- 24 files changed, 2419 insertions(+), 2375 deletions(-) diff --git a/aptos_sdk/account.py b/aptos_sdk/account.py index 54cc3dd..2583a73 100644 --- a/aptos_sdk/account.py +++ b/aptos_sdk/account.py @@ -16,12 +16,12 @@ class Account: """Represents a complete Aptos blockchain account with cryptographic key management. - + The Account class encapsulates the fundamental components needed to interact with the Aptos blockchain: an account address and its associated private key. It provides comprehensive functionality for account creation, transaction signing, key management, and persistent storage. - + Key Features: - **Multiple Key Types**: Supports Ed25519 and Secp256k1 ECDSA cryptographic schemes - **Random Generation**: Secure random account creation with proper entropy @@ -30,79 +30,79 @@ class Account: - **Address Derivation**: Automatic address calculation from public keys - **Persistence**: Save and load account data to/from files - **Authentication**: Generate authentication keys and proof challenges - + Cryptographic Support: - **Ed25519**: Default signature scheme, fast and secure - **Secp256k1 ECDSA**: Ethereum-compatible signatures for interoperability - **Multi-key**: Support for threshold and multi-signature schemes - + Examples: Create a new account:: - + from aptos_sdk.account import Account - + # Generate new Ed25519 account account = Account.generate() print(f"Address: {account.address()}") print(f"Private key: {account.private_key}") - + Create Secp256k1 account:: - + # Generate Secp256k1 ECDSA account (Ethereum-compatible) secp_account = Account.generate_secp256k1_ecdsa() print(f"Secp256k1 address: {secp_account.address()}") - + Load existing account:: - + # From hex private key hex_key = "***1234567890abcdef..." imported_account = Account.load_key(hex_key) - + # From JSON file saved_account = Account.load("./my_account.json") - + Sign transactions:: - + from aptos_sdk.async_client import RestClient - + async def transfer_tokens(): client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") - + # Create transfer transaction recipient = Account.generate().address() txn_hash = await client.transfer(account, recipient, 1000) - + # Wait for completion result = await client.wait_for_transaction(txn_hash) print(f"Transfer successful: {result['success']}") - + Persistent storage:: - + # Save account to file account.store("./wallet.json") - + # Load account later restored_account = Account.load("./wallet.json") assert account == restored_account - + Sign arbitrary data:: - + # Sign custom message message = b"Hello, Aptos!" signature = account.sign(message) - + # Verify signature public_key = account.public_key() is_valid = public_key.verify(message, signature) print(f"Signature valid: {is_valid}") - + Security Considerations: - **Private Key Protection**: Never expose private keys in logs or UI - **Secure Storage**: Use encrypted storage for production private keys - **Key Rotation**: Consider implementing key rotation for long-lived accounts - **Testnet First**: Always test on devnet/testnet before mainnet deployment - **Entropy**: The random generation uses cryptographically secure random sources - + Note: Account addresses are derived deterministically from public keys using SHA3-256 hashing. The same private key will always generate the same @@ -116,29 +116,29 @@ def __init__( self, account_address: AccountAddress, private_key: asymmetric_crypto.PrivateKey ): """Initialize an Account with the given address and private key. - + This constructor creates an Account instance from an existing address and private key pair. It's typically used internally by factory methods like generate() or load_key() rather than being called directly. - + Args: account_address: The blockchain address for this account. private_key: The private key that controls this account. Must correspond to the given address. - + Examples: Direct construction (advanced usage):: - + from aptos_sdk.ed25519 import PrivateKey from aptos_sdk.account_address import AccountAddress - + # Create components separately private_key = PrivateKey.random() address = AccountAddress.from_key(private_key.public_key()) - + # Construct account account = Account(address, private_key) - + Note: The constructor does not validate that the address corresponds to the private key. Use the factory methods (generate, load_key) for @@ -164,52 +164,52 @@ def __eq__(self, other: object) -> bool: @staticmethod def generate() -> Account: """Generate a new Account with a cryptographically secure random Ed25519 private key. - + This method creates a completely new account with a randomly generated Ed25519 private key and derives the corresponding account address. Ed25519 is the default and recommended signature scheme for Aptos due to its security and performance characteristics. - + Returns: Account: A new account with randomly generated Ed25519 credentials. - + Examples: Create new accounts:: - + # Generate single account alice = Account.generate() print(f"Alice's address: {alice.address()}") - + # Generate multiple accounts accounts = [Account.generate() for _ in range(5)] for i, account in enumerate(accounts): print(f"Account {i}: {account.address()}") - + Use in async context:: - + import asyncio from aptos_sdk.async_client import FaucetClient, RestClient - + async def setup_test_account(): # Generate account account = Account.generate() - + # Fund from faucet client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") faucet = FaucetClient("https://faucet.devnet.aptoslabs.com", client) - + await faucet.fund_account(account.address(), 100_000_000) balance = await client.account_balance(account.address()) print(f"Account funded with {balance} APT") - + return account - + Security: - Uses cryptographically secure random number generation - Each call produces a unique, unpredictable private key - Private key entropy comes from system random sources - No two generated accounts will have the same private key - + Note: The generated account exists only in memory until explicitly saved using the store() method. The address is deterministically derived @@ -222,56 +222,56 @@ async def setup_test_account(): @staticmethod def generate_secp256k1_ecdsa() -> Account: """Generate a new Account with a cryptographically secure random Secp256k1 ECDSA private key. - + This method creates a new account using the Secp256k1 ECDSA signature scheme, which is compatible with Ethereum and Bitcoin. This enables interoperability with Ethereum tooling and allows users familiar with Ethereum to use the same cryptographic primitives. - + Returns: Account: A new account with randomly generated Secp256k1 ECDSA credentials. - + Examples: Create Ethereum-compatible account:: - + # Generate Secp256k1 account eth_compatible_account = Account.generate_secp256k1_ecdsa() print(f"Secp256k1 address: {eth_compatible_account.address()}") - + # The private key can be used with Ethereum tooling private_key_hex = str(eth_compatible_account.private_key) print(f"Private key (Ethereum format): {private_key_hex}") - + Mixed signature schemes:: - + # Create accounts with different signature schemes ed25519_account = Account.generate() # Default Ed25519 secp256k1_account = Account.generate_secp256k1_ecdsa() - + print(f"Ed25519 account: {ed25519_account.address()}") print(f"Secp256k1 account: {secp256k1_account.address()}") - + # Both can interact with Aptos equally # but have different signature formats - + Use Cases: - **Ethereum Migration**: Users migrating from Ethereum ecosystems - **Cross-Chain Applications**: Applications spanning Ethereum and Aptos - **Hardware Wallets**: Some hardware wallets prefer Secp256k1 - **Enterprise Integration**: Systems already using Secp256k1 - + Performance Considerations: - Secp256k1 signatures are larger than Ed25519 (64 vs 32 bytes) - Ed25519 has faster verification times - Secp256k1 has wider hardware support - Both are equally secure when implemented correctly - + Security: - Uses the same secure random generation as Ed25519 - Follows Bitcoin/Ethereum security practices - Compatible with standard Secp256k1 implementations - Addresses are derived using Aptos's standard address scheme - + Note: While Secp256k1 is supported, Ed25519 is recommended for new applications due to its superior performance characteristics. Use Secp256k1 primarily @@ -285,58 +285,58 @@ def generate_secp256k1_ecdsa() -> Account: @staticmethod def load_key(key: str) -> Account: """Create an Account from a hex-encoded Ed25519 private key string. - + This method reconstructs an Account from a previously exported private key. It's commonly used to import accounts from external sources, CLI tools, or when restoring accounts from backup storage. - + Args: key: Hex-encoded Ed25519 private key string (64 characters, 32 bytes). Can be with or without '***' prefix. - + Returns: Account: An account instance created from the given private key. - + Raises: ValueError: If the key format is invalid or cannot be parsed. - + Examples: Import from hex string:: - + # Standard hex format (64 characters) private_key_hex = "1a2b3c4d5e6f789..." # 64 hex chars account = Account.load_key(private_key_hex) - + # With '***' prefix prefixed_key = "***1a2b3c4d5e6f789..." account = Account.load_key(prefixed_key) - + Restore from backup:: - + # Export account key for backup original_account = Account.generate() backup_key = str(original_account.private_key) - + # Later, restore from backup restored_account = Account.load_key(backup_key) - + # Verify they're the same assert original_account.address() == restored_account.address() - + CLI integration:: - + # Import from Aptos CLI output # aptos init --profile my-account # aptos account list --profile my-account cli_private_key = "***a1b2c3d4e5f6..." account = Account.load_key(cli_private_key) - + Security Considerations: - **Never hardcode private keys** in source code - Use environment variables or secure key management - Validate key sources to prevent injection attacks - Consider using encrypted storage for sensitive keys - + Note: This method only supports Ed25519 private keys. For Secp256k1 keys, you'll need to use the appropriate Secp256k1 import methods or @@ -349,48 +349,48 @@ def load_key(key: str) -> Account: @staticmethod def load(path: str) -> Account: """Load an Account from a JSON file containing account data. - + This method reads account information from a JSON file created by the store() method or compatible external tools. It provides persistent storage and retrieval of account credentials. - + Args: path: Path to the JSON file containing account data. The file must contain 'account_address' and 'private_key' fields. - + Returns: Account: An account instance loaded from the file data. - + Raises: FileNotFoundError: If the specified file doesn't exist. json.JSONDecodeError: If the file contains invalid JSON. KeyError: If required fields are missing from the JSON. ValueError: If the account data is malformed. - + File Format: Expected JSON structure:: - + { "account_address": "***1234567890abcdef...", "private_key": "***abcdef1234567890..." } - + Examples: Save and load account:: - + # Create and save account original_account = Account.generate() original_account.store("./wallet.json") - + # Load account later loaded_account = Account.load("./wallet.json") - + # Verify integrity assert original_account.address() == loaded_account.address() assert original_account == loaded_account - + Load from CLI-generated file:: - + # If you used: aptos init --profile myaccount # The profile data can be imported try: @@ -398,11 +398,11 @@ def load(path: str) -> Account: print(f"Loaded account: {account.address()}") except Exception as e: print(f"Failed to load account: {e}") - + Batch operations:: - + import os - + # Load multiple accounts from directory accounts = [] for filename in os.listdir("./wallets/"): @@ -410,21 +410,21 @@ def load(path: str) -> Account: filepath = os.path.join("./wallets/", filename) account = Account.load(filepath) accounts.append(account) - + print(f"Loaded {len(accounts)} accounts") - + Security Considerations: - **File Permissions**: Ensure JSON files have restricted permissions - **Encryption**: Consider encrypting files containing private keys - **Backup**: Keep secure backups of account files - **Access Control**: Limit access to account files in production - + Integration: Compatible with files created by: - The Account.store() method - Aptos CLI account exports - Custom wallet implementations using the same format - + Note: The loaded account will have Ed25519 keys. The address format is flexible and accepts both strict and relaxed address formats. @@ -438,64 +438,64 @@ def load(path: str) -> Account: def store(self, path: str): """Store the Account data to a JSON file for persistent storage. - + This method serializes the account's address and private key to a JSON file that can be later loaded using the load() method. It provides a simple way to persist account credentials across application sessions. - + Args: path: File path where to save the account data. Will create or overwrite the file at the specified location. - + Raises: PermissionError: If the file cannot be written due to permissions. OSError: If there are filesystem-related errors. - + Security Warning: The JSON file will contain the private key in plaintext. Ensure proper file permissions and consider encryption for sensitive data. - + Examples: Basic storage and retrieval:: - + # Create account account = Account.generate() - + # Store to file account.store("./my_wallet.json") - + # Load later loaded_account = Account.load("./my_wallet.json") assert account == loaded_account - + Secure file permissions:: - + import os import stat - + # Store account account.store("./secure_wallet.json") - + # Set restrictive permissions (owner read/write only) os.chmod("./secure_wallet.json", stat.S_IRUSR | stat.S_IWUSR) - + Backup multiple accounts:: - + accounts = [Account.generate() for _ in range(5)] - + for i, account in enumerate(accounts): filename = f"./backups/account_{i}.json" account.store(filename) print(f"Saved account {account.address()} to {filename}") - + File Format: Creates JSON with structure:: - + { "account_address": "***", "private_key": "***" } - + Note: - The file will be created or overwritten if it exists - Only Ed25519 private keys are currently supported for storage @@ -510,53 +510,53 @@ def store(self, path: str): def address(self) -> AccountAddress: """Get the blockchain address associated with this account. - + The account address is a unique identifier derived from the account's public key using SHA3-256 hashing. This address is used to identify the account on the blockchain and in transactions. - + Returns: AccountAddress: The unique address for this account on the blockchain. - + Examples: Get account address:: - + account = Account.generate() address = account.address() print(f"Account address: {address}") # Output: Account address: ***a1b2c3d4e5f67890... - + Use address in transactions:: - + from aptos_sdk.async_client import RestClient - + async def check_balance(): client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") - + # Use account address for queries balance = await client.account_balance(account.address()) print(f"Balance: {balance} APT") - + # Use as transaction recipient recipient_address = account.address() - + Address comparison:: - + account1 = Account.generate() account2 = Account.generate() - + # Addresses are unique assert account1.address() != account2.address() - + # Same account always has same address assert account1.address() == account1.address() - + Properties: - **Deterministic**: Same private key always produces same address - **Unique**: Each private key produces a unique address - **Immutable**: Address cannot change without changing the private key - **Format**: 32-byte hex string with '***' prefix - + Note: The address is computed from the public key, not stored separately. This ensures consistency and reduces the risk of address/key mismatches. @@ -565,60 +565,60 @@ async def check_balance(): def auth_key(self) -> str: """Get the authentication key for this account. - + The authentication key is derived from the account's public key and represents the current key that can authenticate transactions for this account. Initially, the auth key equals the account address, but it can change through key rotation operations. - + Returns: str: The authentication key as a hex string with '***' prefix. - + Examples: Check initial auth key:: - + account = Account.generate() address = str(account.address()) auth_key = account.auth_key() - + # Initially, auth key equals address assert address == auth_key print(f"Address: {address}") print(f"Auth key: {auth_key}") - + Use in authentication:: - + # Auth key is used for verifying transaction signatures transaction_data = b"transaction_payload" signature = account.sign(transaction_data) - + # The auth key identifies which public key to use for verification public_key = account.public_key() is_valid = public_key.verify(transaction_data, signature) - + Key rotation scenario:: - + # After key rotation, auth key would differ from original address # but the account address remains the same for identification original_address = account.address() current_auth_key = account.auth_key() - + # Address is permanent, auth key can change print(f"Permanent address: {original_address}") print(f"Current auth key: {current_auth_key}") - + Key Concepts: - **Account Address**: Permanent identifier, never changes - **Authentication Key**: Current key for signing, can be rotated - **Initial State**: Auth key == address for new accounts - **After Rotation**: Auth key != address, but address stays same - + Use Cases: - Verifying transaction signatures - Key rotation operations - Multi-signature account management - Authentication in smart contracts - + Note: For newly generated accounts, the authentication key will be identical to the account address. They only differ after key rotation operations. @@ -627,37 +627,37 @@ def auth_key(self) -> str: def sign(self, data: bytes) -> asymmetric_crypto.Signature: """Sign arbitrary data with the account's private key. - + This method creates a cryptographic signature over any data using the account's private key. The signature can be verified using the corresponding public key, providing proof of data authenticity and account ownership. - + Args: data: The raw bytes to be signed. Can be any binary data including transaction payloads, messages, or arbitrary content. - + Returns: asymmetric_crypto.Signature: A signature object that can be verified with the account's public key. - + Examples: Sign custom message:: - + account = Account.generate() message = b"Hello, Aptos blockchain!" - + # Create signature signature = account.sign(message) - + # Verify signature public_key = account.public_key() is_valid = public_key.verify(message, signature) print(f"Signature valid: {is_valid}") # True - + Sign structured data:: - + import json - + # Sign JSON data data_dict = { "action": "transfer", @@ -666,33 +666,33 @@ def sign(self, data: bytes) -> asymmetric_crypto.Signature: } data_bytes = json.dumps(data_dict, sort_keys=True).encode() signature = account.sign(data_bytes) - + Authentication proof:: - + # Prove account ownership challenge = b"prove_ownership_2023" proof_signature = account.sign(challenge) - + # Others can verify you own the account # without revealing your private key - + Transaction component:: - + # This is typically used internally by transaction signing # but can be used for custom transaction construction raw_transaction_bytes = serialize_transaction(...) transaction_signature = account.sign(raw_transaction_bytes) - + Security Properties: - **Non-repudiation**: Only the private key holder can create valid signatures - **Data Integrity**: Signatures detect any modification to signed data - **Authentication**: Proves the signer owns the private key - **Unforgeable**: Cryptographically impossible to forge without the private key - + Signature Schemes: - **Ed25519**: Default, fast verification, 64-byte signatures - **Secp256k1 ECDSA**: Ethereum-compatible, variable-length signatures - + Note: The signature is deterministic for Ed25519 but may be randomized for Secp256k1, meaning the same data might produce different valid signatures @@ -724,70 +724,70 @@ def sign_transaction( def public_key(self) -> asymmetric_crypto.PublicKey: """Get the public key corresponding to this account's private key. - + The public key is the cryptographic counterpart to the private key and is used for signature verification, address derivation, and sharing with others who need to verify signatures or send transactions to this account. - + Returns: asymmetric_crypto.PublicKey: The public key that corresponds to this account's private key. - + Examples: Get public key for verification:: - + account = Account.generate() public_key = account.public_key() - + # Use for signature verification message = b"test message" signature = account.sign(message) is_valid = public_key.verify(message, signature) print(f"Signature valid: {is_valid}") # True - + Share public key safely:: - + # Public keys are safe to share public_key_hex = str(account.public_key()) print(f"My public key: {public_key_hex}") - + # Others can use it to: # 1. Verify signatures from you # 2. Derive your account address # 3. Send you transactions - + Address derivation:: - + from aptos_sdk.account_address import AccountAddress - + # Address is derived from public key derived_address = AccountAddress.from_key(account.public_key()) account_address = account.address() - + assert derived_address == account_address - + Multi-signature setup:: - + # Collect public keys for multi-sig account accounts = [Account.generate() for _ in range(3)] public_keys = [acc.public_key() for acc in accounts] - + # Use public_keys to create multi-signature account # (threshold signatures, etc.) - + Key Properties: - **Safe to Share**: Public keys can be shared openly without risk - **Deterministic**: Always the same for a given private key - **Verification**: Used to verify signatures created by the private key - **Address Derivation**: Account addresses are computed from public keys - + Common Uses: - Signature verification by other parties - Creating multi-signature accounts - Address computation and validation - Key rotation proofs and challenges - Smart contract public key storage - + Note: Unlike private keys, public keys are safe to store, transmit, and share. They enable others to interact with your account without compromising security. @@ -798,11 +798,11 @@ def public_key(self) -> asymmetric_crypto.PublicKey: class RotationProofChallenge: """ Represents a rotation proof challenge for rotating authentication keys. - + This challenge is used to prove ownership when rotating an account's authentication key to a new public key. """ - + type_info_account_address: AccountAddress = AccountAddress.from_str("0x1") type_info_module_name: str = "account" type_info_struct_name: str = "RotationProofChallenge" diff --git a/aptos_sdk/account_address.py b/aptos_sdk/account_address.py index 529eed2..d2ba7c1 100644 --- a/aptos_sdk/account_address.py +++ b/aptos_sdk/account_address.py @@ -4,8 +4,8 @@ """ Account address management for the Aptos blockchain. -This module provides comprehensive support for managing account addresses on the Aptos -blockchain, including address parsing, validation, derivation, and formatting according +This module provides comprehensive support for managing account addresses on the Aptos +blockchain, including address parsing, validation, derivation, and formatting according to the AIP-40 address standard. Key features: @@ -21,39 +21,39 @@ Examples: Basic address operations:: - + # Parse from string (strict) addr = AccountAddress.from_str("0x1") - + # Parse from string (relaxed) addr = AccountAddress.from_str_relaxed("1") - + # Derive from public key addr = AccountAddress.from_key(public_key) - + Special address handling:: - + # Special addresses use SHORT form special_addr = AccountAddress.from_str("0xa") print(special_addr) # "0xa" - + # Non-special addresses use LONG form regular_addr = AccountAddress.from_str( "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ) - + Resource and object addresses:: - + # Create resource account address resource_addr = AccountAddress.for_resource_account( creator_address, b"seed" ) - + # Create named object address object_addr = AccountAddress.for_named_object( creator_address, b"object_name" ) - + # Create token collection address collection_addr = AccountAddress.for_named_collection( creator_address, "My Collection" @@ -72,12 +72,12 @@ class AuthKeyScheme: """Authentication key schemes for address derivation. - + This class defines the byte constants used as suffixes when deriving addresses from various sources. Each scheme represents a different method of address derivation and ensures that addresses derived through different methods cannot collide. - + Attributes: Ed25519: Single Ed25519 key authentication (0x00) MultiEd25519: Multi-signature Ed25519 authentication (0x01) @@ -87,6 +87,7 @@ class AuthKeyScheme: DeriveObjectAddressFromSeed: Object address from seed (0xFE) DeriveResourceAccountAddress: Resource account address (0xFF) """ + Ed25519: bytes = b"\x00" MultiEd25519: bytes = b"\x01" SingleKey: bytes = b"\x02" @@ -98,14 +99,14 @@ class AuthKeyScheme: class ParseAddressError(Exception): """Exception raised when there's an error parsing an account address. - + This exception is raised when an address string or byte sequence cannot be parsed into a valid AccountAddress, typically due to invalid length, format, or content. - + Examples: Catching parse errors:: - + try: addr = AccountAddress.from_str("invalid") except ParseAddressError as e: @@ -115,55 +116,56 @@ class ParseAddressError(Exception): class AccountAddress: """Represents an account address on the Aptos blockchain. - + An AccountAddress is a 32-byte identifier for accounts, objects, and resources on the Aptos blockchain. It implements the AIP-40 address standard for parsing and formatting addresses in both strict and relaxed modes. - + The address system supports: - Special addresses (0x0 through 0xf) with SHORT representation - Regular addresses with LONG representation (64 hex characters) - Address derivation from public keys - Resource account and named object address generation - + Attributes: address: The raw 32-byte address data LENGTH: The required byte length of all addresses (32) - + Examples: Creating addresses:: - + # From hex string (strict parsing) addr1 = AccountAddress.from_str("0x1") - + # From hex string (relaxed parsing) addr2 = AccountAddress.from_str_relaxed("abc123") - + # From public key addr3 = AccountAddress.from_key(public_key) - + # From raw bytes addr4 = AccountAddress(b"\x00" * 32) - + Address formatting:: - + special_addr = AccountAddress.from_str("0xa") print(special_addr) # "0xa" (SHORT form) - + regular_addr = AccountAddress.from_str( "0x" + "1" * 64 ) print(regular_addr) # Long form with 0x prefix """ + address: bytes LENGTH: int = 32 def __init__(self, address: bytes): """Initialize an AccountAddress with raw address bytes. - + Args: address: The 32-byte address data. - + Raises: ParseAddressError: If the address is not exactly 32 bytes. """ @@ -174,10 +176,10 @@ def __init__(self, address: bytes): def __eq__(self, other: object) -> bool: """Check equality with another AccountAddress. - + Args: other: The object to compare with. - + Returns: True if both addresses have the same raw bytes. """ @@ -187,29 +189,29 @@ def __eq__(self, other: object) -> bool: def __str__(self): """Get the AIP-40 compliant string representation of this address. - + Represents an account address according to the v1 address standard defined in AIP-40. Special addresses (0x0 through 0xf) are shown in SHORT form, while all other addresses are shown in LONG form. - + The formatting rules are: - Special addresses: "0x0" through "0xf" (SHORT form) - Regular addresses: "0x" + 64 hex characters (LONG form) - + Returns: AIP-40 compliant string representation with "0x" prefix. - + Examples: Special address formatting:: - + addr = AccountAddress(b"\x00" * 32) str(addr) # "0x0" - + Regular address formatting:: - + addr = AccountAddress(b"\x10" + b"\x00" * 31) str(addr) # "0x1000000000000000000000000000000000000000000000000000000000000000" - + See Also: AIP-40 standard: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md """ @@ -220,7 +222,7 @@ def __str__(self): def __repr__(self): """Get the string representation for debugging. - + Returns: Same as __str__ for consistency. """ @@ -228,30 +230,30 @@ def __repr__(self): def is_special(self): """Check if this address qualifies as a "special" address. - + Special addresses are those in the range 0x0 to 0xf (inclusive) that can be represented in SHORT form according to AIP-40. An address is considered special if: - The first 31 bytes are all zero - The last byte is less than 16 (0x10) - + This corresponds to addresses that match the regex pattern: ^0{63}[0-9a-f]$ in hexadecimal representation. - + Returns: True if this is a special address that can use SHORT form. - + Examples: Special addresses:: - + AccountAddress(b"\x00" * 32).is_special() # True (0x0) AccountAddress(b"\x00" * 31 + b"\x0f").is_special() # True (0xf) - + Non-special addresses:: - + AccountAddress(b"\x00" * 31 + b"\x10").is_special() # False (0x10) AccountAddress(b"\x01" + b"\x00" * 31).is_special() # False - + See Also: AIP-40 standard: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md """ @@ -260,51 +262,51 @@ def is_special(self): @staticmethod def from_str(address: str) -> AccountAddress: """Create an AccountAddress from a hex string with strict AIP-40 validation. - + This function enforces the strictest address format requirements defined by AIP-40. It only accepts properly formatted addresses with appropriate prefixes and length requirements. - + Accepted formats: - LONG form: "0x" + exactly 64 hex characters - SHORT form: "0x" + single hex character (0-f) for special addresses only - + Args: address: A hex string representing the account address. - + Returns: A new AccountAddress instance. - + Raises: RuntimeError: If the address format doesn't meet strict AIP-40 requirements: - Missing "0x" prefix - Wrong length for address type - Padding zeroes in special addresses - Short form used for non-special addresses - + Examples: Valid strict format usage:: - + # Special addresses in SHORT form addr1 = AccountAddress.from_str("0x0") addr2 = AccountAddress.from_str("0xf") - + # Regular addresses in LONG form addr3 = AccountAddress.from_str( "0x" + "1" * 64 ) - + Invalid formats (will raise RuntimeError):: - + # Missing 0x prefix AccountAddress.from_str("123abc...") - + # Padded special address AccountAddress.from_str("0x0f") - + # Short form for non-special address AccountAddress.from_str("0x10") - + See Also: - from_str_relaxed: For lenient parsing - AIP-40: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md @@ -343,47 +345,47 @@ def from_str(address: str) -> AccountAddress: @staticmethod def from_str_relaxed(address: str) -> AccountAddress: """Create an AccountAddress from a hex string with relaxed validation. - + This function provides backward compatibility by accepting various address formats beyond the strict AIP-40 requirements. It's more permissive than from_str() and handles padding and missing prefixes automatically. - + Accepted formats: - LONG form: 64 hex characters (with or without "0x" prefix) - SHORT form: 1-63 hex characters (with or without "0x" prefix) - Padding zeroes are automatically added as needed - + Args: address: A hex string representing the account address. - + Returns: A new AccountAddress instance. - + Raises: RuntimeError: If the hex string is invalid: - Empty or too long (>64 characters after removing "0x") - Contains non-hexadecimal characters - + Examples: Flexible format handling:: - + # With or without 0x prefix addr1 = AccountAddress.from_str_relaxed("0x1") addr2 = AccountAddress.from_str_relaxed("1") - + # Padding handled automatically addr3 = AccountAddress.from_str_relaxed("abc123") addr4 = AccountAddress.from_str_relaxed("0x00abc123") - + # Long addresses addr5 = AccountAddress.from_str_relaxed( "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" ) - + Note: Use from_str() instead when possible for strict AIP-40 compliance. This method is primarily for backward compatibility. - + See Also: - from_str: For strict AIP-40 compliant parsing - AIP-40: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-40.md @@ -417,17 +419,17 @@ def from_str_relaxed(address: str) -> AccountAddress: @staticmethod def from_key(key: asymmetric_crypto.PublicKey) -> AccountAddress: """Derive an account address from a public key. - + Creates an account address by hashing the public key bytes along with the appropriate authentication scheme identifier. This ensures that different key types produce different addresses even with identical key material. - + The derivation process: 1. Hash the public key's cryptographic bytes 2. Append the appropriate AuthKeyScheme suffix 3. Take the SHA3-256 hash to produce the 32-byte address - + Args: key: A public key implementing the asymmetric_crypto.PublicKey interface. Supported types: @@ -435,25 +437,25 @@ def from_key(key: asymmetric_crypto.PublicKey) -> AccountAddress: - ed25519.MultiPublicKey (multi-signature Ed25519) - asymmetric_crypto_wrapper.PublicKey (single key wrapper) - asymmetric_crypto_wrapper.MultiPublicKey (multi-key wrapper) - + Returns: The derived AccountAddress for the given public key. - + Raises: Exception: If the key type is not supported. - + Examples: Deriving addresses from different key types:: - + # From Ed25519 public key ed25519_key = ed25519.PrivateKey.random().public_key() addr1 = AccountAddress.from_key(ed25519_key) - + # From multi-signature key keys = [ed25519.PrivateKey.random().public_key() for _ in range(3)] multisig_key = ed25519.MultiPublicKey(keys, threshold=2) addr2 = AccountAddress.from_key(multisig_key) - + Note: The same public key will always produce the same address, but different key types (even with identical cryptographic material) @@ -478,23 +480,23 @@ def from_key(key: asymmetric_crypto.PublicKey) -> AccountAddress: @staticmethod def for_resource_account(creator: AccountAddress, seed: bytes) -> AccountAddress: """Generate a resource account address. - + Resource accounts are special accounts that don't have corresponding private keys and are used to hold resources on behalf of other accounts. They are created deterministically from a creator address and seed. - + Args: creator: The address of the account creating the resource account. seed: Arbitrary bytes used to ensure uniqueness. - + Returns: The deterministic address for the resource account. - + Examples: Creating resource account addresses:: - + creator_addr = AccountAddress.from_str("0x1") - + # Different seeds produce different addresses resource1 = AccountAddress.for_resource_account( creator_addr, b"my_resource_1" @@ -502,13 +504,13 @@ def for_resource_account(creator: AccountAddress, seed: bytes) -> AccountAddress resource2 = AccountAddress.for_resource_account( creator_addr, b"my_resource_2" ) - + # Same creator + seed = same address (deterministic) resource3 = AccountAddress.for_resource_account( creator_addr, b"my_resource_1" ) assert resource1 == resource3 - + Note: Resource accounts are commonly used for storing program resources and don't have associated private keys, making them secure for @@ -523,33 +525,33 @@ def for_resource_account(creator: AccountAddress, seed: bytes) -> AccountAddress @staticmethod def for_guid_object(creator: AccountAddress, creation_num: int) -> AccountAddress: """Generate an object address from a GUID (Globally Unique Identifier). - + Creates a deterministic object address using the creator's address and a creation number. This is used for objects that are created sequentially and need unique addresses. - + Args: creator: The address of the account creating the object. creation_num: A sequential number used for uniqueness (typically incremented for each object created by this account). - + Returns: The deterministic address for the object. - + Examples: Creating sequential object addresses:: - + creator = AccountAddress.from_str("0x123abc...") - + # Sequential object creation obj1 = AccountAddress.for_guid_object(creator, 0) obj2 = AccountAddress.for_guid_object(creator, 1) obj3 = AccountAddress.for_guid_object(creator, 2) - + # Same parameters = same address obj1_duplicate = AccountAddress.for_guid_object(creator, 0) assert obj1 == obj1_duplicate - + Note: The creation_num is typically managed by the blockchain to ensure uniqueness. Each creator maintains their own sequence counter. @@ -565,39 +567,39 @@ def for_guid_object(creator: AccountAddress, creation_num: int) -> AccountAddres @staticmethod def for_named_object(creator: AccountAddress, seed: bytes) -> AccountAddress: """Generate a named object address from a seed. - + Creates a deterministic object address using the creator's address and an arbitrary seed. This allows for creating objects with predictable addresses based on meaningful names or identifiers. - + Args: creator: The address of the account creating the object. seed: Arbitrary bytes that uniquely identify this object. Often derived from human-readable names. - + Returns: The deterministic address for the named object. - + Examples: Creating named object addresses:: - + creator = AccountAddress.from_str("0x123abc...") - + # Objects named with meaningful identifiers config_obj = AccountAddress.for_named_object( creator, b"global_config" ) - + metadata_obj = AccountAddress.for_named_object( creator, b"metadata_store" ) - + # Same name = same address (deterministic) config_duplicate = AccountAddress.for_named_object( creator, b"global_config" ) assert config_obj == config_duplicate - + Note: This is commonly used for singleton objects or well-known resources that need predictable addresses for easy reference. @@ -613,39 +615,39 @@ def for_named_token( creator: AccountAddress, collection_name: str, token_name: str ) -> AccountAddress: """Generate a token address from collection and token names. - + Creates a deterministic address for a specific token within a collection. The address is derived from the creator address and a combination of the collection name and token name. - + Args: creator: The address of the account that created the collection. collection_name: The name of the token collection. token_name: The name of the specific token within the collection. - + Returns: The deterministic address for the named token. - + Examples: Creating token addresses:: - + creator = AccountAddress.from_str("0x123abc...") - + # Tokens in different collections token1 = AccountAddress.for_named_token( creator, "My NFT Collection", "Token #1" ) - + token2 = AccountAddress.for_named_token( creator, "My NFT Collection", "Token #2" ) - + # Same collection + token name = same address token1_duplicate = AccountAddress.for_named_token( creator, "My NFT Collection", "Token #1" ) assert token1 == token1_duplicate - + Note: The seed format is: collection_name + "::" + token_name This ensures tokens in different collections have different addresses @@ -662,37 +664,37 @@ def for_named_collection( creator: AccountAddress, collection_name: str ) -> AccountAddress: """Generate a collection address from a collection name. - + Creates a deterministic address for a token collection based on the creator address and collection name. - + Args: creator: The address of the account creating the collection. collection_name: The human-readable name of the collection. - + Returns: The deterministic address for the named collection. - + Examples: Creating collection addresses:: - + creator = AccountAddress.from_str("0x123abc...") - + # Collections with different names collection1 = AccountAddress.for_named_collection( creator, "My First Collection" ) - + collection2 = AccountAddress.for_named_collection( creator, "My Second Collection" ) - + # Same name = same address (deterministic) collection1_duplicate = AccountAddress.for_named_collection( creator, "My First Collection" ) assert collection1 == collection1_duplicate - + Note: This is commonly used for NFT collections and other grouped assets where a predictable address is needed for the collection metadata. @@ -702,13 +704,13 @@ def for_named_collection( @staticmethod def deserialize(deserializer: Deserializer) -> AccountAddress: """Deserialize an AccountAddress from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized AccountAddress instance. - + Raises: Exception: If there are insufficient bytes in the stream. """ @@ -716,7 +718,7 @@ def deserialize(deserializer: Deserializer) -> AccountAddress: def serialize(self, serializer: Serializer): """Serialize this AccountAddress to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -816,7 +818,7 @@ class TestAddresses: class Test(unittest.TestCase): """Comprehensive test suite for AccountAddress functionality. - + Tests all aspects of address handling including: - Address derivation from various sources - String parsing in strict and relaxed modes @@ -824,6 +826,7 @@ class Test(unittest.TestCase): - Special address handling - Resource and object address generation """ + def test_multi_ed25519(self): private_key_1 = ed25519.PrivateKey.from_str( "4e5e3be60f4bbd5e98d086d932f3ce779ff4b58da99bf9e5241ae1212a29e5fe" diff --git a/aptos_sdk/account_sequence_number.py b/aptos_sdk/account_sequence_number.py index 3159823..45daadd 100644 --- a/aptos_sdk/account_sequence_number.py +++ b/aptos_sdk/account_sequence_number.py @@ -21,47 +21,47 @@ Examples: Basic sequence number management:: - + from aptos_sdk.async_client import RestClient from aptos_sdk.account_address import AccountAddress from aptos_sdk.account_sequence_number import AccountSequenceNumber - + # Create client and sequence manager client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") account_addr = AccountAddress.from_str("0x123...") seq_manager = AccountSequenceNumber(client, account_addr) - + # Get next sequence number for transaction seq_num = await seq_manager.next_sequence_number() - + # Submit transaction with seq_num... - + # Wait for all pending transactions to complete await seq_manager.synchronize() - + High-throughput transaction submission:: - + # Submit multiple transactions concurrently tasks = [] for i in range(50): seq_num = await seq_manager.next_sequence_number() task = submit_transaction_with_sequence(seq_num) tasks.append(task) - + # Wait for all transactions await asyncio.gather(*tasks) await seq_manager.synchronize() - + Custom configuration:: - + from aptos_sdk.account_sequence_number import AccountSequenceNumberConfig - + # Custom flow control settings config = AccountSequenceNumberConfig() config.maximum_in_flight = 50 # Lower concurrency config.maximum_wait_time = 60 # Longer timeout config.sleep_time = 0.05 # Less aggressive polling - + seq_manager = AccountSequenceNumber(client, account_addr, config) """ @@ -79,10 +79,10 @@ class AccountSequenceNumberConfig: """Configuration parameters for account sequence number management. - + This class defines the flow control parameters used by AccountSequenceNumber to manage transaction submission rates and handle network congestion. - + Attributes: maximum_in_flight: Maximum number of unconfirmed transactions allowed per account (default: 100). This matches Aptos mempool limits. @@ -90,21 +90,21 @@ class AccountSequenceNumberConfig: before forcing a resync (default: 30). sleep_time: Seconds to sleep between network polls when waiting (default: 0.01). - + Examples: Custom configuration:: - + config = AccountSequenceNumberConfig() config.maximum_in_flight = 50 # More conservative config.maximum_wait_time = 60 # Longer timeout config.sleep_time = 0.05 # Less aggressive polling - + Low-latency configuration:: - + config = AccountSequenceNumberConfig() config.maximum_in_flight = 10 # Fewer concurrent txns config.sleep_time = 0.001 # Very frequent polling - + Note: The default values are optimized for the Aptos mainnet and testnet environments. Adjust based on network conditions and requirements. @@ -117,25 +117,25 @@ class AccountSequenceNumberConfig: class AccountSequenceNumber: """Thread-safe sequence number manager for high-throughput transaction submission. - + This class manages sequence number allocation for an Aptos account with built-in flow control to prevent mempool overflow. It implements the same strategy used by the Aptos faucet for reliable high-volume transaction processing. - + Flow Control Strategy: - Allows up to 100 transactions in flight simultaneously (configurable) - Monitors network state to track transaction confirmations - Implements automatic backoff when mempool capacity is reached - Provides timeout-based recovery for stuck transactions - Ensures FIFO ordering of sequence number allocation - + Key Features: - **Concurrency Safe**: Multiple async tasks can safely request sequence numbers - **Automatic Initialization**: Syncs with on-chain state on first use - **Flow Control**: Respects mempool limits to prevent rejection - **Recovery Mechanisms**: Handles network issues and stuck transactions - **Ordering Guarantees**: FIFO sequence number allocation via async locks - + Attributes: _client: REST client for network communication _account: The account address being managed @@ -146,59 +146,59 @@ class AccountSequenceNumber: _last_committed_number: Last confirmed on-chain sequence number _current_number: Next sequence number to allocate _initialized: Whether the manager has been initialized - + Important Assumptions: - Each account should be managed by exactly one AccountSequenceNumber instance - The account should not be used for manual transaction submission while managed - Network connectivity is generally stable (handles temporary failures) - Transactions eventually confirm or fail (not permanently stuck) - + Usage Guidelines: - Call synchronize() after transaction failures to reset state - Use non-blocking mode (block=False) to check availability without waiting - Monitor logs for timeout warnings indicating potential issues - Configure parameters based on network conditions and requirements - + Examples: Basic usage:: - + seq_manager = AccountSequenceNumber(client, account_address) - + # Get next sequence number seq_num = await seq_manager.next_sequence_number() - + # Submit transaction... - + # Wait for completion await seq_manager.synchronize() - + High-throughput submission:: - + # Submit 50 transactions concurrently tasks = [] for i in range(50): seq_num = await seq_manager.next_sequence_number() task = submit_transaction(seq_num) tasks.append(task) - + await asyncio.gather(*tasks) await seq_manager.synchronize() - + Error handling:: - + try: seq_num = await seq_manager.next_sequence_number(block=False) if seq_num is None: print("Too many transactions in flight, try later") return - + # Submit transaction... - + except Exception as e: # Reset state after errors await seq_manager.synchronize() raise - + Warning: Do not use the same account with multiple AccountSequenceNumber instances simultaneously, as this will lead to sequence number conflicts and transaction @@ -224,28 +224,28 @@ def __init__( config: AccountSequenceNumberConfig = AccountSequenceNumberConfig(), ): """Initialize a sequence number manager for the given account. - + Args: client: REST client for communicating with the Aptos network. account: The account address to manage sequence numbers for. config: Configuration parameters for flow control behavior. Defaults to standard settings optimized for Aptos networks. - + Examples: Standard initialization:: - + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") account = AccountAddress.from_str("0x123...") seq_manager = AccountSequenceNumber(client, account) - + Custom configuration:: - + config = AccountSequenceNumberConfig() config.maximum_in_flight = 50 config.maximum_wait_time = 60 - + seq_manager = AccountSequenceNumber(client, account, config) - + Note: The sequence manager starts uninitialized and will automatically sync with the on-chain state on first use. @@ -260,50 +260,50 @@ def __init__( async def next_sequence_number(self, block: bool = True) -> Optional[int]: """Get the next available sequence number for transaction submission. - + This method provides thread-safe allocation of sequence numbers with built-in flow control. It ensures FIFO ordering through an async lock and respects mempool limits to prevent transaction rejection. - + Args: block: If True (default), wait for an available sequence number when the maximum number of transactions are in flight. If False, return None immediately when no sequence numbers are available. - + Returns: The next sequence number to use for transaction submission, or None if block=False and the maximum number of transactions are in flight. - + Raises: Exception: Network communication errors or other failures during synchronization with the blockchain state. - + Examples: Blocking mode (default):: - + # This will wait if necessary seq_num = await seq_manager.next_sequence_number() transaction.sequence_number = seq_num - + Non-blocking mode:: - + # Check availability without waiting seq_num = await seq_manager.next_sequence_number(block=False) if seq_num is None: print("Account busy, try again later") return - + Batch processing:: - + batch_size = 10 sequence_numbers = [] - + for i in range(batch_size): seq_num = await seq_manager.next_sequence_number() sequence_numbers.append(seq_num) - + # Submit all transactions... - + Note: This method automatically initializes the sequence manager on first use by querying the current on-chain sequence number. Subsequent calls use @@ -336,11 +336,11 @@ async def next_sequence_number(self, block: bool = True) -> Optional[int]: async def _initialize(self): """Initialize the sequence manager with current on-chain state. - + This method is automatically called on first use of next_sequence_number. It queries the network to get the current sequence number for the account and sets up the internal state tracking. - + Note: This is an internal method. Users should not call it directly as it's automatically invoked when needed. @@ -351,35 +351,35 @@ async def _initialize(self): async def synchronize(self): """Wait for all pending transactions to complete or timeout. - + This method creates a synchronization barrier that blocks all other operations until either: 1. All pending transactions are confirmed on-chain, or 2. The maximum wait time is exceeded - + During synchronization, no new sequence numbers can be allocated, ensuring a consistent view of the account state. - + Use Cases: - After transaction submission to ensure completion - Before critical operations requiring known account state - After errors to reset and resync with network state - At application shutdown to wait for pending operations - + Examples: Wait for transaction batch completion:: - + # Submit transactions for data in transaction_batch: seq_num = await seq_manager.next_sequence_number() await submit_transaction(data, seq_num) - + # Wait for all to complete await seq_manager.synchronize() print("All transactions processed") - + Error recovery:: - + try: # Transaction operations... pass @@ -387,11 +387,11 @@ async def synchronize(self): logging.error(f"Transaction failed: {e}") # Reset state await seq_manager.synchronize() - + Raises: Exception: Network communication errors or other failures during the synchronization process. - + Warning: This method may take significant time to complete if transactions are slow to confirm. Monitor the logs for timeout warnings. @@ -404,15 +404,15 @@ async def synchronize(self): async def _resync(self, check: Callable[[AccountSequenceNumber], bool]): """Force resynchronization with the blockchain state. - + This internal method implements the timeout and recovery logic when transactions are not confirming as expected. It polls the network state and attempts to determine which transactions have confirmed. - + Args: check: A callable that returns True while resync should continue. Used to implement different resync conditions. - + Note: This is an internal method called within the async lock context. It should not be called directly by users. @@ -450,10 +450,10 @@ async def _resync(self, check: Callable[[AccountSequenceNumber], bool]): async def _update(self): """Update the last committed sequence number from the network. - + Returns: The current sequence number from the blockchain. - + Note: This is an internal method for network state synchronization. """ @@ -462,10 +462,10 @@ async def _update(self): async def _current_sequence_number(self) -> int: """Get the current sequence number for the account from the network. - + Returns: The current sequence number as reported by the blockchain. - + Note: This is an internal method that queries the network directly. """ @@ -474,16 +474,17 @@ async def _current_sequence_number(self) -> int: class Test(unittest.IsolatedAsyncioTestCase): """Test suite for AccountSequenceNumber functionality. - + Tests the sequence number management including: - Sequential number allocation - Flow control when at capacity - Network state synchronization - Blocking vs non-blocking behavior """ + async def test_common_path(self): """Test the common usage patterns of AccountSequenceNumber. - + This test verifies: - Sequential number allocation starting from the current on-chain state - Proper handling of on-chain state updates (e.g., 0 -> 5 -> 100+) diff --git a/aptos_sdk/aptos_cli_wrapper.py b/aptos_sdk/aptos_cli_wrapper.py index 65ed548..73cc860 100644 --- a/aptos_sdk/aptos_cli_wrapper.py +++ b/aptos_sdk/aptos_cli_wrapper.py @@ -33,47 +33,47 @@ Examples: Compile a Move package:: - + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper from aptos_sdk.account_address import AccountAddress - + # Define named addresses named_addresses = { "MyModule": AccountAddress.from_str("***1234..."), "Treasury": AccountAddress.from_str("***5678...") } - + # Compile the package AptosCLIWrapper.compile_package( package_dir="./my-move-package", named_addresses=named_addresses ) - + Run Move unit tests:: - + # Test the package with same named addresses AptosCLIWrapper.test_package( package_dir="./my-move-package", named_addresses=named_addresses ) - + Start a local testnet:: - + # Start local node with faucet local_node = AptosCLIWrapper.start_node() - + # Wait for node to become operational is_ready = await local_node.wait_until_operational() - + if is_ready: print("Local testnet is ready!") # Use LOCAL_NODE and LOCAL_FAUCET constants for connections - + # Clean up when done local_node.stop() - + Check CLI availability:: - + if AptosCLIWrapper.does_cli_exist(): print("Aptos CLI is available") else: @@ -81,7 +81,7 @@ Environment Variables: APTOS_CLI_PATH: Path to the Aptos CLI executable if not in system PATH. - + Requirements: - Aptos CLI installed and accessible - Rust toolchain (for Move compilation) @@ -92,7 +92,7 @@ The module provides specific exception types: - MissingCLIError: CLI not found or not accessible - CLIError: CLI command execution failed - + Security Considerations: - Local testnets are for development only - Private keys generated by local testnets are not secure @@ -132,47 +132,47 @@ class AptosCLIWrapper: """Python wrapper for the official Aptos CLI with integrated tooling support. - + This class provides static methods for invoking Aptos CLI operations from Python, including Move package compilation, testing, and local testnet management. It handles subprocess management, error reporting, and named address resolution. - + Key Features: - **Static Interface**: All methods are static for easy access - **Error Handling**: Comprehensive error reporting with detailed output - **Named Addresses**: Automatic formatting of named address parameters - **Process Management**: Robust subprocess execution with output capture - **Validation**: CLI availability checking before execution - + Examples: Basic package operations:: - + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper - + # Check if CLI is available if not AptosCLIWrapper.does_cli_exist(): raise Exception("Aptos CLI not found") - + # Compile a Move package AptosCLIWrapper.compile_package("./my-package", {}) - + # Run tests AptosCLIWrapper.test_package("./my-package", {}) - + With named addresses:: - + from aptos_sdk.account_address import AccountAddress - + named_addresses = { "admin": AccountAddress.from_str("***1"), "user": AccountAddress.from_str("***2") } - + AptosCLIWrapper.compile_package( package_dir="./complex-package", named_addresses=named_addresses ) - + Note: All methods require the Aptos CLI to be installed and accessible. Use does_cli_exist() to verify availability before calling other methods. @@ -183,31 +183,31 @@ def prepare_named_addresses( named_addresses: Dict[str, AccountAddress] ) -> List[str]: """Convert named addresses dictionary to CLI argument format. - + This method transforms a Python dictionary of named addresses into the command-line argument format expected by the Aptos CLI. It handles the proper formatting and comma separation required by the CLI. - + Args: named_addresses: Dictionary mapping address names to AccountAddress objects. - + Returns: List of CLI arguments for named addresses, empty if no addresses provided. - + Examples: Empty addresses:: - + >>> AptosCLIWrapper.prepare_named_addresses({}) [] - + Single address:: - + >>> addresses = {"admin": AccountAddress.from_str("***1")} >>> AptosCLIWrapper.prepare_named_addresses(addresses) ['--named-addresses', 'admin=***1'] - + Multiple addresses:: - + >>> addresses = { ... "admin": AccountAddress.from_str("***1"), ... "user": AccountAddress.from_str("***2") @@ -215,7 +215,7 @@ def prepare_named_addresses( >>> args = AptosCLIWrapper.prepare_named_addresses(addresses) >>> args ['--named-addresses', 'admin=***1,user=***2'] - + Note: The CLI expects named addresses in a comma-separated format after the --named-addresses flag. This method handles the formatting @@ -237,50 +237,50 @@ def prepare_named_addresses( @staticmethod def compile_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): """Compile a Move package using the Aptos CLI. - + This method compiles a Move package with the specified named addresses, generating bytecode and metadata required for package publishing. The compilation process validates Move code, resolves dependencies, and produces deployable artifacts. - + Args: package_dir: Path to the directory containing the Move package (must contain a Move.toml file). named_addresses: Dictionary mapping named address identifiers to their resolved AccountAddress values. - + Raises: MissingCLIError: If the Aptos CLI is not found or accessible. CLIError: If the compilation process fails with detailed error output. - + Examples: Compile basic package:: - + AptosCLIWrapper.compile_package( package_dir="./my-move-package", named_addresses={} ) - + Compile with named addresses:: - + from aptos_sdk.account_address import AccountAddress - + named_addresses = { "deployer": AccountAddress.from_str("***1234..."), "resource_account": AccountAddress.from_str("***5678...") } - + AptosCLIWrapper.compile_package( package_dir="./complex-package", named_addresses=named_addresses ) - + Compilation Output: - Generated bytecode in build/ directory - Package metadata for publishing - Dependency resolution artifacts - ABI files for integration - + Note: - Requires Move.toml configuration file in package directory - Named addresses must match those declared in Move.toml @@ -305,42 +305,42 @@ def compile_package(package_dir: str, named_addresses: Dict[str, AccountAddress] @staticmethod def start_node() -> AptosInstance: """Start a local Aptos testnet for development and testing. - + This method launches a complete local Aptos testnet including: - A single validator node - Built-in faucet service - REST API endpoint - Pre-funded test accounts - + Returns: AptosInstance object representing the running testnet. - + Raises: MissingCLIError: If the Aptos CLI is not found or accessible. - + Examples: Basic local testnet:: - + # Start the testnet testnet = AptosCLIWrapper.start_node() - + # Wait for it to become operational is_ready = await testnet.wait_until_operational() - + if is_ready: # Use LOCAL_NODE and LOCAL_FAUCET for connections from aptos_sdk.async_client import RestClient, FaucetClient - + client = RestClient(LOCAL_NODE) faucet = FaucetClient(LOCAL_FAUCET, client) - + # Perform operations... - + # Clean up testnet.stop() - + Context manager pattern:: - + async def with_testnet(): testnet = AptosCLIWrapper.start_node() try: @@ -348,14 +348,14 @@ async def with_testnet(): yield testnet finally: testnet.stop() - + Testnet Features: - Fresh blockchain state for each run - Pre-funded accounts for testing - Fast block times for rapid iteration - Full Move VM and transaction processing - REST API compatible with mainnet/testnet - + Note: - Creates temporary directories that are cleaned up on stop - Uses default ports (8080 for REST API, 8081 for faucet) @@ -368,63 +368,63 @@ async def with_testnet(): @staticmethod def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): """Run Move unit tests for a package using the Aptos CLI. - + This method executes all unit tests defined in the Move package, providing comprehensive test coverage and validation. Tests run in an isolated environment with proper address resolution. - + Args: package_dir: Path to the directory containing the Move package (must contain a Move.toml file). named_addresses: Dictionary mapping named address identifiers to their resolved AccountAddress values. - + Raises: MissingCLIError: If the Aptos CLI is not found or accessible. CLIError: If any tests fail or the test process encounters errors. - + Examples: Run basic tests:: - + AptosCLIWrapper.test_package( package_dir="./my-move-package", named_addresses={} ) - + Run tests with named addresses:: - + from aptos_sdk.account_address import AccountAddress - + test_addresses = { "test_admin": AccountAddress.from_str("***cafe"), "test_user": AccountAddress.from_str("***beef") } - + AptosCLIWrapper.test_package( package_dir="./my-package", named_addresses=test_addresses ) - + Test Features: - Isolated test execution environment - Access to Move testing framework - Proper address and resource simulation - Detailed test result reporting - Coverage analysis capabilities - + Test Structure: Tests should be defined using the Move testing framework:: - + #[test] public fun test_basic_functionality() { // Test logic here } - + #[test(admin = @***1, user = @***2)] public fun test_with_addresses(admin: &signer, user: &signer) { // Test with specific signers } - + Note: - Tests run in a simulated blockchain environment - Named addresses are resolved during test execution @@ -448,15 +448,15 @@ def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): @staticmethod def assert_cli_exists(): """Assert that the Aptos CLI is available and accessible. - + Raises: MissingCLIError: If the Aptos CLI cannot be found in the system PATH or at the location specified by APTOS_CLI_PATH environment variable. - + Example: >>> AptosCLIWrapper.assert_cli_exists() # Raises MissingCLIError if CLI not found - + Note: This method is called internally by other CLI operations to ensure the CLI is available before attempting to execute commands. @@ -467,34 +467,34 @@ def assert_cli_exists(): @staticmethod def does_cli_exist(): """Check if the Aptos CLI is available and accessible. - + This method verifies that the Aptos CLI binary can be found and executed from the current environment. It checks both the system PATH and any custom path specified via the APTOS_CLI_PATH environment variable. - + Returns: True if the CLI is available, False otherwise. - + Examples: Check CLI availability:: - + if AptosCLIWrapper.does_cli_exist(): print("Aptos CLI is ready to use") AptosCLIWrapper.compile_package("./package", {}) else: print("Please install the Aptos CLI") print("Visit: https://aptos.dev/tools/aptos-cli/") - + Conditional operations:: - + def safe_compile(package_dir, named_addresses): if not AptosCLIWrapper.does_cli_exist(): raise RuntimeError("Aptos CLI not available") return AptosCLIWrapper.compile_package(package_dir, named_addresses) - + Environment Variables: APTOS_CLI_PATH: Custom path to the Aptos CLI binary if not in PATH. - + Note: This method uses shutil.which() to locate the CLI binary, which respects the system PATH and executable permissions. @@ -504,59 +504,61 @@ def safe_compile(package_dir, named_addresses): class MissingCLIError(Exception): """Exception raised when the Aptos CLI cannot be found or accessed. - + This error indicates that the Aptos CLI binary is not available in the system PATH or at the location specified by the APTOS_CLI_PATH environment variable. This prevents any CLI operations from being executed. - + Common Causes: - Aptos CLI is not installed - CLI binary is not in system PATH - APTOS_CLI_PATH points to incorrect location - Insufficient permissions to execute the CLI - CLI binary is corrupted or incompatible - + Resolution: 1. Install the Aptos CLI from https://aptos.dev/tools/aptos-cli/ 2. Ensure the binary is in your system PATH 3. Set APTOS_CLI_PATH environment variable if using custom location 4. Verify executable permissions on the CLI binary - + Examples: Handling the error:: - + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper, MissingCLIError - + try: AptosCLIWrapper.compile_package("./package", {}) except MissingCLIError as e: print(f"CLI Error: {e}") print("Please install the Aptos CLI") - + Pre-emptive checking:: - + if not AptosCLIWrapper.does_cli_exist(): raise MissingCLIError() - + # Safe to use CLI operations AptosCLIWrapper.compile_package("./package", {}) - + Attributes: message: Detailed error message indicating the expected CLI path. """ def __init__(self): """Initialize MissingCLIError with information about the expected CLI path.""" - super().__init__(f"The CLI was not found in the expected path, {DEFAULT_BINARY}") + super().__init__( + f"The CLI was not found in the expected path, {DEFAULT_BINARY}" + ) class CLIError(Exception): """Exception raised when an Aptos CLI command execution fails. - + This error captures the details of a failed CLI operation, including the command that was executed, its output, and any error messages. It provides comprehensive information for debugging CLI integration issues. - + Common Causes: - Compilation errors in Move code - Missing dependencies or configuration @@ -565,27 +567,27 @@ class CLIError(Exception): - File system permission problems - Invalid Move.toml configuration - Conflicting named addresses - + Examples: Handling compilation errors:: - + from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper, CLIError - + try: AptosCLIWrapper.compile_package("./faulty-package", {}) except CLIError as e: print(f"Compilation failed: {e}") # Error includes command, output, and stderr for debugging - + Handling test failures:: - + try: AptosCLIWrapper.test_package("./package-with-failing-tests", {}) except CLIError as e: print("Tests failed with details:") print(e) # Includes test failure details and error output - + Attributes: command: List of command arguments that were executed. output: Standard output from the failed command. @@ -595,7 +597,7 @@ class CLIError(Exception): def __init__(self, command, output, error): """Initialize CLIError with command execution details. - + Args: command: List of command arguments that were executed. output: Standard output bytes from the command. @@ -604,7 +606,7 @@ def __init__(self, command, output, error): self.command = command self.output = output self.error = error - + super().__init__( f"The CLI operation failed:\n\tCommand: {' '.join(command)}\n\tOutput: {output}\n\tError: {error}" ) @@ -612,41 +614,41 @@ def __init__(self, command, output, error): class AptosInstance: """Manages a local Aptos testnet instance for development and testing. - + This class represents a complete local Aptos testnet running as a subprocess, including validator node, faucet service, and REST API endpoints. It provides lifecycle management with start, stop, and health monitoring capabilities. - + Architecture: - Single validator node configuration - Built-in faucet for test token distribution - REST API endpoint for transaction submission - WebAPI for blockchain queries - Temporary storage that's cleaned up on stop - + Lifecycle Management: - Automatic subprocess management - Background output/error capture - Health monitoring with REST client - Graceful shutdown and cleanup - Temporary directory management - + Current Limitations: - Fixed port configuration (8080 for REST, 8081 for faucet) - Single instance per machine due to port conflicts - No persistent storage across restarts - Limited configuration customization - + Examples: Basic usage:: - + # Start local testnet testnet = AptosInstance.start() - + try: # Wait for testnet to be ready is_ready = await testnet.wait_until_operational() - + if is_ready: # Use testnet for operations from aptos_sdk.async_client import RestClient @@ -658,26 +660,26 @@ class AptosInstance: finally: # Always clean up testnet.stop() - + Monitoring testnet health:: - + testnet = AptosInstance.start() - + # Check if testnet is operational if await testnet.is_operational(): print("Testnet is healthy") else: print("Testnet is not responding") - + # Check for early termination if testnet.is_stopped(): print("Testnet process has stopped") errors = testnet.errors() if errors: print(f"Error output: {errors}") - + Context management pattern:: - + async def with_local_testnet(): testnet = AptosInstance.start() try: @@ -686,24 +688,24 @@ async def with_local_testnet(): except Exception: testnet.stop() raise - + Network Configuration: - REST API: http://127.0.0.1:8080/v1 - Faucet API: http://127.0.0.1:8081 - Chain ID: Dynamically generated for each instance - + Resource Management: - Creates temporary directory for blockchain data - Spawns subprocess for node execution - Background threads for output capture - Automatic cleanup on stop() or destruction - + Security Notes: - Intended for development and testing only - Private keys are generated for convenience, not security - No persistent storage or backup mechanisms - Local network access only - + Future Improvements: - Configurable port assignments for multiple instances - Enhanced process monitoring and restart capabilities @@ -718,11 +720,11 @@ async def with_local_testnet(): def __del__(self): """Destructor that ensures testnet cleanup on object destruction. - + This method is called automatically when the AptosInstance object is garbage collected, ensuring that the testnet process is properly terminated and resources are cleaned up. - + Note: While this provides a safety net, it's better to explicitly call stop() to ensure timely resource cleanup. @@ -733,15 +735,15 @@ def __init__( self, node_runner: subprocess.Popen, temp_dir: tempfile.TemporaryDirectory ): """Initialize AptosInstance with subprocess and temporary directory. - + This constructor sets up the testnet instance with process management and output capture. It starts background threads to continuously capture stdout and stderr from the node process. - + Args: node_runner: Subprocess running the Aptos node. temp_dir: Temporary directory for blockchain data storage. - + Note: This constructor is typically called by the start() class method rather than directly by user code. @@ -754,7 +756,7 @@ def __init__( def queue_lines(pipe, target): """Background thread function to capture process output. - + Args: pipe: Process pipe (stdout or stderr). target: List to store captured lines. @@ -782,39 +784,39 @@ def queue_lines(pipe, target): @staticmethod def start() -> AptosInstance: """Start a new local Aptos testnet instance. - + This factory method creates and starts a complete local Aptos testnet with validator node, faucet service, and REST API. The testnet runs in an isolated temporary directory and is ready for development use. - + Returns: AptosInstance object representing the running testnet. - + Raises: subprocess.SubprocessError: If the testnet process fails to start. OSError: If temporary directory creation fails. - + CLI Arguments Used: - run-local-testnet: Main command to start local testnet - --test-dir: Directory for blockchain data storage - --with-faucet: Enable built-in faucet service - --force-restart: Clean start, removing existing data - --assume-yes: Skip interactive confirmations - + Examples: Basic startup:: - + testnet = AptosInstance.start() - + # Wait for testnet to become ready if await testnet.wait_until_operational(): print("Testnet is ready for use!") - + # Clean up when done testnet.stop() - + Error handling:: - + try: testnet = AptosInstance.start() await testnet.wait_until_operational() @@ -822,18 +824,18 @@ def start() -> AptosInstance: print(f"Failed to start testnet: {e}") except OSError as e: print(f"Filesystem error: {e}") - + Network Endpoints: After successful startup, the testnet provides: - REST API: http://127.0.0.1:8080/v1 - Faucet: http://127.0.0.1:8081 - + Resource Allocation: - Temporary directory for blockchain storage - Subprocess for node execution - Background threads for output capture - Network ports (8080, 8081) - + Note: The testnet starts immediately but may take a few seconds to become fully operational. Use wait_until_operational() to ensure readiness @@ -857,26 +859,26 @@ def start() -> AptosInstance: def stop(self): """Stop the local testnet and clean up all associated resources. - + This method gracefully terminates the testnet process, waits for it to exit, and cleans up the temporary directory. It ensures complete resource cleanup and prevents resource leaks. - + Process: 1. Send termination signal to the node process 2. Wait for process to exit cleanly 3. Clean up temporary directory and files 4. Release any held resources - + Examples: Basic cleanup:: - + testnet = AptosInstance.start() # ... use testnet ... testnet.stop() # Always clean up - + Exception-safe cleanup:: - + testnet = AptosInstance.start() try: # Testnet operations @@ -884,51 +886,51 @@ def stop(self): # ... perform tests ... finally: testnet.stop() # Ensure cleanup even if tests fail - + Note: - This method is idempotent - safe to call multiple times - Automatically called by destructor as safety net - Blocks until process termination is complete - All blockchain data is permanently lost after cleanup - + Warning: After calling stop(), the AptosInstance should not be used for any further operations. Create a new instance if needed. """ - if hasattr(self, '_node_runner') and self._node_runner: + if hasattr(self, "_node_runner") and self._node_runner: self._node_runner.terminate() self._node_runner.wait() - if hasattr(self, '_temp_dir') and self._temp_dir: + if hasattr(self, "_temp_dir") and self._temp_dir: self._temp_dir.cleanup() def errors(self) -> List[str]: """Get error output lines from the testnet process. - + Returns: List of error messages from the node's stderr stream. - + Examples: Checking for errors:: - + testnet = AptosInstance.start() - + if testnet.is_stopped(): errors = testnet.errors() if errors: print("Testnet failed with errors:") for error in errors: print(f" {error.strip()}") - + Debugging startup issues:: - + testnet = AptosInstance.start() await asyncio.sleep(5) # Wait a bit - + if not await testnet.is_operational(): print("Testnet not ready, checking errors:") for error in testnet.errors(): print(error) - + Note: - Errors are captured continuously in background thread - List grows over time as more errors occur @@ -938,31 +940,31 @@ def errors(self) -> List[str]: def output(self) -> List[str]: """Get standard output lines from the testnet process. - + Returns: List of output messages from the node's stdout stream. - + Examples: Monitoring testnet output:: - + testnet = AptosInstance.start() await testnet.wait_until_operational() - + # Check what the testnet has logged output = testnet.output() print("Testnet output:") for line in output[-10:]: # Last 10 lines print(f" {line.strip()}") - + Debugging connectivity:: - + testnet = AptosInstance.start() - + # Look for specific startup messages output = testnet.output() if any("REST API" in line for line in output): print("REST API is ready") - + Note: - Output is captured continuously in background thread - Includes all informational and debug messages @@ -972,37 +974,37 @@ def output(self) -> List[str]: async def wait_until_operational(self) -> bool: """Wait for the testnet to become fully operational. - + This method polls the testnet's health endpoints until either the testnet becomes operational or the maximum wait time is exceeded. It checks both the REST API and faucet service for readiness. - + Returns: True if testnet becomes operational within timeout, False otherwise. - + Timeout: Maximum wait time is controlled by MAXIMUM_WAIT_TIME_SEC (30 seconds). - + Examples: Basic startup waiting:: - + testnet = AptosInstance.start() - + if await testnet.wait_until_operational(): print("Testnet is ready for use!") # Safe to make API calls else: print("Testnet failed to start within timeout") testnet.stop() - + With custom timeout handling:: - + testnet = AptosInstance.start() - + start_time = time.time() is_ready = await testnet.wait_until_operational() elapsed = time.time() - start_time - + if is_ready: print(f"Testnet ready in {elapsed:.2f} seconds") else: @@ -1010,18 +1012,18 @@ async def wait_until_operational(self) -> bool: errors = testnet.errors() if errors: print("Error output:", errors[-5:]) # Last 5 errors - + Health Checks: - REST API chain_id() call must succeed - Faucet healthy() check must return True - Process must still be running - + Polling Behavior: - Checks health every 100ms (0.1 seconds) - Exits early if testnet becomes operational - Exits early if testnet process stops - Times out after MAXIMUM_WAIT_TIME_SEC seconds - + Note: This method is essential for ensuring testnet readiness before performing operations. Using the testnet before it's operational @@ -1043,32 +1045,32 @@ async def wait_until_operational(self) -> bool: async def is_operational(self) -> bool: """Check if the testnet is currently operational. - + This method performs health checks on both the REST API and faucet service to determine if the testnet is ready to handle requests. - + Returns: True if both REST API and faucet are responding correctly. - + Health Checks: 1. REST API chain_id() call - validates core node functionality 2. Faucet healthy() check - validates faucet service availability - + Examples: Simple health check:: - + testnet = AptosInstance.start() await asyncio.sleep(2) # Give it time to start - + if await testnet.is_operational(): print("Testnet is healthy") else: print("Testnet is not responding") - + Continuous monitoring:: - + testnet = AptosInstance.start() - + for i in range(10): if await testnet.is_operational(): print(f"Healthy at attempt {i+1}") @@ -1076,13 +1078,13 @@ async def is_operational(self) -> bool: await asyncio.sleep(1) else: print("Testnet failed to become healthy") - + Error Handling: - Any exception during health checks returns False - Network timeouts are treated as non-operational - HTTP errors indicate non-operational state - Client connections are properly cleaned up - + Note: This method creates temporary client connections that are automatically closed after the health check completes. @@ -1104,38 +1106,38 @@ async def is_operational(self) -> bool: def is_stopped(self) -> bool: """Check if the testnet process has stopped running. - + Returns: True if the node process has terminated, False if still running. - + Examples: Check for early termination:: - + testnet = AptosInstance.start() await asyncio.sleep(1) - + if testnet.is_stopped(): print("Testnet stopped unexpectedly") errors = testnet.errors() if errors: print("Last error:", errors[-1]) - + Monitor during operations:: - + testnet = AptosInstance.start() await testnet.wait_until_operational() - + # Perform operations... - + if testnet.is_stopped(): print("Testnet crashed during operation") # Handle the failure - + Return Code: - None: Process is still running - 0: Process exited successfully - Non-zero: Process exited with error - + Note: This method checks the subprocess return code without blocking. It's useful for detecting unexpected termination during operations. diff --git a/aptos_sdk/aptos_token_client.py b/aptos_sdk/aptos_token_client.py index 3d44551..2209a5c 100644 --- a/aptos_sdk/aptos_token_client.py +++ b/aptos_sdk/aptos_token_client.py @@ -20,7 +20,7 @@ Digital Asset Architecture: The Aptos token standard uses the object model where each token is represented as an independent object on-chain. This provides several benefits: - + - **Composability**: Tokens can be extended with additional resources - **Flexibility**: Properties can be modified after creation (if permitted) - **Efficiency**: Direct object addressing without complex lookups @@ -48,16 +48,16 @@ Examples: Create a basic NFT collection and mint tokens:: - + from aptos_sdk.aptos_token_client import AptosTokenClient, PropertyMap, Property from aptos_sdk.async_client import RestClient from aptos_sdk.account import Account - + # Setup client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") token_client = AptosTokenClient(client) creator = Account.load("./creator_account.json") - + # Create collection collection_txn = await token_client.create_collection( creator=creator, @@ -77,12 +77,12 @@ royalty_numerator=5, # 5% royalty royalty_denominator=100 ) - + await client.wait_for_transaction(collection_txn) print(f"Collection created: {collection_txn}") - + Mint a token with custom properties:: - + # Create properties for the token properties = PropertyMap([ Property.string("rarity", "legendary"), @@ -90,7 +90,7 @@ Property.bool("is_special", True), Property.bytes("metadata", b"custom_data") ]) - + # Mint the token mint_txn = await token_client.mint_token( creator=creator, @@ -100,20 +100,20 @@ uri="https://example.com/tokens/sword1.json", properties=properties ) - + await client.wait_for_transaction(mint_txn) - + # Get the minted token addresses token_addresses = await token_client.tokens_minted_from_transaction(mint_txn) print(f"Minted token at: {token_addresses[0]}") - + Create a soul-bound token (non-transferable):: - + from aptos_sdk.account_address import AccountAddress - + # Soul-bound tokens cannot be transferred recipient = AccountAddress.from_str("***abc123...") - + soul_bound_txn = await token_client.mint_soul_bound_token( creator=creator, collection="Amazing NFTs", @@ -123,45 +123,45 @@ properties=PropertyMap([Property.string("achievement", "quest_master")]), soul_bound_to=recipient ) - + Read token information:: - + # Read token details from blockchain token_address = AccountAddress.from_str("***token_address...") token_data = await token_client.read_object(token_address) - + print(f"Token data: {token_data}") - + # Access specific resources if Token in token_data.resources: token = token_data.resources[Token] print(f"Token name: {token.name}") print(f"Description: {token.description}") - + if PropertyMap in token_data.resources: props = token_data.resources[PropertyMap] print(f"Properties: {props}") - + Transfer and manage tokens:: - + from aptos_sdk.account_address import AccountAddress - + # Transfer token to another account recipient = AccountAddress.from_str("***recipient_address...") owner = Account.load("./token_owner.json") - + transfer_txn = await token_client.transfer_token( owner=owner, token=token_addresses[0], to=recipient ) - + # Freeze token (prevent transfers) freeze_txn = await token_client.freeze_token( creator=creator, token=token_addresses[0] ) - + # Update token properties (if allowed) new_property = Property.u64("level", 50) # Level up! update_txn = await token_client.update_token_property( @@ -175,7 +175,7 @@ - Token minting: ~150,000 gas units - Property updates: ~50,000 gas units - Transfers: ~20,000 gas units - + Security Best Practices: - **Mutable Permissions**: Carefully configure what aspects can be changed - **Royalty Settings**: Set reasonable royalty percentages (typically 2.5-10%) @@ -219,20 +219,20 @@ class Object: """Represents an Aptos object with ownership and transfer permissions. - + The Object class encapsulates the core object metadata including ownership and transfer restrictions. This is the base resource for all objects on Aptos, including digital assets (tokens). - + Attributes: allow_ungated_transfer (bool): Whether the object can be transferred without explicit permission from the owner. owner (AccountAddress): The current owner of the object. struct_tag (str): The Move struct identifier for object resources. - + Examples: Parse object data from blockchain response:: - + resource_data = { "allow_ungated_transfer": True, "owner": "***abc123..." @@ -240,11 +240,12 @@ class Object: obj = Object.parse(resource_data) print(f"Object owner: {obj.owner}") print(f"Transferable: {obj.allow_ungated_transfer}") - + Note: Objects with allow_ungated_transfer=False require explicit approval from the owner or authorized parties for transfers. """ + allow_ungated_transfer: bool owner: AccountAddress @@ -273,21 +274,21 @@ def __str__(self) -> str: class Collection: """Represents a token collection on the Aptos blockchain. - + A collection is a container for related tokens (NFTs) that share common properties and governance. Collections define the rules and metadata for all tokens within them. - + Attributes: creator (AccountAddress): The address of the account that created the collection. description (str): Human-readable description of the collection. name (str): Unique name of the collection. uri (str): URI pointing to collection metadata (JSON). struct_tag (str): The Move struct identifier for collection resources. - + Examples: Parse collection data from blockchain:: - + resource_data = { "creator": "***abc123...", "description": "A collection of unique digital art pieces", @@ -296,11 +297,12 @@ class Collection: } collection = Collection.parse(resource_data) print(f"Collection: {collection.name} by {collection.creator}") - + Note: The collection URI should point to a JSON file following the standard collection metadata schema for proper marketplace compatibility. """ + creator: AccountAddress description: str name: str @@ -335,26 +337,26 @@ def parse(resource: dict[str, Any]) -> Collection: class Royalty: """Represents royalty information for token collections and secondary sales. - + Royalties enable creators to earn a percentage of secondary sales of their tokens on marketplaces and other platforms. The royalty is represented as a fraction (numerator/denominator) and paid to a specific address. - + Attributes: numerator (int): The numerator of the royalty fraction. denominator (int): The denominator of the royalty fraction. payee_address (AccountAddress): The address that receives royalty payments. struct_tag (str): The Move struct identifier for royalty resources. - + Examples: Calculate royalty percentage:: - + royalty = Royalty(250, 10000, payee_address) # 2.5% royalty percentage = (royalty.numerator / royalty.denominator) * 100 print(f"Royalty: {percentage}% to {royalty.payee_address}") - + Parse royalty from blockchain data:: - + resource_data = { "numerator": 500, "denominator": 10000, @@ -362,11 +364,12 @@ class Royalty: } royalty = Royalty.parse(resource_data) print(f"Royalty: {royalty}") # 5% royalty - + Note: Common royalty percentages range from 2.5% to 10%. The fraction should be simplified to avoid unnecessary precision (e.g., use 1/40 instead of 25/1000). """ + numerator: int denominator: int payee_address: AccountAddress @@ -398,10 +401,10 @@ def parse(resource: dict[str, Any]) -> Royalty: class Token: """Represents an individual token (NFT) on the Aptos blockchain. - + A token is a unique digital asset within a collection. Each token has its own metadata, properties, and can be individually owned and transferred. - + Attributes: collection (AccountAddress): Address of the collection this token belongs to. index (int): Unique index of the token within its collection. @@ -409,10 +412,10 @@ class Token: name (str): Name of the token. uri (str): URI pointing to token metadata (typically JSON). struct_tag (str): The Move struct identifier for token resources. - + Examples: Parse token data from blockchain:: - + resource_data = { "collection": {"inner": "***collection_address..."}, "index": 42, @@ -422,12 +425,13 @@ class Token: } token = Token.parse(resource_data) print(f"Token: {token.name} in collection {token.collection}") - + Note: The token URI should point to a JSON file following the standard token metadata schema (similar to ERC-721 metadata) for marketplace compatibility. """ + collection: AccountAddress index: int description: str @@ -483,16 +487,16 @@ def __init__(self, property_type: Any): class Property: """Represents a typed property for tokens with serialization capabilities. - + Properties are key-value pairs that can be attached to tokens to store additional metadata and attributes. Each property has a name, type, and value, and supports various primitive and complex types. - + Attributes: name (str): The name/key of the property. property_type (str): The Move type of the property value. value (Any): The actual value of the property. - + Type Constants: BOOL (int): Boolean type identifier (0) U8 (int): 8-bit unsigned integer type identifier (1) @@ -504,48 +508,49 @@ class Property: ADDRESS (int): Account address type identifier (7) BYTE_VECTOR (int): Byte vector type identifier (8) STRING (int): String type identifier (9) - + Examples: Create different types of properties:: - + # Boolean property is_rare = Property.bool("is_rare", True) - + # Numeric properties level = Property.u64("level", 25) damage = Property.u32("damage", 150) - + # String property category = Property.string("category", "weapon") - - # Address property + + # Address property creator = Property("creator", "address", creator_address) - + # Byte data property metadata = Property.bytes("metadata", b"custom_data") - + Use in transactions:: - + # Convert to transaction arguments for on-chain calls tx_args = property.to_transaction_arguments() - + Parse from blockchain data:: - + # Parse property from resource data prop = Property.parse("level", Property.U64, serialized_value) print(f"Property: {prop.name} = {prop.value}") - + Supported Types: - **bool**: Boolean values (true/false) - **u8, u16, u32, u64, u128, u256**: Unsigned integers of various sizes - **address**: Aptos account addresses - **string**: UTF-8 encoded strings - **vector**: Arbitrary byte arrays - + Note: Properties are strongly typed and values must match the specified type. BCS serialization is used for efficient on-chain storage and transmission. """ + name: str property_type: str value: Any @@ -667,18 +672,18 @@ def bytes(name: str, value: bytes) -> Property: class PropertyMap: """Container for multiple token properties with serialization support. - + PropertyMap manages a collection of Property objects and provides utilities for converting them to formats suitable for blockchain transactions and parsing them from on-chain data. - + Attributes: properties (List[Property]): List of properties contained in this map. struct_tag (str): The Move struct identifier for property map resources. - + Examples: Create a property map with various property types:: - + properties = PropertyMap([ Property.string("name", "Legendary Sword"), Property.u64("level", 50), @@ -686,21 +691,21 @@ class PropertyMap: Property.bytes("metadata", b"custom_data"), Property.u32("damage", 200) ]) - + print(f"Property map: {properties}") - + Convert to transaction format:: - + # Get tuple format for transaction arguments names, types, values = properties.to_tuple() - + # These can be used directly in transaction calls # names = ["name", "level", "is_rare", "metadata", "damage"] # types = ["***::string::String", "u64", "bool", "vector", "u32"] # values = [b"...", b"...", b"...", b"...", b"..."] # BCS serialized - + Parse from blockchain data:: - + # Parse from resource data retrieved from blockchain resource_data = { "inner": { @@ -710,19 +715,19 @@ class PropertyMap: ] } } - + parsed_map = PropertyMap.parse(resource_data) print(f"Parsed properties: {parsed_map}") - + Usage in Token Operations: Property maps are essential for token minting and property management:: - + # Create property map props = PropertyMap([ Property.string("category", "weapon"), Property.u64("attack_power", 150) ]) - + # Use in token minting await token_client.mint_token( creator=creator, @@ -732,11 +737,12 @@ class PropertyMap: uri="https://example.com/sword.json", properties=props ) - + Note: The to_tuple method returns data in the format expected by Move entry functions for property operations on tokens. """ + properties: List[Property] struct_tag: str = "***::property_map::PropertyMap" @@ -783,108 +789,109 @@ def parse(resource: dict[str, Any]) -> PropertyMap: class ReadObject: """Aggregated view of parsed blockchain resources for token objects. - + ReadObject provides a structured interface for accessing multiple resource types associated with a token object address. It automatically parses known resource types and makes them available through a unified interface. - + Attributes: resource_map (dict): Mapping of Move struct identifiers to Python classes for automatic resource parsing. resources (dict): Dictionary mapping resource classes to parsed instances. - + Supported Resource Types: - **Collection**: Collection metadata and configuration - **Object**: Core object ownership and transfer permissions - **PropertyMap**: Token properties and custom attributes - **Royalty**: Royalty information for secondary sales - **Token**: Token metadata and collection reference - + Examples: Read and parse token object resources:: - + from aptos_sdk.account_address import AccountAddress - + # Read object from blockchain token_address = AccountAddress.from_str("***token_address...") read_object = await token_client.read_object(token_address) - + # Access different resource types if Token in read_object.resources: token = read_object.resources[Token] print(f"Token name: {token.name}") print(f"Description: {token.description}") print(f"Collection: {token.collection}") - + if PropertyMap in read_object.resources: properties = read_object.resources[PropertyMap] print(f"Properties: {properties}") for prop in properties.properties: print(f" {prop.name}: {prop.value}") - + if Object in read_object.resources: obj = read_object.resources[Object] print(f"Owner: {obj.owner}") print(f"Transferable: {obj.allow_ungated_transfer}") - + if Royalty in read_object.resources: royalty = read_object.resources[Royalty] percentage = (royalty.numerator / royalty.denominator) * 100 print(f"Royalty: {percentage}% to {royalty.payee_address}") - + Check for specific resource types:: - + # Check what resources are available print(f"Available resources: {list(read_object.resources.keys())}") - + # Safely access optional resources token = read_object.resources.get(Token) if token: print(f"Found token: {token.name}") else: print("No token resource found") - + Full object inspection:: - + # Print all resources (uses __str__ method) print(read_object) - + # This will show something like: # ReadObject # ***::token::Token: Token[collection: ***abc..., name: Sword #1, ...] # ***::property_map::PropertyMap: PropertyMap[Property[level, u64, 42], ...] # ***::object::ObjectCore: Object[allow_ungated_transfer: True, owner: ***def...] - + Usage Patterns: Conditional resource access:: - + def analyze_token_object(read_object: ReadObject): analysis = {} - + # Basic token info if Token in read_object.resources: token = read_object.resources[Token] analysis["name"] = token.name analysis["description"] = token.description - + # Properties analysis if PropertyMap in read_object.resources: prop_map = read_object.resources[PropertyMap] analysis["property_count"] = len(prop_map.properties) analysis["properties"] = {p.name: p.value for p in prop_map.properties} - + # Ownership info if Object in read_object.resources: obj = read_object.resources[Object] analysis["owner"] = str(obj.owner) analysis["transferable"] = obj.allow_ungated_transfer - + return analysis - + Note: Only resources that match known struct tags in resource_map will be parsed and included. Unknown resource types are ignored during parsing. """ + resource_map: dict[str, Any] = { Collection.struct_tag: Collection, Object.struct_tag: Object, diff --git a/aptos_sdk/aptos_tokenv1_client.py b/aptos_sdk/aptos_tokenv1_client.py index 9fdc528..470f641 100644 --- a/aptos_sdk/aptos_tokenv1_client.py +++ b/aptos_sdk/aptos_tokenv1_client.py @@ -39,16 +39,16 @@ Examples: Basic Token V1 workflow:: - + from aptos_sdk.aptos_tokenv1_client import AptosTokenV1Client from aptos_sdk.async_client import RestClient from aptos_sdk.account import Account - + # Setup client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") token_client = AptosTokenV1Client(client) creator = Account.load("./creator.json") - + # Create a collection collection_txn = await token_client.create_collection( account=creator, @@ -57,7 +57,7 @@ uri="https://example.com/collection.json" ) await client.wait_for_transaction(collection_txn) - + # Create a token in the collection token_txn = await token_client.create_token( account=creator, @@ -69,14 +69,14 @@ royalty_points_per_million=50000 # 5% royalty ) await client.wait_for_transaction(token_txn) - + Token transfer using offer/claim pattern:: - + from aptos_sdk.account_address import AccountAddress - + recipient_address = AccountAddress.from_str("***recipient...") recipient = Account.load("./recipient.json") - + # Offer token to recipient offer_txn = await token_client.offer_token( account=creator, @@ -88,7 +88,7 @@ amount=1 ) await client.wait_for_transaction(offer_txn) - + # Recipient claims the token claim_txn = await token_client.claim_token( account=recipient, @@ -99,9 +99,9 @@ property_version=0 ) await client.wait_for_transaction(claim_txn) - + Direct token transfer (requires both accounts):: - + # Direct transfer between accounts transfer_txn = await token_client.direct_transfer_token( sender=creator, @@ -113,9 +113,9 @@ amount=1 ) await client.wait_for_transaction(transfer_txn) - + Reading token information:: - + # Get token data (metadata) token_data = await token_client.get_token_data( creator=creator.address(), @@ -126,7 +126,7 @@ print(f"Token: {token_data['name']}") print(f"Description: {token_data['description']}") print(f"Supply: {token_data['supply']}") - + # Get token balance for an account balance = await token_client.get_token_balance( owner=recipient_address, @@ -136,7 +136,7 @@ property_version=0 ) print(f"Balance: {balance}") - + # Get collection information collection_data = await token_client.get_collection( creator=creator.address(), @@ -181,16 +181,16 @@ class AptosTokenV1Client: """Client for interacting with legacy Aptos Token V1 standard. - + AptosTokenV1Client provides a high-level interface for working with the original Aptos token implementation (Token V1). While this standard is still supported for backward compatibility, new projects should consider using Token Objects via AptosTokenClient for better functionality and composability. - + Token V1 uses a table-based storage model where tokens are identified by a combination of (creator, collection_name, token_name, property_version) and stored in various on-chain tables rather than as independent objects. - + Key Features: - **Legacy Compatibility**: Supports existing Token V1 deployments - **Collection Management**: Create and manage token collections @@ -198,22 +198,22 @@ class AptosTokenV1Client: - **Offer/Claim Transfers**: Asynchronous token transfer mechanism - **Direct Transfers**: Synchronous multi-agent transfers - **Royalty System**: Built-in creator royalty support - + Attributes: _client (RestClient): The underlying REST client for blockchain communication. - + Examples: Initialize and create a basic NFT:: - + from aptos_sdk.aptos_tokenv1_client import AptosTokenV1Client from aptos_sdk.async_client import RestClient from aptos_sdk.account import Account - + # Setup client rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") token_client = AptosTokenV1Client(rest_client) creator = Account.load("./creator_key.json") - + # Create collection await token_client.create_collection( account=creator, @@ -221,7 +221,7 @@ class AptosTokenV1Client: description="Digital art pieces", uri="https://example.com/collection.json" ) - + # Create NFT await token_client.create_token( account=creator, @@ -232,9 +232,9 @@ class AptosTokenV1Client: uri="https://example.com/art1.json", royalty_points_per_million=25000 # 2.5% ) - + Transfer tokens using offer/claim:: - + # Offer token to recipient await token_client.offer_token( account=current_owner, @@ -245,7 +245,7 @@ class AptosTokenV1Client: property_version=0, amount=1 ) - + # Recipient claims the token await token_client.claim_token( account=recipient, @@ -255,7 +255,7 @@ class AptosTokenV1Client: token_name="Artwork #1", property_version=0 ) - + Note: This client is for Token V1 compatibility. For new projects, consider using AptosTokenClient which implements the modern Token Objects standard @@ -266,16 +266,16 @@ class AptosTokenV1Client: def __init__(self, client: RestClient): """Initialize the Token V1 client with a REST client. - + Args: client: The RestClient instance to use for blockchain communication. Must be configured for the appropriate Aptos network. - + Examples: Create client for devnet:: - + from aptos_sdk.async_client import RestClient - + rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") token_client = AptosTokenV1Client(rest_client) """ @@ -285,27 +285,27 @@ async def create_collection( self, account: Account, name: str, description: str, uri: str ) -> str: """Create a new token collection using the Token V1 standard. - + Creates a collection that can hold multiple tokens. In Token V1, collections are stored in the creator's account and have a maximum supply limit (set to U64_MAX by default for unlimited). - + Args: account: The account that will own the collection and pay transaction fees. name: Unique name for the collection within the creator's account. Must be unique per creator. description: Human-readable description of the collection. uri: URI pointing to collection metadata JSON file. - + Returns: str: Transaction hash of the collection creation transaction. - + Raises: ApiError: If the transaction fails or collection name already exists. - + Examples: Create a basic art collection:: - + tx_hash = await token_client.create_collection( account=creator, name="Digital Art Collection", @@ -313,7 +313,7 @@ async def create_collection( uri="https://example.com/collection-metadata.json" ) await client.wait_for_transaction(tx_hash) - + Note: Collection names must be unique per creator. The collection is created with unlimited maximum supply and default mutability settings (all false). @@ -352,11 +352,11 @@ async def create_token( royalty_points_per_million: int, ) -> str: """Create a new token within an existing collection. - + Creates a token with the specified supply and metadata. In Token V1, tokens have both initial and maximum supply values, with royalties specified as points per million (e.g., 25000 = 2.5%). - + Args: account: The account creating the token (must be collection owner). collection_name: Name of the collection to create token in. @@ -367,17 +367,17 @@ async def create_token( uri: URI pointing to token metadata JSON file. royalty_points_per_million: Royalty percentage as points per million. 25000 = 2.5%, 50000 = 5%, etc. - + Returns: str: Transaction hash of the token creation transaction. - + Raises: ApiError: If the transaction fails, collection doesn't exist, or token name already exists. - + Examples: Create an NFT (supply = 1):: - + tx_hash = await token_client.create_token( account=creator, collection_name="Art Collection", @@ -387,9 +387,9 @@ async def create_token( uri="https://example.com/token1.json", royalty_points_per_million=25000 # 2.5% royalty ) - + Create a fungible token:: - + tx_hash = await token_client.create_token( account=creator, collection_name="Game Tokens", @@ -399,7 +399,7 @@ async def create_token( uri="https://example.com/gold-coins.json", royalty_points_per_million=10000 # 1% royalty ) - + Note: Token names must be unique within the collection. The royalty recipient is set to the token creator's address by default. @@ -448,11 +448,11 @@ async def offer_token( amount: int, ) -> str: """Offer tokens to another account using the async transfer mechanism. - + Creates a pending token offer that the recipient can claim. This is the first step of the two-phase Token V1 transfer process (offer -> claim). The tokens remain in the sender's account until claimed. - + Args: account: The account offering the tokens (current owner). receiver: Address of the account to receive the token offer. @@ -461,17 +461,17 @@ async def offer_token( token_name: Name of the specific token being offered. property_version: Property version of the token (usually 0). amount: Number of tokens to offer. - + Returns: str: Transaction hash of the offer transaction. - + Raises: ApiError: If the transaction fails, token doesn't exist, or insufficient token balance. - + Examples: Offer an NFT:: - + tx_hash = await token_client.offer_token( account=current_owner, receiver=recipient_address, @@ -481,9 +481,9 @@ async def offer_token( property_version=0, amount=1 ) - + Offer fungible tokens:: - + tx_hash = await token_client.offer_token( account=token_holder, receiver=buyer_address, @@ -493,7 +493,7 @@ async def offer_token( property_version=0, amount=100 ) - + Note: The recipient must call claim_token() to complete the transfer. Offers can potentially be revoked or expire based on implementation. @@ -528,10 +528,10 @@ async def claim_token( property_version: int, ) -> str: """Claim tokens that were offered by another account. - + Completes the second step of the Token V1 async transfer process. Claims all tokens that were offered for the specified token ID. - + Args: account: The account claiming the tokens (recipient). sender: Address of the account that offered the tokens. @@ -539,16 +539,16 @@ async def claim_token( collection_name: Name of the collection containing the token. token_name: Name of the specific token being claimed. property_version: Property version of the token (usually 0). - + Returns: str: Transaction hash of the claim transaction. - + Raises: ApiError: If the transaction fails or no pending offer exists. - + Examples: Claim an offered NFT:: - + tx_hash = await token_client.claim_token( account=recipient, sender=previous_owner.address(), @@ -557,7 +557,7 @@ async def claim_token( token_name="Masterpiece #1", property_version=0 ) - + Note: This claims all tokens that were offered for this token ID. The amount is determined by the original offer transaction. @@ -592,11 +592,11 @@ async def direct_transfer_token( amount: int, ) -> str: """Transfer tokens directly between two accounts in a single transaction. - + Performs a synchronous token transfer that requires both sender and receiver to sign the transaction. This is more efficient than the offer/claim mechanism but requires coordination between both parties. - + Args: sender: The account sending the tokens (must sign). receiver: The account receiving the tokens (must sign). @@ -605,17 +605,17 @@ async def direct_transfer_token( token_name: Name of the specific token being transferred. property_version: Property version of the token (usually 0). amount: Number of tokens to transfer. - + Returns: str: Transaction hash of the direct transfer transaction. - + Raises: ApiError: If the transaction fails, token doesn't exist, insufficient balance, or either party fails to sign. - + Examples: Direct transfer of an NFT:: - + tx_hash = await token_client.direct_transfer_token( sender=current_owner, receiver=new_owner, @@ -625,7 +625,7 @@ async def direct_transfer_token( property_version=0, amount=1 ) - + Note: This creates a multi-agent transaction requiring both accounts to sign. Both sender and receiver must be available to sign @@ -666,29 +666,29 @@ async def get_token( property_version: int, ) -> Any: """Retrieve token information for a specific owner and token ID. - + Queries the owner's TokenStore to get information about their holdings of a specific token, including the amount owned. - + Args: owner: Address of the account that owns the token. creator: Address of the account that created the token. collection_name: Name of the collection containing the token. token_name: Name of the specific token. property_version: Property version of the token (usually 0). - + Returns: Dict containing token information including: - 'id': Token identifier object - 'amount': String representation of amount owned Returns {'id': token_id, 'amount': '0'} if not found. - + Raises: ApiError: If the query fails (except for 404 not found). - + Examples: Get token ownership info:: - + token_info = await token_client.get_token( owner=holder_address, creator=creator.address(), @@ -696,13 +696,13 @@ async def get_token( token_name="Masterpiece #1", property_version=0 ) - + amount = token_info['amount'] if amount == '0': print("Account does not own this token") else: print(f"Account owns {amount} of this token") - + Note: Returns amount as '0' if the account has no TokenStore resource or doesn't own the specified token. @@ -743,24 +743,24 @@ async def get_token_balance( property_version: int, ) -> str: """Get the token balance for a specific owner and token ID. - + Convenience method that extracts just the amount from get_token(). Returns the number of tokens of the specified type owned by the account. - + Args: owner: Address of the account to check balance for. creator: Address of the account that created the token. collection_name: Name of the collection containing the token. token_name: Name of the specific token. property_version: Property version of the token (usually 0). - + Returns: str: String representation of the token balance. Returns '0' if the account doesn't own any of this token. - + Examples: Check NFT ownership:: - + balance = await token_client.get_token_balance( owner=user_address, creator=creator.address(), @@ -768,12 +768,12 @@ async def get_token_balance( token_name="Masterpiece #1", property_version=0 ) - + owns_nft = balance != '0' print(f"User owns NFT: {owns_nft}") - + Check fungible token balance:: - + balance = await token_client.get_token_balance( owner=player_address, creator=game_creator.address(), @@ -781,7 +781,7 @@ async def get_token_balance( token_name="Gold Coins", property_version=0 ) - + print(f"Player has {balance} gold coins") """ info = await self.get_token( @@ -797,44 +797,44 @@ async def get_token_data( property_version: int, ) -> Any: """Retrieve metadata and configuration for a specific token. - + Queries the token creator's Collections resource to get the canonical token data including metadata, supply, and properties. - + Args: creator: Address of the account that created the token. collection_name: Name of the collection containing the token. token_name: Name of the specific token. property_version: Property version of the token (usually 0). - + Returns: Dict containing token metadata including: - 'name': Token name - - 'description': Token description + - 'description': Token description - 'uri': Metadata URI - 'supply': Current supply - 'maximum': Maximum supply - 'royalty': Royalty information - Other token-specific fields - + Raises: ApiError: If the token doesn't exist or query fails. - + Examples: Get token metadata:: - + token_data = await token_client.get_token_data( creator=creator.address(), collection_name="Art Collection", token_name="Masterpiece #1", property_version=0 ) - + print(f"Token: {token_data['name']}") print(f"Description: {token_data['description']}") print(f"URI: {token_data['uri']}") print(f"Supply: {token_data['supply']}/{token_data['maximum']}") - + Note: This returns the canonical token definition, not ownership information. Use get_token() to check specific ownership. @@ -861,14 +861,14 @@ async def get_collection( self, creator: AccountAddress, collection_name: str ) -> Any: """Retrieve metadata and configuration for a specific collection. - + Queries the collection creator's Collections resource to get collection metadata and configuration settings. - + Args: creator: Address of the account that created the collection. collection_name: Name of the collection to query. - + Returns: Dict containing collection information including: - 'name': Collection name @@ -877,23 +877,23 @@ async def get_collection( - 'maximum': Maximum number of tokens allowed - 'supply': Current number of tokens created - Mutability settings for various fields - + Raises: ApiError: If the collection doesn't exist or query fails. - + Examples: Get collection info:: - + collection_data = await token_client.get_collection( creator=creator.address(), collection_name="Art Collection" ) - + print(f"Collection: {collection_data['name']}") print(f"Description: {collection_data['description']}") print(f"URI: {collection_data['uri']}") print(f"Supply: {collection_data['supply']}/{collection_data['maximum']}") - + Note: This provides collection-level metadata. Use get_token_data() to get information about specific tokens within the collection. @@ -914,22 +914,22 @@ async def transfer_object( self, owner: Account, object: AccountAddress, to: AccountAddress ) -> str: """Transfer an object-based resource to another account. - + This method is for transferring object-based resources and may be used for hybrid Token V1/Object scenarios. Not typically used for standard Token V1 transfers. - + Args: owner: The current owner of the object. object: Address of the object to transfer. to: Address of the account to receive the object. - + Returns: str: Transaction hash of the transfer transaction. - + Raises: ApiError: If the transaction fails or object doesn't exist. - + Note: This method is primarily for object-based transfers and may not be applicable to standard Token V1 tokens. Use direct_transfer_token diff --git a/aptos_sdk/asymmetric_crypto.py b/aptos_sdk/asymmetric_crypto.py index abe96aa..b3c5875 100644 --- a/aptos_sdk/asymmetric_crypto.py +++ b/aptos_sdk/asymmetric_crypto.py @@ -26,26 +26,26 @@ Examples: Working with private key formatting:: - + from aptos_sdk.asymmetric_crypto import PrivateKey, PrivateKeyVariant - + # Format a raw hex key as AIP-80 compliant raw_key = "0x1234abcd..." formatted = PrivateKey.format_private_key(raw_key, PrivateKeyVariant.Ed25519) # Returns: "ed25519-priv-0x1234abcd..." - + # Parse various formats to bytes key_bytes = PrivateKey.parse_hex_input( "ed25519-priv-0x1234abcd...", PrivateKeyVariant.Ed25519 ) - + Using protocol interfaces:: - + # All concrete key implementations follow these protocols def sign_data(private_key: PrivateKey, message: bytes) -> Signature: return private_key.sign(message) - + def verify_signature(public_key: PublicKey, message: bytes, sig: Signature) -> bool: return public_key.verify(message, sig) @@ -65,109 +65,111 @@ def verify_signature(public_key: PublicKey, message: bytes, sig: Signature) -> b class PrivateKeyVariant(Enum): """Enumeration of supported private key cryptographic algorithms. - + This enum defines the cryptographic signature schemes supported by the Aptos blockchain and their corresponding string identifiers used in AIP-80 compliant formatting. - + Attributes: Ed25519: The Ed25519 signature scheme (primary for Aptos). Secp256k1: The secp256k1 ECDSA signature scheme (Ethereum compatibility). - + Examples: Using the enum values:: - + # Check key type if key_type == PrivateKeyVariant.Ed25519: print("Using Ed25519 cryptography") - + # Get string representation scheme_name = PrivateKeyVariant.Secp256k1.value # "secp256k1" - + Iterating over supported schemes:: - + for scheme in PrivateKeyVariant: print(f"Supported: {scheme.value}") - + Note: These values are used internally for key type identification and correspond to the prefixes defined in the AIP-80 standard. """ + Ed25519 = "ed25519" Secp256k1 = "secp256k1" class PrivateKey(Deserializable, Serializable, Protocol): """Protocol defining the interface for asymmetric cryptographic private keys. - + This protocol establishes the standard interface that all private key implementations must follow in the Aptos SDK. It combines cryptographic operations with serialization capabilities and AIP-80 compliance utilities. - + The protocol ensures that all private key types can: - Generate corresponding public keys - Sign arbitrary data - Serialize/deserialize for network transmission - Format according to AIP-80 standards - + Key Management Standards: - Implements AIP-80 compliant string formatting - Supports multiple input formats (hex, bytes, AIP-80 strings) - Provides type-safe parsing and validation - Maintains backward compatibility with legacy formats - + Methods: hex() -> str: Get hexadecimal representation of the private key public_key() -> PublicKey: Derive the corresponding public key sign(data: bytes) -> Signature: Sign data and return signature - + Static Methods: format_private_key(): Format keys as AIP-80 compliant strings parse_hex_input(): Parse various input formats to bytes - + Examples: Implementing a private key class:: - + class MyPrivateKey(PrivateKey): def __init__(self, key_bytes: bytes): self._key_bytes = key_bytes - + def hex(self) -> str: return self._key_bytes.hex() - + def public_key(self) -> PublicKey: # Derive public key from private key return MyPublicKey.from_private(self) - + def sign(self, data: bytes) -> Signature: # Implementation-specific signing return MySignature(self._sign_bytes(data)) - + Using the formatting utilities:: - + # Format existing key formatted = PrivateKey.format_private_key( - "0xabcd1234...", + "0xabcd1234...", PrivateKeyVariant.Ed25519 ) - + # Parse different input formats key_bytes = PrivateKey.parse_hex_input( "ed25519-priv-0xabcd1234...", PrivateKeyVariant.Ed25519 ) - + Note: This is a Protocol (structural typing), not a base class. Concrete implementations don't need to explicitly inherit from this protocol, they just need to implement the required methods. """ + def hex(self) -> str: """Return the hexadecimal string representation of the private key. - + Returns: Hexadecimal string of the private key bytes, typically prefixed with '0x'. - + Example: >>> private_key.hex() '0x1234abcd...' @@ -176,11 +178,11 @@ def hex(self) -> str: def public_key(self) -> PublicKey: """Derive the corresponding public key from this private key. - + Returns: The public key derived from this private key using the appropriate cryptographic algorithm. - + Example: >>> pub_key = private_key.public_key() >>> isinstance(pub_key, PublicKey) @@ -190,14 +192,14 @@ def public_key(self) -> PublicKey: def sign(self, data: bytes) -> Signature: """Sign the given data using this private key. - + Args: data: The raw bytes to be signed. - + Returns: A signature object that can be used to verify the data was signed by the holder of this private key. - + Example: >>> message = b"Hello, Aptos!" >>> signature = private_key.sign(message) @@ -208,22 +210,22 @@ def sign(self, data: bytes) -> Signature: """ AIP-80 compliant prefixes for private key serialization. - + The Aptos Improvement Proposal 80 (AIP-80) defines standardized string formats for private keys to improve interoperability and user experience. Each supported cryptographic scheme has a unique prefix that identifies the key type. - + Format: "{algorithm}-priv-{hex_value}" - + Supported Prefixes: - "ed25519-priv-": For Ed25519 private keys - "secp256k1-priv-": For secp256k1 ECDSA private keys - + Examples: Ed25519 key: "ed25519-priv-0x1234abcd..." secp256k1 key: "secp256k1-priv-0xabcd1234..." - + References: [AIP-80 Specification](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md) """ @@ -237,50 +239,50 @@ def format_private_key( private_key: bytes | str, key_type: PrivateKeyVariant ) -> str: """Format a private key as an AIP-80 compliant string. - + This method converts various private key input formats into the standardized AIP-80 string format, which includes the algorithm prefix and ensures consistent representation across the Aptos ecosystem. - + Args: private_key: The private key in hex string or bytes format. Can be a raw hex string (with or without '0x' prefix), bytes object, or already AIP-80 formatted string. key_type: The cryptographic algorithm type for this key. - + Returns: AIP-80 compliant string in the format: "{algorithm}-priv-{hex_value}" - + Raises: ValueError: If the key_type is not supported. TypeError: If the private_key is not string or bytes. - + Examples: Format a raw hex string:: - + key = "0x1234abcd..." formatted = PrivateKey.format_private_key( key, PrivateKeyVariant.Ed25519 ) # Returns: "ed25519-priv-0x1234abcd..." - + Format bytes:: - + key_bytes = bytes.fromhex("1234abcd") formatted = PrivateKey.format_private_key( key_bytes, PrivateKeyVariant.Secp256k1 ) # Returns: "secp256k1-priv-0x1234abcd" - + Handle already formatted keys:: - + formatted_key = "ed25519-priv-0x1234abcd..." result = PrivateKey.format_private_key( formatted_key, PrivateKeyVariant.Ed25519 ) # Returns: "ed25519-priv-0x1234abcd..." (unchanged) - + Note: If the input is already AIP-80 compliant for the specified key type, the method will extract and reformat the hex portion to ensure @@ -308,11 +310,11 @@ def parse_hex_input( value: str | bytes, key_type: PrivateKeyVariant, strict: bool | None = None ) -> bytes: """Parse various private key input formats to standardized bytes. - + This method handles multiple input formats for private keys and converts them to a consistent bytes representation. It supports legacy hex strings, AIP-80 compliant strings, and raw bytes. - + Args: value: The private key in various formats: - Raw hex string ("1234abcd" or "0x1234abcd") @@ -323,40 +325,40 @@ def parse_hex_input( - True: Only accept AIP-80 compliant strings - False: Accept legacy hex formats without warning - None (default): Accept legacy formats with deprecation warning - + Returns: The private key as a bytes object, ready for cryptographic operations. - + Raises: ValueError: If key_type is unsupported, or if strict=True and input is not AIP-80 compliant, or if input format is invalid. TypeError: If value is not string or bytes. - + Examples: Parse AIP-80 compliant string:: - + key_bytes = PrivateKey.parse_hex_input( "ed25519-priv-0x1234abcd...", PrivateKeyVariant.Ed25519 ) - + Parse legacy hex string:: - + key_bytes = PrivateKey.parse_hex_input( "0x1234abcd...", PrivateKeyVariant.Ed25519, strict=False # Suppress warning ) - + Parse raw bytes:: - + key_bytes = PrivateKey.parse_hex_input( bytes.fromhex("1234abcd"), PrivateKeyVariant.Ed25519 ) - + Strict mode (AIP-80 only):: - + try: key_bytes = PrivateKey.parse_hex_input( "0x1234abcd...", # Legacy format @@ -365,7 +367,7 @@ def parse_hex_input( ) except ValueError: print("Must use AIP-80 format in strict mode") - + Note: When strict=None (default), legacy hex formats trigger a deprecation warning encouraging migration to AIP-80 compliant formats. @@ -404,83 +406,83 @@ def parse_hex_input( class PublicKey(Deserializable, Serializable, Protocol): """Protocol defining the interface for asymmetric cryptographic public keys. - + This protocol establishes the standard interface that all public key implementations must follow in the Aptos SDK. Public keys are used for signature verification and address derivation in the Aptos blockchain. - + The protocol ensures that all public key types can: - Verify signatures created by corresponding private keys - Serialize for network transmission and storage - Generate specialized byte representations for different contexts - + Key Features: - **Signature Verification**: Cryptographic validation of signed data - **Flexible Encoding**: Support for both BCS and specialized encodings - **Multi-Algorithm Support**: Compatible with Ed25519, secp256k1, etc. - **Address Derivation**: Foundation for generating blockchain addresses - + Methods: to_crypto_bytes() -> bytes: Get specialized cryptographic byte encoding verify(data: bytes, signature: Signature) -> bool: Verify a signature - + Examples: Implementing a public key class:: - + class MyPublicKey(PublicKey): def __init__(self, key_bytes: bytes): self._key_bytes = key_bytes - + def to_crypto_bytes(self) -> bytes: # Return algorithm-specific encoding return self._key_bytes - + def verify(self, data: bytes, signature: Signature) -> bool: # Implementation-specific verification return self._verify_signature(data, signature) - + def serialize(self, serializer) -> None: # BCS serialization serializer.bytes(self._key_bytes) - + Using public key verification:: - + message = b"Hello, Aptos!" signature = private_key.sign(message) - + # Verify the signature if public_key.verify(message, signature): print("Signature is valid!") else: print("Invalid signature!") - + Note: The to_crypto_bytes() method exists for historical reasons where some key types (like MultiEd25519) require special encoding beyond standard BCS serialization. """ + def to_crypto_bytes(self) -> bytes: """Get the specialized cryptographic byte representation. - + This method provides an algorithm-specific byte encoding that may differ from the standard BCS serialization. It exists primarily for compatibility with legacy systems and specialized key types like MultiEd25519. - + Returns: The public key in its specialized cryptographic byte format. - + Note: For most single-signature schemes, this typically returns the same bytes as BCS serialization. Multi-signature schemes may use different encodings. - + Example: >>> crypto_bytes = public_key.to_crypto_bytes() >>> len(crypto_bytes) # Length depends on algorithm 32 # Ed25519 public keys are 32 bytes - """ - """ + A long time ago, someone decided that we should have both bcs and a special representation for MultiEd25519, so we use this to let keys self-define a special encoding. """ @@ -488,19 +490,19 @@ def to_crypto_bytes(self) -> bytes: def verify(self, data: bytes, signature: Signature) -> bool: """Verify that a signature was created by the corresponding private key. - + This method performs cryptographic verification to ensure that the given signature was created by signing the provided data with the private key corresponding to this public key. - + Args: data: The original data that was signed. signature: The signature to verify. - + Returns: True if the signature is valid for the given data and this public key, False otherwise. - + Example: >>> message = b"transaction data" >>> signature = private_key.sign(message) @@ -508,7 +510,7 @@ def verify(self, data: bytes, signature: Signature) -> bool: True >>> public_key.verify(b"different data", signature) False - + Note: This method should be constant-time to prevent timing attacks in security-critical applications. @@ -518,57 +520,58 @@ def verify(self, data: bytes, signature: Signature) -> bool: class Signature(Deserializable, Serializable, Protocol): """Protocol defining the interface for cryptographic signatures. - + This protocol establishes the standard interface that all signature implementations must follow in the Aptos SDK. Signatures are the cryptographic proofs that verify the authenticity and integrity of signed data. - + The protocol ensures that all signature types can: - Serialize for network transmission and storage - Deserialize from various input formats - Integrate seamlessly with the BCS serialization system - + Key Properties: - **Immutable**: Signatures should be treated as immutable once created - **Verifiable**: Can be verified using corresponding public keys - **Serializable**: Compatible with Aptos network protocols - **Type-Safe**: Maintains algorithm-specific signature formats - + Examples: Implementing a signature class:: - + class MySignature(Signature): def __init__(self, signature_bytes: bytes): self._signature_bytes = signature_bytes - + def serialize(self, serializer) -> None: # BCS serialization serializer.bytes(self._signature_bytes) - + @classmethod def deserialize(cls, deserializer) -> 'MySignature': # BCS deserialization signature_bytes = deserializer.bytes() return cls(signature_bytes) - + Using signatures in verification:: - + # Create signature message = b"Hello, Aptos!" signature = private_key.sign(message) - + # Verify signature is_valid = public_key.verify(message, signature) - + # Serialize for transmission serializer = Serializer() signature.serialize(serializer) signature_bytes = serializer.output() - + Note: This is a Protocol (structural typing), not a base class. Concrete signature implementations don't need to explicitly inherit from this protocol, they just need to implement the required serialization methods. """ + ... diff --git a/aptos_sdk/asymmetric_crypto_wrapper.py b/aptos_sdk/asymmetric_crypto_wrapper.py index d272153..cb4da87 100644 --- a/aptos_sdk/asymmetric_crypto_wrapper.py +++ b/aptos_sdk/asymmetric_crypto_wrapper.py @@ -29,47 +29,47 @@ Examples: Single key usage:: - + from aptos_sdk import ed25519 from aptos_sdk.asymmetric_crypto_wrapper import PublicKey, Signature - + # Wrap an Ed25519 key ed25519_key = ed25519.PrivateKey.generate() wrapped_public = PublicKey(ed25519_key.public_key()) - + # Sign and verify message = b"Hello, Aptos!" ed25519_sig = ed25519_key.sign(message) wrapped_sig = Signature(ed25519_sig) - + # Verify through wrapper is_valid = wrapped_public.verify(message, wrapped_sig) - + Multi-signature setup:: - + from aptos_sdk.asymmetric_crypto_wrapper import MultiPublicKey, MultiSignature - + # Create multi-sig with 2-of-3 threshold keys = [key1.public_key(), key2.public_key(), key3.public_key()] multi_key = MultiPublicKey(keys, threshold=2) - + # Create multi-signature (keys 0 and 2 sign) sig1 = key1.sign(message) sig3 = key3.sign(message) multi_sig = MultiSignature([(0, sig1), (2, sig3)]) - + # Verify multi-signature is_valid = multi_key.verify(message, multi_sig) - + Serialization example:: - + from aptos_sdk.bcs import Serializer, Deserializer - + # Serialize wrapped key serializer = Serializer() wrapped_public.serialize(serializer) key_bytes = serializer.output() - + # Deserialize wrapped key deserializer = Deserializer(key_bytes) restored_key = PublicKey.deserialize(deserializer) @@ -90,42 +90,43 @@ class PublicKey(asymmetric_crypto.PublicKey): """Unified wrapper for different cryptographic public key types. - + This class provides a common interface for Ed25519 and secp256k1 public keys, enabling polymorphic handling of different signature schemes within the Aptos ecosystem. The wrapper maintains type information through variant tagging. - + Type Variants: ED25519 (0): Ed25519 elliptic curve signature scheme SECP256K1_ECDSA (1): secp256k1 ECDSA signature scheme - + Attributes: variant: Integer identifier for the wrapped key type public_key: The underlying concrete public key implementation - + Examples: Wrapping different key types:: - + # Ed25519 key ed25519_private = ed25519.PrivateKey.generate() wrapped_ed25519 = PublicKey(ed25519_private.public_key()) assert wrapped_ed25519.variant == PublicKey.ED25519 - + # secp256k1 key secp256k1_private = secp256k1_ecdsa.PrivateKey.generate() wrapped_secp256k1 = PublicKey(secp256k1_private.public_key()) assert wrapped_secp256k1.variant == PublicKey.SECP256K1_ECDSA - + Polymorphic verification:: - + def verify_message(public_key: PublicKey, message: bytes, signature: Signature) -> bool: # Works regardless of underlying algorithm return public_key.verify(message, signature) - + Note: The wrapper automatically detects the key type during construction and sets the appropriate variant identifier for serialization. """ + ED25519: int = 0 SECP256K1_ECDSA: int = 1 @@ -134,13 +135,13 @@ def verify_message(public_key: PublicKey, message: bytes, signature: Signature) def __init__(self, public_key: asymmetric_crypto.PublicKey): """Initialize a public key wrapper for the given concrete key. - + Args: public_key: An Ed25519 or secp256k1 public key to be wrapped. - + Raises: NotImplementedError: If the public key type is not supported. - + Example: >>> ed25519_key = ed25519.PrivateKey.generate().public_key() >>> wrapped = PublicKey(ed25519_key) @@ -157,13 +158,13 @@ def __init__(self, public_key: asymmetric_crypto.PublicKey): def to_crypto_bytes(self) -> bytes: """Get the specialized cryptographic byte representation. - + Returns the public key in BCS-serialized format including the variant tag, suitable for cryptographic operations and storage. - + Returns: BCS-serialized bytes including variant tag and key data. - + Example: >>> wrapped_key = PublicKey(ed25519_key) >>> crypto_bytes = wrapped_key.to_crypto_bytes() @@ -176,18 +177,18 @@ def to_crypto_bytes(self) -> bytes: def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: """Verify a signature against this public key. - + Unwraps the signature and delegates verification to the underlying concrete public key implementation. Handles type coercion to ensure the signature wrapper matches the key type. - + Args: data: The original data that was signed. signature: A wrapped signature to verify. - + Returns: True if the signature is valid, False otherwise. - + Example: >>> message = b"Hello, Aptos!" >>> signature = private_key.sign(message) @@ -203,19 +204,19 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: @staticmethod def deserialize(deserializer: Deserializer) -> PublicKey: """Deserialize a public key wrapper from BCS-encoded data. - + Reads the variant tag and delegates deserialization to the appropriate concrete key implementation based on the detected type. - + Args: deserializer: BCS deserializer containing the key data. - + Returns: A new PublicKey wrapper containing the deserialized key. - + Raises: Exception: If the variant tag is not recognized. - + Example: >>> serializer = Serializer() >>> original_key.serialize(serializer) @@ -238,13 +239,13 @@ def deserialize(deserializer: Deserializer) -> PublicKey: def serialize(self, serializer: Serializer): """Serialize the public key wrapper to BCS format. - + Writes the variant tag followed by the underlying public key data in BCS format. The variant enables proper deserialization. - + Args: serializer: BCS serializer to write the key data to. - + Example: >>> serializer = Serializer() >>> wrapped_key.serialize(serializer) @@ -256,44 +257,45 @@ def serialize(self, serializer: Serializer): class Signature(asymmetric_crypto.Signature): """Unified wrapper for different cryptographic signature types. - + This class provides a common interface for Ed25519 and secp256k1 signatures, enabling polymorphic handling and verification across different signature schemes. Like PublicKey, it uses variant tagging for type identification. - + Type Variants: ED25519 (0): Ed25519 signature SECP256K1_ECDSA (1): secp256k1 ECDSA signature - + Attributes: variant: Integer identifier for the wrapped signature type signature: The underlying concrete signature implementation - + Examples: Wrapping different signature types:: - + message = b"Hello, Aptos!" - + # Ed25519 signature ed25519_sig = ed25519_private.sign(message) wrapped_ed25519_sig = Signature(ed25519_sig) assert wrapped_ed25519_sig.variant == Signature.ED25519 - + # secp256k1 signature secp256k1_sig = secp256k1_private.sign(message) wrapped_secp256k1_sig = Signature(secp256k1_sig) assert wrapped_secp256k1_sig.variant == Signature.SECP256K1_ECDSA - + Polymorphic operations:: - + def verify_any_signature(key: PublicKey, msg: bytes, sig: Signature) -> bool: # Works regardless of underlying algorithm return key.verify(msg, sig) - + Note: The signature wrapper automatically detects and tags the signature type, ensuring compatibility with the corresponding public key wrapper. """ + ED25519: int = 0 SECP256K1_ECDSA: int = 1 @@ -302,13 +304,13 @@ def verify_any_signature(key: PublicKey, msg: bytes, sig: Signature) -> bool: def __init__(self, signature: asymmetric_crypto.Signature): """Initialize a signature wrapper for the given concrete signature. - + Args: signature: An Ed25519 or secp256k1 signature to be wrapped. - + Raises: NotImplementedError: If the signature type is not supported. - + Example: >>> message = b"test message" >>> ed25519_sig = ed25519_private.sign(message) @@ -327,19 +329,19 @@ def __init__(self, signature: asymmetric_crypto.Signature): @staticmethod def deserialize(deserializer: Deserializer) -> Signature: """Deserialize a signature wrapper from BCS-encoded data. - + Reads the variant tag and delegates deserialization to the appropriate concrete signature implementation based on the detected type. - + Args: deserializer: BCS deserializer containing the signature data. - + Returns: A new Signature wrapper containing the deserialized signature. - + Raises: Exception: If the variant tag is not recognized. - + Example: >>> serializer = Serializer() >>> original_sig.serialize(serializer) @@ -362,13 +364,13 @@ def deserialize(deserializer: Deserializer) -> Signature: def serialize(self, serializer: Serializer): """Serialize the signature wrapper to BCS format. - + Writes the variant tag followed by the underlying signature data in BCS format. The variant enables proper deserialization. - + Args: serializer: BCS serializer to write the signature data to. - + Example: >>> serializer = Serializer() >>> wrapped_signature.serialize(serializer) @@ -380,60 +382,61 @@ def serialize(self, serializer: Serializer): class MultiPublicKey(asymmetric_crypto.PublicKey): """Multi-signature public key implementing threshold-based authentication. - + This class represents a collection of public keys with a threshold requirement, enabling N-of-M multi-signature schemes. It's commonly used for multi-signature wallets, governance systems, and enhanced security scenarios. - + Threshold Mechanics: - Requires at least `threshold` valid signatures from the key set - Signatures are indexed by position in the key array - Uses bitmap encoding to identify which keys provided signatures - Supports heterogeneous key types (Ed25519, secp256k1) within the same set - + Constraints: MIN_KEYS (2): Minimum number of keys required MAX_KEYS (32): Maximum number of keys allowed MIN_THRESHOLD (1): Minimum threshold value - + Attributes: keys: List of wrapped public keys in the multi-signature scheme threshold: Minimum number of signatures required for validity - + Examples: Creating a 2-of-3 multi-signature key:: - + key1 = ed25519.PrivateKey.generate().public_key() key2 = ed25519.PrivateKey.generate().public_key() key3 = secp256k1_ecdsa.PrivateKey.generate().public_key() - + multi_key = MultiPublicKey([key1, key2, key3], threshold=2) print(multi_key) # "2-of-3 Multi key" - + Verification with multi-signature:: - + message = b"Multi-sig transaction" - + # Keys 0 and 2 sign (satisfies threshold of 2) sig1 = private_key1.sign(message) sig3 = private_key3.sign(message) multi_sig = MultiSignature([(0, sig1), (2, sig3)]) - + # Verify is_valid = multi_key.verify(message, multi_sig) - + Creating governance multi-sig:: - + # 3-of-5 governance setup governance_keys = [generate_key() for _ in range(5)] governance_multi = MultiPublicKey(governance_keys, threshold=3) - + # Requires 3 signatures for any governance action - + Note: All keys are automatically wrapped in PublicKey wrappers for consistency, enabling mixed-algorithm multi-signature schemes. """ + keys: List[PublicKey] threshold: int @@ -443,16 +446,16 @@ class MultiPublicKey(asymmetric_crypto.PublicKey): def __init__(self, keys: List[asymmetric_crypto.PublicKey], threshold: int): """Initialize a multi-signature public key with the given parameters. - + Args: keys: List of public keys that can participate in signing. Must be between MIN_KEYS and MAX_KEYS in length. threshold: Minimum number of signatures required for validity. Must be between MIN_THRESHOLD and the number of keys. - + Raises: AssertionError: If key count or threshold is outside valid ranges. - + Example: >>> keys = [key1, key2, key3] >>> multi_key = MultiPublicKey(keys, threshold=2) @@ -480,10 +483,10 @@ def __init__(self, keys: List[asymmetric_crypto.PublicKey], threshold: int): def __str__(self) -> str: """Return a human-readable string representation. - + Returns: String in the format "{threshold}-of-{total} Multi key". - + Example: >>> str(MultiPublicKey([key1, key2, key3], 2)) '2-of-3 Multi key' @@ -492,30 +495,30 @@ def __str__(self) -> str: def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: """Verify a multi-signature against this multi-public-key. - + Validates that the provided multi-signature contains at least the threshold number of valid signatures from keys in this multi-key set. - + Args: data: The original data that was signed. signature: A MultiSignature containing indexed signatures. - + Returns: True if the multi-signature satisfies the threshold requirement and all included signatures are valid, False otherwise. - + Verification Process: 1. Ensures sufficient signatures are provided (>= threshold) 2. Validates each signature index is within the key set bounds 3. Verifies each signature against its corresponding public key 4. Returns False if any validation step fails - + Example: >>> message = b"transaction data" >>> multi_sig = MultiSignature([(0, sig1), (2, sig3)]) >>> multi_key.verify(message, multi_sig) # 2 sigs >= threshold True - + Note: This method uses exception handling for robustness, returning False for any verification failure rather than propagating exceptions. @@ -541,13 +544,13 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: @staticmethod def from_crypto_bytes(indata: bytes) -> MultiPublicKey: """Deserialize a MultiPublicKey from its byte representation. - + Args: indata: BCS-serialized bytes of the multi-public-key. - + Returns: A new MultiPublicKey instance. - + Example: >>> original_bytes = multi_key.to_crypto_bytes() >>> restored_key = MultiPublicKey.from_crypto_bytes(original_bytes) @@ -557,10 +560,10 @@ def from_crypto_bytes(indata: bytes) -> MultiPublicKey: def to_crypto_bytes(self) -> bytes: """Serialize the MultiPublicKey to its byte representation. - + Returns: BCS-serialized bytes suitable for storage or transmission. - + Example: >>> multi_bytes = multi_key.to_crypto_bytes() >>> len(multi_bytes) # Depends on number and types of keys @@ -572,13 +575,13 @@ def to_crypto_bytes(self) -> bytes: @staticmethod def deserialize(deserializer: Deserializer) -> MultiPublicKey: """Deserialize a MultiPublicKey from a BCS deserializer. - + Args: deserializer: BCS deserializer containing the multi-key data. - + Returns: A new MultiPublicKey instance with the deserialized keys and threshold. - + Example: >>> deserializer = Deserializer(serialized_data) >>> multi_key = MultiPublicKey.deserialize(deserializer) @@ -589,10 +592,10 @@ def deserialize(deserializer: Deserializer) -> MultiPublicKey: def serialize(self, serializer: Serializer): """Serialize the MultiPublicKey to a BCS serializer. - + Args: serializer: BCS serializer to write the multi-key data to. - + Example: >>> serializer = Serializer() >>> multi_key.serialize(serializer) @@ -604,69 +607,70 @@ def serialize(self, serializer: Serializer): class MultiSignature(asymmetric_crypto.Signature): """Multi-signature implementation with bitmap-based key indexing. - + This class represents a collection of signatures created by different keys within a multi-signature scheme. It uses bitmap encoding to efficiently track which keys in the set provided signatures. - + Bitmap Encoding: - Each signature is associated with an index (position in the key array) - A bitmap efficiently encodes which positions have signatures - Supports up to MAX_SIGNATURES concurrent signatures - Optimizes storage and verification performance - + Constraints: MAX_SIGNATURES (16): Maximum number of signatures in one multi-signature - + Attributes: signatures: List of (index, signature) tuples where index refers to the position in the corresponding MultiPublicKey's key array - + Examples: Creating a multi-signature:: - + # Keys at positions 0 and 2 sign sig1 = private_key1.sign(message) sig3 = private_key3.sign(message) # Note: key3 is at index 2 - + multi_sig = MultiSignature([(0, sig1), (2, sig3)]) - + Verifying with corresponding multi-key:: - + # MultiPublicKey with keys [key1, key2, key3], threshold=2 is_valid = multi_key.verify(message, multi_sig) # True because we have 2 signatures >= threshold - + Mixed algorithm signatures:: - + # Ed25519 signature at index 0 ed25519_sig = ed25519_private.sign(message) - + # secp256k1 signature at index 1 secp256k1_sig = secp256k1_private.sign(message) - + mixed_multi_sig = MultiSignature([ (0, ed25519_sig), (1, secp256k1_sig) ]) - + Note: Signatures are automatically wrapped in Signature wrappers to ensure type consistency and proper serialization. """ + signatures: List[Tuple[int, Signature]] MAX_SIGNATURES: int = 16 def __init__(self, signatures: List[Tuple[int, asymmetric_crypto.Signature]]): """Initialize a multi-signature with indexed signatures. - + Args: signatures: List of (index, signature) tuples where index corresponds to the position in the MultiPublicKey's key array. - + Raises: AssertionError: If any index exceeds MAX_SIGNATURES. - + Example: >>> sig1 = private_key1.sign(message) >>> sig2 = private_key2.sign(message) @@ -686,13 +690,13 @@ def __init__(self, signatures: List[Tuple[int, asymmetric_crypto.Signature]]): def __eq__(self, other: object): """Check equality with another MultiSignature. - + Args: other: Object to compare with. - + Returns: True if both MultiSignatures have identical signatures and indices. - + Example: >>> multi_sig1 == multi_sig2 True # If they contain the same (index, signature) pairs @@ -703,10 +707,10 @@ def __eq__(self, other: object): def __str__(self) -> str: """Return a string representation of the multi-signature. - + Returns: String representation showing the (index, signature) pairs. - + Example: >>> str(multi_sig) '[(0, ), (2, )]' @@ -716,21 +720,21 @@ def __str__(self) -> str: @staticmethod def deserialize(deserializer: Deserializer) -> MultiSignature: """Deserialize a MultiSignature from BCS-encoded data. - + Reads the signature sequence and bitmap to reconstruct the indexed signatures. The bitmap indicates which key positions have signatures. - + Args: deserializer: BCS deserializer containing the multi-signature data. - + Returns: A new MultiSignature with the deserialized indexed signatures. - + Deserialization Process: 1. Read the sequence of signatures 2. Read the bitmap indicating which keys signed 3. Reconstruct (index, signature) pairs using bitmap - + Example: >>> deserializer = Deserializer(serialized_data) >>> multi_sig = MultiSignature.deserialize(deserializer) @@ -752,18 +756,18 @@ def deserialize(deserializer: Deserializer) -> MultiSignature: def serialize(self, serializer: Serializer): """Serialize the MultiSignature to BCS format. - + Creates a compact representation using a signature sequence and bitmap. The bitmap efficiently encodes which key indices have signatures. - + Args: serializer: BCS serializer to write the multi-signature data to. - + Serialization Format: 1. Sequence of signatures (without indices) 2. Bitmap indicating which key positions signed 3. Variable-length bitmap encoding (1 or 2 bytes) - + Example: >>> serializer = Serializer() >>> multi_sig.serialize(serializer) @@ -783,21 +787,21 @@ def serialize(self, serializer: Serializer): def index_to_bitmap_value(i: int) -> int: """Convert a key index to its corresponding bitmap bit value. - + This function implements the bitmap encoding used in multi-signatures to efficiently represent which keys in a set have provided signatures. - + Args: i: The key index (0-based position in the key array). - + Returns: The bitmap value with the appropriate bit set for the given index. - + Bitmap Layout: - Bits are ordered with the most significant bit representing index 0 - Multiple bytes are used for indices > 7 - Little-endian byte ordering is used - + Examples: >>> index_to_bitmap_value(0) # First key 128 # Binary: 10000000 @@ -805,7 +809,7 @@ def index_to_bitmap_value(i: int) -> int: 1 # Binary: 00000001 >>> index_to_bitmap_value(8) # Ninth key (second byte) 32768 # Binary: 10000000 00000000 - + Note: This encoding matches the Aptos blockchain's multi-signature bitmap format. """ diff --git a/aptos_sdk/async_client.py b/aptos_sdk/async_client.py index 84ef056..2f5dcf4 100644 --- a/aptos_sdk/async_client.py +++ b/aptos_sdk/async_client.py @@ -21,7 +21,7 @@ Client Types: RestClient: Primary interface to Aptos full nodes via REST API - IndexerClient: GraphQL interface to Aptos indexer services + IndexerClient: GraphQL interface to Aptos indexer services FaucetClient: Test network funding and account creation Transaction Types: @@ -40,68 +40,68 @@ Examples: Basic client setup and account query:: - + from aptos_sdk.async_client import RestClient, ClientConfig from aptos_sdk.account_address import AccountAddress - + # Create client with custom configuration config = ClientConfig( max_gas_amount=200_000, gas_unit_price=150, transaction_wait_in_seconds=30 ) - + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1", config) - + # Query account information address = AccountAddress.from_str("***123...") account_info = await client.account(address) balance = await client.account_balance(address) - + print(f"Sequence number: {account_info['sequence_number']}") print(f"Balance: {balance} octas") - + await client.close() - + Transaction submission:: - + from aptos_sdk.account import Account - + # Create sender account sender = Account.generate() recipient = AccountAddress.from_str("***456...") - + # Transfer 1 APT (1 * 10^8 octas) txn_hash = await client.bcs_transfer( sender=sender, recipient=recipient, amount=100_000_000 # 1 APT in octas ) - + # Wait for transaction completion await client.wait_for_transaction(txn_hash) txn_info = await client.transaction_by_hash(txn_hash) - + Multi-agent transaction:: - + # Create multi-agent transaction requiring multiple signatures signed_txn = await client.create_multi_agent_bcs_transaction( sender=primary_account, secondary_accounts=[account2, account3], payload=transaction_payload ) - + txn_hash = await client.submit_bcs_transaction(signed_txn) - + IndexerClient usage:: - + indexer = IndexerClient( "https://indexer.devnet.aptoslabs.com/v1/graphql", bearer_token="optional_token" ) - + # GraphQL query example - query = """ + query = \"\"\" query GetTransactions($address: String!) { account_transactions(where: {account_address: {_eq: $address}}) { transaction_version @@ -109,33 +109,33 @@ success } } - """ - + \"\"\" + result = await indexer.query(query, {"address": str(address)}) - + Faucet usage for testnet:: - + faucet = FaucetClient( "https://faucet.devnet.aptoslabs.com", rest_client=client ) - + # Fund account with test coins account = Account.generate() txn_hash = await faucet.fund_account( address=account.address(), amount=500_000_000 # 5 APT ) - + await faucet.close() Error Handling: The module provides specific exception types for different error scenarios: - + - ApiError: General API request failures (HTTP 4xx/5xx) - AccountNotFound: Requested account doesn't exist - ResourceNotFound: Requested resource not found in account - + Best Practices: - Always call client.close() when done to clean up connections - Use context managers or try/finally blocks for resource cleanup @@ -180,51 +180,51 @@ @dataclass class ClientConfig: """Configuration parameters for Aptos REST API clients. - + This class encapsulates common settings used by REST clients for transaction submission, gas management, and network communication. These parameters affect transaction costs, execution timeouts, and API authentication. - + Transaction Parameters: expiration_ttl: Time-to-live for transactions in seconds (default: 600) gas_unit_price: Price per unit of gas in octas (default: 100) max_gas_amount: Maximum gas units allowed per transaction (default: 100,000) transaction_wait_in_seconds: Timeout for transaction confirmation (default: 20) - + Network Parameters: http2: Enable HTTP/2 for better performance (default: True) api_key: Optional API key for authenticated requests (default: None) - + Examples: Default configuration:: - + config = ClientConfig() client = RestClient(node_url, config) - + High-throughput configuration:: - + config = ClientConfig( gas_unit_price=150, # Higher gas price for faster processing max_gas_amount=200_000, # Higher gas limit for complex transactions transaction_wait_in_seconds=60, # Longer wait for busy networks expiration_ttl=300 # Shorter expiration for high-frequency ops ) - + Authenticated requests:: - + config = ClientConfig( api_key="your-api-key-here", http2=True # Recommended for API services ) - + Conservative settings:: - + config = ClientConfig( gas_unit_price=100, # Standard gas price max_gas_amount=50_000, # Lower gas limit to prevent runaway expiration_ttl=1200 # Longer expiration for manual workflows ) - + Notes: - Gas prices may need adjustment based on network congestion - Higher gas limits allow more complex transactions but cost more @@ -243,19 +243,19 @@ class ClientConfig: class IndexerClient: """GraphQL client for querying indexed Aptos blockchain data. - + This client provides access to the Aptos Indexer Service, which indexes blockchain data into a PostgreSQL database exposed via Hasura GraphQL API. The indexer provides rich querying capabilities for transactions, accounts, events, and other blockchain data. - + Key Features: - **Rich Queries**: Complex filtering, sorting, and aggregation of blockchain data - **Real-time Data**: Access to up-to-date indexed blockchain information - **Flexible API**: GraphQL interface supporting custom query structures - **Authentication**: Optional bearer token authentication for premium access - **High Performance**: Optimized database queries for fast data retrieval - + Use Cases: - Analytics and reporting on blockchain activity - Transaction history and account analysis @@ -263,20 +263,20 @@ class IndexerClient: - DeFi protocol data aggregation - NFT marketplace data queries - Portfolio tracking applications - + Attributes: client: The underlying GraphQL client for executing queries - + Examples: Basic setup and query:: - + indexer = IndexerClient( "https://indexer.mainnet.aptoslabs.com/v1/graphql", bearer_token="optional-auth-token" ) - + # Query account transactions - query = """ + query = \"\"\" query GetAccountTransactions($address: String!, $limit: Int!) { account_transactions( where: {account_address: {_eq: $address}}, @@ -289,16 +289,16 @@ class IndexerClient: transaction_timestamp } } - """ - + \"\"\" + result = await indexer.query(query, { "address": "0x1", "limit": 100 }) - + Token transfer queries:: - - query = """ + + query = \"\"\" query GetTokenTransfers($token_address: String!) { token_activities( where: { @@ -315,15 +315,15 @@ class IndexerClient: transaction_timestamp } } - """ - + \"\"\" + transfers = await indexer.query(query, { "token_address": "0xabc123..." }) - + Account resource tracking:: - - query = """ + + query = \"\"\" query GetAccountResources($address: String!) { account_resources( where: {account_address: {_eq: $address}} @@ -334,10 +334,10 @@ class IndexerClient: transaction_version } } - """ - + \"\"\" + resources = await indexer.query(query, {"address": address}) - + Note: The indexer service may have query limits and rate limiting. Some advanced features may require authentication tokens. Check the specific @@ -348,20 +348,20 @@ class IndexerClient: def __init__(self, indexer_url: str, bearer_token: Optional[str] = None): """Initialize the IndexerClient with connection parameters. - + Args: indexer_url: The GraphQL endpoint URL for the Aptos indexer service. bearer_token: Optional authentication token for premium access. - + Examples: Public access:: - + client = IndexerClient( "https://indexer.devnet.aptoslabs.com/v1/graphql" ) - + Authenticated access:: - + client = IndexerClient( "https://indexer.mainnet.aptoslabs.com/v1/graphql", bearer_token="your-token-here" @@ -383,17 +383,17 @@ async def query(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]: Args: query: GraphQL query string with proper syntax and structure. variables: Dictionary of variables to substitute in the query. - + Returns: Dictionary containing the GraphQL response data and metadata. - + Raises: Exception: On GraphQL syntax errors, network issues, or server errors. - + Examples: Simple account query:: - - query = """ + + query = \"\"\" query GetAccount($address: String!) { account_transactions( where: {account_address: {_eq: $address}} @@ -403,14 +403,14 @@ async def query(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]: success } } - """ - + \"\"\" + result = await indexer.query(query, {"address": "0x1"}) transactions = result["data"]["account_transactions"] - + Complex aggregation query:: - - query = """ + + query = \"\"\" query GetDailyStats($date: timestamptz!) { transactions_aggregate( where: { @@ -425,12 +425,12 @@ async def query(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]: } } } - """ - + \"\"\" + stats = await indexer.query(query, { "date": "2024-01-01T00:00:00Z" }) - + Note: The query must follow GraphQL syntax. Use the indexer's schema documentation to understand available fields and relationships. @@ -440,45 +440,45 @@ async def query(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]: class RestClient: """Comprehensive async client for the Aptos blockchain REST API. - + This client provides complete access to Aptos full node functionality through the REST API, supporting all blockchain operations including account management, transaction submission, resource queries, event monitoring, and more. - + Core Capabilities: - **Account Operations**: Balance queries, resource access, transaction history - **Transaction Management**: Submission, simulation, status tracking, waiting - **Blockchain Queries**: Block data, ledger info, event streams - **Move Integration**: View functions, resource inspection, module access - **Advanced Features**: Multi-agent transactions, BCS encoding, gas estimation - + Performance Features: - **HTTP/2 Support**: Efficient connection reuse and multiplexing - - **Connection Pooling**: Optimized for high-throughput applications + - **Connection Pooling**: Optimized for high-throughput applications - **Async/Await**: Non-blocking operations for better concurrency - **Configurable Timeouts**: Flexible timeout management for different use cases - **Automatic Retries**: Built-in resilience for transient network issues - + Transaction Types: - Single-signature transactions (most common) - Multi-signature transactions (shared accounts, DAOs) - Script transactions (custom Move code execution) - Entry function calls (smart contract interactions) - + Attributes: _chain_id: Cached network chain ID (mainnet=1, testnet=2, etc.) client: Underlying HTTP client with connection pooling client_config: Configuration for gas, timeouts, and other parameters base_url: Base URL of the Aptos full node REST API - + Examples: Basic client setup:: - + from aptos_sdk.async_client import RestClient, ClientConfig - + # Use default configuration client = RestClient("https://fullnode.mainnet.aptoslabs.com/v1") - + # Custom configuration for high-throughput apps config = ClientConfig( max_gas_amount=200_000, @@ -486,79 +486,79 @@ class RestClient: transaction_wait_in_seconds=60 ) client = RestClient("https://fullnode.devnet.aptoslabs.com/v1", config) - + Account operations:: - + from aptos_sdk.account_address import AccountAddress - + address = AccountAddress.from_str("0x1") - + # Get account information account_data = await client.account(address) sequence_number = account_data["sequence_number"] - + # Check balance balance = await client.account_balance(address) print(f"Balance: {balance / 10**8} APT") - + # Get all resources resources = await client.account_resources(address) for resource in resources: print(f"Resource: {resource['type']}") - + Transaction submission:: - + from aptos_sdk.account import Account - + # Create accounts sender = Account.generate() recipient = AccountAddress.from_str("0x456...") - + # Simple transfer txn_hash = await client.bcs_transfer( sender=sender, - recipient=recipient, + recipient=recipient, amount=100_000_000 # 1 APT in octas ) - + # Wait for confirmation await client.wait_for_transaction(txn_hash) txn_data = await client.transaction_by_hash(txn_hash) - + if txn_data["success"]: print(f"Transfer successful! Gas used: {txn_data['gas_used']}") - + Transaction simulation:: - + # Create transaction without submitting raw_txn = await client.create_bcs_transaction( sender=sender_account, payload=transaction_payload ) - + # Simulate to estimate gas simulation = await client.simulate_transaction( transaction=raw_txn, sender=sender_account, estimate_gas_usage=True ) - + print(f"Estimated gas: {simulation[0]['gas_used']}") print(f"Success: {simulation[0]['success']}") - + Multi-agent transactions:: - + # Transactions requiring multiple signatures signed_txn = await client.create_multi_agent_bcs_transaction( sender=primary_account, secondary_accounts=[account2, account3], payload=shared_transaction_payload ) - + txn_hash = await client.submit_bcs_transaction(signed_txn) - + View function calls:: - + # Read-only function calls (no gas cost) result = await client.view( function="0x1::coin::balance", @@ -566,25 +566,25 @@ class RestClient: arguments=[str(address)] ) balance = int(result[0]) - + Event monitoring:: - + # Get events by creation number events = await client.event_by_creation_number( account_address=contract_address, creation_number=0, # First event stream limit=100 ) - + for event in events: print(f"Event: {event['type']}, Data: {event['data']}") - + Error Handling: The client raises specific exceptions for different failure modes: - ApiError: HTTP errors (4xx, 5xx status codes) - AccountNotFound: Account doesn't exist on-chain - ResourceNotFound: Requested resource not found in account - + Best Practices: - Always call await client.close() when done - Use try/finally or async context managers for cleanup @@ -593,7 +593,7 @@ class RestClient: - Implement exponential backoff for retries on failures - Use BCS transactions for better performance and fees - Monitor gas usage and adjust pricing as needed - + Note: This client is designed for production use with proper connection management, timeout handling, and error recovery. It supports both @@ -607,21 +607,21 @@ class RestClient: def __init__(self, base_url: str, client_config: ClientConfig = ClientConfig()): """Initialize the REST client with configuration parameters. - + Args: base_url: Base URL of the Aptos full node REST API. Examples: "https://fullnode.mainnet.aptoslabs.com/v1", "https://fullnode.devnet.aptoslabs.com/v1" client_config: Configuration for gas, timeouts, and networking. Defaults to standard settings if not provided. - + Examples: Mainnet client:: - + client = RestClient("https://fullnode.mainnet.aptoslabs.com/v1") - + Testnet with custom config:: - + config = ClientConfig( gas_unit_price=200, max_gas_amount=150_000, @@ -631,11 +631,11 @@ def __init__(self, base_url: str, client_config: ClientConfig = ClientConfig()): "https://fullnode.testnet.aptoslabs.com/v1", config ) - + Local development node:: - + client = RestClient("http://localhost:8080/v1") - + Note: The client automatically configures HTTP/2, connection pooling, proper headers, and timeouts for optimal performance. diff --git a/aptos_sdk/authenticator.py b/aptos_sdk/authenticator.py index 4f56946..019fcde 100644 --- a/aptos_sdk/authenticator.py +++ b/aptos_sdk/authenticator.py @@ -50,66 +50,66 @@ Examples: Basic single signature transaction:: - + from aptos_sdk.authenticator import Ed25519Authenticator, Authenticator from aptos_sdk import ed25519 - + # Create Ed25519 authenticator private_key = ed25519.PrivateKey.random() public_key = private_key.public_key() - + # Sign transaction hash tx_hash = b"transaction_hash_bytes" signature = private_key.sign(tx_hash) - + # Create authenticator account_auth = Ed25519Authenticator(public_key, signature) tx_auth = Authenticator(account_auth) - + # Verify signature is_valid = tx_auth.verify(tx_hash) - + Multi-signature authentication:: - + from aptos_sdk.authenticator import MultiEd25519Authenticator from aptos_sdk import ed25519 - + # Create 2-of-3 multisig private_keys = [ed25519.PrivateKey.random() for _ in range(3)] public_keys = [pk.public_key() for pk in private_keys] multi_pub_key = ed25519.MultiPublicKey(public_keys, threshold=2) - + # Sign with 2 keys (indices 0 and 2) signatures = [ (0, private_keys[0].sign(tx_hash)), (2, private_keys[2].sign(tx_hash)) ] multi_signature = ed25519.MultiSignature(signatures) - + # Create multi-signature authenticator multi_auth = MultiEd25519Authenticator(multi_pub_key, multi_signature) tx_auth = Authenticator(multi_auth) - + Multi-agent transaction:: - + # Transaction requiring multiple distinct signers sender_auth = Ed25519Authenticator(sender_public_key, sender_signature) agent1_auth = Ed25519Authenticator(agent1_public_key, agent1_signature) agent2_auth = Ed25519Authenticator(agent2_public_key, agent2_signature) - + # Create multi-agent authenticator multi_agent_auth = MultiAgentAuthenticator( sender=sender_auth, secondary_signers=[agent1_auth, agent2_auth] ) tx_auth = Authenticator(multi_agent_auth) - + Fee payer transaction:: - + # Transaction where someone else pays the gas fees sender_auth = Ed25519Authenticator(sender_public_key, sender_signature) fee_payer_auth = Ed25519Authenticator(fee_payer_public_key, fee_payer_signature) - + fee_payer_tx_auth = FeePayerAuthenticator( sender=sender_auth, secondary_signers=[], @@ -156,54 +156,54 @@ class Authenticator: """Top-level transaction authenticator for the Aptos blockchain. - + Each transaction submitted to the Aptos blockchain contains a TransactionAuthenticator that proves the transaction was authorized by the appropriate accounts. During transaction execution, the executor validates that every signature is well-formed and matches the AuthenticationKey stored under each participating account. - + The Authenticator class serves as a wrapper that can contain different types of authentication schemes, from simple single signatures to complex multi-party transactions with fee delegation. - + Supported Authentication Types: ED25519 (0): Legacy single Ed25519 signature MULTI_ED25519 (1): Legacy multi-Ed25519 signatures MULTI_AGENT (2): Multiple distinct signers for complex transactions FEE_PAYER (3): Transactions with fee sponsorship SINGLE_SENDER (4): Modern unified single signature format - + Attributes: variant (int): Integer identifier for the authentication type authenticator (typing.Any): The underlying concrete authenticator implementation - + Examples: Simple single signature:: - + private_key = ed25519.PrivateKey.random() signature = private_key.sign(transaction_hash) - + ed25519_auth = Ed25519Authenticator(private_key.public_key(), signature) tx_auth = Authenticator(ed25519_auth) - + Multi-agent transaction:: - + sender_auth = Ed25519Authenticator(sender_key.public_key(), sender_sig) agent_auth = Ed25519Authenticator(agent_key.public_key(), agent_sig) - + multi_agent = MultiAgentAuthenticator(sender_auth, [agent_auth]) tx_auth = Authenticator(multi_agent) - + Serialization and verification:: - + # Serialize for blockchain submission serializer = Serializer() tx_auth.serialize(serializer) auth_bytes = serializer.output() - + # Verify signatures is_valid = tx_auth.verify(transaction_hash) - + Note: The authenticator type is automatically determined from the wrapped authenticator implementation and cannot be changed after construction. @@ -305,11 +305,11 @@ def serialize(self, serializer: Serializer): class AccountAuthenticator: """ An authenticator for a single account signature. - + This wraps different types of signature schemes that can be used to authenticate an account's authorization of a transaction. """ - + ED25519: int = 0 MULTI_ED25519: int = 1 SINGLE_KEY: int = 2 @@ -389,10 +389,10 @@ def serialize(self, serializer: Serializer): class Ed25519Authenticator: """ An authenticator that uses Ed25519 signature scheme. - + This is the most common signature scheme used in Aptos. """ - + public_key: ed25519.PublicKey signature: ed25519.Signature @@ -443,11 +443,11 @@ def serialize(self, serializer: Serializer): class FeePayerAuthenticator: """ An authenticator for fee-payer transactions. - + This allows a different account to pay the transaction fees while still requiring signatures from all participants. """ - + sender: AccountAuthenticator secondary_signers: List[typing.Tuple[AccountAddress, AccountAuthenticator]] fee_payer: typing.Tuple[AccountAddress, AccountAuthenticator] @@ -541,10 +541,10 @@ def serialize(self, serializer: Serializer): class MultiAgentAuthenticator: """ An authenticator for multi-agent transactions. - + This requires signatures from multiple accounts to authorize a transaction. """ - + sender: AccountAuthenticator secondary_signers: List[typing.Tuple[AccountAddress, AccountAuthenticator]] @@ -613,36 +613,36 @@ def serialize(self, serializer: Serializer): class MultiEd25519Authenticator: """An authenticator that uses multi-signature Ed25519 scheme. - + This authenticator supports threshold signatures using multiple Ed25519 keys, requiring a minimum number of signatures (threshold) from a set of public keys to authorize a transaction. This is useful for shared accounts, multi-party custody, and governance scenarios. - + Features: - N-of-M threshold signatures (e.g., 2-of-3, 3-of-5) - Efficient Ed25519 cryptography - Legacy support for older multi-signature formats - BCS serialization compatibility - + Security Properties: - Requires threshold number of valid signatures - Each signature must be from a different key in the set - Provides non-repudiation and authenticity - Resistant to single key compromise - + Examples: Create a 2-of-3 multi-signature:: - + import ed25519 - + # Generate 3 key pairs private_keys = [ed25519.PrivateKey.random() for _ in range(3)] public_keys = [pk.public_key() for pk in private_keys] - + # Create multi-public key with threshold 2 multi_pub_key = ed25519.MultiPublicKey(public_keys, threshold=2) - + # Sign with keys 0 and 2 (meeting threshold) tx_hash = b"transaction_hash" signatures = [ @@ -650,59 +650,61 @@ class MultiEd25519Authenticator: (2, private_keys[2].sign(tx_hash)) ] multi_signature = ed25519.MultiSignature(signatures) - + # Create authenticator auth = MultiEd25519Authenticator(multi_pub_key, multi_signature) - + Verify the multi-signature:: - + is_valid = auth.verify(tx_hash) # Should return True - + Attributes: public_key (ed25519.MultiPublicKey): The multi-public key containing all keys and threshold signature (ed25519.MultiSignature): The multi-signature containing threshold signatures - + Note: This is a legacy format. New applications should consider using MultiKeyAuthenticator for better algorithm flexibility. """ - + public_key: ed25519.MultiPublicKey signature: ed25519.MultiSignature - def __init__(self, public_key: ed25519.MultiPublicKey, signature: ed25519.MultiSignature): + def __init__( + self, public_key: ed25519.MultiPublicKey, signature: ed25519.MultiSignature + ): """Initialize a multi-Ed25519 authenticator. Args: public_key: The multi-public key containing all keys and threshold requirements signature: The multi-signature with the required threshold signatures - + Examples: Basic initialization:: - + multi_pub_key = ed25519.MultiPublicKey([pk1, pk2, pk3], threshold=2) multi_sig = ed25519.MultiSignature([(0, sig1), (2, sig3)]) auth = MultiEd25519Authenticator(multi_pub_key, multi_sig) """ self.public_key = public_key self.signature = signature - + def __eq__(self, other: object) -> bool: """Check equality with another MultiEd25519Authenticator. - + Args: other: Object to compare with - + Returns: True if public keys and signatures are equal, False otherwise """ if not isinstance(other, MultiEd25519Authenticator): return NotImplemented return self.public_key == other.public_key and self.signature == other.signature - + def __str__(self) -> str: """String representation of the multi-Ed25519 authenticator. - + Returns: Human-readable string showing public key and signature details """ @@ -710,18 +712,18 @@ def __str__(self) -> str: def verify(self, data: bytes) -> bool: """Verify the multi-signature against the provided data. - + This method validates that: 1. The threshold number of signatures is provided 2. Each signature is from a different key in the multi-public key 3. Each signature is cryptographically valid - + Args: data: The data that was signed (typically a transaction hash) - + Returns: True if the multi-signature is valid, False otherwise - + Note: This method is currently not implemented in the base class. Implementations should delegate to the underlying ed25519.MultiPublicKey.verify method. @@ -731,13 +733,13 @@ def verify(self, data: bytes) -> bool: @staticmethod def deserialize(deserializer: Deserializer) -> MultiEd25519Authenticator: """Deserialize a MultiEd25519Authenticator from BCS bytes. - + Args: deserializer: The BCS deserializer containing the authenticator data - + Returns: A MultiEd25519Authenticator instance - + Raises: DeserializationError: If the data is malformed or incomplete """ @@ -747,7 +749,7 @@ def deserialize(deserializer: Deserializer) -> MultiEd25519Authenticator: def serialize(self, serializer: Serializer): """Serialize this multi-Ed25519 authenticator using BCS serialization. - + This serializes both the multi-public key (including all public keys and the threshold) and the multi-signature (including signature indices and the actual signature bytes). @@ -761,74 +763,74 @@ def serialize(self, serializer: Serializer): class SingleSenderAuthenticator: """Modern unified single signature authenticator for the Aptos blockchain. - + This is the preferred authenticator format for simple single-signature transactions in newer versions of Aptos. It provides a clean, unified interface that can wrap different types of single-key authentication schemes. - + The SingleSenderAuthenticator is part of the Transaction Authenticator V2 format and is designed to be more extensible and consistent than the legacy authenticator formats. - + Features: - Modern unified interface for single signatures - Supports multiple signature algorithms through AccountAuthenticator - Consistent with Transaction Authenticator V2 specification - Efficient serialization and verification - Forward compatibility with future signature schemes - + Use Cases: - Standard single-account transactions - Modern applications preferring the unified format - Systems requiring forward compatibility - Clean integration with newer Aptos features - + Examples: Create with Ed25519 signature:: - + from aptos_sdk import ed25519 from aptos_sdk.authenticator import ( - Ed25519Authenticator, + Ed25519Authenticator, AccountAuthenticator, SingleSenderAuthenticator, Authenticator ) - + # Generate key and sign private_key = ed25519.PrivateKey.random() public_key = private_key.public_key() signature = private_key.sign(transaction_hash) - + # Create authenticator chain ed25519_auth = Ed25519Authenticator(public_key, signature) account_auth = AccountAuthenticator(ed25519_auth) single_sender = SingleSenderAuthenticator(account_auth) tx_auth = Authenticator(single_sender) - + Create with modern single key:: - + from aptos_sdk.authenticator import SingleKeyAuthenticator - + # Using the modern single key format single_key_auth = SingleKeyAuthenticator(public_key, signature) account_auth = AccountAuthenticator(single_key_auth) single_sender = SingleSenderAuthenticator(account_auth) - + Verification:: - + # Verify the signature is_valid = single_sender.verify(transaction_hash) print(f"Signature valid: {is_valid}") - + Attributes: sender (AccountAuthenticator): The account authenticator for the sender - + Note: While this format is more modern, existing applications using Ed25519Authenticator directly can continue to work. This format provides better extensibility for future signature scheme additions. """ - + sender: AccountAuthenticator def __init__( @@ -836,13 +838,13 @@ def __init__( sender: AccountAuthenticator, ): """Initialize a single sender authenticator. - + Args: sender: The account authenticator for the transaction sender - + Examples: Basic initialization:: - + ed25519_auth = Ed25519Authenticator(public_key, signature) account_auth = AccountAuthenticator(ed25519_auth) single_sender = SingleSenderAuthenticator(account_auth) @@ -851,20 +853,20 @@ def __init__( def __eq__(self, other: object) -> bool: """Check equality with another SingleSenderAuthenticator. - + Args: other: Object to compare with - + Returns: True if sender authenticators are equal, False otherwise """ if not isinstance(other, SingleSenderAuthenticator): return NotImplemented return self.sender == other.sender - + def __str__(self) -> str: """String representation of the single sender authenticator. - + Returns: Human-readable string showing sender details """ @@ -872,13 +874,13 @@ def __str__(self) -> str: def verify(self, data: bytes) -> bool: """Verify the sender's signature against the provided data. - + This delegates verification to the underlying account authenticator, which in turn delegates to the specific signature implementation. - + Args: data: The data that was signed (typically a transaction hash) - + Returns: True if the signature is valid, False otherwise """ @@ -887,13 +889,13 @@ def verify(self, data: bytes) -> bool: @staticmethod def deserialize(deserializer: Deserializer) -> SingleSenderAuthenticator: """Deserialize a SingleSenderAuthenticator from BCS bytes. - + Args: deserializer: The BCS deserializer containing the authenticator data - + Returns: A SingleSenderAuthenticator instance - + Raises: DeserializationError: If the data is malformed or incomplete """ @@ -902,10 +904,10 @@ def deserialize(deserializer: Deserializer) -> SingleSenderAuthenticator: def serialize(self, serializer: Serializer): """Serialize this single sender authenticator using BCS serialization. - + This serializes the underlying account authenticator which contains the specific signature scheme and signature data. - + Args: serializer: The BCS serializer to write to """ @@ -914,80 +916,80 @@ def serialize(self, serializer: Serializer): class SingleKeyAuthenticator: """Modern single-key authenticator with algorithm flexibility. - + This is the preferred single-key authentication format in newer Aptos versions. Unlike Ed25519Authenticator which is tied to a specific algorithm, SingleKeyAuthenticator can work with multiple signature algorithms through the asymmetric_crypto_wrapper. - + The authenticator uses the AIP-80 compliant key format, providing a unified interface for different cryptographic algorithms while maintaining compatibility with existing Aptos authentication infrastructure. - + Supported Algorithms: - Ed25519: Fast and secure elliptic curve signatures - - Secp256k1: Bitcoin-compatible signatures + - Secp256k1: Bitcoin-compatible signatures - Future algorithms: Extensible through the wrapper interface - + Features: - Algorithm-agnostic interface - AIP-80 compliant key formatting - Efficient serialization and verification - Forward compatibility with new signature schemes - Consistent API across different algorithms - + Examples: Create with Ed25519:: - + from aptos_sdk import ed25519 from aptos_sdk.authenticator import SingleKeyAuthenticator - + # Generate Ed25519 key pair private_key = ed25519.PrivateKey.random() public_key = private_key.public_key() - + # Sign transaction hash tx_hash = b"transaction_hash_bytes" signature = private_key.sign(tx_hash) - + # Create single key authenticator auth = SingleKeyAuthenticator(public_key, signature) - + # Verify signature is_valid = auth.verify(tx_hash) - + Create with secp256k1:: - + from aptos_sdk import secp256k1_ecdsa - + # Generate secp256k1 key pair private_key = secp256k1_ecdsa.PrivateKey.random() public_key = private_key.public_key() signature = private_key.sign(tx_hash) - + # Create authenticator (same interface) auth = SingleKeyAuthenticator(public_key, signature) - + Serialization:: - + # Serialize for blockchain submission serializer = Serializer() auth.serialize(serializer) auth_bytes = serializer.output() - + # Deserialize from bytes deserializer = Deserializer(auth_bytes) restored_auth = SingleKeyAuthenticator.deserialize(deserializer) - + Attributes: public_key (asymmetric_crypto_wrapper.PublicKey): Wrapped public key with algorithm info signature (asymmetric_crypto_wrapper.Signature): Wrapped signature with algorithm info - + Note: The wrapper classes automatically handle algorithm detection and provide a unified interface for verification. This authenticator is preferred over algorithm-specific authenticators for new applications. """ - + public_key: asymmetric_crypto_wrapper.PublicKey signature: asymmetric_crypto_wrapper.Signature @@ -997,23 +999,23 @@ def __init__( signature: asymmetric_crypto.Signature, ): """Initialize a single key authenticator with algorithm detection. - + The constructor automatically wraps the provided public key and signature with the appropriate wrapper classes that handle algorithm-specific details. - + Args: public_key: The public key (Ed25519, secp256k1, etc.) signature: The signature corresponding to the public key - + Examples: With raw Ed25519 objects:: - + ed25519_key = ed25519.PublicKey.from_str("...") ed25519_sig = ed25519.Signature.from_str("...") auth = SingleKeyAuthenticator(ed25519_key, ed25519_sig) - + With pre-wrapped objects:: - + wrapped_key = asymmetric_crypto_wrapper.PublicKey(ed25519_key) wrapped_sig = asymmetric_crypto_wrapper.Signature(ed25519_sig) auth = SingleKeyAuthenticator(wrapped_key, wrapped_sig) @@ -1027,23 +1029,23 @@ def __init__( self.signature = signature else: self.signature = asymmetric_crypto_wrapper.Signature(signature) - + def __eq__(self, other: object) -> bool: """Check equality with another SingleKeyAuthenticator. - + Args: other: Object to compare with - + Returns: True if public keys and signatures are equal, False otherwise """ if not isinstance(other, SingleKeyAuthenticator): return NotImplemented return self.public_key == other.public_key and self.signature == other.signature - + def __str__(self) -> str: """String representation of the single key authenticator. - + Returns: Human-readable string showing key and signature details """ @@ -1051,13 +1053,13 @@ def __str__(self) -> str: def verify(self, data: bytes) -> bool: """Verify the signature against the provided data. - + This method delegates to the wrapped public key's verification method, which automatically handles the algorithm-specific verification logic. - + Args: data: The data that was signed (typically a transaction hash) - + Returns: True if the signature is valid, False otherwise """ @@ -1066,13 +1068,13 @@ def verify(self, data: bytes) -> bool: @staticmethod def deserialize(deserializer: Deserializer) -> SingleKeyAuthenticator: """Deserialize a SingleKeyAuthenticator from BCS bytes. - + Args: deserializer: The BCS deserializer containing the authenticator data - + Returns: A SingleKeyAuthenticator instance with the deserialized key and signature - + Raises: DeserializationError: If the data is malformed or incomplete """ @@ -1082,10 +1084,10 @@ def deserialize(deserializer: Deserializer) -> SingleKeyAuthenticator: def serialize(self, serializer: Serializer): """Serialize this single key authenticator using BCS serialization. - + This serializes the wrapped public key and signature, including their algorithm identifiers as specified in AIP-80. - + Args: serializer: The BCS serializer to write to """ @@ -1095,22 +1097,22 @@ def serialize(self, serializer: Serializer): class MultiKeyAuthenticator: """Modern multi-key authenticator with algorithm flexibility and threshold signatures. - + This is the preferred multi-signature authentication format in newer Aptos versions. Unlike MultiEd25519Authenticator which is tied to Ed25519, MultiKeyAuthenticator can work with mixed signature algorithms through the asymmetric_crypto_wrapper, allowing for heterogeneous multi-signature schemes. - + The authenticator uses the AIP-80 compliant key format and supports threshold signatures where N-of-M keys must sign to authorize a transaction. This provides flexible multi-party authentication with algorithm diversity. - + Supported Algorithm Combinations: - Mixed Ed25519 and secp256k1 keys in the same multi-signature - Pure Ed25519 multi-signatures (recommended for performance) - Pure secp256k1 multi-signatures (for Bitcoin compatibility) - Future algorithm combinations through the wrapper interface - + Features: - Algorithm-agnostic multi-signature interface - Heterogeneous key mixing (Ed25519 + secp256k1 + future algorithms) @@ -1119,36 +1121,36 @@ class MultiKeyAuthenticator: - Efficient serialization and verification - Forward compatibility with new signature schemes - Superior to legacy MultiEd25519Authenticator - + Use Cases: - Multi-party custody with different cryptographic preferences - Governance scenarios requiring diverse signature algorithms - Cross-chain compatibility requiring secp256k1 support - Organizations with mixed cryptographic infrastructure - Future-proofing against algorithm deprecation - + Examples: Mixed Ed25519 and secp256k1 2-of-3:: - + from aptos_sdk import ed25519, secp256k1_ecdsa from aptos_sdk import asymmetric_crypto_wrapper from aptos_sdk.authenticator import MultiKeyAuthenticator - + # Generate mixed key pairs ed25519_key1 = ed25519.PrivateKey.random() ed25519_key2 = ed25519.PrivateKey.random() secp256k1_key = secp256k1_ecdsa.PrivateKey.random() - + # Create public key list public_keys = [ ed25519_key1.public_key(), ed25519_key2.public_key(), secp256k1_key.public_key() ] - + # Create multi-public key with threshold 2 multi_pub_key = asymmetric_crypto_wrapper.MultiPublicKey(public_keys, threshold=2) - + # Sign with keys 0 and 2 (Ed25519 + secp256k1) tx_hash = b"transaction_hash" signatures = [ @@ -1156,22 +1158,22 @@ class MultiKeyAuthenticator: (2, secp256k1_key.sign(tx_hash)) ] multi_signature = asymmetric_crypto_wrapper.MultiSignature(signatures) - + # Create authenticator auth = MultiKeyAuthenticator(multi_pub_key, multi_signature) - + # Verify the mixed multi-signature is_valid = auth.verify(tx_hash) - + Pure Ed25519 3-of-5 (recommended for performance):: - + # Generate Ed25519 keys only ed25519_keys = [ed25519.PrivateKey.random() for _ in range(5)] public_keys = [key.public_key() for key in ed25519_keys] - + # Create 3-of-5 threshold multi_pub_key = asymmetric_crypto_wrapper.MultiPublicKey(public_keys, threshold=3) - + # Sign with keys 1, 2, and 4 signatures = [ (1, ed25519_keys[1].sign(tx_hash)), @@ -1180,41 +1182,41 @@ class MultiKeyAuthenticator: ] multi_signature = asymmetric_crypto_wrapper.MultiSignature(signatures) auth = MultiKeyAuthenticator(multi_pub_key, multi_signature) - + Integration with SingleSenderAuthenticator:: - + # Wrap in account and transaction authenticators account_auth = AccountAuthenticator(multi_key_auth) single_sender = SingleSenderAuthenticator(account_auth) tx_auth = Authenticator(single_sender) - + # Submit to blockchain serializer = Serializer() tx_auth.serialize(serializer) auth_bytes = serializer.output() - + Attributes: public_key (asymmetric_crypto_wrapper.MultiPublicKey): Multi-public key with mixed algorithms signature (asymmetric_crypto_wrapper.MultiSignature): Multi-signature with threshold validation - + Security Considerations: - Mixed algorithms provide defense against algorithm-specific attacks - Threshold must be set appropriately (not too low, not too high) - Each signature algorithm contributes its own security properties - Key management complexity increases with algorithm diversity - + Performance Notes: - Pure Ed25519 multi-signatures are fastest - Mixed algorithms have slight verification overhead - Serialization size increases with algorithm diversity - Network latency impact depends on signature sizes - + Note: This is the modern replacement for MultiEd25519Authenticator. New applications should prefer this format for its flexibility and future compatibility. """ - + public_key: asymmetric_crypto_wrapper.MultiPublicKey signature: asymmetric_crypto_wrapper.MultiSignature @@ -1224,43 +1226,43 @@ def __init__( signature: asymmetric_crypto_wrapper.MultiSignature, ): """Initialize a multi-key authenticator with mixed algorithms. - + Args: public_key: Multi-public key containing keys from different algorithms signature: Multi-signature with the required threshold signatures - + Examples: Basic mixed algorithm initialization:: - + # Assume we have ed25519 and secp256k1 keys mixed_keys = [ed25519_pub, secp256k1_pub, another_ed25519_pub] multi_pub_key = asymmetric_crypto_wrapper.MultiPublicKey(mixed_keys, threshold=2) - + # Signatures from threshold keys (indices 0 and 2) signatures = [(0, ed25519_sig), (2, another_ed25519_sig)] multi_sig = asymmetric_crypto_wrapper.MultiSignature(signatures) - + auth = MultiKeyAuthenticator(multi_pub_key, multi_sig) """ self.public_key = public_key self.signature = signature - + def __eq__(self, other: object) -> bool: """Check equality with another MultiKeyAuthenticator. - + Args: other: Object to compare with - + Returns: True if public keys and signatures are equal, False otherwise """ if not isinstance(other, MultiKeyAuthenticator): return NotImplemented return self.public_key == other.public_key and self.signature == other.signature - + def __str__(self) -> str: """String representation of the multi-key authenticator. - + Returns: Human-readable string showing multi-key details """ @@ -1268,16 +1270,16 @@ def __str__(self) -> str: def verify(self, data: bytes) -> bool: """Verify the multi-signature against the provided data. - + This method validates that: 1. The threshold number of signatures is provided 2. Each signature is from a different key in the multi-public key 3. Each signature is cryptographically valid for its algorithm 4. Mixed algorithm signatures are handled correctly - + Args: data: The data that was signed (typically a transaction hash) - + Returns: True if the multi-signature meets the threshold and all signatures are valid """ @@ -1286,13 +1288,13 @@ def verify(self, data: bytes) -> bool: @staticmethod def deserialize(deserializer: Deserializer) -> MultiKeyAuthenticator: """Deserialize a MultiKeyAuthenticator from BCS bytes. - + Args: deserializer: The BCS deserializer containing the authenticator data - + Returns: A MultiKeyAuthenticator instance with mixed algorithm support - + Raises: DeserializationError: If the data is malformed or incomplete """ @@ -1302,11 +1304,11 @@ def deserialize(deserializer: Deserializer) -> MultiKeyAuthenticator: def serialize(self, serializer: Serializer): """Serialize this multi-key authenticator using BCS serialization. - + This serializes the multi-public key (including all public keys with their algorithm identifiers and the threshold) and the multi-signature (including signature indices and algorithm-specific signature bytes). - + Args: serializer: The BCS serializer to write to """ @@ -1316,11 +1318,11 @@ def serialize(self, serializer: Serializer): class Test(unittest.TestCase): """Unit tests for authenticator functionality. - + Tests serialization, deserialization, and verification of various authenticator types, including mixed-algorithm multi-key authentication scenarios. """ - + def test_multi_key_auth(self): expected_output = bytes.fromhex( "040303002020fdbac9b10b7587bba7b5bc163bce69e796d71e4ed44c10fcb4488689f7a1440141049b8327d929a0e45285c04d19c9fffbee065c266b701972922d807228120e43f34ad68ac77f6ec0205fe39f7c5b6055dad973a03464a3a743302de0feaf6ec6d90141049b8327d929a0e45285c04d19c9fffbee065c266b701972922d807228120e43f34ad68ac77f6ec0205fe39f7c5b6055dad973a03464a3a743302de0feaf6ec6d902020040a9839b56be99b48c285ec252cf9bf779e42d3b62eb8664c31b18c1fdb29b574b1bfde0b89aedddb9fb8304ca5913c9feefea75d332d8f72ac3ab4598a884ea0801402bd50683abe6332a496121f8ec7db7be351f49b0087fa0dfb258c469822bd52e59fc9344944a1f338b0f0a61c7173453e0cd09cf961e45cb9396808fa67eeef301c0" diff --git a/aptos_sdk/bcs.py b/aptos_sdk/bcs.py index 1d19ec1..e0ee4b5 100644 --- a/aptos_sdk/bcs.py +++ b/aptos_sdk/bcs.py @@ -19,25 +19,25 @@ Examples: Basic serialization:: - + from aptos_sdk.bcs import Serializer, Deserializer - + # Serialize a string ser = Serializer() ser.str("hello") data = ser.output() - + # Deserialize back to string der = Deserializer(data) result = der.str() # "hello" - + Working with custom structures:: - + class MyStruct: def serialize(self, serializer): serializer.str(self.name) serializer.u32(self.value) - + @staticmethod def deserialize(deserializer): name = deserializer.str() @@ -64,27 +64,27 @@ def deserialize(deserializer): class Deserializable(Protocol): """Protocol for objects that can be deserialized from a BCS byte stream. - + This protocol defines the interface that classes must implement to support BCS deserialization. Classes implementing this protocol can be automatically deserialized from binary data. - + The protocol requires: - A `from_bytes` class method that creates an instance from raw bytes - A `deserialize` static method that reads from a Deserializer - + Examples: Implementing a deserializable class:: - + class MyClass: def __init__(self, value: str): self.value = value - + @staticmethod def deserialize(deserializer: Deserializer) -> 'MyClass': value = deserializer.str() return MyClass(value) - + # Usage data = b'\x05hello' # BCS-encoded string obj = MyClass.from_bytes(data) @@ -93,13 +93,13 @@ def deserialize(deserializer: Deserializer) -> 'MyClass': @classmethod def from_bytes(cls, indata: bytes) -> Deserializable: """Create an instance of this class from BCS-encoded bytes. - + Args: indata: The BCS-encoded byte data to deserialize. - + Returns: An instance of the implementing class. - + Raises: Exception: If the data cannot be deserialized or is malformed. """ @@ -109,13 +109,13 @@ def from_bytes(cls, indata: bytes) -> Deserializable: @staticmethod def deserialize(deserializer: Deserializer) -> Deserializable: """Deserialize an instance from a Deserializer. - + Args: deserializer: The Deserializer to read data from. - + Returns: A deserialized instance of the implementing class. - + Note: This is an abstract method that must be implemented by concrete classes. """ @@ -124,25 +124,25 @@ def deserialize(deserializer: Deserializer) -> Deserializable: class Serializable(Protocol): """Protocol for objects that can be serialized into a BCS byte stream. - + This protocol defines the interface that classes must implement to support BCS serialization. Classes implementing this protocol can be automatically serialized to binary data. - + The protocol requires: - A `to_bytes` method that converts the instance to raw bytes - A `serialize` method that writes data to a Serializer - + Examples: Implementing a serializable class:: - + class MyClass: def __init__(self, value: str): self.value = value - + def serialize(self, serializer: Serializer): serializer.str(self.value) - + # Usage obj = MyClass("hello") data = obj.to_bytes() # Returns BCS-encoded bytes @@ -150,10 +150,10 @@ def serialize(self, serializer: Serializer): def to_bytes(self) -> bytes: """Convert this object to BCS-encoded bytes. - + Returns: The BCS-encoded representation of this object as bytes. - + Raises: Exception: If the object cannot be serialized. """ @@ -163,10 +163,10 @@ def to_bytes(self) -> bytes: def serialize(self, serializer: Serializer): """Serialize this object using the provided Serializer. - + Args: serializer: The Serializer to write data to. - + Note: This is an abstract method that must be implemented by concrete classes. """ @@ -175,38 +175,39 @@ def serialize(self, serializer: Serializer): class Deserializer: """A BCS deserializer for reading data from a byte stream. - + The Deserializer class provides methods to read various data types from BCS-encoded byte data. It maintains an internal position in the byte stream and provides methods to read primitive types, collections, and custom structures. - + Attributes: _input: Internal BytesIO stream for reading data. _length: Total length of the input data. - + Examples: Basic usage:: - + data = b'\x01\x05hello' # BCS-encoded bool and string der = Deserializer(data) - + flag = der.bool() # True text = der.str() # "hello" - + Reading collections:: - + # Deserialize a sequence of strings values = der.sequence(Deserializer.str) - + # Deserialize a map mapping = der.map(Deserializer.str, Deserializer.u32) """ + _input: io.BytesIO _length: int def __init__(self, data: bytes): """Initialize the deserializer with byte data. - + Args: data: The BCS-encoded bytes to deserialize from. """ @@ -215,7 +216,7 @@ def __init__(self, data: bytes): def remaining(self) -> int: """Get the number of bytes remaining in the input stream. - + Returns: The number of unread bytes remaining in the stream. """ @@ -223,12 +224,12 @@ def remaining(self) -> int: def bool(self) -> bool: """Read a boolean value from the stream. - + BCS encodes booleans as a single byte: 0 for False, 1 for True. - + Returns: The deserialized boolean value. - + Raises: Exception: If the byte value is not 0 or 1, or if there's insufficient data in the stream. @@ -243,12 +244,12 @@ def bool(self) -> bool: def to_bytes(self) -> bytes: """Read a byte array from the stream. - + BCS encodes byte arrays as a ULEB128 length followed by the raw bytes. - + Returns: The deserialized byte array. - + Raises: Exception: If there's insufficient data in the stream or if the length encoding is invalid. @@ -257,13 +258,13 @@ def to_bytes(self) -> bytes: def fixed_bytes(self, length: int) -> bytes: """Read a fixed-length byte array from the stream. - + Args: length: The exact number of bytes to read. - + Returns: The deserialized byte array of the specified length. - + Raises: Exception: If there are insufficient bytes remaining in the stream. """ @@ -275,23 +276,23 @@ def map( value_decoder: typing.Callable[[Deserializer], typing.Any], ) -> Dict[typing.Any, typing.Any]: """Read a map (dictionary) from the stream. - + BCS encodes maps as a ULEB128 length followed by key-value pairs. The pairs are sorted by the BCS encoding of the keys. - + Args: key_decoder: Function to decode each key from the stream. value_decoder: Function to decode each value from the stream. - + Returns: A dictionary containing the deserialized key-value pairs. - + Raises: Exception: If there's insufficient data or if the decoders fail. - + Examples: Reading a map of string keys to u32 values:: - + mapping = der.map(Deserializer.str, Deserializer.u32) """ length = self.uleb128() @@ -307,25 +308,25 @@ def sequence( value_decoder: typing.Callable[[Deserializer], typing.Any], ) -> List[typing.Any]: """Read a sequence (list) from the stream. - + BCS encodes sequences as a ULEB128 length followed by the elements. - + Args: value_decoder: Function to decode each element from the stream. - + Returns: A list containing the deserialized elements. - + Raises: Exception: If there's insufficient data or if the decoder fails. - + Examples: Reading a sequence of strings:: - + strings = der.sequence(Deserializer.str) - + Reading a sequence of u32 values:: - + numbers = der.sequence(Deserializer.u32) """ length = self.uleb128() @@ -336,13 +337,13 @@ def sequence( def str(self) -> str: """Read a UTF-8 string from the stream. - + BCS encodes strings as byte arrays (ULEB128 length + bytes) that contain valid UTF-8 data. - + Returns: The deserialized string. - + Raises: Exception: If there's insufficient data in the stream. UnicodeDecodeError: If the bytes don't form valid UTF-8. @@ -351,16 +352,16 @@ def str(self) -> str: def struct(self, struct: typing.Any) -> typing.Any: """Deserialize a custom struct from the stream. - + This method delegates to the struct's `deserialize` method to handle custom deserialization logic. - + Args: struct: A class or type that implements the `deserialize` method. - + Returns: The deserialized struct instance. - + Raises: Exception: If the struct doesn't have a deserialize method or if deserialization fails. @@ -369,10 +370,10 @@ def struct(self, struct: typing.Any) -> typing.Any: def u8(self) -> int: """Read an 8-bit unsigned integer from the stream. - + Returns: The deserialized u8 value (0-255). - + Raises: Exception: If there's insufficient data in the stream. """ @@ -380,10 +381,10 @@ def u8(self) -> int: def u16(self) -> int: """Read a 16-bit unsigned integer from the stream. - + Returns: The deserialized u16 value (0-65535). - + Raises: Exception: If there's insufficient data in the stream. """ @@ -391,10 +392,10 @@ def u16(self) -> int: def u32(self) -> int: """Read a 32-bit unsigned integer from the stream. - + Returns: The deserialized u32 value (0-4294967295). - + Raises: Exception: If there's insufficient data in the stream. """ @@ -402,10 +403,10 @@ def u32(self) -> int: def u64(self) -> int: """Read a 64-bit unsigned integer from the stream. - + Returns: The deserialized u64 value (0-18446744073709551615). - + Raises: Exception: If there's insufficient data in the stream. """ @@ -413,10 +414,10 @@ def u64(self) -> int: def u128(self) -> int: """Read a 128-bit unsigned integer from the stream. - + Returns: The deserialized u128 value (0-340282366920938463463374607431768211455). - + Raises: Exception: If there's insufficient data in the stream. """ @@ -424,10 +425,10 @@ def u128(self) -> int: def u256(self) -> int: """Read a 256-bit unsigned integer from the stream. - + Returns: The deserialized u256 value. - + Raises: Exception: If there's insufficient data in the stream. """ @@ -435,14 +436,14 @@ def u256(self) -> int: def uleb128(self) -> int: """Read a ULEB128 (unsigned little-endian base 128) encoded integer. - + ULEB128 is a variable-length encoding where each byte contains 7 bits of data and a continuation bit. It's commonly used for encoding lengths and small integers efficiently. - + Returns: The decoded integer value (0-4294967295). - + Raises: Exception: If the encoded value exceeds u32 range or if there's insufficient data in the stream. @@ -464,13 +465,13 @@ def uleb128(self) -> int: def _read(self, length: int) -> bytes: """Read a specified number of bytes from the input stream. - + Args: length: Number of bytes to read. - + Returns: The requested bytes. - + Raises: Exception: If there are insufficient bytes remaining in the stream. """ @@ -485,13 +486,13 @@ def _read(self, length: int) -> bytes: def _read_int(self, length: int) -> int: """Read an integer of specified byte length from the stream. - + Args: length: Number of bytes representing the integer. - + Returns: The integer value interpreted as little-endian unsigned. - + Raises: Exception: If there are insufficient bytes in the stream. """ @@ -500,30 +501,31 @@ def _read_int(self, length: int) -> int: class Serializer: """A BCS serializer for writing data to a byte stream. - + The Serializer class provides methods to write various data types to a BCS-encoded byte stream. It maintains an internal output buffer and provides methods to serialize primitive types, collections, and custom structures. - + Attributes: _output: Internal BytesIO buffer for accumulating serialized data. - + Examples: Basic usage:: - + ser = Serializer() ser.bool(True) ser.str("hello") data = ser.output() # Get the serialized bytes - + Serializing collections:: - + # Serialize a sequence of strings ser.sequence(["a", "b", "c"], Serializer.str) - + # Serialize a map ser.map({"key": 42}, Serializer.str, Serializer.u32) """ + _output: io.BytesIO def __init__(self): @@ -532,7 +534,7 @@ def __init__(self): def output(self) -> bytes: """Get the accumulated serialized data as bytes. - + Returns: The BCS-encoded bytes written to this serializer. """ @@ -540,9 +542,9 @@ def output(self) -> bytes: def bool(self, value: bool): """Write a boolean value to the stream. - + BCS encodes booleans as a single byte: 0 for False, 1 for True. - + Args: value: The boolean value to serialize. """ @@ -550,9 +552,9 @@ def bool(self, value: bool): def to_bytes(self, value: bytes): """Write a byte array to the stream. - + BCS encodes byte arrays as a ULEB128 length followed by the raw bytes. - + Args: value: The byte array to serialize. """ @@ -561,9 +563,9 @@ def to_bytes(self, value: bytes): def fixed_bytes(self, value): """Write a fixed-length byte array to the stream. - + This method writes raw bytes without any length prefix. - + Args: value: The byte array to write directly to the stream. """ @@ -576,19 +578,19 @@ def map( value_encoder: typing.Callable[[Serializer, typing.Any], None], ): """Write a map (dictionary) to the stream. - + BCS encodes maps as a ULEB128 length followed by key-value pairs. The pairs are sorted by the BCS encoding of the keys to ensure canonical ordering. - + Args: values: The dictionary to serialize. key_encoder: Function to encode each key. value_encoder: Function to encode each value. - + Examples: Serializing a map of string keys to u32 values:: - + mapping = {"a": 1, "b": 2} ser.map(mapping, Serializer.str, Serializer.u32) """ @@ -609,20 +611,20 @@ def sequence_serializer( value_encoder: typing.Callable[[Serializer, typing.Any], None], ): """Create a reusable sequence serializer function. - + This is a helper method that returns a function that can be used to serialize sequences with a specific encoder. - + Args: value_encoder: Function to encode each element in sequences. - + Returns: A function that takes a serializer and a list of values and serializes the sequence. - + Examples: Creating a string sequence serializer:: - + str_seq = Serializer.sequence_serializer(Serializer.str) str_seq(ser, ["a", "b", "c"]) """ @@ -634,20 +636,20 @@ def sequence( value_encoder: typing.Callable[[Serializer, typing.Any], None], ): """Write a sequence (list) to the stream. - + BCS encodes sequences as a ULEB128 length followed by the elements. - + Args: values: The list of values to serialize. value_encoder: Function to encode each element. - + Examples: Serializing a sequence of strings:: - + ser.sequence(["a", "b", "c"], Serializer.str) - + Serializing a sequence of u32 values:: - + ser.sequence([1, 2, 3], Serializer.u32) """ self.uleb128(len(values)) @@ -656,13 +658,13 @@ def sequence( def str(self, value: str): """Write a UTF-8 string to the stream. - + BCS encodes strings as byte arrays (ULEB128 length + bytes) containing valid UTF-8 data. - + Args: value: The string to serialize. - + Raises: UnicodeEncodeError: If the string cannot be encoded as UTF-8. """ @@ -670,13 +672,13 @@ def str(self, value: str): def struct(self, value: typing.Any): """Serialize a custom struct to the stream. - + This method delegates to the struct's `serialize` method to handle custom serialization logic. - + Args: value: An object that implements the `serialize` method. - + Raises: AttributeError: If the value doesn't have a serialize method. Exception: If serialization fails. @@ -685,10 +687,10 @@ def struct(self, value: typing.Any): def u8(self, value: int): """Write an 8-bit unsigned integer to the stream. - + Args: value: The u8 value to serialize (0-255). - + Raises: Exception: If the value is outside the valid range. """ @@ -699,10 +701,10 @@ def u8(self, value: int): def u16(self, value: int): """Write a 16-bit unsigned integer to the stream. - + Args: value: The u16 value to serialize (0-65535). - + Raises: Exception: If the value is outside the valid range. """ @@ -713,10 +715,10 @@ def u16(self, value: int): def u32(self, value: int): """Write a 32-bit unsigned integer to the stream. - + Args: value: The u32 value to serialize (0-4294967295). - + Raises: Exception: If the value is outside the valid range. """ @@ -727,10 +729,10 @@ def u32(self, value: int): def u64(self, value: int): """Write a 64-bit unsigned integer to the stream. - + Args: value: The u64 value to serialize (0-18446744073709551615). - + Raises: Exception: If the value is outside the valid range. """ @@ -741,10 +743,10 @@ def u64(self, value: int): def u128(self, value: int): """Write a 128-bit unsigned integer to the stream. - + Args: value: The u128 value to serialize (0-340282366920938463463374607431768211455). - + Raises: Exception: If the value is outside the valid range. """ @@ -755,10 +757,10 @@ def u128(self, value: int): def u256(self, value: int): """Write a 256-bit unsigned integer to the stream. - + Args: value: The u256 value to serialize. - + Raises: Exception: If the value is outside the valid range. """ @@ -769,14 +771,14 @@ def u256(self, value: int): def uleb128(self, value: int): """Write a ULEB128 (unsigned little-endian base 128) encoded integer. - + ULEB128 is a variable-length encoding where each byte contains 7 bits of data and a continuation bit. It's commonly used for encoding lengths and small integers efficiently. - + Args: value: The integer value to encode (0-4294967295). - + Raises: Exception: If the value exceeds the u32 range. """ @@ -794,7 +796,7 @@ def uleb128(self, value: int): def _write_int(self, value: int, length: int): """Write an integer of specified byte length to the stream. - + Args: value: The integer value to write. length: Number of bytes to use for the integer representation. @@ -806,24 +808,24 @@ def encoder( value: typing.Any, encoder: typing.Callable[[Serializer, typing.Any], typing.Any] ) -> bytes: """Encode a single value using the specified encoder function. - + This is a convenience function that creates a new Serializer, uses the provided encoder function to serialize the value, and returns the bytes. - + Args: value: The value to encode. encoder: Function that takes a serializer and value and encodes the value. - + Returns: The BCS-encoded bytes for the value. - + Examples: Encoding a string:: - + data = encoder("hello", Serializer.str) - + Encoding an integer:: - + data = encoder(42, Serializer.u32) """ ser = Serializer() @@ -833,12 +835,13 @@ def encoder( class Test(unittest.TestCase): """Test suite for BCS serialization and deserialization. - + This test class contains comprehensive tests for all BCS data types and operations to ensure correct serialization and deserialization behavior. Each test follows the pattern of serializing a value, deserializing it, and verifying the round-trip preserves the original value. """ + def test_bool_true(self): in_value = True diff --git a/aptos_sdk/cli.py b/aptos_sdk/cli.py index 5e890d8..b2d0afc 100644 --- a/aptos_sdk/cli.py +++ b/aptos_sdk/cli.py @@ -29,15 +29,15 @@ Examples: Basic package publishing:: - + python -m aptos_sdk.cli publish-package \ --package-dir ./my-move-package \ --account ***1234... \ --private-key-path ./private_key.txt \ --rest-api https://fullnode.devnet.aptoslabs.com/v1 - + Package with named addresses:: - + python -m aptos_sdk.cli publish-package \ --package-dir ./my-move-package \ --account ***1234... \ @@ -45,12 +45,12 @@ --rest-api https://fullnode.devnet.aptoslabs.com/v1 \ --named-address my_addr=***5678... \ --named-address other_addr=***9abc... - + Programmatic usage:: - + import asyncio from aptos_sdk.cli import main - + # Run CLI command programmatically await main([ 'publish-package', @@ -59,18 +59,18 @@ '--private-key-path', './key.txt', '--rest-api', 'https://fullnode.devnet.aptoslabs.com/v1' ]) - + Integration with scripts:: - + from aptos_sdk.cli import publish_package from aptos_sdk.account import Account from aptos_sdk.account_address import AccountAddress from aptos_sdk.ed25519 import PrivateKey - + # Direct function call private_key = PrivateKey.from_str("ed25519-priv-...") account = Account(AccountAddress.from_str("***123..."), private_key) - + await publish_package( package_dir="./my-package", named_addresses={"MyModule": AccountAddress.from_str("***456...")}, @@ -87,7 +87,7 @@ File Format Requirements: Private Key File: Should contain a single line with the private key in Ed25519 format, either as raw hex or AIP-80 compliant string. - + Move Package: Must have proper Move.toml configuration file with dependencies and named addresses properly specified. @@ -128,35 +128,35 @@ async def publish_package( rest_api: str, ): """Compile and publish a Move package to the Aptos blockchain. - + This function orchestrates the complete package publishing workflow: 1. Compiles the Move package using the Aptos CLI 2. Creates a REST client connection to the specified network 3. Publishes the compiled package to the blockchain - + Args: package_dir: Path to the Move package directory containing Move.toml. named_addresses: Dictionary mapping named address identifiers to their resolved AccountAddress values. signer: Account that will sign and pay for the package publication. rest_api: URL of the Aptos REST API endpoint to publish to. - + Raises: Exception: If the Move package compilation fails. ApiError: If the package publication transaction fails. FileNotFoundError: If the package directory or files don't exist. - + Examples: Basic package publishing:: - + from aptos_sdk.account import Account from aptos_sdk.account_address import AccountAddress from aptos_sdk.ed25519 import PrivateKey - + # Create account from private key private_key = PrivateKey.from_str("ed25519-priv-...") account = Account(AccountAddress.from_str("***123..."), private_key) - + # Publish package await publish_package( package_dir="./my-move-package", @@ -164,21 +164,21 @@ async def publish_package( signer=account, rest_api="https://fullnode.devnet.aptoslabs.com/v1" ) - + Package with named addresses:: - + named_addresses = { "MyContract": AccountAddress.from_str("***456..."), "Treasury": AccountAddress.from_str("***789...") } - + await publish_package( package_dir="./complex-package", named_addresses=named_addresses, signer=deployer_account, rest_api="https://fullnode.mainnet.aptoslabs.com/v1" ) - + Note: - Requires the Aptos CLI to be installed and available - The signer account must have sufficient APT to pay for gas @@ -194,46 +194,46 @@ async def publish_package( def key_value(indata: str) -> Tuple[str, AccountAddress]: """Parse a named address string into name and AccountAddress components. - + This function parses command-line named address arguments in the format "name=address" and returns a tuple suitable for use in named address dictionaries. - + Args: indata: String in format "name=address" where address can be any valid AccountAddress format (hex string, shortened address, etc.) - + Returns: Tuple of (name, AccountAddress) where name is the identifier and AccountAddress is the parsed address object. - + Raises: ValueError: If the input string is not in the expected "name=address" format. Exception: If the address portion cannot be parsed as a valid AccountAddress. - + Examples: Parse named address:: - + >>> name, addr = key_value("MyContract=***1234...") >>> print(f"Name: {name}, Address: {addr}") Name: MyContract, Address: ***1234... - + Multiple named addresses:: - + named_pairs = [ key_value("TokenContract=***1111..."), key_value("Treasury=***2222..."), key_value("Admin=***3333...") ] - + # Convert to dictionary named_addresses = dict(named_pairs) - + Command-line usage:: - + --named-address MyContract=***1234... \ --named-address Treasury=***5678... - + Note: This function is primarily used by the argument parser to convert command-line string arguments into structured data for Move compilation. @@ -248,32 +248,32 @@ def key_value(indata: str) -> Tuple[str, AccountAddress]: async def main(args: List[str]): """Main entry point for the Aptos Python SDK CLI. - + This function sets up the argument parser, validates inputs, and dispatches to the appropriate command handlers. It provides comprehensive error checking and user-friendly error messages. - + Args: args: List of command-line arguments (typically from sys.argv[1:]) - + Raises: SystemExit: On invalid arguments, missing requirements, or command failure. - + Examples: Run from command line:: - + python -m aptos_sdk.cli publish-package \ --package-dir ./my-package \ --account ***1234... \ --private-key-path ./key.txt \ --rest-api https://fullnode.devnet.aptoslabs.com/v1 - + Run programmatically:: - + import asyncio from aptos_sdk.cli import main - - await main([ + + await main([= 'publish-package', '--package-dir', './package', '--account', '***1234...', @@ -281,22 +281,22 @@ async def main(args: List[str]): '--rest-api', 'https://fullnode.devnet.aptoslabs.com/v1', '--named-address', 'MyAddr=***5678...' ]) - + Supported Commands: publish-package: Compile and deploy a Move package - + Required Arguments (for publish-package): --account: Account address that will publish the package --package-dir: Path to Move package directory --private-key-path: Path to private key file --rest-api: Aptos REST API endpoint URL - + Optional Arguments: --named-address: Named address mappings (can be specified multiple times) - + Environment Variables: APTOS_CLI_PATH: Path to Aptos CLI executable (if not in PATH) - + Note: The function performs extensive validation before executing commands to provide clear error messages for common configuration issues. @@ -318,14 +318,14 @@ async def main(args: List[str]): default=[], ) parser.add_argument( - "--package-dir", - help="Path to the Move package directory containing Move.toml", - type=str + "--package-dir", + help="Path to the Move package directory containing Move.toml", + type=str, ) parser.add_argument( - "--private-key-path", - help="Path to file containing the signer's private key", - type=str + "--private-key-path", + help="Path to file containing the signer's private key", + type=str, ) parser.add_argument( "--rest-api", diff --git a/aptos_sdk/ed25519.py b/aptos_sdk/ed25519.py index 88ce51f..6e64e75 100644 --- a/aptos_sdk/ed25519.py +++ b/aptos_sdk/ed25519.py @@ -20,43 +20,43 @@ Examples: Basic key generation and signing:: - + # Generate a random private key private_key = PrivateKey.random() public_key = private_key.public_key() - + # Sign some data message = b"Hello, Aptos!" signature = private_key.sign(message) - + # Verify the signature is_valid = public_key.verify(message, signature) - + Multi-signature operations:: - + # Create a 2-of-3 multisig keys = [PrivateKey.random().public_key() for _ in range(3)] multisig_key = MultiPublicKey(keys, threshold=2) - + # Create signatures from 2 signers sig1 = private_key1.sign(message) sig2 = private_key2.sign(message) - + # Combine into multisig multisig = MultiSignature.from_key_map(multisig_key, [ (keys[0], sig1), (keys[1], sig2) ]) - + # Verify multisig is_valid = multisig_key.verify(message, multisig) - + AIP-80 compliant key formats:: - + # Create from AIP-80 format key = PrivateKey.from_str( "ed25519-priv-0x123...", strict=True ) - + # Export to AIP-80 format aip80_string = key.aip80() """ @@ -74,45 +74,46 @@ class PrivateKey(asymmetric_crypto.PrivateKey): """Ed25519 private key for digital signatures on Aptos. - + A private key is used to create digital signatures and derive the corresponding public key. This implementation uses the NaCl library for cryptographic operations and supports AIP-80 compliant key formats for interoperability. - + The private key is exactly 32 bytes (256 bits) as specified by the Ed25519 signature scheme. - + Attributes: LENGTH: The byte length of Ed25519 private keys (32) key: The underlying NaCl SigningKey instance - + Examples: Creating and using private keys:: - + # Generate a random private key private_key = PrivateKey.random() - + # Create from hex string hex_key = PrivateKey.from_hex("0x123...") - + # Create from AIP-80 format aip80_key = PrivateKey.from_str( "ed25519-priv-0x123...", strict=True ) - + # Sign data signature = private_key.sign(b"message") - + # Get public key public_key = private_key.public_key() """ + LENGTH: int = 32 key: SigningKey def __init__(self, key: SigningKey): """Initialize a PrivateKey with a NaCl SigningKey. - + Args: key: The NaCl SigningKey instance to wrap. """ @@ -120,10 +121,10 @@ def __init__(self, key: SigningKey): def __eq__(self, other: object): """Check equality with another PrivateKey. - + Args: other: The object to compare with. - + Returns: True if both private keys are identical. """ @@ -133,7 +134,7 @@ def __eq__(self, other: object): def __str__(self): """Get the AIP-80 compliant string representation. - + Returns: AIP-80 formatted private key string (e.g., "ed25519-priv-0x123..."). """ @@ -142,38 +143,38 @@ def __str__(self): @staticmethod def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: """Parse a hex input to create an Ed25519 private key. - + Supports multiple input formats including plain hex strings, byte arrays, - and AIP-80 compliant prefixed strings. This provides flexibility for + and AIP-80 compliant prefixed strings. This provides flexibility for different key storage and transmission formats. - + Args: - value: A hex string (with or without "0x" prefix), byte array, + value: A hex string (with or without "0x" prefix), byte array, or AIP-80 compliant string ("ed25519-priv-0x..."). strict: If True, the value MUST be AIP-80 compliant. If False, accepts plain hex. If None, auto-detects format. - + Returns: A new PrivateKey instance. - + Raises: Exception: If the input format is invalid or the key data has incorrect length. - + Examples: Different input formats:: - + # Plain hex string key1 = PrivateKey.from_hex("123abc...") - + # Hex with 0x prefix key2 = PrivateKey.from_hex("0x123abc...") - + # AIP-80 format (strict mode) key3 = PrivateKey.from_hex( "ed25519-priv-0x123abc...", strict=True ) - + # Raw bytes key4 = PrivateKey.from_hex(b"\x12\x3a\xbc...") """ @@ -188,19 +189,19 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: @staticmethod def from_str(value: str, strict: bool | None = None) -> PrivateKey: """Parse a string representation to create an Ed25519 private key. - + This is a convenience method that delegates to from_hex() for string inputs. Supports both plain hex strings and AIP-80 compliant formats. - + Args: value: A hex string (with or without "0x" prefix) or AIP-80 compliant string ("ed25519-priv-0x..."). strict: If True, the value MUST be AIP-80 compliant. If False, accepts plain hex. If None, auto-detects format. - + Returns: A new PrivateKey instance. - + Raises: Exception: If the input format is invalid or the key data has incorrect length. @@ -209,7 +210,7 @@ def from_str(value: str, strict: bool | None = None) -> PrivateKey: def hex(self) -> str: """Get the hexadecimal representation of the private key. - + Returns: Hex string with "0x" prefix representing the 32-byte private key. """ @@ -217,11 +218,11 @@ def hex(self) -> str: def aip80(self) -> str: """Get the AIP-80 compliant string representation. - + AIP-80 (Aptos Improvement Proposal 80) defines a standard format for representing private keys with type prefixes for improved safety and interoperability. - + Returns: AIP-80 formatted string ("ed25519-priv-0x..."). """ @@ -231,7 +232,7 @@ def aip80(self) -> str: def public_key(self) -> PublicKey: """Derive the corresponding public key from this private key. - + Returns: The PublicKey that corresponds to this private key. """ @@ -240,10 +241,10 @@ def public_key(self) -> PublicKey: @staticmethod def random() -> PrivateKey: """Generate a cryptographically secure random private key. - + Uses the system's secure random number generator to create a new Ed25519 private key. - + Returns: A new randomly generated PrivateKey instance. """ @@ -251,10 +252,10 @@ def random() -> PrivateKey: def sign(self, data: bytes) -> Signature: """Create a digital signature for the given data. - + Args: data: The raw bytes to sign. - + Returns: An Ed25519 Signature for the input data. """ @@ -263,13 +264,13 @@ def sign(self, data: bytes) -> Signature: @staticmethod def deserialize(deserializer: Deserializer) -> PrivateKey: """Deserialize a PrivateKey from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized PrivateKey instance. - + Raises: Exception: If the key data is not exactly 32 bytes. """ @@ -281,7 +282,7 @@ def deserialize(deserializer: Deserializer) -> PrivateKey: def serialize(self, serializer: Serializer): """Serialize this PrivateKey to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -290,35 +291,36 @@ def serialize(self, serializer: Serializer): class PublicKey(asymmetric_crypto.PublicKey): """Ed25519 public key for signature verification on Aptos. - + A public key is derived from a private key and used to verify digital signatures. Ed25519 public keys are exactly 32 bytes and provide strong security guarantees for signature verification. - + Attributes: LENGTH: The byte length of Ed25519 public keys (32) key: The underlying NaCl VerifyKey instance - + Examples: Creating and using public keys:: - + # Derive from private key private_key = PrivateKey.random() public_key = private_key.public_key() - + # Create from hex string hex_key = PublicKey.from_str("0x123abc...") - + # Verify a signature is_valid = public_key.verify(message, signature) """ + LENGTH: int = 32 key: VerifyKey def __init__(self, key: VerifyKey): """Initialize a PublicKey with a NaCl VerifyKey. - + Args: key: The NaCl VerifyKey instance to wrap. """ @@ -326,10 +328,10 @@ def __init__(self, key: VerifyKey): def __eq__(self, other: object): """Check equality with another PublicKey. - + Args: other: The object to compare with. - + Returns: True if both public keys are identical. """ @@ -339,7 +341,7 @@ def __eq__(self, other: object): def __str__(self) -> str: """Get the hexadecimal string representation. - + Returns: Hex string with "0x" prefix representing the 32-byte public key. """ @@ -348,14 +350,14 @@ def __str__(self) -> str: @staticmethod def from_str(value: str) -> PublicKey: """Create a PublicKey from its hexadecimal string representation. - + Args: value: Hex string representing the public key, with or without "0x" prefix. - + Returns: A new PublicKey instance. - + Raises: ValueError: If the hex string is invalid or has wrong length. """ @@ -365,14 +367,14 @@ def from_str(value: str) -> PublicKey: def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: """Verify a digital signature against the given data. - + Args: data: The original data that was signed. signature: The signature to verify (must be an Ed25519 Signature). - + Returns: True if the signature is valid for the given data, False otherwise. - + Note: This method safely handles verification failures and returns False for any exception during verification. @@ -386,7 +388,7 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: def to_crypto_bytes(self) -> bytes: """Get the raw cryptographic bytes of the public key. - + Returns: The 32-byte Ed25519 public key as raw bytes. """ @@ -395,13 +397,13 @@ def to_crypto_bytes(self) -> bytes: @staticmethod def deserialize(deserializer: Deserializer) -> PublicKey: """Deserialize a PublicKey from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized PublicKey instance. - + Raises: Exception: If the key data is not exactly 32 bytes. """ @@ -413,7 +415,7 @@ def deserialize(deserializer: Deserializer) -> PublicKey: def serialize(self, serializer: Serializer): """Serialize this PublicKey to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -422,35 +424,36 @@ def serialize(self, serializer: Serializer): class MultiPublicKey(asymmetric_crypto.PublicKey): """Multi-signature public key for threshold signature schemes. - + A MultiPublicKey represents a collection of Ed25519 public keys with a threshold requirement. It enables M-of-N signature schemes where M signatures from N possible signers are required to validate a transaction. - + This is useful for multi-party custody, governance, and other scenarios requiring distributed authorization. - + Attributes: keys: List of individual Ed25519 public keys. threshold: Minimum number of signatures required for validation. MIN_KEYS: Minimum number of keys allowed (2). MAX_KEYS: Maximum number of keys allowed (32). MIN_THRESHOLD: Minimum threshold value (1). - + Examples: Creating a 2-of-3 multisig:: - + keys = [ PrivateKey.random().public_key(), - PrivateKey.random().public_key(), + PrivateKey.random().public_key(), PrivateKey.random().public_key() ] multisig = MultiPublicKey(keys, threshold=2) - + Verifying a multisig signature:: - + is_valid = multisig.verify(message, multi_signature) """ + keys: List[PublicKey] threshold: int @@ -460,11 +463,11 @@ class MultiPublicKey(asymmetric_crypto.PublicKey): def __init__(self, keys: List[PublicKey], threshold: int): """Initialize a MultiPublicKey with keys and threshold. - + Args: keys: List of Ed25519 public keys (2-32 keys). threshold: Number of signatures required (1 to len(keys)). - + Raises: AssertionError: If key count or threshold is outside valid ranges. """ @@ -480,7 +483,7 @@ def __init__(self, keys: List[PublicKey], threshold: int): def __str__(self) -> str: """Get string representation of the multisig configuration. - + Returns: Human-readable description (e.g., "2-of-3 Multi-Ed25519 public key"). """ @@ -533,39 +536,40 @@ def serialize(self, serializer: Serializer): class Signature(asymmetric_crypto.Signature): """Ed25519 digital signature. - + Represents a 64-byte Ed25519 signature created by signing data with an Ed25519 private key. Signatures can be verified using the corresponding public key. - + Attributes: LENGTH: The byte length of Ed25519 signatures (64). signature: The raw signature bytes. - + Examples: Creating and using signatures:: - + private_key = PrivateKey.random() message = b"Hello, Aptos!" - + # Create signature signature = private_key.sign(message) - + # Verify signature public_key = private_key.public_key() is_valid = public_key.verify(message, signature) - + # Convert to/from hex string hex_sig = str(signature) parsed_sig = Signature.from_str(hex_sig) """ + LENGTH: int = 64 signature: bytes def __init__(self, signature: bytes): """Initialize a Signature with raw signature bytes. - + Args: signature: The 64-byte Ed25519 signature data. """ @@ -573,10 +577,10 @@ def __init__(self, signature: bytes): def __eq__(self, other: object): """Check equality with another Signature. - + Args: other: The object to compare with. - + Returns: True if both signatures are identical. """ @@ -586,7 +590,7 @@ def __eq__(self, other: object): def __str__(self) -> str: """Get hexadecimal string representation. - + Returns: Hex string with "0x" prefix representing the 64-byte signature. """ @@ -594,7 +598,7 @@ def __str__(self) -> str: def data(self) -> bytes: """Get the raw signature bytes. - + Returns: The 64-byte signature as raw bytes. """ @@ -603,13 +607,13 @@ def data(self) -> bytes: @staticmethod def deserialize(deserializer: Deserializer) -> Signature: """Deserialize a Signature from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized Signature instance. - + Raises: Exception: If the signature data is not exactly 64 bytes. """ @@ -622,14 +626,14 @@ def deserialize(deserializer: Deserializer) -> Signature: @staticmethod def from_str(value: str) -> Signature: """Create a Signature from its hexadecimal string representation. - + Args: value: Hex string representing the signature, with or without "0x" prefix. - + Returns: A new Signature instance. - + Raises: ValueError: If the hex string is invalid or has wrong length. """ @@ -639,7 +643,7 @@ def from_str(value: str) -> Signature: def serialize(self, serializer: Serializer): """Serialize this Signature to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -648,45 +652,46 @@ def serialize(self, serializer: Serializer): class MultiSignature(asymmetric_crypto.Signature): """Multi-signature combining multiple Ed25519 signatures. - + A MultiSignature aggregates individual signatures from multiple signers along with a bitmap indicating which signers participated. This enables efficient threshold signature verification. - + The encoding uses a 4-byte bitmap to track which of the up to 32 possible signers provided signatures, followed by the actual signature data. - + Attributes: signatures: List of (signer_index, signature) tuples. BITMAP_NUM_OF_BYTES: Size of the signer bitmap (4 bytes). - + Examples: Creating a multisig from individual signatures:: - + # Create signatures from 2 of 3 signers sig1 = private_key1.sign(message) sig2 = private_key3.sign(message) # Skip signer 2 - + # Create multisig multisig = MultiSignature.from_key_map( multisig_public_key, [(public_key1, sig1), (public_key3, sig2)] ) - + Verifying a multisig:: - + is_valid = multisig_public_key.verify(message, multisig) """ + signatures: List[Tuple[int, Signature]] BITMAP_NUM_OF_BYTES: int = 4 def __init__(self, signatures: List[Tuple[int, Signature]]): """Initialize a MultiSignature with signer indices and signatures. - + Args: signatures: List of (signer_index, signature) tuples where signer_index is the position in the MultiPublicKey. - + Raises: AssertionError: If any signer index exceeds bitmap capacity (32). """ @@ -698,10 +703,10 @@ def __init__(self, signatures: List[Tuple[int, Signature]]): def __eq__(self, other: object): """Check equality with another MultiSignature. - + Args: other: The object to compare with. - + Returns: True if both multisigs have identical signatures. """ @@ -711,7 +716,7 @@ def __eq__(self, other: object): def __str__(self) -> str: """Get string representation of the multisig. - + Returns: String showing the list of (index, signature) pairs. """ @@ -723,17 +728,17 @@ def from_key_map( signatures_map: List[Tuple[PublicKey, Signature]], ) -> MultiSignature: """Create a MultiSignature from a key-signature mapping. - + This convenience method maps public keys to their indices in the MultiPublicKey and creates the appropriate MultiSignature structure. - + Args: public_key: The MultiPublicKey containing the signer keys. signatures_map: List of (public_key, signature) pairs. - + Returns: A new MultiSignature with the mapped indices. - + Raises: ValueError: If a public key is not found in the MultiPublicKey. """ @@ -746,16 +751,16 @@ def from_key_map( @staticmethod def deserialize(deserializer: Deserializer) -> MultiSignature: """Deserialize a MultiSignature from a BCS byte stream. - + The format is: [signature_1][signature_2]...[4-byte bitmap] The bitmap indicates which signer positions have signatures. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized MultiSignature instance. - + Raises: AssertionError: If the byte length doesn't match expected format. """ @@ -783,10 +788,10 @@ def deserialize(deserializer: Deserializer) -> MultiSignature: def serialize(self, serializer: Serializer): """Serialize this MultiSignature to a BCS byte stream. - + The format is: [signature_1][signature_2]...[4-byte bitmap] The bitmap has bits set for each signer position that has a signature. - + Args: serializer: The BCS serializer to write to. """ @@ -806,7 +811,7 @@ def serialize(self, serializer: Serializer): class Test(unittest.TestCase): """Comprehensive test suite for Ed25519 cryptographic operations. - + Tests all aspects of Ed25519 functionality including: - Key generation and parsing - AIP-80 format compliance @@ -815,6 +820,7 @@ class Test(unittest.TestCase): - Multi-signature operations and validation - Range checking and error handling """ + def test_private_key_from_str(self): private_key_hex = PrivateKey.from_str( "0x4e5e3be60f4bbd5e98d086d932f3ce779ff4b58da99bf9e5241ae1212a29e5fe", False diff --git a/aptos_sdk/metadata.py b/aptos_sdk/metadata.py index b959089..74b0439 100644 --- a/aptos_sdk/metadata.py +++ b/aptos_sdk/metadata.py @@ -20,36 +20,36 @@ Examples: Get SDK version header:: - + from aptos_sdk.metadata import Metadata - + # Get the header value for HTTP requests header_value = Metadata.get_aptos_header_val() print(f"Client identifier: {header_value}") # Output: "aptos-python-sdk/1.2.3" - + Use in HTTP requests:: - + import httpx from aptos_sdk.metadata import Metadata - + # Add SDK identification to HTTP headers headers = { Metadata.APTOS_HEADER: Metadata.get_aptos_header_val(), "Content-Type": "application/json" } - + # Make request with proper identification response = httpx.get( "https://fullnode.devnet.aptoslabs.com/v1", headers=headers ) - + Integration with REST clients:: - + # The RestClient automatically includes this header from aptos_sdk.async_client import RestClient - + client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") # Automatically includes x-aptos-client header @@ -67,49 +67,49 @@ class Metadata: """Utility class for managing Aptos SDK metadata and HTTP headers. - + This class provides static methods and constants for SDK identification in HTTP requests to Aptos services. It ensures proper client identification for analytics, debugging, and API compliance purposes. - + Constants: APTOS_HEADER: The standard HTTP header name for Aptos client identification - + Examples: Access header constants:: - + from aptos_sdk.metadata import Metadata - + # Get the header name header_name = Metadata.APTOS_HEADER print(f"Header name: {header_name}") # Output: "x-aptos-client" - + Generate header values:: - + # Get the full header value with version header_value = Metadata.get_aptos_header_val() print(f"Header value: {header_value}") # Output: "aptos-python-sdk/1.2.3" - + Use in custom HTTP clients:: - + import requests - + headers = { Metadata.APTOS_HEADER: Metadata.get_aptos_header_val() } - + response = requests.get( "https://fullnode.mainnet.aptoslabs.com/v1", headers=headers ) - + Note: The metadata class is designed to be used statically and does not require instantiation. """ - + # HTTP header name for Aptos client identification APTOS_HEADER = "x-aptos-client" @@ -120,40 +120,40 @@ def get_aptos_header_val(): This method constructs a standardized client identification string that includes the SDK name and version. This header is automatically included in requests made by the Aptos REST clients. - + The header format follows the pattern: "aptos-python-sdk/{version}" where version is automatically detected from the installed package. Returns: str: Header value in the format "aptos-python-sdk/{version}" - + Examples: Get version header:: - + >>> from aptos_sdk.metadata import Metadata >>> header = Metadata.get_aptos_header_val() >>> print(header) 'aptos-python-sdk/1.2.3' - + Use in HTTP request:: - + import httpx - + headers = { "x-aptos-client": Metadata.get_aptos_header_val(), "Content-Type": "application/json" } - + async with httpx.AsyncClient() as client: response = await client.get( "https://fullnode.devnet.aptoslabs.com/v1", headers=headers ) - + Raises: PackageNotFoundError: If the aptos-sdk package is not properly installed or metadata cannot be accessed. - + Note: - Version is automatically detected from package installation - Development installations may show version as "0.0.0" or similar diff --git a/aptos_sdk/package_publisher.py b/aptos_sdk/package_publisher.py index 9fdc9d9..24685e7 100644 --- a/aptos_sdk/package_publisher.py +++ b/aptos_sdk/package_publisher.py @@ -21,13 +21,13 @@ - Traditional deployment model where code is stored in an account - Code is published to the sender's account storage - Suitable for simple contracts and legacy compatibility - + Object-Based Deployment: - Modern deployment model using Aptos objects - Code is stored in a dedicated object with its own address - Better isolation and more flexible upgrade policies - Recommended for new packages - + Package Upgrades: - Update existing deployed packages with new code - Supports compatibility policies and authorization checks @@ -36,7 +36,7 @@ Large Package Handling: Aptos transactions have a size limit (currently 64KB). This module automatically detects packages that exceed this limit and uses a chunked publishing strategy: - + 1. **Chunking**: Package data is split into manageable chunks 2. **Staging**: Chunks are uploaded using the large_packages module 3. **Assembly**: The final transaction triggers on-chain reassembly @@ -44,50 +44,50 @@ Examples: Basic package deployment:: - + from aptos_sdk.package_publisher import PackagePublisher, PublishMode from aptos_sdk.async_client import RestClient from aptos_sdk.account import Account - + # Setup client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") publisher = PackagePublisher(client) account = Account.load("./deployer_account.json") - + # Deploy package txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./my_move_package" ) - + # Wait for completion for txn_hash in txn_hashes: await client.wait_for_transaction(txn_hash) - + print(f"Package deployed in {len(txn_hashes)} transactions") - + Object-based deployment with address prediction:: - + # Predict deployment address object_address = await publisher.derive_object_address(account.address()) print(f"Package will be deployed to: {object_address}") - + # Deploy to object txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./my_package", publish_mode=PublishMode.OBJECT_DEPLOY ) - + # Verify deployment for txn_hash in txn_hashes: await client.wait_for_transaction(txn_hash) - + Package upgrade workflow:: - + # Identify the object to upgrade code_object = AccountAddress.from_str("***existing_object_address") - + # Deploy upgrade txn_hashes = await publisher.publish_package_in_path( sender=account, @@ -95,18 +95,18 @@ publish_mode=PublishMode.OBJECT_UPGRADE, code_object=code_object ) - + Low-level publishing with custom data:: - + # Read compiled package data with open("package-metadata.bcs", "rb") as f: metadata = f.read() - + modules = [] for module_file in os.listdir("bytecode_modules"): with open(f"bytecode_modules/{module_file}", "rb") as f: modules.append(f.read()) - + # Publish directly txn_hash = await publisher.publish_package(account, metadata, modules) @@ -115,10 +115,10 @@ 2. **Directory Structure**: Ensure proper build directory layout 3. **Account Setup**: Have a funded account for transaction fees 4. **Network Configuration**: Connect to the appropriate Aptos network - + Directory Structure: Expected package directory layout:: - + my_package/ ├── Move.toml # Package manifest ├── sources/ # Move source files @@ -136,13 +136,13 @@ - Large packages: 200,000+ gas units per chunk - Object deployment: Slightly higher gas costs - Upgrades: Variable based on compatibility checks - + Security Considerations: - **Package Verification**: Review all Move code before deployment - **Upgrade Policies**: Set appropriate upgrade policies in Move.toml - **Access Control**: Ensure only authorized accounts can upgrade packages - **Testing**: Thoroughly test packages on devnet/testnet before mainnet - + Error Handling: Common deployment errors: - **Compilation Errors**: Fix Move source code issues @@ -197,12 +197,12 @@ class PublishMode(Enum): class PackagePublisher: """Move package compilation and deployment manager for Aptos blockchain. - + The PackagePublisher provides a comprehensive interface for compiling and publishing Move smart contract packages to the Aptos blockchain. It supports various deployment modes including traditional account-based deployment, object-based deployment, and package upgrades. - + Key Features: - **Package Compilation**: Compile Move source code to bytecode - **Metadata Generation**: Create package metadata for deployment @@ -210,58 +210,58 @@ class PackagePublisher: - **Chunked Publishing**: Automatic splitting of large packages across transactions - **Object Deployment**: Support for object-based code deployment model - **Package Upgrades**: Upgrading existing deployed packages - + Deployment Modes: - **Account Deploy**: Traditional deployment to an account (default) - **Object Deploy**: Deployment to an object (newer model) - **Object Upgrade**: Upgrading existing object-based packages - + Examples: Basic package deployment:: - + from aptos_sdk.package_publisher import PackagePublisher from aptos_sdk.async_client import RestClient from aptos_sdk.account import Account - + # Create client and publisher client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") publisher = PackagePublisher(client) - + # Deploy a package from a local directory account = Account.load("./my_account.json") txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./my_move_package" ) - + # Wait for transactions to complete for txn_hash in txn_hashes: await client.wait_for_transaction(txn_hash) - + Object-based deployment:: - + # Deploy to an object instead of an account from aptos_sdk.package_publisher import PublishMode - + # Deploy as a new object txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./my_package", publish_mode=PublishMode.OBJECT_DEPLOY ) - + # Get the deployed object address object_address = await publisher.derive_object_address(account.address()) print(f"Package deployed to object: {object_address}") - + Upgrading a package:: - + # Upgrade an existing object-based package from aptos_sdk.account_address import AccountAddress - + # Address of the existing code object code_object = AccountAddress.from_str("***abcdef...") - + # Publish the upgrade txn_hashes = await publisher.publish_package_in_path( sender=account, @@ -269,31 +269,31 @@ class PackagePublisher: publish_mode=PublishMode.OBJECT_UPGRADE, code_object=code_object ) - + Large package handling:: - + # For packages exceeding transaction size limits # Chunked publishing happens automatically txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./large_package" ) - + print(f"Package published in {len(txn_hashes)} transactions") - + Workflow: 1. Compile Move package (using CLI or other tools) 2. Create PackagePublisher with RestClient 3. Call publish_package_in_path with appropriate sender and package path 4. Monitor transaction hashes for completion 5. (Optional) Derive object address for object deployments - + Technical Details: - Packages over ~62KB are automatically chunked across multiple transactions - Object deployment uses a deterministic address derived from publisher and sequence number - Package metadata and compiled bytecode modules are read from the build directory - BCS serialization is used for efficient binary encoding - + Note: This class requires Move packages to be precompiled with the Move compiler. The package directory must contain a build subdirectory with compiled artifacts. @@ -303,35 +303,35 @@ class PackagePublisher: def __init__(self, client: RestClient): """Initialize a PackagePublisher with a REST client. - + Creates a new package publisher that uses the provided REST client for blockchain interactions. The client must be properly configured for the target network (mainnet, testnet, etc.). - + Args: client: The RestClient instance to use for blockchain communication and transaction submission. - + Examples: Create with default client:: - + from aptos_sdk.async_client import RestClient - + # Create for devnet client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") publisher = PackagePublisher(client) - + Create with custom client configuration:: - + from aptos_sdk.async_client import RestClient, ClientConfig - + # Custom gas settings for large packages config = ClientConfig( max_gas_amount=300_000, # Higher gas limit gas_unit_price=150, # Higher priority transaction_wait_in_seconds=60 # Longer timeout ) - + client = RestClient("https://fullnode.mainnet.aptoslabs.com/v1", config) publisher = PackagePublisher(client) """ @@ -341,22 +341,22 @@ async def publish_package( self, sender: Account, package_metadata: bytes, modules: List[bytes] ) -> str: """Publish a Move package to an account on the Aptos blockchain. - + This method submits a transaction to publish a Move package to the sender's account. It requires pre-compiled package metadata and module bytecode. - + Args: sender: The account that will sign and pay for the transaction. package_metadata: The BCS-encoded package metadata bytes. modules: List of BCS-encoded bytecode modules. - + Returns: str: The transaction hash of the submitted transaction. - + Transaction Details: This calls the 0x1::code::publish_package_txn entry function with the package metadata and modules as arguments. - + Note: This is the low-level publish method. Most users should use publish_package_in_path instead, which handles reading files @@ -385,23 +385,23 @@ async def publish_package_to_object( self, sender: Account, package_metadata: bytes, modules: List[bytes] ) -> str: """Publish a Move package to a new object on the Aptos blockchain. - + This method submits a transaction to publish a Move package to a new object instead of an account. This uses the object-based code deployment model, which is newer and provides better isolation. - + Args: sender: The account that will sign and pay for the transaction. package_metadata: The BCS-encoded package metadata bytes. modules: List of BCS-encoded bytecode modules. - + Returns: str: The transaction hash of the submitted transaction. - + Transaction Details: This calls the 0x1::object_code_deployment::publish entry function with the package metadata and modules as arguments. - + Note: After publishing, you can derive the object address using the derive_object_address method. Object-based deployment is the recommended @@ -434,29 +434,29 @@ async def upgrade_package_object( object_address: AccountAddress, ) -> str: """Upgrade an existing object-based Move package. - + This method submits a transaction to upgrade an existing object-based Move package with new code. The sender must have the appropriate permissions to upgrade the package (typically, must be the original publisher). - + Args: sender: The account that will sign and pay for the transaction. package_metadata: The BCS-encoded package metadata bytes for the upgrade. modules: List of BCS-encoded bytecode modules for the upgrade. object_address: The address of the object containing the code to upgrade. - + Returns: str: The transaction hash of the submitted transaction. - + Transaction Details: This calls the 0x1::object_code_deployment::upgrade entry function with the package metadata, modules, and object address as arguments. - + Upgrade Rules: - The upgrade policy in the original package must allow upgrades - The sender must be authorized to perform the upgrade - Module compatibility requirements must be satisfied based on policy - + Note: This only works for packages deployed with the object-based model. For traditional account-based packages, use a different upgrade mechanism. @@ -490,12 +490,12 @@ async def publish_package_in_path( code_object: Optional[AccountAddress] = None, ) -> List[str]: """Publish a Move package from a local directory to the Aptos blockchain. - + This high-level method handles reading compiled Move package files from a directory and publishing them to the blockchain. It automatically determines if chunked publishing is needed for large packages and supports different deployment modes. - + Args: sender: The account that will sign and pay for the transaction(s). package_dir: Path to the Move package directory containing the compiled build. @@ -506,51 +506,51 @@ async def publish_package_in_path( or OBJECT_UPGRADE). code_object: The address of the object to upgrade (required only for OBJECT_UPGRADE mode). - + Returns: List[str]: List of transaction hashes. For small packages, this will contain a single hash. For large packages, it will contain multiple hashes corresponding to the chunked transactions. - + Raises: ValueError: If code_object is not provided for OBJECT_UPGRADE mode, if the publish_mode is invalid, or if required files are missing. FileNotFoundError: If the package directory or required files don't exist. - + Directory Structure: The package directory must contain: - Move.toml: Package manifest file - build/{package_name}/bytecode_modules/: Compiled module bytecode (.mv files) - build/{package_name}/package-metadata.bcs: Package metadata file - + Examples: Publish a package to an account:: - + txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./my_move_package" ) - + Publish a package to an object:: - + txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./my_package", publish_mode=PublishMode.OBJECT_DEPLOY ) - + # Get the deployed object address object_address = await publisher.derive_object_address(account.address()) - + Upgrade an existing object-based package:: - + txn_hashes = await publisher.publish_package_in_path( sender=account, package_dir="./updated_package", publish_mode=PublishMode.OBJECT_UPGRADE, code_object=AccountAddress.from_str("***abcdef...") ) - + Note: This method requires the package to be already compiled. It does not compile the Move source code itself, but reads the compiled artifacts. @@ -602,31 +602,31 @@ async def derive_object_address( self, publisher_address: AccountAddress ) -> AccountAddress: """Derive the address of a newly deployed object-based package. - + This method calculates the address where a package will be deployed when using OBJECT_DEPLOY mode. It uses the publisher's address and next sequence number to deterministically derive the object address. - + Args: publisher_address: The address of the account publishing the package. - + Returns: AccountAddress: The derived address where the package object will be created. - + Examples: Get the address before deployment:: - + # Calculate where the package will be deployed object_address = await publisher.derive_object_address(account.address()) print(f"Package will be deployed to: {object_address}") - + # Deploy the package await publisher.publish_package_in_path( sender=account, package_dir="./my_package", publish_mode=PublishMode.OBJECT_DEPLOY ) - + Note: This method gets the current sequence number from the blockchain and adds 1 to calculate the next sequence number that will be used for @@ -642,25 +642,25 @@ def create_object_deployment_address( creator_address: AccountAddress, creator_sequence_number: int ) -> AccountAddress: """Calculate the deterministic address for an object-based code deployment. - + This static method computes the address where a package will be deployed when using object-based deployment. The address is deterministically derived from the creator's address and sequence number. - + Args: creator_address: The address of the account creating the object. creator_sequence_number: The sequence number of the creator account that will be used for the deployment transaction. - + Returns: AccountAddress: The deterministic address where the object will be created. - + Technical Details: The address is derived using a domain-specific seed combining: - The domain separator "aptos_framework::object_code_deployment" - The creator's sequence number - The creator's address - + Note: This is a low-level method used by derive_object_address. Most users should use derive_object_address instead, which automatically fetches @@ -682,12 +682,12 @@ async def chunked_package_publish( publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY, ) -> List[str]: """Publish a large package by splitting it across multiple transactions. - + This method handles publishing packages that exceed the transaction size limit (currently 64KB) by splitting the package data across multiple transactions. It optimizes the chunking to use as few transactions as possible while staying within size limits. - + Args: sender: The account that will sign and pay for the transactions. package_metadata: The BCS-encoded package metadata bytes. @@ -696,21 +696,21 @@ async def chunked_package_publish( publishing (default: predefined MODULE_ADDRESS). publish_mode: The deployment mode to use (ACCOUNT_DEPLOY, OBJECT_DEPLOY, or OBJECT_UPGRADE). - + Returns: List[str]: List of transaction hashes for all the chunked transactions. - + Transaction Batching: - Each transaction has a conservative 62KB size limit (below the 64KB max) - Metadata is chunked first, followed by module bytecode - Data is packed efficiently to minimize the number of transactions - Transactions are submitted in sequence to maintain ordering - + Technical Details: The chunked publishing uses the large_package_publisher module to handle reassembly of the chunks on-chain. This module stores the chunks temporarily until all chunks are received, then performs the actual deployment. - + Note: This method is automatically called by publish_package_in_path when needed. Most users should not need to call this directly. diff --git a/aptos_sdk/secp256k1_ecdsa.py b/aptos_sdk/secp256k1_ecdsa.py index c3b754c..4800e2c 100644 --- a/aptos_sdk/secp256k1_ecdsa.py +++ b/aptos_sdk/secp256k1_ecdsa.py @@ -38,67 +38,67 @@ Examples: Basic key generation and signing:: - + from aptos_sdk.secp256k1_ecdsa import PrivateKey - + # Generate a new private key private_key = PrivateKey.random() public_key = private_key.public_key() - + # Sign a message message = b"Hello, Aptos!" signature = private_key.sign(message) - + # Verify the signature is_valid = public_key.verify(message, signature) print(f"Signature valid: {is_valid}") - + Working with hex strings:: - + # Create from hex string hex_key = "***234abcd..." private_key = PrivateKey.from_hex(hex_key) - + # Get hex representation print(f"Private key: {private_key.hex()}") print(f"Public key: {public_key.hex()}") print(f"Signature: {signature.hex()}") - + AIP-80 compliant formatting:: - + # AIP-80 formatted private key aip80_key = "secp256k1-priv-***234abcd..." private_key = PrivateKey.from_str(aip80_key, strict=True) - + # Convert to AIP-80 format formatted = private_key.aip80() print(f"AIP-80 format: {formatted}") - + Serialization for storage/transmission:: - + from aptos_sdk.bcs import Serializer, Deserializer - + # Serialize private key serializer = Serializer() private_key.serialize(serializer) key_bytes = serializer.output() - + # Deserialize private key deserializer = Deserializer(key_bytes) restored_key = PrivateKey.deserialize(deserializer) - + assert private_key == restored_key - + Cross-chain compatibility:: - + # Import Ethereum private key ethereum_key = "***456789abcdef..." aptos_key = PrivateKey.from_hex(ethereum_key) - + # Same key can be used on both chains # (though with different address derivation) eth_style_pubkey = aptos_key.public_key().hex() - + Security Considerations: - Always use secure random number generation for key creation - Store private keys securely (encrypted, hardware wallets) @@ -126,60 +126,61 @@ class PrivateKey(asymmetric_crypto.PrivateKey): """secp256k1 ECDSA private key implementation. - + This class implements secp256k1 private keys with deterministic signing, signature normalization, and full compatibility with the Aptos asymmetric cryptography interfaces. - + Key Properties: - **Curve**: secp256k1 elliptic curve (same as Bitcoin/Ethereum) - **Hash Function**: Keccak-256 for all cryptographic operations - **Key Length**: 32 bytes (256 bits) - **Deterministic**: Uses RFC 6979 for deterministic signing - **Normalized**: Ensures canonical signatures with s < n/2 - + Attributes: LENGTH: The byte length of secp256k1 private keys (32) key: The underlying ECDSA signing key object - + Examples: Generate a new private key:: - + private_key = PrivateKey.random() print(f"New key: {private_key.hex()}") - + Create from existing key material:: - + hex_key = "***234567890abcdef..." private_key = PrivateKey.from_hex(hex_key) - + Create from AIP-80 format:: - + aip80_key = "secp256k1-priv-***234567890abcdef..." private_key = PrivateKey.from_str(aip80_key, strict=True) - + Sign and verify:: - + message = b"Important transaction data" signature = private_key.sign(message) public_key = private_key.public_key() - + assert public_key.verify(message, signature) - + Note: Private keys should be generated using cryptographically secure random number generators and stored securely. """ + LENGTH: int = 32 key: SigningKey def __init__(self, key: SigningKey): """Initialize a private key with the given ECDSA signing key. - + Args: key: The ECDSA SigningKey object for secp256k1 operations. - + Example: This is typically not called directly. Use the factory methods: >>> private_key = PrivateKey.random() @@ -189,13 +190,13 @@ def __init__(self, key: SigningKey): def __eq__(self, other: object): """Check equality with another PrivateKey. - + Args: other: Object to compare with. - + Returns: True if both private keys are cryptographically equivalent. - + Example: >>> key1 = PrivateKey.from_hex("***abc123...") >>> key2 = PrivateKey.from_hex("***abc123...") @@ -208,10 +209,10 @@ def __eq__(self, other: object): def __str__(self): """Return the AIP-80 formatted string representation. - + Returns: AIP-80 compliant private key string with secp256k1-priv- prefix. - + Example: >>> str(private_key) 'secp256k1-priv-***234567890abcdef...' @@ -221,10 +222,10 @@ def __str__(self): @staticmethod def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: """Create a private key from hex string, bytes, or AIP-80 format. - + This method parses various input formats and creates a secp256k1 private key. It handles legacy hex formats and AIP-80 compliant strings. - + Args: value: Private key in various formats: - Raw hex string: "***234567890abcdef..." @@ -235,31 +236,31 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: - True: Only accept AIP-80 compliant strings - False: Accept legacy formats without warning - None: Accept legacy formats with warning - + Returns: A new secp256k1 PrivateKey instance. - + Raises: Exception: If the key length is invalid (not 32 bytes). ValueError: If strict=True and format is not AIP-80 compliant. - + Examples: From raw hex:: - + key = PrivateKey.from_hex("***234567890abcdef...") - + From AIP-80 format:: - + key = PrivateKey.from_hex( "secp256k1-priv-***234567890abcdef...", strict=True ) - + From bytes:: - + key_bytes = bytes.fromhex("234567890abcdef...") key = PrivateKey.from_hex(key_bytes) - + Note: The private key must be exactly 32 bytes (64 hex characters). """ @@ -275,16 +276,16 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: @staticmethod def from_str(value: str, strict: bool | None = None) -> PrivateKey: """Create a private key from a hex or AIP-80 compliant string. - + Convenience method that delegates to from_hex() for string inputs. - + Args: value: Hex string or AIP-80 compliant string. strict: AIP-80 compliance mode (see from_hex() for details). - + Returns: A new secp256k1 PrivateKey instance. - + Example: >>> key = PrivateKey.from_str("secp256k1-priv-***abc123...") >>> key = PrivateKey.from_str("***abc123...", strict=False) @@ -293,10 +294,10 @@ def from_str(value: str, strict: bool | None = None) -> PrivateKey: def hex(self) -> str: """Get the hexadecimal representation of the private key. - + Returns: Hex string with '0x' prefix representing the 32-byte private key. - + Example: >>> private_key.hex() '***abc123456789def...' @@ -305,10 +306,10 @@ def hex(self) -> str: def aip80(self) -> str: """Get the AIP-80 compliant string representation. - + Returns: AIP-80 formatted string with secp256k1-priv- prefix. - + Example: >>> private_key.aip80() 'secp256k1-priv-***abc123456789def...' @@ -319,10 +320,10 @@ def aip80(self) -> str: def public_key(self) -> PublicKey: """Derive the corresponding public key. - + Returns: The public key derived from this private key. - + Example: >>> private_key = PrivateKey.random() >>> public_key = private_key.public_key() @@ -334,18 +335,18 @@ def public_key(self) -> PublicKey: @staticmethod def random() -> PrivateKey: """Generate a new random secp256k1 private key. - + Uses cryptographically secure random number generation to create a new private key suitable for production use. - + Returns: A new randomly generated PrivateKey instance. - + Example: >>> private_key = PrivateKey.random() >>> len(private_key.key.to_string()) 32 - + Note: This method uses the system's secure random number generator. The generated key is suitable for production cryptographic use. @@ -356,23 +357,23 @@ def random() -> PrivateKey: def sign(self, data: bytes) -> Signature: """Sign data using this private key with deterministic ECDSA. - + Creates a deterministic signature using RFC 6979, ensuring the same input always produces the same signature. The signature is normalized to ensure canonical form (s < n/2) to prevent malleability. - + Args: data: The data to sign (typically a hash of the actual message). - + Returns: A normalized secp256k1 signature. - + Example: >>> message = b"Hello, Aptos!" >>> signature = private_key.sign(message) >>> public_key.verify(message, signature) True - + Note: - Uses Keccak-256 as the hash function - Implements RFC 6979 deterministic signing @@ -391,16 +392,16 @@ def sign(self, data: bytes) -> Signature: @staticmethod def deserialize(deserializer: Deserializer) -> PrivateKey: """Deserialize a private key from BCS-encoded bytes. - + Args: deserializer: BCS deserializer containing the private key bytes. - + Returns: A new PrivateKey instance from the deserialized data. - + Raises: Exception: If the key length is not 32 bytes. - + Example: >>> serializer = Serializer() >>> original_key.serialize(serializer) @@ -418,10 +419,10 @@ def deserialize(deserializer: Deserializer) -> PrivateKey: def serialize(self, serializer: Serializer): """Serialize the private key to BCS format. - + Args: serializer: BCS serializer to write the private key bytes to. - + Example: >>> serializer = Serializer() >>> private_key.serialize(serializer) @@ -434,46 +435,47 @@ def serialize(self, serializer: Serializer): class PublicKey(asymmetric_crypto.PublicKey): """secp256k1 ECDSA public key implementation. - + This class implements secp256k1 public keys for verification of signatures and address derivation. It follows the common format for secp256k1 public keys with support for both compressed and uncompressed formats. - + Key Properties: - **Curve**: secp256k1 elliptic curve (same as Bitcoin/Ethereum) - **Format**: Uncompressed format with 0x04 prefix - **Key Length**: 64 bytes (uncompressed without prefix) - **Serialized Length**: 65 bytes (with prefix) - + Attributes: LENGTH: The byte length of uncompressed secp256k1 public keys (64) LENGTH_WITH_PREFIX_LENGTH: Length including 0x04 prefix byte (65) key: The underlying ECDSA verification key object - + Examples: Derive from private key:: - + private_key = PrivateKey.random() public_key = private_key.public_key() - + Create from hex string:: - + # With or without 0x04 prefix hex_key = "***4..." # 65 bytes with prefix public_key = PublicKey.from_str(hex_key) - + Verify a signature:: - + message = b"Important message" signature = private_key.sign(message) - + is_valid = public_key.verify(message, signature) assert is_valid == True - + Note: This implementation uses the uncompressed format (65 bytes) for compatibility with common Ethereum and Bitcoin libraries. """ + LENGTH: int = 64 LENGTH_WITH_PREFIX_LENGTH: int = 65 @@ -481,10 +483,10 @@ class PublicKey(asymmetric_crypto.PublicKey): def __init__(self, key: VerifyingKey): """Initialize a public key with the given ECDSA verifying key. - + Args: key: The ECDSA VerifyingKey object for secp256k1 operations. - + Example: This is typically not called directly. Use factory methods or derive from a private key: @@ -495,13 +497,13 @@ def __init__(self, key: VerifyingKey): def __eq__(self, other: object): """Check equality with another PublicKey. - + Args: other: Object to compare with. - + Returns: True if both public keys are cryptographically equivalent. - + Example: >>> pk1 = private_key1.public_key() >>> pk2 = private_key2.public_key() # Different key @@ -514,10 +516,10 @@ def __eq__(self, other: object): def __str__(self) -> str: """Return the hexadecimal string representation. - + Returns: Hex string representing the public key. - + Example: >>> str(public_key) '***4...' # 65 bytes with 0x04 prefix @@ -527,26 +529,26 @@ def __str__(self) -> str: @staticmethod def from_str(value: str) -> PublicKey: """Create a public key from a hex string. - + Args: value: Hex string representing the public key. Can be with or without '0x' prefix. Can be 64 bytes (raw key) or 65 bytes (with 0x04 prefix). - + Returns: A new PublicKey instance. - + Raises: Exception: If the key length is invalid. - + Examples: From uncompressed format with prefix:: - + # 130 hex chars (65 bytes) with 0x04 prefix key = PublicKey.from_str("***4210c9129e...") - + From raw format:: - + # 128 hex chars (64 bytes) without prefix key = PublicKey.from_str("210c9129e...") """ @@ -564,14 +566,14 @@ def from_str(value: str) -> PublicKey: def hex(self) -> str: """Get the hexadecimal representation of the public key. - + Returns: Hex string with '0x04' prefix (uncompressed format). - + Example: >>> public_key.hex() '***4210c9129e35337ff5d6488f90f18d842cf...' # 65 bytes with prefix - + Note: The '0x04' prefix indicates an uncompressed public key format. """ @@ -579,17 +581,17 @@ def hex(self) -> str: def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: """Verify a signature against this public key. - + Verifies that the signature was created by the private key corresponding to this public key when signing the provided data. - + Args: data: The original data that was signed. signature: The signature to verify. - + Returns: True if the signature is valid, False otherwise. - + Example: >>> message = b"Hello, world!" >>> signature = private_key.sign(message) @@ -597,7 +599,7 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: True >>> public_key.verify(b"Different message", signature) False - + Note: Catches all exceptions during verification and returns False for any failure, making it safe to use in validation code. @@ -611,17 +613,17 @@ def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: def to_crypto_bytes(self) -> bytes: """Get the raw byte representation with prefix for cryptographic use. - + Returns: 65-byte representation with 0x04 prefix followed by the 64-byte key. - + Example: >>> key_bytes = public_key.to_crypto_bytes() >>> len(key_bytes) 65 >>> key_bytes[0] == 0x04 True - + Note: The 0x04 prefix indicates an uncompressed secp256k1 public key. """ @@ -630,18 +632,18 @@ def to_crypto_bytes(self) -> bytes: @staticmethod def deserialize(deserializer: Deserializer) -> PublicKey: """Deserialize a public key from BCS-encoded bytes. - + Handles both raw 64-byte keys and 65-byte keys with prefix. - + Args: deserializer: BCS deserializer containing the public key bytes. - + Returns: A new PublicKey instance from the deserialized data. - + Raises: Exception: If the key length is invalid (not 64 or 65 bytes). - + Example: >>> serializer = Serializer() >>> original_key.serialize(serializer) @@ -663,12 +665,12 @@ def deserialize(deserializer: Deserializer) -> PublicKey: def serialize(self, serializer: Serializer): """Serialize the public key to BCS format with prefix. - + Writes the 65-byte representation (0x04 prefix + 64-byte key). - + Args: serializer: BCS serializer to write the public key bytes to. - + Example: >>> serializer = Serializer() >>> public_key.serialize(serializer) @@ -681,51 +683,52 @@ def serialize(self, serializer: Serializer): class Signature(asymmetric_crypto.Signature): """secp256k1 ECDSA signature implementation. - + This class represents secp256k1 signatures in canonical form (s < n/2) and provides methods for serialization, deserialization, and comparison. - + Key Properties: - **Format**: Raw r, s values concatenated (64 bytes total) - **Normalized**: Uses canonical form with s < n/2 - **Length**: 64 bytes (32 bytes for r + 32 bytes for s) - + Attributes: LENGTH: The byte length of secp256k1 signatures (64) signature: The raw signature bytes - + Examples: Create from signing:: - + private_key = PrivateKey.random() message = b"Hello, Aptos!" signature = private_key.sign(message) - + Create from hex string:: - + sig_hex = "***1234abcd..." signature = Signature.from_str(sig_hex) - + Verify with public key:: - + public_key = private_key.public_key() is_valid = public_key.verify(message, signature) assert is_valid == True - + Note: Unlike some other secp256k1 implementations, this class uses the raw r,s format (64 bytes) rather than DER encoding. """ + LENGTH: int = 64 signature: bytes def __init__(self, signature: bytes): """Initialize a signature with the given raw bytes. - + Args: signature: The 64-byte signature data (r, s values concatenated). - + Example: This is typically not called directly. Signatures are usually created by signing with a private key: @@ -735,13 +738,13 @@ def __init__(self, signature: bytes): def __eq__(self, other: object): """Check equality with another Signature. - + Args: other: Object to compare with. - + Returns: True if both signatures contain the same bytes. - + Example: >>> sig1 = private_key.sign(message) >>> sig2 = Signature(sig1.data()) # Same data @@ -754,10 +757,10 @@ def __eq__(self, other: object): def __str__(self) -> str: """Return the hexadecimal string representation. - + Returns: Hex string with '0x' prefix representing the signature. - + Example: >>> str(signature) '***c9a34d6...' # 64 bytes @@ -766,10 +769,10 @@ def __str__(self) -> str: def hex(self) -> str: """Get the hexadecimal representation of the signature. - + Returns: Hex string with '0x' prefix representing the 64-byte signature. - + Example: >>> signature.hex() '***a1b2c3d4...' # 64 bytes as hex @@ -779,18 +782,18 @@ def hex(self) -> str: @staticmethod def from_str(value: str) -> Signature: """Create a signature from a hex string. - + Args: value: Hex string representing the signature. Can be with or without '0x' prefix. Must be exactly 64 bytes (128 hex characters). - + Returns: A new Signature instance. - + Raises: Exception: If the signature length is invalid. - + Example: >>> sig = Signature.from_str("***a1b2c3d4...") >>> len(sig.data()) @@ -804,10 +807,10 @@ def from_str(value: str) -> Signature: def data(self) -> bytes: """Get the raw signature bytes. - + Returns: The 64-byte raw signature data. - + Example: >>> raw_bytes = signature.data() >>> len(raw_bytes) @@ -818,16 +821,16 @@ def data(self) -> bytes: @staticmethod def deserialize(deserializer: Deserializer) -> Signature: """Deserialize a signature from BCS-encoded bytes. - + Args: deserializer: BCS deserializer containing the signature bytes. - + Returns: A new Signature instance from the deserialized data. - + Raises: Exception: If the signature length is not 64 bytes. - + Example: >>> serializer = Serializer() >>> original_sig.serialize(serializer) @@ -845,10 +848,10 @@ def deserialize(deserializer: Deserializer) -> Signature: def serialize(self, serializer: Serializer): """Serialize the signature to BCS format. - + Args: serializer: BCS serializer to write the signature bytes to. - + Example: >>> serializer = Serializer() >>> signature.serialize(serializer) diff --git a/aptos_sdk/transaction_worker.py b/aptos_sdk/transaction_worker.py index bb47d9a..49d33b9 100644 --- a/aptos_sdk/transaction_worker.py +++ b/aptos_sdk/transaction_worker.py @@ -22,25 +22,25 @@ class TransactionWorker: """High-throughput transaction processing framework for Aptos blockchain. - + The TransactionWorker provides an asynchronous framework for processing large volumes of transactions efficiently. It manages sequence number coordination, transaction generation, submission, and result tracking through separate concurrent tasks. - + Architecture: - **Sequence Management**: Automatically acquires sequential transaction numbers - **Concurrent Submission**: Submits transactions asynchronously for high throughput - **Batch Processing**: Processes transaction results in batches for efficiency - **Error Handling**: Captures and reports transaction submission errors - **Queue-Based**: Uses asyncio queues for task coordination - + Key Features: - **High Performance**: Designed for bulk transaction processing - **Sequence Safety**: Ensures proper transaction ordering - **Non-blocking**: Asynchronous operation doesn't block the caller - **Error Tracking**: Comprehensive error reporting and exception handling - **Result Monitoring**: Track transaction outcomes and failures - + Workflow: 1. **Start**: Initialize worker tasks for submission and processing 2. **Generate**: Transaction generator creates signed transactions with sequence numbers @@ -48,30 +48,30 @@ class TransactionWorker: 4. **Track**: Monitor submission results and errors 5. **Process**: Batch process results for efficiency 6. **Stop**: Clean shutdown of worker tasks - + Examples: Basic transaction worker:: - + import asyncio from aptos_sdk.async_client import RestClient from aptos_sdk.account import Account from aptos_sdk.transaction_worker import TransactionWorker - + async def transaction_generator(account, sequence_number): # Create transfer transaction recipient = Account.generate().address() return await client.create_bcs_signed_transaction( account, transfer_payload, sequence_number=sequence_number ) - + async def bulk_transfers(): client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") sender = Account.generate() - + # Create and start worker worker = TransactionWorker(sender, client, transaction_generator) worker.start() - + try: # Process transaction results for _ in range(100): # Process 100 transactions @@ -82,43 +82,43 @@ async def bulk_transfers(): print(f"Transaction {seq_num} succeeded: {tx_hash}") finally: worker.stop() - + Token distribution example:: - + async def token_generator(account, sequence_number): # Distribute tokens to random recipients recipients = [Account.generate().address() for _ in range(10)] recipient = random.choice(recipients) - + return await client.transfer_transaction( account, recipient, 1000, sequence_number=sequence_number ) - + # Process 1000 token distributions worker = TransactionWorker(distributor_account, client, token_generator) worker.start() - + success_count = 0 for _ in range(1000): seq_num, tx_hash, error = await worker.next_processed_transaction() if not error: success_count += 1 - + print(f"Successfully distributed tokens in {success_count} transactions") worker.stop() - + Performance Considerations: - **Batch Size**: Processes multiple transactions concurrently - **Memory Usage**: Queues consume memory; monitor for large workloads - **Network Limits**: Respects node rate limits and connection pooling - **Sequence Coordination**: May wait for sequence numbers under high load - + Limitations: - **No Retry Logic**: Failed transactions are not automatically retried - **No Health Monitoring**: Doesn't check node health or connectivity - **Basic Error Handling**: Errors are reported but not automatically resolved - **Single Account**: Designed for single account use (sequence number coordination) - + Error Handling: Transaction errors are captured and reported through the result queue. Common error scenarios: @@ -126,11 +126,11 @@ async def token_generator(account, sequence_number): - Insufficient account balance - Transaction validation failures - Node overload or rate limiting - + Thread Safety: The TransactionWorker is designed for single-threaded async use. Don't share instances across multiple async contexts. - + Note: This is a basic framework suitable for development and testing. Production systems should implement additional features like: @@ -162,11 +162,11 @@ def __init__( ], ): """Initialize a TransactionWorker for high-throughput transaction processing. - + Creates a transaction worker that will use the provided account for signing transactions and submit them through the REST client. The transaction generator function is called to create each transaction with the appropriate sequence number. - + Args: account: The Account to use for signing transactions. Must have sufficient balance for the transactions being generated. @@ -174,10 +174,10 @@ def __init__( transaction_generator: Async function that takes (Account, int) and returns a SignedTransaction. This function is called for each transaction with the next available sequence number. - + Examples: Simple transfer generator:: - + async def transfer_generator(account, seq_num): recipient = Account.generate().address() return await client.create_bcs_signed_transaction( @@ -185,11 +185,11 @@ async def transfer_generator(account, seq_num): transfer_payload(recipient, 1000), sequence_number=seq_num ) - + worker = TransactionWorker(account, client, transfer_generator) - + Complex transaction generator:: - + async def complex_generator(account, seq_num): # Randomly choose transaction type if random.random() < 0.5: @@ -198,9 +198,9 @@ async def complex_generator(account, seq_num): else: # Smart contract interaction return create_contract_txn(account, seq_num) - + worker = TransactionWorker(account, client, complex_generator) - + Note: The worker is initialized but not started. Call start() to begin processing transactions. @@ -222,18 +222,18 @@ async def complex_generator(account, seq_num): def address(self) -> AccountAddress: """Get the address of the account used by this transaction worker. - + Returns: AccountAddress: The address of the account that signs transactions. - + Examples: Check worker account:: - + worker = TransactionWorker(account, client, generator) print(f"Worker using account: {worker.address()}") - + Verify account balance:: - + worker_address = worker.address() balance = await client.account_balance(worker_address) print(f"Worker account balance: {balance} APT") @@ -302,36 +302,36 @@ async def next_processed_transaction( self, ) -> typing.Tuple[int, typing.Optional[str], typing.Optional[Exception]]: """Get the next processed transaction result from the worker. - + This method blocks until a transaction result is available. Results include both successful submissions (with transaction hash) and failures (with error). - + Returns: Tuple containing: - int: The sequence number of the processed transaction - Optional[str]: Transaction hash if successful, None if failed - Optional[Exception]: Exception if failed, None if successful - + Examples: Process results sequentially:: - + worker.start() - + while True: seq_num, tx_hash, error = await worker.next_processed_transaction() - + if error: print(f"Transaction {seq_num} failed: {error}") else: print(f"Transaction {seq_num} succeeded: {tx_hash}") - + Batch processing with timeout:: - + import asyncio - + results = [] timeout_seconds = 30 - + try: while len(results) < expected_count: result = await asyncio.wait_for( @@ -341,11 +341,11 @@ async def next_processed_transaction( results.append(result) except asyncio.TimeoutError: print(f"Timeout after {timeout_seconds}s, got {len(results)} results") - + Error handling:: - + seq_num, tx_hash, error = await worker.next_processed_transaction() - + if error: if "insufficient balance" in str(error).lower(): print("Account needs more funds") @@ -353,7 +353,7 @@ async def next_processed_transaction( print("Being rate limited, slow down") else: print(f"Unexpected error: {error}") - + Note: This method will block indefinitely if no more transactions are being processed. Make sure to call stop() when done to clean up resources. @@ -362,38 +362,38 @@ async def next_processed_transaction( def stop(self): """Stop the transaction worker and cancel all background tasks. - + This method gracefully shuts down the transaction worker by canceling the background tasks for transaction submission and processing. Any pending transactions will be canceled. - + Raises: Exception: If the worker hasn't been started yet or is already stopped. - + Examples: Proper shutdown:: - + worker = TransactionWorker(account, client, generator) worker.start() - + try: # Process transactions... pass finally: worker.stop() # Always clean up - + Context manager pattern:: - + async def process_with_worker(): worker = TransactionWorker(account, client, generator) worker.start() - + try: # Do work... yield worker finally: worker.stop() - + Note: After calling stop(), the worker cannot be restarted. Create a new TransactionWorker instance if you need to resume processing. @@ -409,44 +409,44 @@ async def process_with_worker(): def start(self): """Start the transaction worker background tasks. - + This method begins the asynchronous tasks for transaction submission and processing. The worker will start generating, submitting, and tracking transactions immediately after this call. - + Raises: Exception: If the worker has already been started. - + Examples: Basic startup:: - + worker = TransactionWorker(account, client, generator) worker.start() - + # Worker is now processing transactions # Get results with next_processed_transaction() - + Startup with immediate processing:: - + worker = TransactionWorker(account, client, generator) worker.start() - + # Start consuming results immediately asyncio.create_task(process_results(worker)) - + Error handling:: - + try: worker.start() except Exception as e: print(f"Failed to start worker: {e}") # Handle startup failure - + Background Tasks: Starting the worker creates two background tasks: - **Submission Task**: Generates and submits transactions - **Processing Task**: Processes submission results in batches - + Note: The worker must be started before calling next_processed_transaction(). Always pair start() with stop() for proper resource cleanup. @@ -465,27 +465,27 @@ def start(self): class TransactionQueue: """Queue-based transaction payload manager for TransactionWorker integration. - + The TransactionQueue provides a simple interface for feeding transaction payloads to a TransactionWorker. It acts as a bridge between application logic that creates transaction payloads and the worker that needs signed transactions. - + Key Features: - **Async Queue**: Built on asyncio.Queue for efficient async operations - **Payload Management**: Handles raw transaction payloads before signing - **Worker Integration**: Designed to work seamlessly with TransactionWorker - **Backpressure**: Built-in flow control through queue size limits - + Examples: Basic queue usage:: - + from aptos_sdk.transaction_worker import TransactionQueue from aptos_sdk.transactions import EntryFunction, TransactionArgument - + # Create queue and connect to worker queue = TransactionQueue(rest_client) worker = TransactionWorker(account, rest_client, queue.next) - + # Push transaction payloads transfer_payload = EntryFunction.natural( "***::aptos_account", @@ -493,40 +493,40 @@ class TransactionQueue: [], [recipient_address, amount] ) - + await queue.push(transfer_payload) - + Batch operations:: - + # Push multiple payloads payloads = [ create_transfer_payload(addr, 1000) for addr in recipient_addresses ] - + for payload in payloads: await queue.push(payload) - + # Worker will process them automatically - + Custom payload generation:: - + async def generate_payloads(): for i in range(1000): payload = create_custom_payload(i) await queue.push(payload) - + # Start payload generation and worker processing asyncio.create_task(generate_payloads()) worker.start() - + Integration Pattern: The typical usage pattern is: 1. Create TransactionQueue with REST client 2. Create TransactionWorker with account and queue.next as generator 3. Push payloads to queue with push() 4. Worker automatically consumes payloads and creates signed transactions - + Note: The queue uses unbounded storage by default. For high-volume applications, consider implementing backpressure or queue size limits to prevent diff --git a/aptos_sdk/transactions.py b/aptos_sdk/transactions.py index f83da5d..dd77d2b 100644 --- a/aptos_sdk/transactions.py +++ b/aptos_sdk/transactions.py @@ -26,7 +26,7 @@ Architecture: Transaction Flow:: - + 1. Create RawTransaction with payload and parameters 2. Sign with appropriate private key(s) → SignedTransaction 3. Serialize to BCS format for network transmission @@ -35,15 +35,15 @@ Examples: Basic transfer transaction:: - + from aptos_sdk.transactions import RawTransaction, EntryFunction from aptos_sdk.account import Account from aptos_sdk.account_address import AccountAddress - + # Create accounts sender = Account.generate() recipient = Account.generate().address() - + # Create transfer payload transfer_payload = EntryFunction.natural( "***::aptos_account", @@ -51,7 +51,7 @@ [], [recipient, 1_000_000] # 1 APT in octas ) - + # Build raw transaction raw_txn = RawTransaction( sender=sender.address(), @@ -62,14 +62,14 @@ expiration_timestamps_secs=int(time.time()) + 600, # 10 minutes chain_id=1 # Mainnet ) - + # Sign transaction signed_txn = SignedTransaction(raw_txn, sender.sign_transaction(raw_txn)) - + Multi-agent transaction:: - + from aptos_sdk.transactions import MultiAgentRawTransaction - + # Create multi-agent transaction multi_agent_txn = MultiAgentRawTransaction( raw_transaction=raw_txn, @@ -78,44 +78,44 @@ account3.address() ] ) - + # Collect signatures from all signers signatures = [ account1.sign_transaction(multi_agent_txn), account2.sign_transaction(multi_agent_txn), account3.sign_transaction(multi_agent_txn) ] - + # Create multi-agent signed transaction signed_txn = SignedTransaction( multi_agent_txn, MultiAgentAuthenticator(signatures[0], signatures[1:]) ) - + Fee-payer transaction:: - + from aptos_sdk.transactions import FeePayerRawTransaction - + # Transaction where fee_payer pays gas instead of sender fee_payer_txn = FeePayerRawTransaction( raw_transaction=raw_txn, secondary_signer_addresses=[], fee_payer_address=fee_payer.address() ) - + # Both sender and fee payer must sign sender_sig = sender.sign_transaction(fee_payer_txn) fee_payer_sig = fee_payer.sign_transaction(fee_payer_txn) - + signed_txn = SignedTransaction( fee_payer_txn, FeePayerAuthenticator(sender_sig, [], fee_payer_sig) ) - + Script execution:: - + from aptos_sdk.transactions import Script, TransactionArgument - + # Execute Move script with arguments script_payload = Script( code=compiled_script_bytes, @@ -125,7 +125,7 @@ TransactionArgument(amount, Serializer.u64) ] ) - + raw_txn = RawTransaction( sender.address(), sequence_num, script_payload, max_gas, gas_price, expiration, chain_id @@ -136,12 +136,12 @@ - Most common transaction type - Call public functions in published Move modules - Type-safe with automatic argument serialization - + Scripts: - Execute arbitrary Move bytecode - More flexible but requires compilation - Deprecated in favor of entry functions - + Module Publishing: - Deploy Move modules to the blockchain - Requires compilation and metadata @@ -151,7 +151,7 @@ - **Execution Cost**: Computation and storage operations - **IO Cost**: Reading/writing blockchain state - **Storage Cost**: Persistent data storage fees - + Gas Parameters: - **max_gas_amount**: Maximum gas units willing to pay - **gas_unit_price**: Price per gas unit in octas (1 APT = 10^8 octas) @@ -276,11 +276,11 @@ def deserialize(deserializer: Deserializer) -> RawTransactionWithData: class RawTransaction(Deserializable, RawTransactionInternal, Serializable): """Represents a raw (unsigned) transaction on the Aptos blockchain. - + A RawTransaction contains all the essential information needed to execute a transaction on the Aptos blockchain, except for cryptographic signatures. It serves as the foundation for all transaction types and must be signed to become a valid SignedTransaction. - + Components: - **Sender**: Account address initiating the transaction - **Sequence Number**: Prevents replay attacks and ensures ordering @@ -288,25 +288,25 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): - **Gas Parameters**: Maximum gas amount and price per unit - **Expiration**: Timestamp after which transaction becomes invalid - **Chain ID**: Network identifier preventing cross-chain attacks - + Transaction Lifecycle: 1. **Construction**: Create RawTransaction with all parameters 2. **Validation**: Verify parameters are valid and consistent 3. **Signing**: Apply cryptographic signatures to create SignedTransaction 4. **Serialization**: Convert to BCS format for network transmission 5. **Submission**: Send to Aptos REST API for execution - + Examples: Basic token transfer:: - + from aptos_sdk.transactions import RawTransaction, EntryFunction from aptos_sdk.account import Account import time - + # Setup sender = Account.generate() recipient = Account.generate().address() - + # Create transfer payload payload = EntryFunction.natural( "***::aptos_account", @@ -314,7 +314,7 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): [], [recipient, 1_000_000] # 1 APT ) - + # Build transaction raw_txn = RawTransaction( sender=sender.address(), @@ -325,13 +325,13 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): expiration_timestamps_secs=int(time.time() + 600), # 10 min chain_id=1 # Mainnet ) - + # Sign and create final transaction authenticator = sender.sign_transaction(raw_txn) signed_txn = SignedTransaction(raw_txn, authenticator) - + Smart contract interaction:: - + # Call custom module function contract_payload = EntryFunction.natural( "***abc123::my_module", @@ -339,16 +339,16 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): ["***::aptos_coin::AptosCoin"], # Type arguments [1000, "hello world", True] # Function arguments ) - + raw_txn = RawTransaction( sender.address(), seq_num, contract_payload, 200_000, 150, expiration, chain_id ) - + Script execution (legacy):: - + from aptos_sdk.transactions import Script, TransactionArgument - + script_payload = Script( code=compiled_move_bytecode, type_arguments=[], @@ -357,34 +357,34 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): TransactionArgument(amount, Serializer.u64) ] ) - + raw_txn = RawTransaction( sender.address(), seq_num, script_payload, gas_limit, gas_price, expiration, chain_id ) - + Gas Economics: Gas calculation:: - + # Total maximum cost in octas max_cost = max_gas_amount * gas_unit_price - + # Example: 100,000 gas units at 100 octas/unit = 10,000,000 octas # That's 0.1 APT maximum (since 1 APT = 100,000,000 octas) - + Gas recommendations: - **Simple transfers**: 100,000 gas units - **Smart contracts**: 200,000-500,000 gas units - **Complex operations**: 1,000,000+ gas units - **Gas price**: 100-150 octas per unit (check network conditions) - + Security Considerations: - **Sequence Number**: Must exactly match next expected sequence for sender - **Expiration Time**: Should be reasonable (10-60 minutes) to prevent stale transactions - **Chain ID**: Must match target network (1=mainnet, 2=testnet, etc.) - **Gas Limits**: Set appropriately to avoid failed transactions or overpayment - **Payload Validation**: Ensure payload parameters are correct and safe - + Validation Rules: - Sender address must be valid and exist on-chain - Sequence number must be >= current account sequence @@ -392,19 +392,19 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): - Expiration must be in the future - Chain ID must match target network - Payload must be properly constructed and valid - + Performance Notes: - Transaction size affects gas cost - Complex payloads require more gas - Ed25519 signatures are faster to verify than Secp256k1 - BCS serialization is optimized for minimal size - + Note: RawTransaction is immutable once created. Any modifications require creating a new instance. This ensures transaction integrity and prevents accidental modifications after signing. """ - + # Sender's address sender: AccountAddress # Sequence number of this transaction. This must match the sequence number in the sender's @@ -432,11 +432,11 @@ def __init__( chain_id: int, ): """Initialize a RawTransaction with all required parameters. - + Creates a new raw transaction that can be signed and submitted to the Aptos blockchain. All parameters are validated for basic correctness but full validation occurs during signing and submission. - + Args: sender: Account address that will sign and send this transaction. Must be a valid 32-byte Aptos address. @@ -452,23 +452,23 @@ def __init__( Should be reasonable future time (10-60 minutes from now). chain_id: Identifier for the target Aptos network. 1=mainnet, 2=testnet, varies for custom networks. - + Raises: ValueError: If any parameters are invalid or inconsistent. TypeError: If parameters are not the expected types. - + Examples: Simple transfer transaction:: - + import time from aptos_sdk.transactions import RawTransaction, EntryFunction - + # Create transfer payload payload = EntryFunction.natural( "***::aptos_account", "transfer", [], [recipient_address, 1_000_000] ) - + # Build raw transaction raw_txn = RawTransaction( sender=alice.address(), @@ -479,9 +479,9 @@ def __init__( expiration_timestamps_secs=int(time.time()) + 600, chain_id=1 ) - + Smart contract call:: - + # Contract interaction with type arguments contract_call = EntryFunction.natural( "***deadbeef::defi_module", @@ -489,7 +489,7 @@ def __init__( ["***::aptos_coin::AptosCoin", "***::test_coin::TestCoin"], [input_amount, min_output_amount, slippage_tolerance] ) - + raw_txn = RawTransaction( sender=trader.address(), sequence_number=get_next_sequence(trader), @@ -499,16 +499,16 @@ def __init__( expiration_timestamps_secs=int(time.time()) + 300, # 5 minutes chain_id=1 ) - + Batch operation setup:: - + # Create multiple transactions with sequential sequence numbers base_sequence = await client.account_sequence_number(sender.address()) - + transactions = [] for i, recipient in enumerate(recipients): payload = create_transfer_payload(recipient, amounts[i]) - + raw_txn = RawTransaction( sender=sender.address(), sequence_number=base_sequence + i, @@ -519,31 +519,31 @@ def __init__( chain_id=chain_id ) transactions.append(raw_txn) - + Parameter Guidelines: Sequence Numbers: - Must be exactly next expected sequence for sender - Get current sequence from: client.account_sequence_number(sender) - Increment by 1 for each subsequent transaction - + Gas Parameters: - max_gas_amount: Start with 100,000 for simple operations - Increase for complex smart contract interactions - Monitor actual gas usage and adjust accordingly - gas_unit_price: Check network congestion and adjust - + Expiration Times: - Not too short: Allow time for network processing - Not too long: Prevent stale transactions - Typical range: 5-60 minutes from creation - Consider network conditions and urgency - + Chain IDs: - Mainnet: 1 - Testnet: 2 - Devnet: Varies - Custom networks: Check with network operator - + Note: Once created, RawTransaction instances are immutable. This prevents accidental modification after creation and ensures signature validity. @@ -621,11 +621,11 @@ def serialize(self, serializer: Serializer) -> None: class MultiAgentRawTransaction(RawTransactionWithData): """ A multi-agent transaction that requires signatures from multiple accounts. - + This is used when a transaction needs to be authorized by more than just the sender account. """ - + secondary_signers: List[AccountAddress] def __init__( @@ -665,10 +665,10 @@ def deserialize_inner(deserializer: Deserializer) -> MultiAgentRawTransaction: class FeePayerRawTransaction(RawTransactionWithData): """ A transaction where fees can be paid by a different account than the sender. - + This allows for sponsored transactions where a third party pays the gas fees. """ - + secondary_signers: List[AccountAddress] fee_payer: Optional[AccountAddress] @@ -722,10 +722,10 @@ def deserialize_inner(deserializer: Deserializer) -> FeePayerRawTransaction: class TransactionPayload: """ Represents the payload of a transaction - what the transaction will execute. - + Can be a Script, ModuleBundle, or EntryFunction (most common). """ - + SCRIPT: int = 0 MODULE_BUNDLE: int = 1 SCRIPT_FUNCTION: int = 2 @@ -906,10 +906,10 @@ def __str__(self): class EntryFunction: """ Represents a call to an entry function in a Move module. - + Entry functions are the most common way to execute code on Aptos. """ - + module: ModuleId function: str ty_args: List[TypeTag] @@ -986,10 +986,10 @@ def serialize(self, serializer: Serializer) -> None: class ModuleId: """ Identifies a Move module by its address and name. - + Modules are the fundamental units of code organization in Move. """ - + address: AccountAddress name: str @@ -1036,10 +1036,10 @@ def serialize(self, serializer: Serializer) -> None: class TransactionArgument: """ Represents an argument to pass to a transaction function. - + Encapsulates a value and its encoding function for BCS serialization. """ - + value: Any encoder: Callable[[Serializer, Any], None] @@ -1071,10 +1071,10 @@ def encode(self) -> bytes: class SignedTransaction: """ A transaction that has been signed and is ready for submission to the blockchain. - + Contains both the raw transaction data and the cryptographic proof of authorization. """ - + transaction: RawTransaction authenticator: Authenticator diff --git a/aptos_sdk/type_tag.py b/aptos_sdk/type_tag.py index 1c48f2f..f853496 100644 --- a/aptos_sdk/type_tag.py +++ b/aptos_sdk/type_tag.py @@ -16,15 +16,15 @@ Examples: Creating and using primitive type tags:: - + # Create a u64 type tag u64_tag = TypeTag(U64Tag(1234567890)) - + # Create a boolean type tag bool_tag = TypeTag(BoolTag(True)) - + Creating struct type tags:: - + # Create a simple struct tag struct_tag = StructTag( AccountAddress.from_str("0x1"), @@ -32,12 +32,12 @@ "Coin", [TypeTag(U64Tag(0))] # Generic type parameter ) - + # Parse from string format parsed = StructTag.from_str("0x1::coin::Coin") - + Serialization:: - + # All type tags support BCS serialization serialized = struct_tag.to_bytes() deserialized = StructTag.from_bytes(serialized) @@ -55,17 +55,17 @@ class TypeTag(Deserializable, Serializable): """Root class representing Move language types in Aptos. - + TypeTag is a discriminated union that can contain any Move type, including primitive types (bool, integers, addresses) and complex types (structs, vectors). Each TypeTag wraps a specific type tag implementation and provides a unified interface for type operations. - + The discriminator values correspond to Move's type system: - + Attributes: BOOL: Discriminator for boolean types (0) - U8: Discriminator for 8-bit unsigned integers (1) + U8: Discriminator for 8-bit unsigned integers (1) U64: Discriminator for 64-bit unsigned integers (2) U128: Discriminator for 128-bit unsigned integers (3) ACCOUNT_ADDRESS: Discriminator for account addresses (4) @@ -73,21 +73,21 @@ class TypeTag(Deserializable, Serializable): VECTOR: Discriminator for vector types (6) - not implemented STRUCT: Discriminator for custom struct types (7) U16: Discriminator for 16-bit unsigned integers (8) - U32: Discriminator for 32-bit unsigned integers (9) + U32: Discriminator for 32-bit unsigned integers (9) U256: Discriminator for 256-bit unsigned integers (10) value: The wrapped type tag implementation - + Examples: Creating type tags for primitives:: - + bool_type = TypeTag(BoolTag(True)) u64_type = TypeTag(U64Tag(12345)) address_type = TypeTag(AccountAddressTag( AccountAddress.from_str("0x1") )) - + Creating struct type tags:: - + struct_type = TypeTag(StructTag( AccountAddress.from_str("0x1"), "coin", "Coin", [] @@ -110,7 +110,7 @@ class TypeTag(Deserializable, Serializable): def __init__(self, value: typing.Any): """Initialize a TypeTag with a specific type implementation. - + Args: value: The type tag implementation (e.g., BoolTag, U64Tag, StructTag) """ @@ -118,10 +118,10 @@ def __init__(self, value: typing.Any): def __eq__(self, other: object) -> bool: """Check equality with another TypeTag. - + Args: other: The object to compare with. - + Returns: True if both TypeTags represent the same type and value. """ @@ -133,7 +133,7 @@ def __eq__(self, other: object) -> bool: def __str__(self): """Get string representation of the type tag. - + Returns: String representation of the underlying type. """ @@ -141,7 +141,7 @@ def __str__(self): def __repr__(self): """Get detailed string representation for debugging. - + Returns: String representation of the type tag. """ @@ -150,13 +150,13 @@ def __repr__(self): @staticmethod def deserialize(deserializer: Deserializer) -> TypeTag: """Deserialize a TypeTag from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized TypeTag instance. - + Raises: NotImplementedError: If the type variant is not supported (SIGNER, VECTOR) or unknown. @@ -188,7 +188,7 @@ def deserialize(deserializer: Deserializer) -> TypeTag: def serialize(self, serializer: Serializer): """Serialize this TypeTag to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -198,27 +198,28 @@ def serialize(self, serializer: Serializer): class BoolTag(Deserializable, Serializable): """Type tag for Move boolean values. - + Represents the Move `bool` primitive type, which can hold true or false values. - + Attributes: value: The boolean value this tag represents. - + Examples: Creating and using BoolTag:: - + true_tag = BoolTag(True) false_tag = BoolTag(False) - + # Serialize/deserialize serialized = true_tag.to_bytes() deserialized = BoolTag.from_bytes(serialized) """ + value: bool def __init__(self, value: bool): """Initialize a BoolTag with a boolean value. - + Args: value: The boolean value to wrap. """ @@ -226,10 +227,10 @@ def __init__(self, value: bool): def __eq__(self, other: object) -> bool: """Check equality with another BoolTag. - + Args: other: The object to compare with. - + Returns: True if both tags represent the same boolean value. """ @@ -239,7 +240,7 @@ def __eq__(self, other: object) -> bool: def __str__(self): """Get string representation of the boolean value. - + Returns: String representation ("True" or "False"). """ @@ -247,7 +248,7 @@ def __str__(self): def variant(self): """Get the type discriminator for this tag. - + Returns: The BOOL type discriminator. """ @@ -256,10 +257,10 @@ def variant(self): @staticmethod def deserialize(deserializer: Deserializer) -> BoolTag: """Deserialize a BoolTag from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized BoolTag instance. """ @@ -267,7 +268,7 @@ def deserialize(deserializer: Deserializer) -> BoolTag: def serialize(self, serializer: Serializer): """Serialize this BoolTag to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -276,27 +277,28 @@ def serialize(self, serializer: Serializer): class U8Tag(Deserializable, Serializable): """Type tag for Move 8-bit unsigned integer values. - + Represents the Move `u8` primitive type, which holds unsigned 8-bit integers in the range 0-255. - + Attributes: value: The u8 integer value this tag represents. - + Examples: Creating and using U8Tag:: - + tag = U8Tag(255) # Maximum u8 value - + # Serialize/deserialize serialized = tag.to_bytes() deserialized = U8Tag.from_bytes(serialized) """ + value: int def __init__(self, value: int): """Initialize a U8Tag with an 8-bit unsigned integer. - + Args: value: The u8 value to wrap (0-255). """ @@ -304,10 +306,10 @@ def __init__(self, value: int): def __eq__(self, other: object) -> bool: """Check equality with another U8Tag. - + Args: other: The object to compare with. - + Returns: True if both tags represent the same u8 value. """ @@ -317,7 +319,7 @@ def __eq__(self, other: object) -> bool: def __str__(self): """Get string representation of the u8 value. - + Returns: String representation of the integer value. """ @@ -325,7 +327,7 @@ def __str__(self): def variant(self): """Get the type discriminator for this tag. - + Returns: The U8 type discriminator. """ @@ -334,10 +336,10 @@ def variant(self): @staticmethod def deserialize(deserializer: Deserializer) -> U8Tag: """Deserialize a U8Tag from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized U8Tag instance. """ @@ -345,7 +347,7 @@ def deserialize(deserializer: Deserializer) -> U8Tag: def serialize(self, serializer: Serializer): """Serialize this U8Tag to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -354,18 +356,19 @@ def serialize(self, serializer: Serializer): class U16Tag(Deserializable, Serializable): """Type tag for Move 16-bit unsigned integer values. - + Represents the Move `u16` primitive type, which holds unsigned 16-bit integers in the range 0-65535. - + Attributes: value: The u16 integer value this tag represents. """ + value: int def __init__(self, value: int): """Initialize a U16Tag with a 16-bit unsigned integer. - + Args: value: The u16 value to wrap (0-65535). """ @@ -397,18 +400,19 @@ def serialize(self, serializer: Serializer): class U32Tag(Deserializable, Serializable): """Type tag for Move 32-bit unsigned integer values. - + Represents the Move `u32` primitive type, which holds unsigned 32-bit integers in the range 0-4294967295. - + Attributes: value: The u32 integer value this tag represents. """ + value: int def __init__(self, value: int): """Initialize a U32Tag with a 32-bit unsigned integer. - + Args: value: The u32 value to wrap (0-4294967295). """ @@ -440,18 +444,19 @@ def serialize(self, serializer: Serializer): class U64Tag(Deserializable, Serializable): """Type tag for Move 64-bit unsigned integer values. - + Represents the Move `u64` primitive type, which holds unsigned 64-bit integers in the range 0-18446744073709551615. - + Attributes: value: The u64 integer value this tag represents. """ + value: int def __init__(self, value: int): """Initialize a U64Tag with a 64-bit unsigned integer. - + Args: value: The u64 value to wrap (0-18446744073709551615). """ @@ -483,18 +488,19 @@ def serialize(self, serializer: Serializer): class U128Tag(Deserializable, Serializable): """Type tag for Move 128-bit unsigned integer values. - + Represents the Move `u128` primitive type, which holds unsigned 128-bit integers in the range 0-340282366920938463463374607431768211455. - + Attributes: value: The u128 integer value this tag represents. """ + value: int def __init__(self, value: int): """Initialize a U128Tag with a 128-bit unsigned integer. - + Args: value: The u128 value to wrap. """ @@ -526,17 +532,18 @@ def serialize(self, serializer: Serializer): class U256Tag(Deserializable, Serializable): """Type tag for Move 256-bit unsigned integer values. - + Represents the Move `u256` primitive type, which holds unsigned 256-bit integers. - + Attributes: value: The u256 integer value this tag represents. """ + value: int def __init__(self, value: int): """Initialize a U256Tag with a 256-bit unsigned integer. - + Args: value: The u256 value to wrap. """ @@ -568,28 +575,29 @@ def serialize(self, serializer: Serializer): class AccountAddressTag(Deserializable, Serializable): """Type tag for Move address values. - + Represents the Move `address` primitive type, which holds account addresses used to identify accounts and resources on the Aptos blockchain. - + Attributes: value: The AccountAddress value this tag represents. - + Examples: Creating and using AccountAddressTag:: - + addr = AccountAddress.from_str("0x1") tag = AccountAddressTag(addr) - + # Serialize/deserialize serialized = tag.to_bytes() deserialized = AccountAddressTag.from_bytes(serialized) """ + value: AccountAddress def __init__(self, value: AccountAddress): """Initialize an AccountAddressTag with an account address. - + Args: value: The AccountAddress to wrap. """ @@ -621,41 +629,42 @@ def serialize(self, serializer: Serializer): class StructTag(Deserializable, Serializable): """Type tag for Move struct types. - + Represents custom Move struct types, which are user-defined composite types that can have generic type parameters. StructTags fully specify a struct type including its location (address and module), name, and type arguments. - + Attributes: address: The account address where the module is published. module: The name of the module containing the struct. name: The name of the struct. type_args: List of type arguments for generic structs. - + Examples: Creating struct tags:: - + # Simple struct without generics struct_tag = StructTag( AccountAddress.from_str("0x1"), "account", "Account", [] ) - + # Generic struct with type parameters coin_tag = StructTag( AccountAddress.from_str("0x1"), "coin", "Coin", [TypeTag(StructTag( - AccountAddress.from_str("0x1"), + AccountAddress.from_str("0x1"), "aptos_coin", "AptosCoin", [] ))] ) - + Parsing from string:: - + tag = StructTag.from_str("0x1::coin::Coin<0x1::aptos_coin::AptosCoin>") print(tag) # "0x1::coin::Coin<0x1::aptos_coin::AptosCoin>" """ + address: AccountAddress module: str name: str @@ -663,7 +672,7 @@ class StructTag(Deserializable, Serializable): def __init__(self, address, module, name, type_args): """Initialize a StructTag. - + Args: address: The account address where the struct's module is published. module: The name of the module containing the struct. @@ -677,10 +686,10 @@ def __init__(self, address, module, name, type_args): def __eq__(self, other: object) -> bool: """Check equality with another StructTag. - + Args: other: The object to compare with. - + Returns: True if both StructTags represent the same struct type. """ @@ -695,9 +704,9 @@ def __eq__(self, other: object) -> bool: def __str__(self) -> str: """Get the canonical string representation of this struct type. - + The format is: address::module::name - + Returns: String representation of the struct type. """ @@ -712,19 +721,19 @@ def __str__(self) -> str: @staticmethod def from_str(type_tag: str) -> StructTag: """Parse a StructTag from its string representation. - + Args: type_tag: String representation of a struct type, e.g., "0x1::coin::Coin<0x1::aptos_coin::AptosCoin>" - + Returns: The parsed StructTag instance. - + Examples: Parsing simple and complex struct types:: - + simple = StructTag.from_str("0x1::account::Account") - + nested = StructTag.from_str( "0x1::coin::Coin<0x1::aptos_coin::AptosCoin>" ) @@ -734,14 +743,14 @@ def from_str(type_tag: str) -> StructTag: @staticmethod def _from_str_internal(type_tag: str, index: int) -> Tuple[List[TypeTag], int]: """Internal recursive parser for struct type strings. - + This method handles the complex parsing of nested generic types, including proper handling of angle brackets and comma separators. - + Args: type_tag: The string to parse. index: Current parsing position. - + Returns: Tuple of (parsed type tags list, new index position). """ @@ -790,7 +799,7 @@ def _from_str_internal(type_tag: str, index: int) -> Tuple[List[TypeTag], int]: def variant(self): """Get the type discriminator for this tag. - + Returns: The STRUCT type discriminator. """ @@ -799,10 +808,10 @@ def variant(self): @staticmethod def deserialize(deserializer: Deserializer) -> StructTag: """Deserialize a StructTag from a BCS byte stream. - + Args: deserializer: The BCS deserializer to read from. - + Returns: The deserialized StructTag instance. """ @@ -814,7 +823,7 @@ def deserialize(deserializer: Deserializer) -> StructTag: def serialize(self, serializer: Serializer): """Serialize this StructTag to a BCS byte stream. - + Args: serializer: The BCS serializer to write to. """ @@ -826,10 +835,11 @@ def serialize(self, serializer: Serializer): class Test(unittest.TestCase): """Test suite for type tag functionality. - + Tests parsing, serialization, and string representation of complex nested struct types with multiple levels of generic type parameters. """ + def test_nested_structs(self): l0 = "0x0::l0::L0" l10 = "0x1::l10::L10" diff --git a/examples/common.py b/examples/common.py index 6c84733..816b7b8 100644 --- a/examples/common.py +++ b/examples/common.py @@ -27,12 +27,12 @@ - Node: https://api.devnet.aptoslabs.com/v1 - Faucet: https://faucet.devnet.aptoslabs.com - Indexer: https://api.devnet.aptoslabs.com/v1/graphql - + Testnet: - Node: https://api.testnet.aptoslabs.com/v1 - Faucet: https://faucet.testnet.aptoslabs.com - Indexer: https://api.testnet.aptoslabs.com/v1/graphql - + Mainnet: - Node: https://api.mainnet.aptoslabs.com/v1 - Faucet: N/A (no public faucet on mainnet) @@ -40,39 +40,39 @@ Usage Examples: Using default devnet configuration:: - + from examples.common import NODE_URL, FAUCET_URL from aptos_sdk.async_client import RestClient, FaucetClient - + # Connect to devnet by default rest_client = RestClient(NODE_URL) faucet_client = FaucetClient(FAUCET_URL, rest_client) - + Switching to testnet:: - + import os os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" - + # Now imports will use testnet URLs from examples.common import NODE_URL, FAUCET_URL - + Using with authentication token:: - + import os os.environ["FAUCET_AUTH_TOKEN"] = "your_faucet_token_here" - + from examples.common import FAUCET_URL, FAUCET_AUTH_TOKEN from aptos_sdk.async_client import FaucetClient, RestClient - + rest_client = RestClient(NODE_URL) faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) - + Development with local aptos-core:: - + import os os.environ["APTOS_CORE_PATH"] = "/path/to/your/aptos-core" - + from examples.common import APTOS_CORE_PATH # Use APTOS_CORE_PATH for local Move package compilation diff --git a/examples/hello_blockchain.py b/examples/hello_blockchain.py index ebe9cd1..45046dd 100644 --- a/examples/hello_blockchain.py +++ b/examples/hello_blockchain.py @@ -31,59 +31,59 @@ Prerequisites: Before running this example, deploy the hello_blockchain Move module: - + Using Aptos CLI:: - + # Install Aptos CLI if not already installed curl -fsSL "https://aptos.dev/scripts/install_cli.py" | python3 - + # Initialize your account aptos init - + # Navigate to the Move example cd ~/aptos-core/aptos-move/move-examples/hello_blockchain - + # Publish the module (replace with your address) aptos move publish --named-addresses hello_blockchain=***your_address*** - + Using this script:: - + # Option 1: Use the publish_contract function contract_addr = await publish_contract("./path/to/hello_blockchain") - + # Option 2: Run with existing contract python -m examples.hello_blockchain ***contract_address*** Usage Examples: Run with existing contract:: - + python -m examples.hello_blockchain ***0x123abc...*** - + Programmatic usage:: - + import asyncio from examples.hello_blockchain import main, publish_contract from aptos_sdk.account_address import AccountAddress - + # Deploy and run async def run_example(): # Option 1: Deploy new contract contract_addr = await publish_contract("./hello_blockchain") await main(contract_addr) - + # Option 2: Use existing contract existing_addr = AccountAddress.from_str("***0x123...***") await main(existing_addr) - + asyncio.run(run_example()) - + Custom network configuration:: - + import os # Switch to testnet os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" - + # Run example on testnet python -m examples.hello_blockchain ***contract_address*** @@ -93,22 +93,22 @@ async def run_example(): - Initial account balances after funding - Message storage and retrieval for both accounts - Transaction hashes and confirmations - + Example output:: - + === Addresses === Alice: ***0xabc123... Bob: ***0xdef456... - + === Initial Balances === Alice: 10000000 Bob: 10000000 - + === Testing Alice === Initial value: None Setting the message to "Hello, Blockchain" New value: {'message': 'Hello, Blockchain', 'message_change_events': {...}} - + === Testing Bob === Initial value: None Setting the message to "Hello, Blockchain" @@ -116,15 +116,15 @@ async def run_example(): Move Smart Contract Structure: The hello_blockchain.move file should contain:: - + module hello_blockchain::message { use std::string::String; use std::signer; - + struct MessageHolder has key { message: String, } - + public entry fun set_message(account: &signer, message: String) { let account_addr = signer::address_of(account); if (!exists(account_addr)) { @@ -149,7 +149,7 @@ async def run_example(): - Private keys are generated randomly and not persisted - All transactions are publicly visible on the blockchain - Smart contracts are immutable once deployed - + Learning Objectives: After running this example, you should understand: 1. How to create and fund Aptos accounts programmatically @@ -181,63 +181,63 @@ async def run_example(): class HelloBlockchainClient(RestClient): """Extended REST client with domain-specific methods for hello_blockchain contract. - + This class demonstrates how to extend the base RestClient to add application-specific functionality for interacting with a particular smart contract. It encapsulates the details of resource queries and transaction construction for the hello_blockchain Move module. - + Key Features: - **Resource Queries**: Simplified access to MessageHolder resources - **Transaction Construction**: Automated entry function payload creation - **Error Handling**: Graceful handling of missing resources - **Type Safety**: Proper typing for contract-specific operations - + Examples: Basic usage:: - + client = HelloBlockchainClient("https://api.devnet.aptoslabs.com/v1") - + # Read message (returns None if not set) message = await client.get_message(contract_addr, user_addr) - + # Set message (creates or updates MessageHolder resource) txn_hash = await client.set_message(contract_addr, account, "Hello!") await client.wait_for_transaction(txn_hash) - + Note: This pattern of extending RestClient is recommended for applications that interact with specific smart contracts frequently. It provides a clean abstraction over raw resource queries and transaction construction. """ - + async def get_message( self, contract_address: AccountAddress, account_address: AccountAddress ) -> Optional[Dict[str, Any]]: """Retrieve the MessageHolder resource for a specific account. - + This method queries the blockchain for the MessageHolder resource stored under the given account address. The resource is created by the hello_blockchain Move module when a user calls set_message for the first time. - + Args: contract_address: The address where the hello_blockchain module is published. account_address: The account address to query for the MessageHolder resource. - + Returns: Dictionary containing the MessageHolder resource data if it exists, including the 'message' field and any event handles. Returns None if the account has never called set_message. - + Examples: Query existing message:: - + message_data = await client.get_message(contract_addr, alice.address()) if message_data: print(f"Alice's message: {message_data['message']}") else: print("Alice hasn't set a message yet") - + Note: This method handles the ResourceNotFound exception gracefully by returning None, making it safe to call even for accounts that haven't @@ -254,44 +254,44 @@ async def set_message( self, contract_address: AccountAddress, sender: Account, message: str ) -> str: """Set or update the message in the sender's MessageHolder resource. - + This method constructs and submits a transaction that calls the set_message entry function in the hello_blockchain Move module. The function will either create a new MessageHolder resource (if this is the first call) or update the existing message. - + Args: contract_address: The address where the hello_blockchain module is published. sender: The account that will sign and send the transaction. message: The string message to store in the MessageHolder resource. - + Returns: The transaction hash as a string. Use wait_for_transaction() to confirm the transaction was processed successfully. - + Raises: ApiError: If the transaction submission fails due to network issues, insufficient funds, or other blockchain-related errors. - + Examples: Set a new message:: - + # Send transaction txn_hash = await client.set_message( - contract_addr, - alice, + contract_addr, + alice, "Hello, Aptos blockchain!" ) - + # Wait for confirmation result = await client.wait_for_transaction(txn_hash) print(f"Transaction successful: {result['success']}") - + Update existing message:: - + # This will update the existing MessageHolder resource await client.set_message(contract_addr, alice, "Updated message!") - + Note: The Move smart contract automatically handles whether to create a new MessageHolder resource or update an existing one. The gas cost is @@ -312,50 +312,50 @@ async def set_message( async def publish_contract(package_dir: str) -> AccountAddress: """Deploy the hello_blockchain Move package to the Aptos blockchain. - + This function demonstrates the complete smart contract deployment workflow: 1. Generate a new publisher account 2. Fund the account from the faucet 3. Compile the Move package using Aptos CLI 4. Extract compiled bytecode and metadata 5. Publish the package to the blockchain - + The deployment process creates a new account specifically for publishing the contract, which becomes the address where the hello_blockchain module is permanently stored on the blockchain. - + Args: package_dir: Path to the Move package directory containing Move.toml and the source files. Should contain the hello_blockchain module. - + Returns: AccountAddress of the deployed contract (same as publisher address). This address is used to interact with the contract functions. - + Raises: Exception: If Move compilation fails due to syntax errors or missing files. ApiError: If blockchain operations fail (funding, publishing, etc.). FileNotFoundError: If compiled bytecode files are not found after compilation. - + Examples: Deploy from local package:: - + contract_address = await publish_contract( "./aptos-move/move-examples/hello_blockchain" ) print(f"Contract deployed at: {contract_address}") - + Deploy and interact:: - + # Deploy the contract contract_addr = await publish_contract("./hello_blockchain") - + # Use the returned address for interactions client = HelloBlockchainClient(NODE_URL) txn = await client.set_message(contract_addr, account, "Hello!") - + Directory Structure Expected:: - + package_dir/ ├── Move.toml # Package configuration ├── sources/ @@ -365,19 +365,19 @@ async def publish_contract(package_dir: str) -> AccountAddress: ├── package-metadata.bcs └── bytecode_modules/ └── message.mv - + Move.toml Configuration:: - + [package] name = "Examples" version = "1.0.0" - + [addresses] hello_blockchain = "_" - + [dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", ... } - + Note: - The function generates a fresh account for each deployment - Named addresses are automatically resolved during compilation @@ -388,7 +388,7 @@ async def publish_contract(package_dir: str) -> AccountAddress: contract_publisher = Account.generate() rest_client = HelloBlockchainClient(NODE_URL) faucet_client = FaucetClient(FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN) - + # Fund the publisher account with enough APT for deployment await faucet_client.fund_account(contract_publisher.address(), 10_000_000) @@ -416,7 +416,7 @@ async def publish_contract(package_dir: str) -> AccountAddress: txn_hash = await package_publisher.publish_package( contract_publisher, metadata, [module] ) - + # Wait for deployment transaction to be confirmed await rest_client.wait_for_transaction(txn_hash) @@ -428,11 +428,11 @@ async def publish_contract(package_dir: str) -> AccountAddress: async def main(contract_address: AccountAddress): """Execute the hello_blockchain smart contract interaction demo. - + This function demonstrates a complete smart contract interaction workflow by creating test accounts, funding them, and showing how multiple users can interact with the deployed hello_blockchain contract independently. - + The demo showcases: 1. **Account Generation**: Create Alice and Bob accounts programmatically 2. **Faucet Funding**: Fund both accounts with test APT tokens @@ -440,12 +440,12 @@ async def main(contract_address: AccountAddress): 4. **Contract Interaction**: Each account sets their own message 5. **State Queries**: Read back the stored messages to verify success 6. **Resource Management**: Properly close network connections - + Args: contract_address: The address where the hello_blockchain module is deployed. This should be the address returned from publish_contract() or obtained from a previous deployment. - + Workflow: 1. Generate two test accounts (Alice and Bob) 2. Fund both accounts with 10 APT each from the faucet @@ -455,33 +455,33 @@ async def main(contract_address: AccountAddress): - Set a message using the smart contract - Query the updated state to verify the message was stored 5. Clean up network connections - + Examples: Run with deployed contract:: - + from aptos_sdk.account_address import AccountAddress - + contract_addr = AccountAddress.from_str("******bc123...***") await main(contract_addr) - + End-to-end deployment and interaction:: - + # Deploy first, then interact contract_addr = await publish_contract("./hello_blockchain") await main(contract_addr) - + Expected Behavior: - Alice and Bob can each store independent messages - Messages are persistent on the blockchain - Each account's MessageHolder resource is separate - All transactions should complete successfully - + Error Scenarios: - Contract not deployed at the given address - Faucet funding failures (network issues, rate limits) - Transaction failures (insufficient gas, network problems) - Resource query failures (node connectivity issues) - + Note: This function uses the extended HelloBlockchainClient which provides convenient methods for interacting with the specific smart contract. diff --git a/examples/multikey.py b/examples/multikey.py index 5cb8282..c5eb87d 100644 --- a/examples/multikey.py +++ b/examples/multikey.py @@ -17,20 +17,20 @@ Key Concepts: - **Multi-Key Account**: An account controlled by multiple cryptographic keys - - **Threshold Signatures**: Requires a minimum number of signatures (threshold) + - **Threshold Signatures**: Requires a minimum number of signatures (threshold) out of the total available keys to authorize transactions - - **Mixed Key Types**: Supports both secp256k1 (ECDSA) and Ed25519 keys in + - **Mixed Key Types**: Supports both secp256k1 (ECDSA) and Ed25519 keys in the same multi-key setup - - **Account Address Derivation**: Multi-key accounts have addresses derived + - **Account Address Derivation**: Multi-key accounts have addresses derived from the combined public keys and threshold Security Benefits: - **Distributed Control**: No single key can authorize transactions alone - - **Reduced Single Points of Failure**: Even if one key is compromised, + - **Reduced Single Points of Failure**: Even if one key is compromised, the account remains secure - - **Flexible Access Patterns**: Different combinations of signers can + - **Flexible Access Patterns**: Different combinations of signers can authorize transactions - - **Key Type Diversity**: Mixing different signature schemes provides + - **Key Type Diversity**: Mixing different signature schemes provides cryptographic diversity Workflow: @@ -111,71 +111,71 @@ async def main(): """ Demonstrate multi-key authentication and transaction signing on Aptos. - + This function showcases the complete workflow for creating a multi-signature account, funding it, and executing a transfer transaction that requires multiple signatures to authorize. - + The example creates a 2-of-3 multi-key account using mixed cryptographic key types (secp256k1 and Ed25519) and demonstrates how to: - + 1. **Setup Phase**: - Initialize REST and Faucet clients for network interaction - Generate 3 private keys of different types (2 secp256k1, 1 Ed25519) - Create a MultiPublicKey with threshold=2 (requires 2 signatures) - Derive the multi-key account address - Create a regular single-key account for Bob - + 2. **Funding Phase**: - Fund both Alice's multi-key account and Bob's account using faucet - Display initial balances for verification - + 3. **Transaction Phase**: - Construct an APT transfer transaction from Alice to Bob - Sign the transaction with 2 out of 3 available keys - Combine individual signatures into a MultiSignature - Create an AccountAuthenticator with MultiKeyAuthenticator - + 4. **Verification Phase**: - Verify each individual signature against its corresponding key - Verify the combined multi-signature against the multi-key - Verify the complete authenticator - + 5. **Submission Phase**: - Submit the signed transaction to the network - Wait for transaction confirmation - Display final balances to confirm the transfer - + Key Security Features: - **Threshold Security**: Requires 2 signatures out of 3 possible - **Cryptographic Diversity**: Uses both secp256k1 and Ed25519 keys - **Signature Verification**: Validates all signatures before submission - **Address Derivation**: Deterministically derives address from multi-key - + Network Requirements: - Active Aptos devnet/testnet connection - Faucet service availability for account funding - Sufficient network tokens for transaction fees - + Error Scenarios Handled: - Network connectivity issues during client operations - Transaction failures during submission or confirmation - Signature verification failures before submission - Account balance insufficiency for transfers - + Raises: Exception: If network operations fail, signature verification fails, or transaction submission encounters errors. - + Example Output: === Addresses === Multikey Alice: ***bcd123... Bob: ***456def... - + === Initial Balances === Alice: 100000000 Bob: 1 - + === Final Balances === Alice: 99999000 # Reduced by transfer amount + fees Bob: 1001 # Increased by transfer amount diff --git a/examples/rotate_key.py b/examples/rotate_key.py index 4b5dd0d..e211db1 100644 --- a/examples/rotate_key.py +++ b/examples/rotate_key.py @@ -33,7 +33,7 @@ The rotation process requires signatures from both: 1. **Current Key**: Proves current control of the account 2. **New Key**: Proves possession of the new private key - + This dual-signature requirement prevents unauthorized key rotations even if an attacker knows the new private key but not the current one. @@ -42,14 +42,14 @@ - Generate accounts (Alice as primary, Bob's key as rotation target) - Fund Alice's account for transaction fees - Display initial account states - + 2. **Single Key Rotation**: - Create rotation proof challenge with sequence number and addresses - Sign the challenge with both current (Alice) and new (Bob) keys - Submit rotation transaction to the blockchain - Verify authentication key change on-chain - Reconstruct Alice's account object with new private key - + 3. **Multi-Key Migration**: - Create multi-key setup combining multiple Ed25519 keys - Generate rotation proof for single-to-multi transition @@ -144,17 +144,17 @@ def truncate(address: str) -> str: """ Truncate a long address string for display purposes. - + Takes a long address string and returns a shortened version showing only the first 6 and last 6 characters, with "..." in between. This is useful for displaying addresses in formatted tables. - + Args: address: The full address string to truncate. - + Returns: A shortened string in the format "123abc...def456". - + Example: >>> truncate("***23456789abcdef") "***23...def" @@ -165,24 +165,24 @@ def truncate(address: str) -> str: def format_account_info(account: Account) -> str: """ Format account information for tabular display. - + Extracts key information from an Account object and formats it into a fixed-width string suitable for table display. Each field is truncated and left-justified to maintain consistent formatting. - + Args: account: The Account object to format. - + Returns: A formatted string containing truncated account information with consistent spacing for table display. - + The formatted string contains: - Account address (truncated) - Authentication key (truncated) - Private key hex representation (truncated) - Public key string representation (truncated) - + Example Output: "***bcd...456 ***def...789 abc123...xyz ed25519..." """ diff --git a/examples/transfer_coin.py b/examples/transfer_coin.py index d5c7bca..8d9cc4d 100644 --- a/examples/transfer_coin.py +++ b/examples/transfer_coin.py @@ -38,30 +38,30 @@ Examples: Run the basic transfer example:: - + python -m examples.transfer_coin - + Expected output shows: - Account addresses for Alice and Bob - Initial balances (Alice: 100,000,000 octas, Bob: 1 octa) - Balance changes after each 1,000 octa transfer - Transaction history from indexer (if available) - + Programmatic usage:: - + import asyncio from examples.transfer_coin import main - + # Run the transfer example asyncio.run(main()) - + Custom network configuration:: - + import os # Switch to testnet os.environ["APTOS_NODE_URL"] = "https://api.testnet.aptoslabs.com/v1" os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" - + # Run on testnet python -m examples.transfer_coin @@ -122,12 +122,12 @@ async def main(): """Execute the basic APT coin transfer demonstration. - + This function demonstrates the fundamental workflow for transferring APT tokens between accounts on the Aptos blockchain. It showcases account generation, faucet funding, balance tracking, and transaction execution using the most efficient BCS (Binary Canonical Serialization) format. - + The demo performs the following operations: 1. **Client Setup**: Initialize REST, Faucet, and optional Indexer clients 2. **Account Creation**: Generate Alice and Bob accounts with new key pairs @@ -137,74 +137,74 @@ async def main(): 6. **Balance Verification**: Track balance changes throughout the process 7. **History Query**: Optional indexer query for transaction history 8. **Cleanup**: Close all network connections properly - + Transaction Flow: Initial State: - Alice: 100,000,000 octas (100 APT from faucet) - Bob: 1 octa (minimal funding from faucet) - + After First Transfer (1,000 octas): - Alice: ~99,997,000 octas (100 APT - 1,000 - gas fees) - Bob: 1,001 octas (1 + 1,000 received) - + After Second Transfer (1,000 octas): - Alice: ~99,994,000 octas (previous - 1,000 - gas fees) - Bob: 2,001 octas (previous + 1,000 received) - + Technical Details: - **Transfer Method**: Uses `bcs_transfer()` for optimal performance - **Gas Management**: Automatic gas calculation and payment from sender - **Confirmation**: Synchronous waiting ensures transaction completion - **Error Handling**: Network operations may raise ApiError exceptions - **Balance Precision**: All amounts in octas (1 APT = 100,000,000 octas) - + Indexer Integration: If INDEXER_URL is configured, the function demonstrates: - GraphQL query construction for transaction history - Account-specific filtering using Bob's address - Coin activity data extraction (amounts, types, timestamps) - Assertion validation that transactions were recorded - + Expected Output:: - + === Addresses === Alice: ***abc123... Bob: ***def456... - + === Initial Balances === Alice: 100000000 Bob: 1 - + === Intermediate Balances === Alice: 99997000 # Approximate after gas fees Bob: 1001 - + === Final Balances === Alice: 99994000 # Approximate after second transfer Bob: 2001 - + Error Scenarios: - **Network Connectivity**: REST API or faucet unavailable - **Insufficient Funds**: Alice doesn't have enough for transfer + gas - **Invalid Configuration**: Malformed URLs in environment variables - **Indexer Issues**: GraphQL queries may fail if indexer is down - + Performance Notes: - **BCS Format**: More efficient than JSON transactions (~30% gas savings) - **Concurrent Operations**: Uses asyncio.gather for parallel balance queries - **Connection Pooling**: REST client reuses connections for efficiency - **Minimal Funding**: Bob gets only 1 octa to show exact transfer amounts - + Network Requirements: - Active internet connection - Access to Aptos devnet endpoints - Faucet service availability for account funding - Optional: Indexer service for transaction history queries - + Raises: ApiError: For network communication failures or blockchain errors Exception: For general application errors or configuration issues - + Note: This function is designed to be educational and uses devnet exclusively. All transactions are on test networks with no real monetary value. @@ -215,7 +215,7 @@ async def main(): faucet_client = FaucetClient( FAUCET_URL, rest_client, FAUCET_AUTH_TOKEN ) # <:!:section_1 - + # Optional indexer client for transaction history queries if INDEXER_URL and INDEXER_URL != "none": indexer_client = IndexerClient(INDEXER_URL) From a53202a72e07d32c4dbd932fbe10f4f59a3a4698 Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Mon, 8 Sep 2025 14:40:31 -0400 Subject: [PATCH 4/4] fix weird AI error --- aptos_sdk/account.py | 26 ++++++------ aptos_sdk/aptos_cli_wrapper.py | 28 ++++++------- aptos_sdk/aptos_token_client.py | 66 +++++++++++++++---------------- aptos_sdk/aptos_tokenv1_client.py | 2 +- aptos_sdk/async_client.py | 12 +++--- aptos_sdk/cli.py | 40 +++++++++---------- aptos_sdk/package_publisher.py | 6 +-- aptos_sdk/secp256k1_ecdsa.py | 60 ++++++++++++++-------------- aptos_sdk/transaction_worker.py | 2 +- aptos_sdk/transactions.py | 16 ++++---- examples/__init__.py | 2 +- examples/hello_blockchain.py | 16 ++++---- examples/multikey.py | 4 +- examples/rotate_key.py | 6 +-- examples/transfer_coin.py | 6 +-- 15 files changed, 146 insertions(+), 146 deletions(-) diff --git a/aptos_sdk/account.py b/aptos_sdk/account.py index 2583a73..69bcb5b 100644 --- a/aptos_sdk/account.py +++ b/aptos_sdk/account.py @@ -55,7 +55,7 @@ class Account: Load existing account:: # From hex private key - hex_key = "***1234567890abcdef..." + hex_key = "0x1234567890abcdef..." imported_account = Account.load_key(hex_key) # From JSON file @@ -292,7 +292,7 @@ def load_key(key: str) -> Account: Args: key: Hex-encoded Ed25519 private key string (64 characters, 32 bytes). - Can be with or without '***' prefix. + Can be with or without '0x' prefix. Returns: Account: An account instance created from the given private key. @@ -307,8 +307,8 @@ def load_key(key: str) -> Account: private_key_hex = "1a2b3c4d5e6f789..." # 64 hex chars account = Account.load_key(private_key_hex) - # With '***' prefix - prefixed_key = "***1a2b3c4d5e6f789..." + # With '0x' prefix + prefixed_key = "0x1a2b3c4d5e6f789..." account = Account.load_key(prefixed_key) Restore from backup:: @@ -328,7 +328,7 @@ def load_key(key: str) -> Account: # Import from Aptos CLI output # aptos init --profile my-account # aptos account list --profile my-account - cli_private_key = "***a1b2c3d4e5f6..." + cli_private_key = "0xa1b2c3d4e5f6..." account = Account.load_key(cli_private_key) Security Considerations: @@ -371,8 +371,8 @@ def load(path: str) -> Account: Expected JSON structure:: { - "account_address": "***1234567890abcdef...", - "private_key": "***abcdef1234567890..." + "account_address": "0x1234567890abcdef...", + "private_key": "0xabcdef1234567890..." } Examples: @@ -492,8 +492,8 @@ def store(self, path: str): Creates JSON with structure:: { - "account_address": "***", - "private_key": "***" + "account_address": "0x", + "private_key": "0x" } Note: @@ -524,7 +524,7 @@ def address(self) -> AccountAddress: account = Account.generate() address = account.address() print(f"Account address: {address}") - # Output: Account address: ***a1b2c3d4e5f67890... + # Output: Account address: 0xa1b2c3d4e5f67890... Use address in transactions:: @@ -555,7 +555,7 @@ async def check_balance(): - **Deterministic**: Same private key always produces same address - **Unique**: Each private key produces a unique address - **Immutable**: Address cannot change without changing the private key - - **Format**: 32-byte hex string with '***' prefix + - **Format**: 32-byte hex string with '0x' prefix Note: The address is computed from the public key, not stored separately. @@ -572,7 +572,7 @@ def auth_key(self) -> str: change through key rotation operations. Returns: - str: The authentication key as a hex string with '***' prefix. + str: The authentication key as a hex string with '0x' prefix. Examples: Check initial auth key:: @@ -662,7 +662,7 @@ def sign(self, data: bytes) -> asymmetric_crypto.Signature: data_dict = { "action": "transfer", "amount": 1000, - "recipient": "***abc123..." + "recipient": "0xabc123..." } data_bytes = json.dumps(data_dict, sort_keys=True).encode() signature = account.sign(data_bytes) diff --git a/aptos_sdk/aptos_cli_wrapper.py b/aptos_sdk/aptos_cli_wrapper.py index 73cc860..15a29cf 100644 --- a/aptos_sdk/aptos_cli_wrapper.py +++ b/aptos_sdk/aptos_cli_wrapper.py @@ -39,8 +39,8 @@ # Define named addresses named_addresses = { - "MyModule": AccountAddress.from_str("***1234..."), - "Treasury": AccountAddress.from_str("***5678...") + "MyModule": AccountAddress.from_str("0x1234..."), + "Treasury": AccountAddress.from_str("0x5678...") } # Compile the package @@ -164,8 +164,8 @@ class AptosCLIWrapper: from aptos_sdk.account_address import AccountAddress named_addresses = { - "admin": AccountAddress.from_str("***1"), - "user": AccountAddress.from_str("***2") + "admin": AccountAddress.from_str("0x1"), + "user": AccountAddress.from_str("0x2") } AptosCLIWrapper.compile_package( @@ -202,19 +202,19 @@ def prepare_named_addresses( Single address:: - >>> addresses = {"admin": AccountAddress.from_str("***1")} + >>> addresses = {"admin": AccountAddress.from_str("0x1")} >>> AptosCLIWrapper.prepare_named_addresses(addresses) - ['--named-addresses', 'admin=***1'] + ['--named-addresses', 'admin=0x1'] Multiple addresses:: >>> addresses = { - ... "admin": AccountAddress.from_str("***1"), - ... "user": AccountAddress.from_str("***2") + ... "admin": AccountAddress.from_str("0x1"), + ... "user": AccountAddress.from_str("0x2") ... } >>> args = AptosCLIWrapper.prepare_named_addresses(addresses) >>> args - ['--named-addresses', 'admin=***1,user=***2'] + ['--named-addresses', 'admin=0x1,user=0x2'] Note: The CLI expects named addresses in a comma-separated format after @@ -266,8 +266,8 @@ def compile_package(package_dir: str, named_addresses: Dict[str, AccountAddress] from aptos_sdk.account_address import AccountAddress named_addresses = { - "deployer": AccountAddress.from_str("***1234..."), - "resource_account": AccountAddress.from_str("***5678...") + "deployer": AccountAddress.from_str("0x1234..."), + "resource_account": AccountAddress.from_str("0x5678...") } AptosCLIWrapper.compile_package( @@ -396,8 +396,8 @@ def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): from aptos_sdk.account_address import AccountAddress test_addresses = { - "test_admin": AccountAddress.from_str("***cafe"), - "test_user": AccountAddress.from_str("***beef") + "test_admin": AccountAddress.from_str("0xcafe"), + "test_user": AccountAddress.from_str("0xbeef") } AptosCLIWrapper.test_package( @@ -420,7 +420,7 @@ def test_package(package_dir: str, named_addresses: Dict[str, AccountAddress]): // Test logic here } - #[test(admin = @***1, user = @***2)] + #[test(admin = @0x1, user = @0x2)] public fun test_with_addresses(admin: &signer, user: &signer) { // Test with specific signers } diff --git a/aptos_sdk/aptos_token_client.py b/aptos_sdk/aptos_token_client.py index 2209a5c..6533942 100644 --- a/aptos_sdk/aptos_token_client.py +++ b/aptos_sdk/aptos_token_client.py @@ -112,7 +112,7 @@ from aptos_sdk.account_address import AccountAddress # Soul-bound tokens cannot be transferred - recipient = AccountAddress.from_str("***abc123...") + recipient = AccountAddress.from_str("0x4abc123...") soul_bound_txn = await token_client.mint_soul_bound_token( creator=creator, @@ -127,7 +127,7 @@ Read token information:: # Read token details from blockchain - token_address = AccountAddress.from_str("***token_address...") + token_address = AccountAddress.from_str("0x4token_address...") token_data = await token_client.read_object(token_address) print(f"Token data: {token_data}") @@ -147,7 +147,7 @@ from aptos_sdk.account_address import AccountAddress # Transfer token to another account - recipient = AccountAddress.from_str("***recipient_address...") + recipient = AccountAddress.from_str("0x4recipient_address...") owner = Account.load("./token_owner.json") transfer_txn = await token_client.transfer_token( @@ -235,7 +235,7 @@ class Object: resource_data = { "allow_ungated_transfer": True, - "owner": "***abc123..." + "owner": "0x4abc123..." } obj = Object.parse(resource_data) print(f"Object owner: {obj.owner}") @@ -249,7 +249,7 @@ class Object: allow_ungated_transfer: bool owner: AccountAddress - struct_tag: str = "***::object::ObjectCore" + struct_tag: str = "0x1::object::ObjectCore" def __init__(self, allow_ungated_transfer, owner): self.allow_ungated_transfer = allow_ungated_transfer @@ -290,7 +290,7 @@ class Collection: Parse collection data from blockchain:: resource_data = { - "creator": "***abc123...", + "creator": "0x4abc123...", "description": "A collection of unique digital art pieces", "name": "Art Collection", "uri": "https://example.com/collection.json" @@ -308,7 +308,7 @@ class Collection: name: str uri: str - struct_tag: str = "***::collection::Collection" + struct_tag: str = "0x4::collection::Collection" def __init__(self, creator, description, name, uri): self.creator = creator @@ -360,7 +360,7 @@ class Royalty: resource_data = { "numerator": 500, "denominator": 10000, - "payee_address": "***abc123..." + "payee_address": "0x4abc123..." } royalty = Royalty.parse(resource_data) print(f"Royalty: {royalty}") # 5% royalty @@ -374,7 +374,7 @@ class Royalty: denominator: int payee_address: AccountAddress - struct_tag: str = "***::royalty::Royalty" + struct_tag: str = "0x4::royalty::Royalty" def __init__(self, numerator, denominator, payee_address): self.numerator = numerator @@ -417,7 +417,7 @@ class Token: Parse token data from blockchain:: resource_data = { - "collection": {"inner": "***collection_address..."}, + "collection": {"inner": "0x4collection_address..."}, "index": 42, "description": "A legendary sword with special powers", "name": "Legendary Sword #42", @@ -438,7 +438,7 @@ class Token: name: str uri: str - struct_tag: str = "***::token::Token" + struct_tag: str = "0x4::token::Token" def __init__( self, @@ -701,7 +701,7 @@ class PropertyMap: # These can be used directly in transaction calls # names = ["name", "level", "is_rare", "metadata", "damage"] - # types = ["***::string::String", "u64", "bool", "vector", "u32"] + # types = ["0x1::string::String", "u64", "bool", "vector", "u32"] # values = [b"...", b"...", b"...", b"...", b"..."] # BCS serialized Parse from blockchain data:: @@ -710,8 +710,8 @@ class PropertyMap: resource_data = { "inner": { "data": [ - {"key": "level", "value": {"type": 4, "value": "***64..."}}, - {"key": "rarity", "value": {"type": 9, "value": "***legendary"}} + {"key": "level", "value": {"type": 4, "value": "0x464..."}}, + {"key": "rarity", "value": {"type": 9, "value": "0x4legendary"}} ] } } @@ -745,7 +745,7 @@ class PropertyMap: properties: List[Property] - struct_tag: str = "***::property_map::PropertyMap" + struct_tag: str = "0x4::property_map::PropertyMap" def __init__(self, properties: List[Property]): self.properties = properties @@ -812,7 +812,7 @@ class ReadObject: from aptos_sdk.account_address import AccountAddress # Read object from blockchain - token_address = AccountAddress.from_str("***token_address...") + token_address = AccountAddress.from_str("0x4token_address...") read_object = await token_client.read_object(token_address) # Access different resource types @@ -857,9 +857,9 @@ class ReadObject: # This will show something like: # ReadObject - # ***::token::Token: Token[collection: ***abc..., name: Sword #1, ...] - # ***::property_map::PropertyMap: PropertyMap[Property[level, u64, 42], ...] - # ***::object::ObjectCore: Object[allow_ungated_transfer: True, owner: ***def...] + # 0x4::token::Token: Token[collection: 0x4abc..., name: Sword #1, ...] + # 0x4::property_map::PropertyMap: PropertyMap[Property[level, u64, 42], ...] + # 0x1::object::ObjectCore: Object[allow_ungated_transfer: True, owner: 0x4def...] Usage Patterns: Conditional resource access:: @@ -1218,9 +1218,9 @@ async def burn_token(self, creator: Account, token: AccountAddress) -> str: :raises ApiError: If transaction submission fails """ payload = EntryFunction.natural( - "***::aptos_token", + "0x4::aptos_token", "burn", - [TypeTag(StructTag.from_str("***::token::Token"))], + [TypeTag(StructTag.from_str("0x4::token::Token"))], [TransactionArgument(token, Serializer.struct)], ) @@ -1239,9 +1239,9 @@ async def freeze_token(self, creator: Account, token: AccountAddress) -> str: :raises ApiError: If transaction submission fails """ payload = EntryFunction.natural( - "***::aptos_token", + "0x4::aptos_token", "freeze_transfer", - [TypeTag(StructTag.from_str("***::token::Token"))], + [TypeTag(StructTag.from_str("0x4::token::Token"))], [TransactionArgument(token, Serializer.struct)], ) @@ -1260,9 +1260,9 @@ async def unfreeze_token(self, creator: Account, token: AccountAddress) -> str: :raises ApiError: If transaction submission fails """ payload = EntryFunction.natural( - "***::aptos_token", + "0x4::aptos_token", "unfreeze_transfer", - [TypeTag(StructTag.from_str("***::token::Token"))], + [TypeTag(StructTag.from_str("0x4::token::Token"))], [TransactionArgument(token, Serializer.struct)], ) @@ -1287,9 +1287,9 @@ async def add_token_property( transaction_arguments.extend(prop.to_transaction_arguments()) payload = EntryFunction.natural( - "***::aptos_token", + "0x4::aptos_token", "add_property", - [TypeTag(StructTag.from_str("***::token::Token"))], + [TypeTag(StructTag.from_str("0x4::token::Token"))], transaction_arguments, ) @@ -1316,9 +1316,9 @@ async def remove_token_property( ] payload = EntryFunction.natural( - "***::aptos_token", + "0x4::aptos_token", "remove_property", - [TypeTag(StructTag.from_str("***::token::Token"))], + [TypeTag(StructTag.from_str("0x4::token::Token"))], transaction_arguments, ) @@ -1343,9 +1343,9 @@ async def update_token_property( transaction_arguments.extend(prop.to_transaction_arguments()) payload = EntryFunction.natural( - "***::aptos_token", + "0x4::aptos_token", "update_property", - [TypeTag(StructTag.from_str("***::token::Token"))], + [TypeTag(StructTag.from_str("0x4::token::Token"))], transaction_arguments, ) @@ -1368,8 +1368,8 @@ async def tokens_minted_from_transaction( mints = [] for event in output["events"]: if event["type"] not in ( - "***::collection::MintEvent", - "***::collection::Mint", + "0x4::collection::MintEvent", + "0x4::collection::Mint", ): continue mints.append(AccountAddress.from_str_relaxed(event["data"]["token"])) diff --git a/aptos_sdk/aptos_tokenv1_client.py b/aptos_sdk/aptos_tokenv1_client.py index 470f641..0ad2f13 100644 --- a/aptos_sdk/aptos_tokenv1_client.py +++ b/aptos_sdk/aptos_tokenv1_client.py @@ -74,7 +74,7 @@ from aptos_sdk.account_address import AccountAddress - recipient_address = AccountAddress.from_str("***recipient...") + recipient_address = AccountAddress.from_str("") recipient = Account.load("./recipient.json") # Offer token to recipient diff --git a/aptos_sdk/async_client.py b/aptos_sdk/async_client.py index 2f5dcf4..ef42145 100644 --- a/aptos_sdk/async_client.py +++ b/aptos_sdk/async_client.py @@ -54,7 +54,7 @@ client = RestClient("https://fullnode.devnet.aptoslabs.com/v1", config) # Query account information - address = AccountAddress.from_str("***123...") + address = AccountAddress.from_str("0x123...") account_info = await client.account(address) balance = await client.account_balance(address) @@ -69,7 +69,7 @@ # Create sender account sender = Account.generate() - recipient = AccountAddress.from_str("***456...") + recipient = AccountAddress.from_str("0x456...") # Transfer 1 APT (1 * 10^8 octas) txn_hash = await client.bcs_transfer( @@ -1493,7 +1493,7 @@ async def bcs_transfer( ] payload = EntryFunction.natural( - "***::aptos_account", + "0x1::aptos_account", "transfer", [], transaction_arguments, @@ -1520,7 +1520,7 @@ async def transfer_coins( :param sender: The account sending the coins :param recipient: Address of the account to receive the coins - :param coin_type: The fully qualified coin type (e.g., "***::usdc::USDC") + :param coin_type: The fully qualified coin type (e.g., "0x123456::usdc::USDC") :param amount: Amount of coins to transfer (in the coin's base units) :param sequence_number: Specific sequence number, or None to fetch from chain :return: The transaction hash as a hex string @@ -1532,7 +1532,7 @@ async def transfer_coins( ] payload = EntryFunction.natural( - "***::aptos_account", + "0x1::aptos_account", "transfer_coins", [TypeTag(StructTag.from_str(coin_type))], transaction_arguments, @@ -1564,7 +1564,7 @@ async def transfer_object( ] payload = EntryFunction.natural( - "***::object", + "0x1::object", "transfer_call", [], transaction_arguments, diff --git a/aptos_sdk/cli.py b/aptos_sdk/cli.py index b2d0afc..290580b 100644 --- a/aptos_sdk/cli.py +++ b/aptos_sdk/cli.py @@ -32,7 +32,7 @@ python -m aptos_sdk.cli publish-package \ --package-dir ./my-move-package \ - --account ***1234... \ + --account 0x1234... \ --private-key-path ./private_key.txt \ --rest-api https://fullnode.devnet.aptoslabs.com/v1 @@ -40,11 +40,11 @@ python -m aptos_sdk.cli publish-package \ --package-dir ./my-move-package \ - --account ***1234... \ + --account 0x1234... \ --private-key-path ./private_key.txt \ --rest-api https://fullnode.devnet.aptoslabs.com/v1 \ - --named-address my_addr=***5678... \ - --named-address other_addr=***9abc... + --named-address my_addr=0x5678... \ + --named-address other_addr=0x9abc... Programmatic usage:: @@ -55,7 +55,7 @@ await main([ 'publish-package', '--package-dir', './my-package', - '--account', '***1234...', + '--account', '0x1234...', '--private-key-path', './key.txt', '--rest-api', 'https://fullnode.devnet.aptoslabs.com/v1' ]) @@ -69,11 +69,11 @@ # Direct function call private_key = PrivateKey.from_str("ed25519-priv-...") - account = Account(AccountAddress.from_str("***123..."), private_key) + account = Account(AccountAddress.from_str("0x123..."), private_key) await publish_package( package_dir="./my-package", - named_addresses={"MyModule": AccountAddress.from_str("***456...")}, + named_addresses={"MyModule": AccountAddress.from_str("0x456...")}, signer=account, rest_api="https://fullnode.devnet.aptoslabs.com/v1" ) @@ -155,7 +155,7 @@ async def publish_package( # Create account from private key private_key = PrivateKey.from_str("ed25519-priv-...") - account = Account(AccountAddress.from_str("***123..."), private_key) + account = Account(AccountAddress.from_str("0x123..."), private_key) # Publish package await publish_package( @@ -168,8 +168,8 @@ async def publish_package( Package with named addresses:: named_addresses = { - "MyContract": AccountAddress.from_str("***456..."), - "Treasury": AccountAddress.from_str("***789...") + "MyContract": AccountAddress.from_str("0x456..."), + "Treasury": AccountAddress.from_str("0x789...") } await publish_package( @@ -214,16 +214,16 @@ def key_value(indata: str) -> Tuple[str, AccountAddress]: Examples: Parse named address:: - >>> name, addr = key_value("MyContract=***1234...") + >>> name, addr = key_value("MyContract=0x1234...") >>> print(f"Name: {name}, Address: {addr}") - Name: MyContract, Address: ***1234... + Name: MyContract, Address: 0x1234... Multiple named addresses:: named_pairs = [ - key_value("TokenContract=***1111..."), - key_value("Treasury=***2222..."), - key_value("Admin=***3333...") + key_value("TokenContract=0x1111..."), + key_value("Treasury=0x2222..."), + key_value("Admin=0x3333...") ] # Convert to dictionary @@ -231,8 +231,8 @@ def key_value(indata: str) -> Tuple[str, AccountAddress]: Command-line usage:: - --named-address MyContract=***1234... \ - --named-address Treasury=***5678... + --named-address MyContract=0x1234... \ + --named-address Treasury=0x5678... Note: This function is primarily used by the argument parser to convert @@ -264,7 +264,7 @@ async def main(args: List[str]): python -m aptos_sdk.cli publish-package \ --package-dir ./my-package \ - --account ***1234... \ + --account 0x1234... \ --private-key-path ./key.txt \ --rest-api https://fullnode.devnet.aptoslabs.com/v1 @@ -276,10 +276,10 @@ async def main(args: List[str]): await main([= 'publish-package', '--package-dir', './package', - '--account', '***1234...', + '--account', '0x1234...', '--private-key-path', './key.txt', '--rest-api', 'https://fullnode.devnet.aptoslabs.com/v1', - '--named-address', 'MyAddr=***5678...' + '--named-address', 'MyAddr=0x5678...' ]) Supported Commands: diff --git a/aptos_sdk/package_publisher.py b/aptos_sdk/package_publisher.py index 24685e7..ff553de 100644 --- a/aptos_sdk/package_publisher.py +++ b/aptos_sdk/package_publisher.py @@ -86,7 +86,7 @@ Package upgrade workflow:: # Identify the object to upgrade - code_object = AccountAddress.from_str("***existing_object_address") + code_object = AccountAddress.from_str("") # Deploy upgrade txn_hashes = await publisher.publish_package_in_path( @@ -260,7 +260,7 @@ class PackagePublisher: from aptos_sdk.account_address import AccountAddress # Address of the existing code object - code_object = AccountAddress.from_str("***abcdef...") + code_object = AccountAddress.from_str("0xabcdef...") # Publish the upgrade txn_hashes = await publisher.publish_package_in_path( @@ -548,7 +548,7 @@ async def publish_package_in_path( sender=account, package_dir="./updated_package", publish_mode=PublishMode.OBJECT_UPGRADE, - code_object=AccountAddress.from_str("***abcdef...") + code_object=AccountAddress.from_str("0xabcdef...") ) Note: diff --git a/aptos_sdk/secp256k1_ecdsa.py b/aptos_sdk/secp256k1_ecdsa.py index 4800e2c..9fb1365 100644 --- a/aptos_sdk/secp256k1_ecdsa.py +++ b/aptos_sdk/secp256k1_ecdsa.py @@ -56,7 +56,7 @@ Working with hex strings:: # Create from hex string - hex_key = "***234abcd..." + hex_key = "0x1234abcd..." private_key = PrivateKey.from_hex(hex_key) # Get hex representation @@ -67,7 +67,7 @@ AIP-80 compliant formatting:: # AIP-80 formatted private key - aip80_key = "secp256k1-priv-***234abcd..." + aip80_key = "secp256k1-priv-0x1234abcd..." private_key = PrivateKey.from_str(aip80_key, strict=True) # Convert to AIP-80 format @@ -92,7 +92,7 @@ Cross-chain compatibility:: # Import Ethereum private key - ethereum_key = "***456789abcdef..." + ethereum_key = "0x123456789abcdef..." aptos_key = PrivateKey.from_hex(ethereum_key) # Same key can be used on both chains @@ -150,12 +150,12 @@ class PrivateKey(asymmetric_crypto.PrivateKey): Create from existing key material:: - hex_key = "***234567890abcdef..." + hex_key = "0x1234567890abcdef..." private_key = PrivateKey.from_hex(hex_key) Create from AIP-80 format:: - aip80_key = "secp256k1-priv-***234567890abcdef..." + aip80_key = "secp256k1-priv-0x1234567890abcdef..." private_key = PrivateKey.from_str(aip80_key, strict=True) Sign and verify:: @@ -184,7 +184,7 @@ def __init__(self, key: SigningKey): Example: This is typically not called directly. Use the factory methods: >>> private_key = PrivateKey.random() - >>> private_key = PrivateKey.from_hex("***abc123...") + >>> private_key = PrivateKey.from_hex("0xabc123...") """ self.key = key @@ -198,8 +198,8 @@ def __eq__(self, other: object): True if both private keys are cryptographically equivalent. Example: - >>> key1 = PrivateKey.from_hex("***abc123...") - >>> key2 = PrivateKey.from_hex("***abc123...") + >>> key1 = PrivateKey.from_hex("0xabc123...") + >>> key2 = PrivateKey.from_hex("0xabc123...") >>> key1 == key2 True """ @@ -215,7 +215,7 @@ def __str__(self): Example: >>> str(private_key) - 'secp256k1-priv-***234567890abcdef...' + 'secp256k1-priv-0x1234567890abcdef...' """ return self.aip80() @@ -228,10 +228,10 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: Args: value: Private key in various formats: - - Raw hex string: "***234567890abcdef..." - - Hex with prefix: "***234567890abcdef..." - - Raw bytes: bytes.fromhex("234567890abcdef...") - - AIP-80 format: "secp256k1-priv-***234567890abcdef..." + - Raw hex string: "0x1234567890abcdef..." + - Hex with prefix: "0x1234567890abcdef..." + - Raw bytes: bytes.fromhex("0x1234567890abcdef...") + - AIP-80 format: "secp256k1-priv-0x1234567890abcdef..." strict: AIP-80 compliance mode: - True: Only accept AIP-80 compliant strings - False: Accept legacy formats without warning @@ -247,12 +247,12 @@ def from_hex(value: str | bytes, strict: bool | None = None) -> PrivateKey: Examples: From raw hex:: - key = PrivateKey.from_hex("***234567890abcdef...") + key = PrivateKey.from_hex("0x1234567890abcdef...") From AIP-80 format:: key = PrivateKey.from_hex( - "secp256k1-priv-***234567890abcdef...", + "secp256k1-priv-0x1234567890abcdef...", strict=True ) @@ -287,8 +287,8 @@ def from_str(value: str, strict: bool | None = None) -> PrivateKey: A new secp256k1 PrivateKey instance. Example: - >>> key = PrivateKey.from_str("secp256k1-priv-***abc123...") - >>> key = PrivateKey.from_str("***abc123...", strict=False) + >>> key = PrivateKey.from_str("secp256k1-priv-0xabc123...") + >>> key = PrivateKey.from_str("0xabc123...", strict=False) """ return PrivateKey.from_hex(value, strict) @@ -300,9 +300,9 @@ def hex(self) -> str: Example: >>> private_key.hex() - '***abc123456789def...' + '0xabc123456789def...' """ - return f"***{self.key.to_string().hex()}" + return f"0x{self.key.to_string().hex()}" def aip80(self) -> str: """Get the AIP-80 compliant string representation. @@ -312,7 +312,7 @@ def aip80(self) -> str: Example: >>> private_key.aip80() - 'secp256k1-priv-***abc123456789def...' + 'secp256k1-priv-0xabc123456789def...' """ return PrivateKey.format_private_key( self.hex(), asymmetric_crypto.PrivateKeyVariant.Secp256k1 @@ -460,7 +460,7 @@ class PublicKey(asymmetric_crypto.PublicKey): Create from hex string:: # With or without 0x04 prefix - hex_key = "***4..." # 65 bytes with prefix + hex_key = "0x4..." # 65 bytes with prefix public_key = PublicKey.from_str(hex_key) Verify a signature:: @@ -522,7 +522,7 @@ def __str__(self) -> str: Example: >>> str(public_key) - '***4...' # 65 bytes with 0x04 prefix + '0x4...' # 65 bytes with 0x04 prefix """ return self.hex() @@ -545,7 +545,7 @@ def from_str(value: str) -> PublicKey: From uncompressed format with prefix:: # 130 hex chars (65 bytes) with 0x04 prefix - key = PublicKey.from_str("***4210c9129e...") + key = PublicKey.from_str("0x4210c9129e...") From raw format:: @@ -572,12 +572,12 @@ def hex(self) -> str: Example: >>> public_key.hex() - '***4210c9129e35337ff5d6488f90f18d842cf...' # 65 bytes with prefix + '0x4210c9129e35337ff5d6488f90f18d842cf...' # 65 bytes with prefix Note: The '0x04' prefix indicates an uncompressed public key format. """ - return f"***4{self.key.to_string().hex()}" + return f"0x04{self.key.to_string().hex()}" def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool: """Verify a signature against this public key. @@ -705,7 +705,7 @@ class Signature(asymmetric_crypto.Signature): Create from hex string:: - sig_hex = "***1234abcd..." + sig_hex = "0x1234abcd..." signature = Signature.from_str(sig_hex) Verify with public key:: @@ -763,7 +763,7 @@ def __str__(self) -> str: Example: >>> str(signature) - '***c9a34d6...' # 64 bytes + '0xc9a34d6...' # 64 bytes """ return self.hex() @@ -775,9 +775,9 @@ def hex(self) -> str: Example: >>> signature.hex() - '***a1b2c3d4...' # 64 bytes as hex + '0xa1b2c3d4...' # 64 bytes as hex """ - return f"***{self.signature.hex()}" + return f"{self.signature.hex()}" @staticmethod def from_str(value: str) -> Signature: @@ -795,7 +795,7 @@ def from_str(value: str) -> Signature: Exception: If the signature length is invalid. Example: - >>> sig = Signature.from_str("***a1b2c3d4...") + >>> sig = Signature.from_str("0xa1b2c3d4...") >>> len(sig.data()) 64 """ diff --git a/aptos_sdk/transaction_worker.py b/aptos_sdk/transaction_worker.py index 49d33b9..0551fe6 100644 --- a/aptos_sdk/transaction_worker.py +++ b/aptos_sdk/transaction_worker.py @@ -488,7 +488,7 @@ class TransactionQueue: # Push transaction payloads transfer_payload = EntryFunction.natural( - "***::aptos_account", + "0x1::aptos_account", "transfer", [], [recipient_address, amount] diff --git a/aptos_sdk/transactions.py b/aptos_sdk/transactions.py index dd77d2b..0e08e32 100644 --- a/aptos_sdk/transactions.py +++ b/aptos_sdk/transactions.py @@ -46,7 +46,7 @@ # Create transfer payload transfer_payload = EntryFunction.natural( - "***::aptos_account", + "0x1::aptos_account", "transfer", [], [recipient, 1_000_000] # 1 APT in octas @@ -309,7 +309,7 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): # Create transfer payload payload = EntryFunction.natural( - "***::aptos_account", + "0x1::aptos_account", "transfer", [], [recipient, 1_000_000] # 1 APT @@ -334,9 +334,9 @@ class RawTransaction(Deserializable, RawTransactionInternal, Serializable): # Call custom module function contract_payload = EntryFunction.natural( - "***abc123::my_module", + "0xabc123::my_module", "custom_function", - ["***::aptos_coin::AptosCoin"], # Type arguments + ["0x1::aptos_coin::AptosCoin"], # Type arguments [1000, "hello world", True] # Function arguments ) @@ -465,7 +465,7 @@ def __init__( # Create transfer payload payload = EntryFunction.natural( - "***::aptos_account", "transfer", [], + "0x1::aptos_account", "transfer", [], [recipient_address, 1_000_000] ) @@ -484,9 +484,9 @@ def __init__( # Contract interaction with type arguments contract_call = EntryFunction.natural( - "***deadbeef::defi_module", + "0xdeadbeef::defi_module", "swap_tokens", - ["***::aptos_coin::AptosCoin", "***::test_coin::TestCoin"], + ["0x1::aptos_coin::AptosCoin", "0x12345::test_coin::TestCoin"], [input_amount, min_output_amount, slippage_tolerance] ) @@ -955,7 +955,7 @@ def natural( """ Create an EntryFunction from natural string representation. - :param module: Module name in string format (e.g., "***::coin") + :param module: Module name in string format (e.g., "0x1::coin") :param function: Function name :param ty_args: Type arguments for the function :param args: Transaction arguments to be encoded diff --git a/examples/__init__.py b/examples/__init__.py index 0104349..ab1fa00 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -28,7 +28,7 @@ Most examples can be run directly from the command line:: # Basic blockchain interaction - python -m examples.hello_blockchain ***contract_address*** + python -m examples.hello_blockchain # NFT operations python -m examples.aptos_token diff --git a/examples/hello_blockchain.py b/examples/hello_blockchain.py index 45046dd..7220a7e 100644 --- a/examples/hello_blockchain.py +++ b/examples/hello_blockchain.py @@ -44,7 +44,7 @@ cd ~/aptos-core/aptos-move/move-examples/hello_blockchain # Publish the module (replace with your address) - aptos move publish --named-addresses hello_blockchain=***your_address*** + aptos move publish --named-addresses hello_blockchain= Using this script:: @@ -52,12 +52,12 @@ contract_addr = await publish_contract("./path/to/hello_blockchain") # Option 2: Run with existing contract - python -m examples.hello_blockchain ***contract_address*** + python -m examples.hello_blockchain Usage Examples: Run with existing contract:: - python -m examples.hello_blockchain ***0x123abc...*** + python -m examples.hello_blockchain 0x123abc... Programmatic usage:: @@ -72,7 +72,7 @@ async def run_example(): await main(contract_addr) # Option 2: Use existing contract - existing_addr = AccountAddress.from_str("***0x123...***") + existing_addr = AccountAddress.from_str("0x123...") await main(existing_addr) asyncio.run(run_example()) @@ -85,7 +85,7 @@ async def run_example(): os.environ["APTOS_FAUCET_URL"] = "https://faucet.testnet.aptoslabs.com" # Run example on testnet - python -m examples.hello_blockchain ***contract_address*** + python -m examples.hello_blockchain Expected Output: The script will display: @@ -97,8 +97,8 @@ async def run_example(): Example output:: === Addresses === - Alice: ***0xabc123... - Bob: ***0xdef456... + Alice: 0xabc123... + Bob: 0xdef456... === Initial Balances === Alice: 10000000 @@ -461,7 +461,7 @@ async def main(contract_address: AccountAddress): from aptos_sdk.account_address import AccountAddress - contract_addr = AccountAddress.from_str("******bc123...***") + contract_addr = AccountAddress.from_str("0xabc123...") await main(contract_addr) End-to-end deployment and interaction:: diff --git a/examples/multikey.py b/examples/multikey.py index c5eb87d..147ffb7 100644 --- a/examples/multikey.py +++ b/examples/multikey.py @@ -169,8 +169,8 @@ async def main(): Example Output: === Addresses === - Multikey Alice: ***bcd123... - Bob: ***456def... + Multikey Alice: 0xbcd123... + Bob: 0x456def... === Initial Balances === Alice: 100000000 diff --git a/examples/rotate_key.py b/examples/rotate_key.py index e211db1..bbcae8e 100644 --- a/examples/rotate_key.py +++ b/examples/rotate_key.py @@ -156,8 +156,8 @@ def truncate(address: str) -> str: A shortened string in the format "123abc...def456". Example: - >>> truncate("***23456789abcdef") - "***23...def" + >>> truncate("0x123456789abcdef") + "0x123...def" """ return address[0:6] + "..." + address[-6:] @@ -184,7 +184,7 @@ def format_account_info(account: Account) -> str: - Public key string representation (truncated) Example Output: - "***bcd...456 ***def...789 abc123...xyz ed25519..." + "0xbcd...456 0xdef...789 abc123...xyz ed25519..." """ vals = [ str(account.address()), diff --git a/examples/transfer_coin.py b/examples/transfer_coin.py index 8d9cc4d..2c3a5b0 100644 --- a/examples/transfer_coin.py +++ b/examples/transfer_coin.py @@ -69,7 +69,7 @@ - **Unit**: APT tokens are measured in "octas" (1 APT = 100,000,000 octas) - **Precision**: 8 decimal places (similar to Bitcoin's satoshis) - **Gas**: Transaction fees are paid in APT and deducted automatically - - **Type**: APT is represented as "***::aptos_coin::AptosCoin" on-chain + - **Type**: APT is represented as "0x1::aptos_coin::AptosCoin" on-chain Indexer Integration: If an indexer URL is configured, the example demonstrates: @@ -168,8 +168,8 @@ async def main(): Expected Output:: === Addresses === - Alice: ***abc123... - Bob: ***def456... + Alice: 0xabc123... + Bob: 0xdef456... === Initial Balances === Alice: 100000000