diff --git a/.gitignore b/.gitignore index b9fdcd0..1fefca8 100644 --- a/.gitignore +++ b/.gitignore @@ -168,11 +168,6 @@ dmypy.json # Cython debug symbols cython_debug/ -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. This file currently -# ignores the entire .idea folder as a more nuclear option (not recommended). .idea/ # Abstra diff --git a/src/decibel/__init__.py b/src/decibel/__init__.py index 4fa5637..ee781fc 100644 --- a/src/decibel/__init__.py +++ b/src/decibel/__init__.py @@ -18,6 +18,10 @@ get_testc_address, get_usdc_address, ) +from decibel._exceptions import ( + TxnConfirmError, + TxnSubmitError, +) from decibel._fee_pay import ( PendingTransactionResponse, submit_fee_paid_transaction, @@ -143,6 +147,8 @@ "DecibelAdminDex", "DecibelAdminDexSync", "ABIErrorEntry", + "TxnConfirmError", + "TxnSubmitError", "ABISummary", "AbiRegistry", "amount_to_chain_units", diff --git a/src/decibel/_base.py b/src/decibel/_base.py index de64cff..ee06ee3 100644 --- a/src/decibel/_base.py +++ b/src/decibel/_base.py @@ -18,6 +18,8 @@ from aptos_sdk.ed25519 import Signature as Ed25519Signature from aptos_sdk.transactions import FeePayerRawTransaction, SignedTransaction +from ._constants import DEFAULT_TXN_CONFIRM_TIMEOUT, DEFAULT_TXN_SUBMIT_TIMEOUT +from ._exceptions import TxnConfirmError, TxnSubmitError from ._fee_pay import ( PendingTransactionResponse, submit_fee_paid_transaction, @@ -170,19 +172,25 @@ async def submit_tx( self, transaction: SimpleTransaction, sender_authenticator: AccountAuthenticator, + *, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: if self._no_fee_payer: - return await self._submit_direct(transaction, sender_authenticator) + return await self._submit_direct(transaction, sender_authenticator, txn_submit_timeout) return await submit_fee_paid_transaction( self._config, transaction, sender_authenticator, + txn_submit_timeout=txn_submit_timeout, ) async def _send_tx( self, payload: InputEntryFunctionData, account_override: Account | None = None, + *, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: signer = account_override if account_override is not None else self._account sender = signer.address() @@ -216,9 +224,37 @@ async def _send_tx( sender_authenticator = self._sign_transaction(signer, transaction) - pending_tx = await self.submit_tx(transaction, sender_authenticator) + if txn_submit_timeout is None: + txn_submit_timeout = DEFAULT_TXN_SUBMIT_TIMEOUT - return await self._wait_for_transaction(pending_tx.hash) + try: + pending_tx = await self.submit_tx( + transaction, sender_authenticator, txn_submit_timeout=txn_submit_timeout + ) + except httpx.ConnectTimeout as e: + raise TxnSubmitError( + f"Failed to submit transaction: connection timeout to {self._config.fullnode_url}", + original_exception=e, + ) from e + except httpx.ConnectError as e: + raise TxnSubmitError( + f"Failed to submit transaction: connection error - {e}", + original_exception=e, + ) from e + except httpx.HTTPStatusError as e: + raise TxnSubmitError( + f"Failed to submit transaction: HTTP {e.response.status_code}", + original_exception=e, + ) from e + except Exception as e: + raise TxnSubmitError( + f"Failed to submit transaction: {e}", + original_exception=e, + ) from e + + return await self._wait_for_transaction( + pending_tx.hash, txn_confirm_timeout=txn_confirm_timeout + ) def _sign_transaction( self, @@ -283,6 +319,7 @@ async def _submit_direct( self, transaction: SimpleTransaction, sender_authenticator: AccountAuthenticator, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: url = f"{self._config.fullnode_url}/transactions" headers = self._build_node_headers() @@ -291,7 +328,9 @@ async def _submit_direct( bcs_bytes = self._serialize_signed_transaction(transaction, sender_authenticator) async with httpx.AsyncClient() as client: - response = await client.post(url, content=bcs_bytes, headers=headers) + response = await client.post( + url, content=bcs_bytes, headers=headers, timeout=txn_submit_timeout + ) if not response.is_success: raise ValueError( @@ -313,9 +352,11 @@ async def _submit_direct( async def _wait_for_transaction( self, tx_hash: str, - timeout_secs: float = 30.0, + txn_confirm_timeout: float | None = None, # Uses DEFAULT_TXN_CONFIRM_TIMEOUT if None poll_interval_secs: float = 1.0, ) -> dict[str, Any]: + if txn_confirm_timeout is None: + txn_confirm_timeout = DEFAULT_TXN_CONFIRM_TIMEOUT url = f"{self._config.fullnode_url}/transactions/by_hash/{tx_hash}" headers = self._build_node_headers() start_time = time.time() @@ -333,12 +374,10 @@ async def _wait_for_transaction( return data elif data.get("success") is False: vm_status = data.get("vm_status", "Unknown error") - raise ValueError(f"Transaction failed: {vm_status}") + raise TxnConfirmError(tx_hash, f"failed: {vm_status}") - if time.time() - start_time > timeout_secs: - raise TimeoutError( - f"Transaction {tx_hash} did not complete within {timeout_secs}s" - ) + if time.time() - start_time > txn_confirm_timeout: + raise TxnConfirmError(tx_hash, f"did not confirm within {txn_confirm_timeout}s") await self._async_sleep(poll_interval_secs) @@ -495,19 +534,25 @@ def submit_tx( self, transaction: SimpleTransaction, sender_authenticator: AccountAuthenticator, + *, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: if self._no_fee_payer: - return self._submit_direct(transaction, sender_authenticator) + return self._submit_direct(transaction, sender_authenticator, txn_submit_timeout) return submit_fee_paid_transaction_sync( self._config, transaction, sender_authenticator, + txn_submit_timeout=txn_submit_timeout, ) def _send_tx( self, payload: InputEntryFunctionData, account_override: Account | None = None, + *, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: signer = account_override if account_override is not None else self._account sender = signer.address() @@ -541,9 +586,35 @@ def _send_tx( sender_authenticator = self._sign_transaction(signer, transaction) - pending_tx = self.submit_tx(transaction, sender_authenticator) + if txn_submit_timeout is None: + txn_submit_timeout = DEFAULT_TXN_SUBMIT_TIMEOUT - return self._wait_for_transaction(pending_tx.hash) + try: + pending_tx = self.submit_tx( + transaction, sender_authenticator, txn_submit_timeout=txn_submit_timeout + ) + except httpx.ConnectTimeout as e: + raise TxnSubmitError( + f"Failed to submit transaction: connection timeout to {self._config.fullnode_url}", + original_exception=e, + ) from e + except httpx.ConnectError as e: + raise TxnSubmitError( + f"Failed to submit transaction: connection error - {e}", + original_exception=e, + ) from e + except httpx.HTTPStatusError as e: + raise TxnSubmitError( + f"Failed to submit transaction: HTTP {e.response.status_code}", + original_exception=e, + ) from e + except Exception as e: + raise TxnSubmitError( + f"Failed to submit transaction: {e}", + original_exception=e, + ) from e + + return self._wait_for_transaction(pending_tx.hash, txn_confirm_timeout=txn_confirm_timeout) def _sign_transaction( self, @@ -614,6 +685,7 @@ def _submit_direct( self, transaction: SimpleTransaction, sender_authenticator: AccountAuthenticator, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: url = f"{self._config.fullnode_url}/transactions" headers = self._build_node_headers() @@ -621,7 +693,9 @@ def _submit_direct( bcs_bytes = self._serialize_signed_transaction(transaction, sender_authenticator) def make_request(client: httpx.Client) -> PendingTransactionResponse: - response = client.post(url, content=bcs_bytes, headers=headers) + response = client.post( + url, content=bcs_bytes, headers=headers, timeout=txn_submit_timeout + ) if not response.is_success: raise ValueError( f"Transaction submission failed: {response.status_code} - {response.text}" @@ -645,9 +719,11 @@ def make_request(client: httpx.Client) -> PendingTransactionResponse: def _wait_for_transaction( self, tx_hash: str, - timeout_secs: float = 30.0, + txn_confirm_timeout: float | None = None, # Uses DEFAULT_TXN_CONFIRM_TIMEOUT if None poll_interval_secs: float = 1.0, ) -> dict[str, Any]: + if txn_confirm_timeout is None: + txn_confirm_timeout = DEFAULT_TXN_CONFIRM_TIMEOUT url = f"{self._config.fullnode_url}/transactions/by_hash/{tx_hash}" headers = self._build_node_headers() start_time = time.time() @@ -664,11 +740,9 @@ def poll_loop(client: httpx.Client) -> dict[str, Any]: return data elif data.get("success") is False: vm_status = data.get("vm_status", "Unknown error") - raise ValueError(f"Transaction failed: {vm_status}") - if time.time() - start_time > timeout_secs: - raise TimeoutError( - f"Transaction {tx_hash} did not complete within {timeout_secs}s" - ) + raise TxnConfirmError(tx_hash, f"failed: {vm_status}") + if time.time() - start_time > txn_confirm_timeout: + raise TxnConfirmError(tx_hash, f"did not confirm within {txn_confirm_timeout}s") time.sleep(poll_interval_secs) if self._http_client is not None: diff --git a/src/decibel/_constants.py b/src/decibel/_constants.py index da35fbd..ad7fa15 100644 --- a/src/decibel/_constants.py +++ b/src/decibel/_constants.py @@ -11,6 +11,8 @@ "Deployment", "DecibelConfig", "DEFAULT_COMPAT_VERSION", + "DEFAULT_TXN_CONFIRM_TIMEOUT", + "DEFAULT_TXN_SUBMIT_TIMEOUT", "MAINNET_CONFIG", "NETNA_CONFIG", "TESTNET_CONFIG", @@ -22,6 +24,14 @@ "get_perp_engine_global_address", ] +# Configurable timeout for transaction confirmation +# Default is 30 seconds +DEFAULT_TXN_CONFIRM_TIMEOUT = 30.0 + +# Configurable timeout for transaction submission +# Default is 10 seconds (should be shorter than confirmation timeout) +DEFAULT_TXN_SUBMIT_TIMEOUT = 10.0 + class Network(str, Enum): MAINNET = "mainnet" diff --git a/src/decibel/_exceptions.py b/src/decibel/_exceptions.py new file mode 100644 index 0000000..d8d92bc --- /dev/null +++ b/src/decibel/_exceptions.py @@ -0,0 +1,51 @@ +"""Custom exceptions for the Decibel SDK. + +These exceptions help callers distinguish between failures that are safe to retry +(submission errors) vs failures that require checking transaction status first +(confirmation errors). +""" + +from __future__ import annotations + + +class TxnConfirmError(Exception): + """ + Transaction was submitted but confirmation failed. + + Causes: + - Transaction did not confirm within timeout (still pending or dropped) + - Transaction executed but reverted (VM error) + - Transaction failed during execution + + CRITICAL: The transaction MAY be on-chain. Check tx_hash status before retrying + to avoid duplicate transactions. + + Attributes: + tx_hash: The transaction hash that was submitted + message: Description of what went wrong + """ + + def __init__(self, tx_hash: str, message: str) -> None: + self.tx_hash = tx_hash + super().__init__(f"Transaction {tx_hash}: {message}") + + +class TxnSubmitError(Exception): + """ + Transaction submission failed before reaching the blockchain. + + Causes: + - Network connectivity issues (timeout, connection refused) + - RPC endpoint unavailable + - HTTP errors (5xx, 429 rate limit) + - Serialization errors + + SAFE TO RETRY: The transaction was never submitted to the blockchain. + + Attributes: + original_exception: The underlying exception that caused the failure + """ + + def __init__(self, message: str, original_exception: Exception | None = None) -> None: + self.original_exception = original_exception + super().__init__(message) diff --git a/src/decibel/_fee_pay.py b/src/decibel/_fee_pay.py index 9d6a3fe..fc677e8 100644 --- a/src/decibel/_fee_pay.py +++ b/src/decibel/_fee_pay.py @@ -34,6 +34,7 @@ async def submit_fee_paid_transaction( sender_authenticator: AccountAuthenticator, *, client: httpx.AsyncClient | None = None, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: if config.gas_station_api_key: return await _submit_via_gas_station_api( @@ -41,6 +42,7 @@ async def submit_fee_paid_transaction( transaction, sender_authenticator, client=client, + txn_submit_timeout=txn_submit_timeout, ) if config.gas_station_url: @@ -49,6 +51,7 @@ async def submit_fee_paid_transaction( transaction, sender_authenticator, client=client, + txn_submit_timeout=txn_submit_timeout, ) raise ValueError("Either gas_station_api_key or gas_station_url must be provided") @@ -60,6 +63,7 @@ def submit_fee_paid_transaction_sync( sender_authenticator: AccountAuthenticator, *, client: httpx.Client | None = None, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: if config.gas_station_api_key: return _submit_via_gas_station_api_sync( @@ -67,6 +71,7 @@ def submit_fee_paid_transaction_sync( transaction, sender_authenticator, client=client, + txn_submit_timeout=txn_submit_timeout, ) if config.gas_station_url: @@ -75,6 +80,7 @@ def submit_fee_paid_transaction_sync( transaction, sender_authenticator, client=client, + txn_submit_timeout=txn_submit_timeout, ) raise ValueError("Either gas_station_api_key or gas_station_url must be provided") @@ -86,6 +92,7 @@ async def _submit_via_gas_station_api( sender_authenticator: AccountAuthenticator, *, client: httpx.AsyncClient | None = None, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: base_url = _get_default_gas_station_url(config) url = f"{base_url}/api/transaction/signAndSubmit" @@ -115,11 +122,14 @@ async def _submit_via_gas_station_api( "Authorization": f"Bearer {config.gas_station_api_key}", } + async def _do_submit(c: httpx.AsyncClient) -> httpx.Response: + return await c.post(url, json=body, headers=headers, timeout=txn_submit_timeout) + if client is not None: - response = await client.post(url, json=body, headers=headers) + response = await _do_submit(client) else: async with httpx.AsyncClient() as temp_client: - response = await temp_client.post(url, json=body, headers=headers) + response = await _do_submit(temp_client) if not response.is_success: raise ValueError(f"Gas station API error: {response.status_code} - {response.text}") @@ -143,6 +153,7 @@ def _submit_via_gas_station_api_sync( sender_authenticator: AccountAuthenticator, *, client: httpx.Client | None = None, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: base_url = _get_default_gas_station_url(config) url = f"{base_url}/api/transaction/signAndSubmit" @@ -172,11 +183,14 @@ def _submit_via_gas_station_api_sync( "Authorization": f"Bearer {config.gas_station_api_key}", } + def _do_submit(c: httpx.Client) -> httpx.Response: + return c.post(url, json=body, headers=headers, timeout=txn_submit_timeout) + if client is not None: - response = client.post(url, json=body, headers=headers) + response = _do_submit(client) else: with httpx.Client() as temp_client: - response = temp_client.post(url, json=body, headers=headers) + response = _do_submit(temp_client) if not response.is_success: raise ValueError(f"Gas station API error: {response.status_code} - {response.text}") @@ -200,6 +214,7 @@ async def _submit_via_legacy_fee_payer( sender_authenticator: AccountAuthenticator, *, client: httpx.AsyncClient | None = None, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: url = f"{config.gas_station_url}/transactions" @@ -219,10 +234,12 @@ async def _submit_via_legacy_fee_payer( headers = {"Content-Type": "application/json"} if client is not None: - response = await client.post(url, json=body, headers=headers) + response = await client.post(url, json=body, headers=headers, timeout=txn_submit_timeout) else: async with httpx.AsyncClient() as temp_client: - response = await temp_client.post(url, json=body, headers=headers) + response = await temp_client.post( + url, json=body, headers=headers, timeout=txn_submit_timeout + ) # TODO: Improve error handling if not response.is_success: @@ -245,6 +262,7 @@ def _submit_via_legacy_fee_payer_sync( sender_authenticator: AccountAuthenticator, *, client: httpx.Client | None = None, + txn_submit_timeout: float | None = None, ) -> PendingTransactionResponse: url = f"{config.gas_station_url}/transactions" @@ -264,10 +282,10 @@ def _submit_via_legacy_fee_payer_sync( headers = {"Content-Type": "application/json"} if client is not None: - response = client.post(url, json=body, headers=headers) + response = client.post(url, json=body, headers=headers, timeout=txn_submit_timeout) else: with httpx.Client() as temp_client: - response = temp_client.post(url, json=body, headers=headers) + response = temp_client.post(url, json=body, headers=headers, timeout=txn_submit_timeout) # TODO: Improve error handling if not response.is_success: diff --git a/src/decibel/abi/_registry.py b/src/decibel/abi/_registry.py index 47ccb9c..28638ee 100644 --- a/src/decibel/abi/_registry.py +++ b/src/decibel/abi/_registry.py @@ -34,7 +34,7 @@ def _load_abi_json(filename: str) -> ABIData: def get_abi_data(chain_id: int | None) -> ABIData: if chain_id == CHAIN_ID_MAINNET: return _load_abi_json("mainnet.json") - if chain_id == CHAIN_ID_NETNA: + elif chain_id == CHAIN_ID_NETNA: return _load_abi_json("netna.json") elif chain_id == CHAIN_ID_TESTNET: return _load_abi_json("testnet.json") diff --git a/src/decibel/abi/generate.py b/src/decibel/abi/generate.py index 32357e6..36c7821 100644 --- a/src/decibel/abi/generate.py +++ b/src/decibel/abi/generate.py @@ -177,7 +177,7 @@ def cli() -> None: "networks", nargs="*", default=["testnet"], - help="Networks to fetch ABIs for (testnet, testnet, mainnet, all). Default: testnet", + help="Networks to fetch ABIs for (testnet, mainnet, all). Default: testnet", ) args = parser.parse_args() diff --git a/src/decibel/write/__init__.py b/src/decibel/write/__init__.py index ad9712a..931a238 100644 --- a/src/decibel/write/__init__.py +++ b/src/decibel/write/__init__.py @@ -147,7 +147,13 @@ async def create_subaccount(self) -> dict[str, Any]: ) ) - async def deposit(self, amount: int, subaccount_addr: str | None = None) -> dict[str, Any]: + async def deposit( + self, + amount: int, + subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, + ) -> dict[str, Any]: pkg = self._config.deployment.package usdc = self._config.deployment.usdc @@ -157,12 +163,20 @@ async def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::deposit_to_subaccount_at", type_arguments=[], function_arguments=[addr, usdc, amount], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) - async def withdraw(self, amount: int, subaccount_addr: str | None = None) -> dict[str, Any]: + async def withdraw( + self, + amount: int, + subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, + ) -> dict[str, Any]: pkg = self._config.deployment.package usdc = self._config.deployment.usdc @@ -172,7 +186,9 @@ async def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::withdraw_from_subaccount", type_arguments=[], function_arguments=[addr, usdc, amount], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -184,6 +200,8 @@ async def configure_user_settings_for_market( subaccount_addr: str, is_cross: bool, user_leverage: int, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -193,7 +211,9 @@ async def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::configure_user_settings_for_market", type_arguments=[], function_arguments=[addr, market_addr, is_cross, user_leverage], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -218,6 +238,8 @@ async def place_order( subaccount_addr: str | None = None, account_override: Account | None = None, tick_size: int | float | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> PlaceOrderResult: try: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) @@ -275,6 +297,8 @@ async def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) tx_response = await self.send_subaccount_tx(_send, subaccount_addr) @@ -298,6 +322,8 @@ async def trigger_matching( *, market_addr: str, max_work_unit: int, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package tx_response = await self._send_tx( @@ -305,7 +331,9 @@ async def trigger_matching( function=f"{pkg}::public_apis::process_perp_market_pending_requests", type_arguments=[], function_arguments=[market_addr, max_work_unit], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return { "success": True, @@ -326,6 +354,8 @@ async def place_twap_order( builder_fees: float | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> PlaceOrderResult: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) pkg = self._config.deployment.package @@ -349,6 +379,8 @@ async def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) tx_response = await self.send_subaccount_tx(_send, subaccount_addr) @@ -369,6 +401,8 @@ async def cancel_order( market_addr: str | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: if market_name is not None: resolved_market_addr = get_market_addr( @@ -389,6 +423,8 @@ async def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, int(order_id), resolved_market_addr], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -406,6 +442,8 @@ async def place_bulk_orders( builder_fee: int | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> PlaceBulkOrdersResult: try: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) @@ -429,6 +467,8 @@ async def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) tx_response = await self.send_subaccount_tx(_send, subaccount_addr) @@ -446,6 +486,8 @@ async def cancel_bulk_order( market_name: str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) pkg = self._config.deployment.package @@ -458,6 +500,8 @@ async def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, market_addr], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -469,6 +513,8 @@ async def cancel_client_order( market_name: str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) pkg = self._config.deployment.package @@ -481,6 +527,8 @@ async def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, client_order_id, market_addr], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -491,6 +539,8 @@ async def delegate_trading_to_for_subaccount( subaccount_addr: str, account_to_delegate_to: str, expiration_timestamp_secs: int | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -504,7 +554,9 @@ async def _send(addr: str) -> dict[str, Any]: account_to_delegate_to, expiration_timestamp_secs, ], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -514,6 +566,8 @@ async def revoke_delegation( *, account_to_revoke: str, subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -523,7 +577,9 @@ async def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::revoke_delegation", type_arguments=[], function_arguments=[addr, account_to_revoke], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -541,6 +597,8 @@ async def place_tp_sl_order_for_position( subaccount_addr: str | None = None, account_override: Account | None = None, tick_size: int | float | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: final_tp_trigger = ( _round_to_tick_size(tp_trigger_price, tick_size) @@ -584,6 +642,8 @@ async def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -598,6 +658,8 @@ async def update_tp_order_for_position( tp_size: float | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -616,6 +678,8 @@ async def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -630,6 +694,8 @@ async def update_sl_order_for_position( sl_size: float | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -648,6 +714,8 @@ async def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -659,6 +727,8 @@ async def cancel_tp_sl_order_for_position( order_id: int | str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -670,6 +740,8 @@ async def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, market_addr, int(order_id)], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -681,6 +753,8 @@ async def cancel_twap_order( order_id: int | str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -692,6 +766,8 @@ async def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, market_addr, int(order_id)], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -719,6 +795,8 @@ async def deactivate_subaccount( subaccount_addr: str, revoke_all_delegations: bool = True, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -730,6 +808,8 @@ async def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, revoke_all_delegations], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -770,6 +850,8 @@ async def create_vault( *, account_override: Account | None = None, subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -799,6 +881,8 @@ async def _send(_: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return await self.send_subaccount_tx(_send, subaccount_addr) @@ -1095,7 +1179,13 @@ def create_subaccount(self) -> dict[str, Any]: ) ) - def deposit(self, amount: int, subaccount_addr: str | None = None) -> dict[str, Any]: + def deposit( + self, + amount: int, + subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, + ) -> dict[str, Any]: pkg = self._config.deployment.package usdc = self._config.deployment.usdc @@ -1105,12 +1195,20 @@ def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::deposit_to_subaccount_at", type_arguments=[], function_arguments=[addr, usdc, amount], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) - def withdraw(self, amount: int, subaccount_addr: str | None = None) -> dict[str, Any]: + def withdraw( + self, + amount: int, + subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, + ) -> dict[str, Any]: pkg = self._config.deployment.package usdc = self._config.deployment.usdc @@ -1120,7 +1218,9 @@ def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::withdraw_from_subaccount", type_arguments=[], function_arguments=[addr, usdc, amount], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1132,6 +1232,8 @@ def configure_user_settings_for_market( subaccount_addr: str, is_cross: bool, user_leverage: int, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1141,7 +1243,9 @@ def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::configure_user_settings_for_market", type_arguments=[], function_arguments=[addr, market_addr, is_cross, user_leverage], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1166,6 +1270,8 @@ def place_order( subaccount_addr: str | None = None, account_override: Account | None = None, tick_size: int | float | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> PlaceOrderResult: try: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) @@ -1223,6 +1329,8 @@ def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) tx_response = self.send_subaccount_tx(_send, subaccount_addr) @@ -1246,6 +1354,8 @@ def trigger_matching( *, market_addr: str, max_work_unit: int, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package tx_response = self._send_tx( @@ -1253,7 +1363,9 @@ def trigger_matching( function=f"{pkg}::public_apis::process_perp_market_pending_requests", type_arguments=[], function_arguments=[market_addr, max_work_unit], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return { "success": True, @@ -1274,6 +1386,8 @@ def place_twap_order( builder_fees: float | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> PlaceOrderResult: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) pkg = self._config.deployment.package @@ -1297,6 +1411,8 @@ def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) tx_response = self.send_subaccount_tx(_send, subaccount_addr) @@ -1317,6 +1433,8 @@ def cancel_order( market_addr: str | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: if market_name is not None: resolved_market_addr = get_market_addr( @@ -1337,6 +1455,8 @@ def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, int(order_id), resolved_market_addr], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1354,6 +1474,8 @@ def place_bulk_orders( builder_fee: int | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> PlaceBulkOrdersResult: try: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) @@ -1377,6 +1499,8 @@ def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) tx_response = self.send_subaccount_tx(_send, subaccount_addr) @@ -1394,6 +1518,8 @@ def cancel_bulk_order( market_name: str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) pkg = self._config.deployment.package @@ -1406,6 +1532,8 @@ def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, market_addr], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1417,6 +1545,8 @@ def cancel_client_order( market_name: str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: market_addr = get_market_addr(market_name, self._config.deployment.perp_engine_global) pkg = self._config.deployment.package @@ -1429,6 +1559,8 @@ def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, client_order_id, market_addr], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1439,6 +1571,8 @@ def delegate_trading_to_for_subaccount( subaccount_addr: str, account_to_delegate_to: str, expiration_timestamp_secs: int | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1452,7 +1586,9 @@ def _send(addr: str) -> dict[str, Any]: account_to_delegate_to, expiration_timestamp_secs, ], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1462,6 +1598,8 @@ def revoke_delegation( *, account_to_revoke: str, subaccount_addr: str | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1471,7 +1609,9 @@ def _send(addr: str) -> dict[str, Any]: function=f"{pkg}::dex_accounts_entry::revoke_delegation", type_arguments=[], function_arguments=[addr, account_to_revoke], - ) + ), + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1489,6 +1629,8 @@ def place_tp_sl_order_for_position( subaccount_addr: str | None = None, account_override: Account | None = None, tick_size: int | float | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: final_tp_trigger = ( _round_to_tick_size(tp_trigger_price, tick_size) @@ -1532,6 +1674,8 @@ def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1546,6 +1690,8 @@ def update_tp_order_for_position( tp_size: float | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1564,6 +1710,8 @@ def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1578,6 +1726,8 @@ def update_sl_order_for_position( sl_size: float | None = None, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1596,6 +1746,8 @@ def _send(addr: str) -> dict[str, Any]: ], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1607,6 +1759,8 @@ def cancel_tp_sl_order_for_position( order_id: int | str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1618,6 +1772,8 @@ def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, market_addr, int(order_id)], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1629,6 +1785,8 @@ def cancel_twap_order( market_addr: str, subaccount_addr: str | None = None, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1637,9 +1795,11 @@ def _send(addr: str) -> dict[str, Any]: InputEntryFunctionData( function=f"{pkg}::dex_accounts_entry::cancel_twap_orders_to_subaccount", type_arguments=[], - function_arguments=[addr, market_addr, order_id], + function_arguments=[addr, market_addr, int(order_id)], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr) @@ -1667,6 +1827,8 @@ def deactivate_subaccount( subaccount_addr: str, revoke_all_delegations: bool = True, account_override: Account | None = None, + txn_submit_timeout: float | None = None, + txn_confirm_timeout: float | None = None, ) -> dict[str, Any]: pkg = self._config.deployment.package @@ -1678,6 +1840,8 @@ def _send(addr: str) -> dict[str, Any]: function_arguments=[addr, revoke_all_delegations], ), account_override, + txn_submit_timeout=txn_submit_timeout, + txn_confirm_timeout=txn_confirm_timeout, ) return self.send_subaccount_tx(_send, subaccount_addr)