diff --git a/developer-docs-site/docs/guides/transaction-management.md b/developer-docs-site/docs/guides/transaction-management.md index 172bcece2b0ef..cf4b882590d0f 100644 --- a/developer-docs-site/docs/guides/transaction-management.md +++ b/developer-docs-site/docs/guides/transaction-management.md @@ -38,7 +38,12 @@ Each transaction requires a distinct sequence number that is sequential to previ In parallel, monitor new transactions submitted. Once the earliest transaction expiration time has expired synchronize up to that transaction. Then repeat the process for the next transaction. -If there is any failure, wait until all outstanding transactions have timed out and leave it to the application to decide how to proceed, e.g., replay failed transactions. +If there is any failure, wait until all outstanding transactions have timed out and leave it to the application to decide how to proceed, e.g., replay failed transactions. The best method for waiting for outstanded transactions is first to query the ledger timestamp and ensure it is at least elapsed the maximum timeout from the last transactions submit time. From there, validate with mempool that all transactions since the last known committed transaction are either committed or no longer exist within the mmempool. This can be done by querying the REST API for transactions of a specific account, specifying the currently being evaluated sequence number and setting a limit to 1. Once these checks are complete, the local transaction number can be resynchronized. + +These failure handling steps are critical for the following reasons: +* Mempool does not immediate evict expired transactions. +* A new transaction cannot overwrite an existing transaction, even if it is expired. +* Consensus, i.e., the ledger timestamp, dictates expirations, the local node will only expire after it sees a committed timestamp after the transactions expiration time and a garbage collection has happened. ### Managing Transactions diff --git a/ecosystem/python/sdk/Makefile b/ecosystem/python/sdk/Makefile index 4096c287621ba..48cf36dae9907 100644 --- a/ecosystem/python/sdk/Makefile +++ b/ecosystem/python/sdk/Makefile @@ -4,14 +4,18 @@ test: poetry run python -m unittest discover -s aptos_sdk/ -p '*.py' -t .. +test-coverage: + poetry run python -m coverage run -m unittest discover -s aptos_sdk/ -p '*.py' -t .. + poetry run python -m coverage report + fmt: find ./examples ./aptos_sdk *.py -type f -name "*.py" | xargs poetry run autoflake -i -r --remove-all-unused-imports --remove-unused-variables --ignore-init-module-imports - poetry run isort aptos_sdk examples setup.py - poetry run black aptos_sdk examples setup.py + poetry run isort aptos_sdk examples + poetry run black aptos_sdk examples lint: - poetry run mypy aptos_sdk - - poetry run flake8 aptos_sdk examples setup.py + - poetry run flake8 aptos_sdk examples examples: poetry run python -m examples.async-read-aggregator diff --git a/ecosystem/python/sdk/aptos_sdk/account_sequence_number.py b/ecosystem/python/sdk/aptos_sdk/account_sequence_number.py new file mode 100644 index 0000000000000..be6343428426c --- /dev/null +++ b/ecosystem/python/sdk/aptos_sdk/account_sequence_number.py @@ -0,0 +1,214 @@ +# Copyright © Aptos Foundation +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import asyncio +import logging +from typing import Callable, Optional + +from aptos_sdk.account_address import AccountAddress +from aptos_sdk.async_client import ApiError, RestClient + + +class AccountSequenceNumberConfig: + """Common configuration for account number generation""" + + maximum_in_flight: int = 100 + maximum_wait_time: int = 30 + sleep_time: float = 0.01 + + +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. + """ + + _client: RestClient + _account: AccountAddress + _lock: asyncio.Lock + + _maximum_in_flight: int = 100 + _maximum_wait_time: int = 30 + _sleep_time: float = 0.01 + + _last_committed_number: Optional[int] + _current_number: Optional[int] + + def __init__( + self, + client: RestClient, + account: AccountAddress, + config: AccountSequenceNumberConfig = AccountSequenceNumberConfig(), + ): + self._client = client + self._account = account + self._lock = asyncio.Lock() + + self._last_uncommitted_number = None + self._current_number = None + + self._maximum_in_flight = config.maximum_in_flight + self._maximum_wait_time = config.maximum_wait_time + 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. + """ + async with self._lock: + if self._last_uncommitted_number is None or self._current_number is None: + await self._initialize() + # If there are more than self._maximum_in_flight in flight, wait for a slot. + # Or at least check to see if there is a slot and exit if in non-blocking mode. + if ( + self._current_number - self._last_uncommitted_number + >= self._maximum_in_flight + ): + await self._update() + if ( + self._current_number - self._last_uncommitted_number + >= self._maximum_in_flight + ): + if not block: + return None + await self._resync( + lambda acn: acn._current_number - acn._last_uncommitted_number + >= acn._maximum_in_flight + ) + + next_number = self._current_number + self._current_number += 1 + return next_number + + async def _initialize(self): + """Optional initializer. called by next_sequence_number if not called prior.""" + 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. + """ + async with self._lock: + await self._update() + await self._resync( + lambda acn: acn._last_uncommitted_number != acn._current_number + ) + + async def _resync(self, check: Callable[[AccountSequenceNumber], bool]): + """Forces a resync with the upstream, this should be called within the lock""" + start_time = await self._client.current_timestamp() + failed = False + while check(self): + ledger_time = await self._client.current_timestamp() + if ledger_time - start_time > self._maximum_wait_time: + logging.warn( + f"Waited over {self._maximum_wait_time} seconds for a transaction to commit, resyncing {self._account}" + ) + failed = True + break + else: + await asyncio.sleep(self._sleep_time) + await self._update() + if not failed: + return + for seq_num in range(self._last_uncommitted_number + 1, self._current_number): + while True: + try: + result = ( + await self._client.account_transaction_sequence_number_status( + self._account, seq_num + ) + ) + if result: + break + except ApiError as error: + if error.status_code == 404: + break + raise + await self._initialize() + + async def _update(self): + self._last_uncommitted_number = await self._current_sequence_number() + return self._last_uncommitted_number + + async def _current_sequence_number(self) -> int: + return await self._client.account_sequence_number(self._account) + + +import unittest +import unittest.mock + + +class Test(unittest.IsolatedAsyncioTestCase): + 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 + """ + patcher = unittest.mock.patch( + "aptos_sdk.async_client.RestClient.account_sequence_number", return_value=0 + ) + patcher.start() + + rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + account_sequence_number = AccountSequenceNumber( + rest_client, AccountAddress.from_hex("b0b") + ) + last_seq_num = 0 + for seq_num in range(5): + last_seq_num = await account_sequence_number.next_sequence_number() + self.assertEqual(last_seq_num, seq_num) + + patcher.stop() + patcher = unittest.mock.patch( + "aptos_sdk.async_client.RestClient.account_sequence_number", return_value=5 + ) + patcher.start() + + for seq_num in range(AccountSequenceNumber._maximum_in_flight): + last_seq_num = await account_sequence_number.next_sequence_number() + self.assertEqual(last_seq_num, seq_num + 5) + + self.assertEqual( + await account_sequence_number.next_sequence_number(block=False), None + ) + next_sequence_number = last_seq_num + 1 + patcher.stop() + patcher = unittest.mock.patch( + "aptos_sdk.async_client.RestClient.account_sequence_number", + return_value=next_sequence_number, + ) + patcher.start() + + self.assertNotEqual(account_sequence_number._current_number, last_seq_num) + await account_sequence_number.synchronize() + self.assertEqual(account_sequence_number._current_number, next_sequence_number) diff --git a/ecosystem/python/sdk/aptos_sdk/aptos_token_client.py b/ecosystem/python/sdk/aptos_sdk/aptos_token_client.py index 330e0591783f3..777eef2f51b68 100644 --- a/ecosystem/python/sdk/aptos_sdk/aptos_token_client.py +++ b/ecosystem/python/sdk/aptos_sdk/aptos_token_client.py @@ -338,9 +338,7 @@ async def read_object(self, address: AccountAddress) -> ReadObject: resources[resource_obj] = resource_obj.parse(resource["data"]) return ReadObject(resources) - async def create_collection( - self, - creator: Account, + def create_collection_payload( description: str, max_supply: int, name: str, @@ -356,7 +354,7 @@ async def create_collection( tokens_freezable_by_creator: bool, royalty_numerator: int, royalty_denominator: int, - ) -> str: + ) -> TransactionPayload: transaction_arguments = [ TransactionArgument(description, Serializer.str), TransactionArgument(max_supply, Serializer.u64), @@ -382,20 +380,56 @@ async def create_collection( transaction_arguments, ) + return TransactionPayload(payload) + + async def create_collection( + self, + creator: Account, + description: str, + max_supply: int, + name: str, + uri: str, + mutable_description: bool, + mutable_royalty: bool, + mutable_uri: bool, + mutable_token_description: bool, + mutable_token_name: bool, + mutable_token_properties: bool, + mutable_token_uri: bool, + tokens_burnable_by_creator: bool, + tokens_freezable_by_creator: bool, + royalty_numerator: int, + royalty_denominator: int, + ) -> str: + payload = create_collection_payload( + description, + max_supply, + name, + uri, + mutable_description, + mutable_royalty, + mutable_uri, + mutable_token_description, + mutable_token_name, + mutable_token_properties, + mutable_token_uri, + tokens_burnable_by_creator, + tokens_freezable_by_creator, + royalty_numerator, + royalty_denominator, + ) signed_transaction = await self.client.create_bcs_signed_transaction( - creator, TransactionPayload(payload) + creator, payload ) return await self.client.submit_bcs_transaction(signed_transaction) - async def mint_token( - self, - creator: Account, + def mint_token_payload( collection: str, description: str, name: str, uri: str, properties: PropertyMap, - ) -> str: + ) -> TransactionPayload: (property_names, property_types, property_values) = properties.to_tuple() transaction_arguments = [ TransactionArgument(collection, Serializer.str), @@ -420,8 +454,20 @@ async def mint_token( transaction_arguments, ) + return TransactionPayload(payload) + + async def mint_token( + self, + creator: Account, + collection: str, + description: str, + name: str, + uri: str, + properties: PropertyMap, + ) -> str: + payload = mint_token_payload(collection, description, name, uri, properties) signed_transaction = await self.client.create_bcs_signed_transaction( - creator, TransactionPayload(payload) + creator, payload ) return await self.client.submit_bcs_transaction(signed_transaction) diff --git a/ecosystem/python/sdk/aptos_sdk/async_client.py b/ecosystem/python/sdk/aptos_sdk/async_client.py index 0d413f477d351..466dee13747e7 100644 --- a/ecosystem/python/sdk/aptos_sdk/async_client.py +++ b/ecosystem/python/sdk/aptos_sdk/async_client.py @@ -1,6 +1,8 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +import asyncio +import logging import time from typing import Any, Dict, List, Optional @@ -20,7 +22,6 @@ TransactionArgument, TransactionPayload, ) -from .type_tag import StructTag, TypeTag U64_MAX = 18446744073709551615 @@ -53,7 +54,10 @@ def __init__(self, base_url: str, client_config: ClientConfig = ClientConfig()): # Default headers headers = {Metadata.APTOS_HEADER: Metadata.get_aptos_header_val()} self.client = httpx.AsyncClient( - http2=client_config.http2, limits=limits, timeout=timeout, headers=headers + http2=client_config.http2, + limits=limits, + timeout=timeout, + headers=headers, ) self.client_config = client_config self._chain_id = None @@ -95,7 +99,7 @@ async def account_balance( "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", ledger_version, ) - return resource["data"]["coin"]["value"] + return int(resource["data"]["coin"]["value"]) async def account_sequence_number( self, account_address: AccountAddress, ledger_version: int = None @@ -140,6 +144,10 @@ async def account_resources( raise ApiError(f"{response.text} - {account_address}", response.status_code) return response.json() + async def current_timestamp(self) -> float: + info = await self.info() + return float(info["ledger_timestamp"]) / 1_000_000 + async def get_table_item( self, handle: str, @@ -323,7 +331,7 @@ async def wait_for_transaction(self, txn_hash: str) -> None: assert ( count < self.client_config.transaction_wait_in_seconds ), f"transaction {txn_hash} timed out" - time.sleep(1) + await asyncio.sleep(1) count += 1 response = await self.client.get( f"{self.base_url}/transactions/by_hash/{txn_hash}" @@ -332,6 +340,20 @@ async def wait_for_transaction(self, txn_hash: str) -> None: "success" in response.json() and response.json()["success"] ), f"{response.text} - {txn_hash}" + async def account_transaction_sequence_number_status( + self, address: AccountAddress, sequence_number: int + ) -> bool: + """Retrieve the state of a transaction by account and sequence number.""" + + response = await self.client.get( + f"{self.base_url}/accounts/{address}/transactions?limit=1&start={sequence_number}" + ) + if response.status_code >= 400: + logging.info(f"k {response}") + raise ApiError(response.text, response.status_code) + data = response.json() + return len(data) == 1 and data[0]["type"] != "pending_transaction" + # # Transaction helpers # @@ -377,11 +399,19 @@ async def create_multi_agent_bcs_transaction( return SignedTransaction(raw_transaction.inner(), authenticator) async def create_bcs_transaction( - self, sender: Account, payload: TransactionPayload + self, + sender: Account, + payload: TransactionPayload, + sequence_number: Optional[int] = None, ) -> RawTransaction: + sequence_number = ( + sequence_number + if sequence_number is not None + else await self.account_sequence_number(sender.address()) + ) return RawTransaction( sender.address(), - await self.account_sequence_number(sender.address()), + sequence_number, payload, self.client_config.max_gas_amount, self.client_config.gas_unit_price, @@ -390,9 +420,14 @@ async def create_bcs_transaction( ) async def create_bcs_signed_transaction( - self, sender: Account, payload: TransactionPayload + self, + sender: Account, + payload: TransactionPayload, + sequence_number: Optional[int] = None, ) -> SignedTransaction: - raw_transaction = await self.create_bcs_transaction(sender, payload) + raw_transaction = await self.create_bcs_transaction( + sender, payload, sequence_number + ) signature = sender.sign(raw_transaction.keyed()) authenticator = Authenticator( Ed25519Authenticator(sender.public_key(), signature) @@ -411,8 +446,8 @@ async def transfer( payload = { "type": "entry_function_payload", - "function": "0x1::coin::transfer", - "type_arguments": ["0x1::aptos_coin::AptosCoin"], + "function": "0x1::aptos_account::transfer", + "type_arguments": [], "arguments": [ f"{recipient}", str(amount), @@ -422,7 +457,11 @@ async def transfer( # :!:>bcs_transfer async def bcs_transfer( - self, sender: Account, recipient: AccountAddress, amount: int + self, + sender: Account, + recipient: AccountAddress, + amount: int, + sequence_number: Optional[int] = None, ) -> str: transaction_arguments = [ TransactionArgument(recipient, Serializer.struct), @@ -430,14 +469,14 @@ async def bcs_transfer( ] payload = EntryFunction.natural( - "0x1::coin", + "0x1::aptos_account", "transfer", - [TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))], + [], transaction_arguments, ) signed_transaction = await self.create_bcs_signed_transaction( - sender, TransactionPayload(payload) + sender, TransactionPayload(payload), sequence_number=sequence_number ) return await self.submit_bcs_transaction(signed_transaction) diff --git a/ecosystem/python/sdk/aptos_sdk/transaction_worker.py b/ecosystem/python/sdk/aptos_sdk/transaction_worker.py new file mode 100644 index 0000000000000..22ec9ca87ac72 --- /dev/null +++ b/ecosystem/python/sdk/aptos_sdk/transaction_worker.py @@ -0,0 +1,226 @@ +# Copyright © Aptos Foundation +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import logging +import typing + +from aptos_sdk.account import Account +from aptos_sdk.account_address import AccountAddress +from aptos_sdk.account_sequence_number import AccountSequenceNumber +from aptos_sdk.async_client import RestClient +from aptos_sdk.transactions import SignedTransaction, TransactionPayload + + +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. + """ + + _account: Account + _account_sequence_number: AccountSequenceNumber + _rest_client: RestClient + _transaction_generator: typing.Callable[ + [Account, int], typing.Awaitable[SignedTransaction] + ] + _started: bool + _stopped: bool + _outstanding_transactions: asyncio.Queue + _outstanding_transactions_task: typing.Optional[asyncio.Task] + _processed_transactions: asyncio.Queue + _process_transactions_task: typing.Optional[asyncio.Task] + + def __init__( + self, + account: Account, + rest_client: RestClient, + transaction_generator: typing.Callable[ + [Account, int], typing.Awaitable[SignedTransaction] + ], + ): + self._account = account + self._account_sequence_number = AccountSequenceNumber( + rest_client, account.address() + ) + self._rest_client = rest_client + self._transaction_generator = transaction_generator + + self._started = False + self._stopped = False + self._outstanding_transactions = asyncio.Queue() + self._processed_transactions = asyncio.Queue() + + def address(self) -> AccountAddress: + return self._account.address() + + async def _submit_transactions_task(self): + try: + while True: + sequence_number = ( + await self._account_sequence_number.next_sequence_number() + ) + transaction = await self._transaction_generator( + self._account, sequence_number + ) + txn_hash_awaitable = self._rest_client.submit_bcs_transaction( + transaction + ) + await self._outstanding_transactions.put( + (txn_hash_awaitable, sequence_number) + ) + except asyncio.CancelledError: + return + except Exception as e: + # This is insufficient, if we hit this we either need to bail or resolve the potential errors + logging.error(e, exc_info=True) + + async def _process_transactions_task(self): + try: + while True: + # Always start waiting for one, that way we can acquire a batch in the loop below. + ( + txn_hash_awaitable, + sequence_number, + ) = await self._outstanding_transactions.get() + awaitables = [txn_hash_awaitable] + sequence_numbers = [sequence_number] + + # Now acquire our batch. + while not self._outstanding_transactions.empty(): + ( + txn_hash_awaitable, + sequence_number, + ) = await self._outstanding_transactions.get() + awaitables.append(txn_hash_awaitable) + sequence_numbers.append(sequence_number) + + outputs = await asyncio.gather(*awaitables, return_exceptions=True) + + for (output, sequence_number) in zip(outputs, sequence_numbers): + if isinstance(output, BaseException): + await self._processed_transactions.put( + (sequence_number, None, output) + ) + else: + await self._processed_transactions.put( + (sequence_number, output, None) + ) + except asyncio.CancelledError: + return + except Exception as e: + # This is insufficient, if we hit this we either need to bail or resolve the potential errors + logging.error(e, exc_info=True) + + async def next_processed_transaction( + self, + ) -> (int, typing.Optional[str], typing.Optional[Exception]): + return await self._processed_transactions.get() + + def stop(self): + """Stop the tasks for managing transactions""" + if not self._started: + raise Exception("Start not yet called") + if self._stopped: + raise Exception("Already stopped") + self._stopped = True + + self._submit_transactions_task.cancel() + self._process_transactions_task.cancel() + + def start(self): + """Begin the tasks for managing transactions""" + if self._started: + raise Exception("Already started") + self._started = True + + self._submit_transactions_task = asyncio.create_task( + self._submit_transactions_task() + ) + self._process_transactions_task = asyncio.create_task( + self._process_transactions_task() + ) + + +class TransactionQueue: + """Provides a queue model for pushing transactions into the TransactionWorker.""" + + _client: RestClient + _outstanding_transactions: asyncio.Queue + + def __init__(self, client: RestClient): + self._client = client + self._outstanding_transactions = asyncio.Queue() + + async def push(self, payload: TransactionPayload): + await self._outstanding_transactions.put(payload) + + async def next(self, sender: Account, sequence_number: int) -> SignedTransaction: + payload = await self._outstanding_transactions.get() + return await self._client.create_bcs_signed_transaction( + sender, payload, sequence_number=sequence_number + ) + + +import unittest +import unittest.mock + +from aptos_sdk.bcs import Serializer +from aptos_sdk.transactions import EntryFunction, TransactionArgument + + +class Test(unittest.IsolatedAsyncioTestCase): + async def test_common_path(self): + transaction_arguments = [ + TransactionArgument(AccountAddress.from_hex("b0b"), Serializer.struct), + TransactionArgument(100, Serializer.u64), + ] + payload = EntryFunction.natural( + "0x1::aptos_accounts", + "transfer", + [], + transaction_arguments, + ) + + seq_num_patcher = unittest.mock.patch( + "aptos_sdk.async_client.RestClient.account_sequence_number", return_value=0 + ) + seq_num_patcher.start() + submit_txn_patcher = unittest.mock.patch( + "aptos_sdk.async_client.RestClient.submit_bcs_transaction", + return_value="0xff", + ) + submit_txn_patcher.start() + + rest_client = RestClient("https://fullnode.devnet.aptoslabs.com/v1") + txn_queue = TransactionQueue(rest_client) + txn_worker = TransactionWorker(Account.generate(), rest_client, txn_queue.next) + txn_worker.start() + + await txn_queue.push(payload) + processed_txn = await txn_worker.next_processed_transaction() + self.assertEqual(processed_txn[0], 0) + self.assertEqual(processed_txn[1], "0xff") + self.assertEqual(processed_txn[2], None) + + submit_txn_patcher.stop() + exception = Exception("Power overwhelming") + submit_txn_patcher = unittest.mock.patch( + "aptos_sdk.async_client.RestClient.submit_bcs_transaction", + side_effect=exception, + ) + submit_txn_patcher.start() + + await txn_queue.push(payload) + processed_txn = await txn_worker.next_processed_transaction() + self.assertEqual(processed_txn[0], 1) + self.assertEqual(processed_txn[1], None) + self.assertEqual(processed_txn[2], exception) + + txn_worker.stop() diff --git a/ecosystem/python/sdk/examples/transaction-batching.py b/ecosystem/python/sdk/examples/transaction-batching.py index 4e5d02cfb9fbc..1bb671b5abc5c 100644 --- a/ecosystem/python/sdk/examples/transaction-batching.py +++ b/ecosystem/python/sdk/examples/transaction-batching.py @@ -1,231 +1,431 @@ # Copyright © Aptos Foundation # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import asyncio import logging import time +from multiprocessing import Pipe, Process +from multiprocessing.connection import Connection +from typing import Any, List from aptos_sdk.account import Account from aptos_sdk.account_address import AccountAddress +from aptos_sdk.account_sequence_number import AccountSequenceNumber +from aptos_sdk.aptos_token_client import AptosTokenClient, Property, PropertyMap from aptos_sdk.async_client import ClientConfig, FaucetClient, RestClient -from aptos_sdk.authenticator import Authenticator, Ed25519Authenticator from aptos_sdk.bcs import Serializer +from aptos_sdk.transaction_worker import TransactionWorker from aptos_sdk.transactions import ( EntryFunction, - RawTransaction, SignedTransaction, TransactionArgument, TransactionPayload, ) -from aptos_sdk.type_tag import StructTag, TypeTag from .common import FAUCET_URL, NODE_URL +class TransactionGenerator: + """ + Demonstrate how one might make a harness for submitting transactions. This class just keeps + submitting the same transaction payload. In practice, this could be a queue, where new payloads + accumulate and are consumed by the call to next_transaction. + + Todo: add tracking of transaction status to this and come up with some general logic to retry + or exit upon failure. + """ + + _client: RestClient + _recipient: AccountAddress + _offset: int + _remaining_transactions: int + _waiting_for_more = asyncio.Event + _complete = asyncio.Event + _lock = asyncio.Lock + + def __init__(self, client: RestClient, recipient: AccountAddress): + self._client = client + self._recipient = recipient + self._waiting_for_more = asyncio.Event() + self._waiting_for_more.clear() + self._complete = asyncio.Event() + self._complete.set() + self._lock = asyncio.Lock() + self._remaining_transactions = 0 + + async def next_transaction( + self, sender: Account, sequence_number: int + ) -> SignedTransaction: + while self._remaining_transactions == 0: + await self._waiting_for_more.wait() + + async with self._lock: + self._remaining_transactions -= 1 + if self._remaining_transactions == 0: + self._waiting_for_more.clear() + self._complete.set() + + return await transfer_transaction( + self._client, sender, sequence_number, self._recipient, 0 + ) + + async def increase_transaction_count(self, number: int): + if number <= 0: + return + + async with self._lock: + self._remaining_transactions += number + self._waiting_for_more.set() + self._complete.clear() + + async def wait(self): + await self._complete.wait() + + +class WorkerContainer: + _conn: Connection + _process: Process + + def __init__(self, node_url: str, account: Account, recipient: AccountAddress): + (self._conn, conn) = Pipe() + self._process = Process( + target=Worker.run, args=(conn, node_url, account, recipient) + ) + + def get(self) -> Any: + self._conn.recv() + + def join(self): + self._process.join() + + def put(self, value: Any): + self._conn.send(value) + + def start(self): + self._process.start() + + +class Worker: + _conn: Connection + _rest_client: RestClient + _account: Account + _recipient: AccountAddress + _txn_generator: TransactionGenerator + _txn_worker: TransactionWorker + + def __init__( + self, + conn: Connection, + node_url: str, + account: Account, + recipient: AccountAddress, + ): + self._conn = conn + self._rest_client = RestClient(node_url) + self._account = account + self._recipient = recipient + self._txn_generator = TransactionGenerator(self._rest_client, self._recipient) + self._txn_worker = TransactionWorker( + self._account, self._rest_client, self._txn_generator.next_transaction + ) + + def run(queue: Pipe, node_url: str, account: Account, recipient: AccountAddress): + worker = Worker(queue, node_url, account, recipient) + asyncio.run(worker.async_run()) + + async def async_run(self): + try: + self._txn_worker.start() + + self._conn.send(True) + num_txns = self._conn.recv() + + await self._txn_generator.increase_transaction_count(num_txns) + + logging.info(f"Increase txns from {self._account.address()}") + self._conn.send(True) + self._conn.recv() + + txn_hashes = [] + while num_txns != 0: + if num_txns % 100 == 0: + logging.info( + f"{self._txn_worker.address()} remaining transactions {num_txns}" + ) + num_txns -= 1 + ( + sequence_number, + txn_hash, + exception, + ) = await self._txn_worker.next_processed_transaction() + if exception: + logging.error( + f"Account {self._txn_worker.address()}, transaction {sequence_number} submission failed.", + exc_info=exception, + ) + else: + txn_hashes.append(txn_hash) + + logging.info(f"Submitted txns from {self._account.address()}") + self._conn.send(True) + self._conn.recv() + + for txn_hash in txn_hashes: + await self._rest_client.wait_for_transaction(txn_hash) + + await self._rest_client.close() + logging.info(f"Verified txns from {self._account.address()}") + self._conn.send(True) + except Exception as e: + logging.error( + "Failed during run.", + exc_info=e, + ) + + +# This performs a simple p2p transaction +async def transfer_transaction( + client: RestClient, + sender: Account, + sequence_number: int, + recipient: AccountAddress, + amount: int, +) -> str: + transaction_arguments = [ + TransactionArgument(recipient, Serializer.struct), + TransactionArgument(amount, Serializer.u64), + ] + payload = EntryFunction.natural( + "0x1::aptos_account", + "transfer", + [], + transaction_arguments, + ) + + return await client.create_bcs_signed_transaction( + sender, TransactionPayload(payload), sequence_number + ) + + +# This will create a collection in the first transaction and then create NFTs thereafter. +# Note: Please adjust the sequence number and the name of the collection if run on the same set of +# accounts, otherwise you may end up not creating a collection and failing all transactions. +async def token_transaction( + client: RestClient, + sender: Account, + sequence_number: int, + recipient: AccountAddress, + amount: int, +) -> str: + collection_name = "Funky Alice's" + if sequence_number == 8351: + payload = AptosTokenClient.create_collection_payload( + "Alice's simple collection", + 20000000000, + collection_name, + "https://aptos.dev", + True, + True, + True, + True, + True, + True, + True, + True, + True, + 0, + 1, + ) + else: + payload = AptosTokenClient.mint_token_payload( + collection_name, + "Alice's simple token", + f"token {sequence_number}", + "https://aptos.dev/img/nyan.jpeg", + PropertyMap([Property.string("string", "string value")]), + ) + return await client.create_bcs_signed_transaction(sender, payload, sequence_number) + + +class Accounts: + source: Account + senders: List[Account] + receivers: List[Account] + + def __init__(self, source, senders, receivers): + self.source = source + self.senders = senders + self.receivers = receivers + + def generate(path: str, num_accounts: int) -> Accounts: + source = Account.generate() + source.store(f"{path}/source.txt") + senders = [] + receivers = [] + for idx in range(num_accounts): + senders.append(Account.generate()) + receivers.append(Account.generate()) + senders[-1].store(f"{path}/sender_{idx}.txt") + receivers[-1].store(f"{path}/receiver_{idx}.txt") + return Accounts(source, senders, receivers) + + def load(path: str, num_accounts: int) -> Accounts: + source = Account.load(f"{path}/source.txt") + senders = [] + receivers = [] + for idx in range(num_accounts): + senders.append(Account.load(f"{path}/sender_{idx}.txt")) + receivers.append(Account.load(f"{path}/receiver_{idx}.txt")) + return Accounts(source, senders, receivers) + + +async def fund_from_faucet(rest_client: RestClient, source: Account): + faucet_client = FaucetClient(FAUCET_URL, rest_client) + + fund_txns = [] + for _ in range(40): + fund_txns.append(faucet_client.fund_account(source.address(), 100_000_000_000)) + await asyncio.gather(*fund_txns) + + +async def distribute_portionally( + rest_client: RestClient, + source: Account, + senders: List[Account], + receivers: List[Account], +): + balance = int(await rest_client.account_balance(source.address())) + per_node_balance = balance // (len(senders) + 1) + await distribute(rest_client, source, senders, receivers, per_node_balance) + + +async def distribute( + rest_client: RestClient, + source: Account, + senders: List[Account], + receivers: List[Account], + per_node_amount: int, +): + all_accounts = list(map(lambda account: (account.address(), True), senders)) + all_accounts.extend(map(lambda account: (account.address(), False), receivers)) + + account_sequence_number = AccountSequenceNumber(rest_client, source.address()) + + txns = [] + txn_hashes = [] + + for (account, fund) in all_accounts: + sequence_number = await account_sequence_number.next_sequence_number( + block=False + ) + if sequence_number is None: + txn_hashes.extend(await asyncio.gather(*txns)) + txns = [] + sequence_number = await account_sequence_number.next_sequence_number() + amount = per_node_amount if fund else 0 + txn = await transfer_transaction( + rest_client, source, sequence_number, account, amount + ) + txns.append(rest_client.submit_bcs_transaction(txn)) + + txn_hashes.extend(await asyncio.gather(*txns)) + for txn_hash in txn_hashes: + await rest_client.wait_for_transaction(txn_hash) + await account_sequence_number.synchronize() + + async def main(): client_config = ClientConfig() - # Toggle to benchmark - client_config.http2 = False client_config.http2 = True rest_client = RestClient(NODE_URL, client_config) - faucet_client = FaucetClient(FAUCET_URL, rest_client) - num_accounts = 5 - read_amplification = 1000 - first_pass = 100 + num_accounts = 64 + transactions = 100000 start = time.time() + logging.getLogger().setLevel(20) + print("Starting...") - accounts = [] - recipient_accounts = [] - for _ in range(num_accounts): - accounts.append(Account.generate()) - recipient_accounts.append(Account.generate()) + # Generate will create new accounts, load will load existing accounts + all_accounts = Accounts.generate("nodes", num_accounts) + # all_accounts = Accounts.load("nodes", num_accounts) + accounts = all_accounts.senders + receivers = all_accounts.receivers + source = all_accounts.source + + print(f"source: {source.address()}") last = time.time() - print(f"Accounts generated at {last - start}") + print(f"Accounts generated / loaded at {last - start}") - funds = [] - for account in accounts: - funds.append(faucet_client.fund_account(account.address(), 100_000_000)) - for account in recipient_accounts: - funds.append(faucet_client.fund_account(account.address(), 0)) - await asyncio.gather(*funds) + await fund_from_faucet(rest_client, source) - print(f"Funded accounts at {time.time() - start} {time.time() - last}") + print(f"Initial account funded at {time.time() - start} {time.time() - last}") last = time.time() - balances = [] - for _ in range(read_amplification): - for account in accounts: - balances.append(rest_client.account_balance(account.address())) - await asyncio.gather(*balances) + balance = await rest_client.account_balance(source.address()) + amount = int(balance * 0.9 / num_accounts) + await distribute(rest_client, source, accounts, receivers, amount) - print(f"Accounts checked at {time.time() - start} {time.time() - last}") + print(f"Funded all accounts at {time.time() - start} {time.time() - last}") last = time.time() - account_sequence_numbers = [] - await_account_sequence_numbers = [] + balances = [] for account in accounts: - account_sequence_number = AccountSequenceNumber(rest_client, account.address()) - await_account_sequence_numbers.append(account_sequence_number.initialize()) - account_sequence_numbers.append(account_sequence_number) - await asyncio.gather(*await_account_sequence_numbers) + balances.append(rest_client.account_balance(account.address())) + await asyncio.gather(*balances) - print(f"Accounts initialized at {time.time() - start} {time.time() - last}") + print(f"Accounts checked at {time.time() - start} {time.time() - last}") last = time.time() - txn_hashes = [] - for _ in range(first_pass): - for idx in range(num_accounts): - sender = accounts[idx] - recipient = recipient_accounts[idx].address() - sequence_number = await account_sequence_numbers[idx].next_sequence_number() - txn_hash = transfer(rest_client, sender, recipient, sequence_number, 1) - txn_hashes.append(txn_hash) - txn_hashes = await asyncio.gather(*txn_hashes) + workers = [] + for (account, recipient) in zip(accounts, receivers): + workers.append(WorkerContainer(NODE_URL, account, recipient.address())) + workers[-1].start() - print(f"Transactions submitted at {time.time() - start} {time.time() - last}") - last = time.time() + for worker in workers: + worker.get() - wait_for = [] - for txn_hash in txn_hashes: - wait_for.append(account_sequence_number.synchronize()) - await asyncio.gather(*wait_for) - - print(f"Transactions committed at {time.time() - start} {time.time() - last}") + print(f"Workers started at {time.time() - start} {time.time() - last}") last = time.time() - await rest_client.close() - + to_take = (transactions // num_accounts) + ( + 1 if transactions % num_accounts != 0 else 0 + ) + remaining_transactions = transactions + for worker in workers: + taking = min(to_take, remaining_transactions) + remaining_transactions -= taking + worker.put(taking) -class AccountSequenceNumber: - """ - A managed wrapper around sequence numbers that implements the trivial flow control used by the - Aptos faucet: - * Submit up to 50 transactions per account in parallel with a timeout of 20 seconds - * If local assumes 50 are in flight, determine the actual committed state from the network - * If there are less than 50 due to some being committed, adjust the window - * If 50 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 make take some time to reset. - * Accounts are automatically initialized if not explicitly - * - """ + for worker in workers: + worker.get() - client: RestClient - account: AccountAddress - last_committed_number: int - current_number: int - maximum_in_flight: int = 50 - lock = asyncio.Lock - sleep_time = 0.01 - maximum_wait_time = 30 - - def __init__(self, client: RestClient, account: AccountAddress): - self.client = client - self.account = account - self.last_uncommitted_number = None - self.current_number = None - self.lock = asyncio.Lock() - - async def next_sequence_number(self) -> int: - await self.lock.acquire() - try: - if self.last_uncommitted_number is None or self.current_number is None: - await self.initialize() - - if ( - self.current_number - self.last_uncommitted_number - >= self.maximum_in_flight - ): - await self.__update() - - start_time = time.time() - while ( - self.last_uncommitted_number - self.current_number - >= self.maximum_in_flight - ): - asyncio.sleep(self.sleep_time) - if time.time() - start_time > self.maximum_wait_time: - logging.warn( - f"Waited over 30 seconds for a transaction to commit, resyncing {self.account.address()}" - ) - await self.__initialize() - else: - await self.__update() - - next_number = self.current_number - self.current_number += 1 - finally: - self.lock.release() - - return next_number - - async def initialize(self): - self.current_number = await self.__current_sequence_number() - self.last_uncommitted_number = self.current_number - - async def synchronize(self): - if self.last_uncommitted_number == self.current_number: - return + print(f"Transactions submitted at {time.time() - start} {time.time() - last}") + last = time.time() - await self.__update() - start_time = time.time() - while self.last_uncommitted_number != self.current_number: - if time.time() - start_time > self.maximum_wait_time: - logging.warn( - f"Waited over 30 seconds for a transaction to commit, resyncing {self.account.address()}" - ) - await self.__initialize() - else: - await asyncio.sleep(self.sleep_time) - await self.__update() + for worker in workers: + worker.put(True) - async def __update(self): - self.last_uncommitted_number = await self.__current_sequence_number() - return self.last_uncommitted_number + for worker in workers: + worker.get() - async def __current_sequence_number(self) -> int: - return await self.client.account_sequence_number(self.account) + print(f"Transactions processed at {time.time() - start} {time.time() - last}") + last = time.time() + for worker in workers: + worker.put(True) -async def transfer( - client: RestClient, - sender: Account, - recipient: AccountAddress, - sequence_number: int, - amount: int, -): - transaction_arguments = [ - TransactionArgument(recipient, Serializer.struct), - TransactionArgument(amount, Serializer.u64), - ] - payload = EntryFunction.natural( - "0x1::coin", - "transfer", - [TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))], - transaction_arguments, - ) + for worker in workers: + worker.get() - raw_transaction = RawTransaction( - sender.address(), - sequence_number, - TransactionPayload(payload), - client.client_config.max_gas_amount, - client.client_config.gas_unit_price, - int(time.time()) + client.client_config.expiration_ttl, - await client.chain_id(), - ) + print(f"Transactions verified at {time.time() - start} {time.time() - last}") + last = time.time() - signature = sender.sign(raw_transaction.keyed()) - authenticator = Authenticator(Ed25519Authenticator(sender.public_key(), signature)) - signed_transaction = SignedTransaction(raw_transaction, authenticator) - return await client.submit_bcs_transaction(signed_transaction) + await rest_client.close() if __name__ == "__main__": diff --git a/ecosystem/python/sdk/poetry.lock b/ecosystem/python/sdk/poetry.lock index 19379e917cf03..e5708e47e2648 100644 --- a/ecosystem/python/sdk/poetry.lock +++ b/ecosystem/python/sdk/poetry.lock @@ -1,32 +1,31 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "anyio" -version = "3.6.2" +version = "3.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" files = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, + {file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"}, + {file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"}, ] [package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] +doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] [[package]] name = "autoflake" version = "1.4" description = "Removes unused imports and unused variables" -category = "dev" optional = false python-versions = "*" files = [ @@ -40,7 +39,6 @@ pyflakes = ">=1.1.0" name = "black" version = "22.12.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -75,21 +73,19 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, ] [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -166,7 +162,6 @@ pycparser = "*" name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -182,7 +177,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -190,11 +184,96 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, + {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -212,7 +291,6 @@ pyflakes = ">=2.5.0,<2.6.0" name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -227,7 +305,6 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} name = "h2" version = "4.1.0" description = "HTTP/2 State-Machine based protocol implementation" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -243,7 +320,6 @@ hyperframe = ">=6.0,<7" name = "hpack" version = "4.0.0" description = "Pure-Python HPACK header compression" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -255,7 +331,6 @@ files = [ name = "httpcore" version = "0.16.3" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -267,17 +342,16 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" version = "0.23.3" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -293,15 +367,14 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "hyperframe" version = "6.0.1" description = "HTTP/2 framing layer for Python" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -313,7 +386,6 @@ files = [ name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -321,22 +393,10 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] -[[package]] -name = "importlib" -version = "1.0.4" -description = "Backport of importlib.import_module() from Python 2.7" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "importlib-1.0.4.zip", hash = "sha256:b6ee7066fea66e35f8d0acee24d98006de1a0a8a94a8ce6efe73a9a23c8d9826"}, -] - [[package]] name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -356,7 +416,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", name = "isort" version = "5.11.5" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -374,7 +433,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -386,7 +444,6 @@ files = [ name = "mypy" version = "0.982" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -431,7 +488,6 @@ reports = ["lxml"] name = "mypy-extensions" version = "0.4.4" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = ">=2.7" files = [ @@ -442,7 +498,6 @@ files = [ name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -452,28 +507,26 @@ files = [ [[package]] name = "platformdirs" -version = "3.2.0" +version = "3.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, - {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, + {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, + {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, ] [package.dependencies] typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -485,7 +538,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -497,7 +549,6 @@ files = [ name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -509,7 +560,6 @@ files = [ name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -536,7 +586,6 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" -category = "main" optional = false python-versions = "*" files = [ @@ -554,7 +603,6 @@ idna2008 = ["idna"] name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -566,7 +614,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -578,7 +625,6 @@ files = [ name = "typed-ast" version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -610,21 +656,19 @@ files = [ [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.6.3" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, + {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, ] [[package]] name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -639,4 +683,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" -content-hash = "bdd9b43bbb77709433347a184874a91816d07bcac6e4b843beeebf44acd9a92f" +content-hash = "58444a4ad25fc804f24845b567348e41f0a28dfc8197fe25f6e2391f8bdf2c1b" diff --git a/ecosystem/python/sdk/pyproject.toml b/ecosystem/python/sdk/pyproject.toml index b4bf7467961c3..40473f3ca53ec 100644 --- a/ecosystem/python/sdk/pyproject.toml +++ b/ecosystem/python/sdk/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aptos-sdk" -version = "0.6.2" +version = "0.6.3" description = "Aptos SDK" authors = ["Aptos Labs "] license = "Apache-2.0" @@ -14,11 +14,11 @@ h2 = "^4.1.0" httpx = "^0.23.0" PyNaCl = "^1.5.0" python = ">=3.7,<4.0" -importlib = "^1.0.4" [tool.poetry.dev-dependencies] autoflake = "1.4.0" black = "^22.6.0" +coverage = "^7.2.4" flake8 = ">=3.8.3,<6.0.0" isort = "^5.10.1" mypy = "^0.982" diff --git a/ecosystem/python/sdk/setup.py b/ecosystem/python/sdk/setup.py deleted file mode 100644 index b63e624d26eae..0000000000000 --- a/ecosystem/python/sdk/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -import setuptools - -with open("README.md", "r", encoding="utf-8") as fh: - long_description = fh.read() - -setuptools.setup( - author="Aptos Labs", - author_email="opensource@aptoslabs.com", - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - ], - include_package_data=True, - install_requires=["httpx", "pynacl"], - long_description=long_description, - long_description_content_type="text/markdown", - name="aptos_sdk", - packages=["aptos_sdk"], - python_requires=">=3.7", - url="https://github.com/aptos-labs/aptos-core", - version="0.6.2", -)