From cca002bcb5922348fe9c173c13bd44e5cece1ea9 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 25 Mar 2025 15:09:00 +0100 Subject: [PATCH 001/173] chore: update --- operate/cli.py | 10 ++++++++++ operate/ledger/profiles.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/operate/cli.py b/operate/cli.py index 4fbcfbaf8..df5726af6 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -37,6 +37,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from typing_extensions import Annotated +from operate.bridge.bridge import BridgeManager from uvicorn.config import Config from uvicorn.server import Server @@ -165,6 +166,15 @@ def wallet_manager(self) -> MasterWalletManager: manager.setup() return manager + def bridge_manager(self) -> BridgeManager: + """Load master wallet.""" + manager = BridgeManager( + path=self._path / "bridge", + wallet_manager=self.wallet_manager, + ) + manager.setup() + return manager + def setup(self) -> None: """Make the root directory.""" self._path.mkdir(exist_ok=True) diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index 6de545600..5f2748093 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -21,6 +21,7 @@ import typing as t +from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain, ContractAddresses @@ -118,6 +119,7 @@ "meme_base_beta": "0x6011E09e7c095e76980b22498d69dF18EB62BeD8", "meme_base_beta_2": "0xfb7669c3AdF673b3A545Fa5acd987dbfdA805e22", "meme_base_beta_3": "0xCA61633b03c54F64b6A7F1f9A9C0A6Feb231Cc4D", + "dual_staking_testnet": "0xd64Cf67500b7d15A41E02DDeb40F3A73CB533eB5", }, Chain.CELO: { "meme_celo_alpha_2": "0x95D12D193d466237Bc1E92a1a7756e4264f574AB", @@ -186,3 +188,14 @@ def get_staking_contract( staking_program_id, staking_program_id, ) + + +def get_token_address_on_chain(token_address: str, target_chain: Chain) -> t.Optional[str]: + """Get the corresponding token address on the target chain.""" + if token_address == ZERO_ADDRESS: + return ZERO_ADDRESS + + for token_dict in ERC20_TOKENS: + if token_address in token_dict.values(): + return token_dict.get(target_chain) + return None From 712f419e2bf35c3ae11b9cd94187aa8d08038979 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 26 Mar 2025 17:18:19 +0100 Subject: [PATCH 002/173] feat: bridge_refill_requirements endpoint --- operate/bridge/bridge.py | 326 +++++++++++++++++++++++++++++++++++++ operate/cli.py | 19 ++- operate/ledger/profiles.py | 19 ++- 3 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 operate/bridge/bridge.py diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py new file mode 100644 index 000000000..3aaace1f8 --- /dev/null +++ b/operate/bridge/bridge.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Bridge manager.""" + + +import logging +import time +import typing as t +import uuid +from abc import abstractmethod +from collections import defaultdict +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path + +import requests +from aea.helpers.logging import setup_logger +from deepdiff import DeepDiff + +from operate.constants import ZERO_ADDRESS +from operate.ledger.profiles import get_target_chain_asset_address +from operate.operate_types import Chain +from operate.resource import LocalResource +from operate.services.manage import get_assets_balances +from operate.wallet.master import MasterWalletManager + + +DEFAULT_MAX_RETRIES = 3 +BRIDGE_PATH = "bridge" +QUOTE_VALIDITY_PERIOD = 3 * 60 + + +class Bridge: + """Abstract Bridge""" + + @abstractmethod + def get_quote( + self, + from_chain: Chain, + from_address: str, + from_token: str, + to_chain: Chain, + to_token: str, + to_address: str, + to_amount: int, + ) -> dict: + """Get bridge quote""" + raise NotImplementedError() + + @abstractmethod + def get_from_token(self, quote: dict) -> int: + """Get the from_token of a quote""" + raise NotImplementedError() + + @abstractmethod + def get_from_amount(self, quote: dict) -> int: + """Get the from_amount of a quote""" + raise NotImplementedError() + + @abstractmethod + def get_transaction_value(self, quote: dict) -> int: + """Get the transaction value to execute a quote""" + raise NotImplementedError() + + def get_quotes(self, from_chain: Chain, from_address: str, to: dict) -> dict: + """Get bridge quotes (destinations specified in `to` dict)""" + quotes = {} + for to_chain_str in to: + for to_address in to[to_chain_str]: + for to_token, to_amount in to[to_chain_str][to_address].items(): + to_chain = Chain(to_chain_str) + from_token = get_target_chain_asset_address( + source_chain=to_chain, + source_asset_address=to_token, + target_chain=from_chain, + ) + + quote = self.get_quote( + from_chain=from_chain, + from_address=from_address, + from_token=from_token, + to_chain=to_chain, + to_address=to_address, + to_token=to_token, + to_amount=to_amount, + ) + + quotes[quote["id"]] = quote + + return quotes + + def get_bridge_requirements(self, quotes: dict) -> dict: + """Get bridge requirements given a collection of quotes""" + bridge_requirements: defaultdict = defaultdict(int) + + for _, quote in quotes.items(): + from_token = self.get_from_token(quote) + from_amount = self.get_from_amount(quote) + transaction_value = self.get_transaction_value(quote) + if from_token != ZERO_ADDRESS: + bridge_requirements[from_token] += from_amount + bridge_requirements[ZERO_ADDRESS] += transaction_value + + return dict(bridge_requirements) + + +class LiFiBridge(Bridge): + """LI.FI Bridge""" + + def get_quote( + self, + from_chain: Chain, + from_address: str, + from_token: str, + to_chain: Chain, + to_token: str, + to_address: str, + to_amount: int, + ) -> dict: + """Get bridge quote""" + + url = "https://li.quest/v1/quote/toAmount" + headers = {"accept": "application/json"} + params = { + "fromChain": from_chain.id, + "fromAddress": from_address, + "fromToken": from_token, + "toChain": to_chain.id, + "toAddress": to_address, + "toToken": to_token, + "toAmount": to_amount, + } + for attempt in range(1, DEFAULT_MAX_RETRIES + 1): + try: + response = requests.get( + url=url, headers=headers, params=params, timeout=30 + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print( + f"[BRIDGE MANAGER] Request quote failed with code {response.status_code} (attempt {attempt}/{DEFAULT_MAX_RETRIES}): {e}" + ) + + if attempt >= DEFAULT_MAX_RETRIES: + print( + f"[BRIDGE MANAGER]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" + ) + raise + return None + + def get_from_token(self, quote: dict) -> int: + """Get the from_token of a quote""" + return quote["action"]["fromToken"]["address"] + + def get_from_amount(self, quote: dict) -> int: + """Get the from_amount of a quote""" + return int(quote["action"]["fromAmount"]) + + def get_transaction_value(self, quote: dict) -> int: + """Get the transaction value to execute a quote""" + return int(quote["transactionRequest"]["value"], 16) + + +class BridgeManagerState(Enum): + """BridgeManagerState""" + + QUOTE_BUNDLE_NOT_REQUESTED = 0 + QUOTE_BUNDLE_REQUESTED = 1 + + +@dataclass +class BridgeManagerData(LocalResource): + """BridgeManagerData""" + + path: Path + version: int = 1 + last_requested_quote_bundle: t.Dict = field(default_factory=dict) # type: ignore + state: BridgeManagerState = BridgeManagerState.QUOTE_BUNDLE_NOT_REQUESTED + + _file = "bridge.json" + + +class BridgeManager: + """BridgeManager""" + + def __init__( + self, + path: Path, + wallet_manager: MasterWalletManager, + logger: t.Optional[logging.Logger] = None, + bridge: t.Optional[Bridge] = None, + ) -> None: + """Initialize bridge manager.""" + self.path = path + self.wallet_manager = wallet_manager + self.logger = logger or setup_logger(name="operate.master_wallet_manager") + self.bridge = bridge or LiFiBridge() + self.path.mkdir(exist_ok=True) + + # TODO Migrate to LocalResource + data_file = path / BridgeManagerData._file + if not data_file.exists(): + data = BridgeManagerData(path=path) + data.store() + # End migrate + + self.data = BridgeManagerData.load(path) + + @staticmethod + def _get_from_tokens(from_chain: Chain, to: dict) -> set: + from_tokens = set() + for to_chain_str in to: + for to_address in to[to_chain_str]: + for to_token in to[to_chain_str][to_address]: + to_chain = Chain(to_chain_str) + from_tokens.add( + get_target_chain_asset_address( + source_chain=to_chain, + source_asset_address=to_token, + target_chain=from_chain, + ) + ) + return from_tokens + + def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: + """Get bridge refill requirements.""" + + if not isinstance(from_, dict) or len(from_) != 1: + raise ValueError( + f"[BRIDGE MANAGER] Invalid 'from_' input: Must contain exactly one chain mapping. {from_=}" + ) + + from_chain, from_address = next(iter(from_.items())) + from_chain = Chain(from_chain) + + wallet = self.wallet_manager.load(from_chain.ledger_type) + + if ( + wallet.safes + and from_chain in wallet.safes + and from_address == wallet.safes[from_chain] + ): + from_safe = True + elif from_address == wallet.address: + from_safe = False + else: + raise ValueError( + f"[BRIDGE MANAGER] Invalid 'from_' input: Address does not match Master Safe nor Master EOA. {from_=}" + ) + + bridge = self.bridge + + quote_bundle = self.data.last_requested_quote_bundle + now = int(time.time()) + + refresh_quote_bundle = ( + not quote_bundle + or now > quote_bundle.get("expiration_timestamp", 0) + or DeepDiff(quote_bundle.get("from", {}), from_) + or DeepDiff(quote_bundle.get("to", {}), to) + ) + + if refresh_quote_bundle: + self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") + quote_bundle = {} + quote_bundle["id"] = f"{uuid.uuid4()}" + quote_bundle["timestamp"] = now + quote_bundle["expiration_timestamp"] = now + QUOTE_VALIDITY_PERIOD + quote_bundle["quotes"] = {} + quote_bundle["from"] = from_ + quote_bundle["to"] = to + quote_bundle["from_safe"] = from_safe + quote_bundle["quotes"] = bridge.get_quotes( + from_chain=from_chain, from_address=from_address, to=to + ) + quote_bundle["bridge_requirements"] = bridge.get_bridge_requirements( + quote_bundle["quotes"] + ) + self.data.last_requested_quote_bundle = quote_bundle + + self.data.state = BridgeManagerState.QUOTE_BUNDLE_REQUESTED + self.data.store() + + balances = get_assets_balances( + ledger_api=wallet.ledger_api(chain=from_chain), + addresses={from_address}, + asset_addresses={ZERO_ADDRESS} + | self._get_from_tokens(from_chain=from_chain, to=to), + raise_on_invalid_address=False, + ) + + bridge_refill_requirements = {} + for from_token, amount in quote_bundle["bridge_requirements"].items(): + bridge_refill_requirements[from_token] = max( + amount - balances[from_address][from_token], 0 + ) + + return { + "id": quote_bundle["id"], + "balances": {from_chain.value: balances}, + "bridge_requirements": { + from_chain.value: quote_bundle["bridge_requirements"] + }, + "bridge_refill_requirements": { + from_chain.value: dict(bridge_refill_requirements) + }, + "expiration_timestamp": quote_bundle["expiration_timestamp"], + } diff --git a/operate/cli.py b/operate/cli.py index df5726af6..6de1c7507 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -37,12 +37,12 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from typing_extensions import Annotated -from operate.bridge.bridge import BridgeManager from uvicorn.config import Config from uvicorn.server import Server from operate import services from operate.account.user import UserAccount +from operate.bridge.bridge import BridgeManager from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES from operate.ledger.profiles import DEFAULT_NEW_SAFE_FUNDS_AMOUNT from operate.operate_types import Chain, DeploymentStatus, LedgerType @@ -172,7 +172,6 @@ def bridge_manager(self) -> BridgeManager: path=self._path / "bridge", wallet_manager=self.wallet_manager, ) - manager.setup() return manager def setup(self) -> None: @@ -797,7 +796,7 @@ async def _get_service_deployment(request: Request) -> JSONResponse: @app.get("/api/v2/service/{service_config_id}/refill_requirements") @with_retries async def _get_refill_requirements(request: Request) -> JSONResponse: - """Get the service balances.""" + """Get the service refill requirements.""" service_config_id = request.path_params["service_config_id"] if not operate.service_manager().exists(service_config_id=service_config_id): @@ -993,6 +992,20 @@ async def _withdraw_onchain(request: Request) -> JSONResponse: return JSONResponse(content={"error": None}) + @app.post("/api/bridge/bridge_refill_requirements") + @with_retries + async def _bridge_refill_requirements(request: Request) -> JSONResponse: + """Get the bridge refill requirements.""" + if operate.password is None: + return USER_NOT_LOGGED_IN_ERROR + + data = await request.json() + return JSONResponse( + content=operate.bridge_manager().bridge_refill_requirements( + from_=data["from"], to=data["to"] + ) + ) + return app diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index 5f2748093..aa5823e01 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -190,12 +190,21 @@ def get_staking_contract( ) -def get_token_address_on_chain(token_address: str, target_chain: Chain) -> t.Optional[str]: +def get_target_chain_asset_address( + source_chain: Chain, source_asset_address: str, target_chain: Chain +) -> str: """Get the corresponding token address on the target chain.""" - if token_address == ZERO_ADDRESS: + if source_asset_address == ZERO_ADDRESS: return ZERO_ADDRESS + target_chain_token_address = None for token_dict in ERC20_TOKENS: - if token_address in token_dict.values(): - return token_dict.get(target_chain) - return None + if source_asset_address == token_dict.get(source_chain): + target_chain_token_address = token_dict.get(target_chain) + + if not target_chain_token_address: + raise ValueError( + f"Token {source_asset_address=} on {source_chain=} not found for {target_chain=}." + ) + + return target_chain_token_address From a6445450e872b6dfcd8f6d549934f19d80b455e3 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 26 Mar 2025 17:22:08 +0100 Subject: [PATCH 003/173] chore: linters --- operate/bridge/bridge.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 3aaace1f8..bb4e938df 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -164,7 +164,7 @@ def get_quote( f"[BRIDGE MANAGER]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" ) raise - return None + return {} def get_from_token(self, quote: dict) -> int: """Get the from_token of a quote""" @@ -222,7 +222,9 @@ def __init__( data.store() # End migrate - self.data = BridgeManagerData.load(path) + self.data: BridgeManagerData = t.cast( + BridgeManagerData, BridgeManagerData.load(path) + ) @staticmethod def _get_from_tokens(from_chain: Chain, to: dict) -> set: From 8471156dfffdb8e09efb099b0259fd34bd79ad46 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 26 Mar 2025 17:44:15 +0100 Subject: [PATCH 004/173] docs: update --- docs/api.md | 65 ++++++++++++++++++++++++++++++++++++++++ operate/bridge/bridge.py | 11 +++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 20c778f6e..53c51cc6f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -893,6 +893,71 @@ The refill requirements are computed based on the fund requirements present on t --- +## Bridge + +### `POST /api/bridge/bridge_refill_requirements` + +Returns the refill requirements on the source chain for bridging assets to target chains. + +
+ Request + +```json + { + "from": { + "ethereum": "0xDe6B572A049B27D349e89aD0cBEF102227e31473" + }, + "to": { + "gnosis": { + "0xDe6B572A049B27D349e89aD0cBEF102227e31473": { + "0x0000000000000000000000000000000000000000": 10000000000000000000, + "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": 0 + }, + "0x28580196F52DB3C95C3d40Df88426e251d115842": { + "0x0000000000000000000000000000000000000000": 10000000000000000000, + "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": 60000000000000000000 + }, + } + } + } +``` + +
+ +
+ Response + +```json + { + "id": "e05509f3-d153-4b69-94cf-f82324f8c226", + "balances": { + "ethereum": { + "0xDe6B572A049B27D349e89aD0cBEF102227e31473": { + "0x0000000000000000000000000000000000000000": 0, + "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0": 0 + } + } + }, + "bridge_requirements": { + "ethereum": { + "0x0000000000000000000000000000000000000000": 10073082159280405, + "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0": 61944358967139717502 + } + }, + "bridge_refill_requirements": { + "ethereum": { + "0x0000000000000000000000000000000000000000": 10073082159280405, + "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0": 61944358967139717502 + } + }, + "expiration_timestamp": 1743007255 + } +``` + +
+ +--- + ## Unused endpoints ### `POST /api/services/{service}/onchain/deploy` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index bb4e938df..8eb19d432 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -43,8 +43,8 @@ DEFAULT_MAX_RETRIES = 3 -BRIDGE_PATH = "bridge" QUOTE_VALIDITY_PERIOD = 3 * 60 +QUOTE_BUNDLE_PREFFIX = "qb-" class Bridge: @@ -102,7 +102,8 @@ def get_quotes(self, from_chain: Chain, from_address: str, to: dict) -> dict: to_amount=to_amount, ) - quotes[quote["id"]] = quote + if quote: + quotes[quote["id"]] = quote return quotes @@ -136,6 +137,9 @@ def get_quote( ) -> dict: """Get bridge quote""" + if to_amount == 0: + return {} + url = "https://li.quest/v1/quote/toAmount" headers = {"accept": "application/json"} params = { @@ -255,6 +259,7 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: wallet = self.wallet_manager.load(from_chain.ledger_type) + from_safe = False if ( wallet.safes and from_chain in wallet.safes @@ -283,7 +288,7 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: if refresh_quote_bundle: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") quote_bundle = {} - quote_bundle["id"] = f"{uuid.uuid4()}" + quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFFIX}{uuid.uuid4()}" quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + QUOTE_VALIDITY_PERIOD quote_bundle["quotes"] = {} From 5f388432c2f625fd9b893161ad75fadff3c56ff2 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 26 Mar 2025 18:54:59 +0100 Subject: [PATCH 005/173] chore: update --- docs/api.md | 9 ++-- operate/bridge/bridge.py | 97 ++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/api.md b/docs/api.md index 53c51cc6f..59992606e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -946,11 +946,14 @@ Returns the refill requirements on the source chain for bridging assets to targe }, "bridge_refill_requirements": { "ethereum": { - "0x0000000000000000000000000000000000000000": 10073082159280405, - "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0": 61944358967139717502 + "0xDe6B572A049B27D349e89aD0cBEF102227e31473": { + "0x0000000000000000000000000000000000000000": 10073082159280405, + "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0": 61944358967139717502 + } } }, - "expiration_timestamp": 1743007255 + "expiration_timestamp": 1743007255, + "is_refill_required": true } ``` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 8eb19d432..76f3bf108 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -44,7 +44,7 @@ DEFAULT_MAX_RETRIES = 3 QUOTE_VALIDITY_PERIOD = 3 * 60 -QUOTE_BUNDLE_PREFFIX = "qb-" +QUOTE_BUNDLE_PREFIX = "qb-" class Bridge: @@ -187,7 +187,7 @@ class BridgeManagerState(Enum): """BridgeManagerState""" QUOTE_BUNDLE_NOT_REQUESTED = 0 - QUOTE_BUNDLE_REQUESTED = 1 + QUOTE_BUNDLE_UP_TO_DATE = 1 @dataclass @@ -229,6 +229,8 @@ def __init__( self.data: BridgeManagerData = t.cast( BridgeManagerData, BridgeManagerData.load(path) ) + self.data.state = BridgeManagerState.QUOTE_BUNDLE_NOT_REQUESTED + self.data.store() @staticmethod def _get_from_tokens(from_chain: Chain, to: dict) -> set: @@ -246,14 +248,8 @@ def _get_from_tokens(from_chain: Chain, to: dict) -> set: ) return from_tokens - def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: - """Get bridge refill requirements.""" - - if not isinstance(from_, dict) or len(from_) != 1: - raise ValueError( - f"[BRIDGE MANAGER] Invalid 'from_' input: Must contain exactly one chain mapping. {from_=}" - ) - + def _get_valid_quote_bundle(self, from_: dict, to: dict) -> dict: + """Ensures to return a valid (non expired) quote bundle for the given inputs.""" from_chain, from_address = next(iter(from_.items())) from_chain = Chain(from_chain) @@ -273,38 +269,59 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: f"[BRIDGE MANAGER] Invalid 'from_' input: Address does not match Master Safe nor Master EOA. {from_=}" ) - bridge = self.bridge - quote_bundle = self.data.last_requested_quote_bundle now = int(time.time()) - refresh_quote_bundle = ( - not quote_bundle - or now > quote_bundle.get("expiration_timestamp", 0) - or DeepDiff(quote_bundle.get("from", {}), from_) - or DeepDiff(quote_bundle.get("to", {}), to) - ) + refresh_quote_bundle = False + if not quote_bundle: + self.logger.info("[BRIDGE MANAGER] No last_requested_quote_bundle.") + refresh_quote_bundle = True + elif DeepDiff(quote_bundle.get("from", {}), from_) or DeepDiff( + quote_bundle.get("to", {}), to + ): + self.logger.info( + "[BRIDGE MANAGER] Different quote bundle input parameters." + ) + refresh_quote_bundle = True + elif now > quote_bundle.get("expiration_timestamp", 0): + self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") + refresh_quote_bundle = True if refresh_quote_bundle: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") quote_bundle = {} - quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFFIX}{uuid.uuid4()}" + quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + QUOTE_VALIDITY_PERIOD quote_bundle["quotes"] = {} quote_bundle["from"] = from_ quote_bundle["to"] = to quote_bundle["from_safe"] = from_safe - quote_bundle["quotes"] = bridge.get_quotes( + quote_bundle["quotes"] = self.bridge.get_quotes( from_chain=from_chain, from_address=from_address, to=to ) - quote_bundle["bridge_requirements"] = bridge.get_bridge_requirements( + quote_bundle["bridge_requirements"] = self.bridge.get_bridge_requirements( quote_bundle["quotes"] ) self.data.last_requested_quote_bundle = quote_bundle - self.data.state = BridgeManagerState.QUOTE_BUNDLE_REQUESTED + self.data.state = BridgeManagerState.QUOTE_BUNDLE_UP_TO_DATE self.data.store() + return quote_bundle + + def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: + """Get bridge refill requirements.""" + + if not isinstance(from_, dict) or len(from_) != 1: + raise ValueError( + f"[BRIDGE MANAGER] Invalid 'from_' input: Must contain exactly one chain mapping. {from_=}" + ) + + from_chain, from_address = next(iter(from_.items())) + from_chain = Chain(from_chain) + wallet = self.wallet_manager.load(from_chain.ledger_type) + + quote_bundle = self._get_valid_quote_bundle(from_, to) balances = get_assets_balances( ledger_api=wallet.ledger_api(chain=from_chain), @@ -315,11 +332,19 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: ) bridge_refill_requirements = {} + bridge_refill_requirements[from_address] = {} for from_token, amount in quote_bundle["bridge_requirements"].items(): - bridge_refill_requirements[from_token] = max( + bridge_refill_requirements[from_address][from_token] = max( amount - balances[from_address][from_token], 0 ) + print(bridge_refill_requirements) + is_refill_required = any( + amount > 0 + for asset in bridge_refill_requirements.values() + for amount in asset.values() + ) + return { "id": quote_bundle["id"], "balances": {from_chain.value: balances}, @@ -330,4 +355,30 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: from_chain.value: dict(bridge_refill_requirements) }, "expiration_timestamp": quote_bundle["expiration_timestamp"], + "is_refill_required": is_refill_required, } + + def execute_quote_bundle(self, quote_bundle_id: str) -> None: + """Execute quote bundle""" + + if self.data.state != BridgeManagerState.QUOTE_BUNDLE_UP_TO_DATE: + raise RuntimeError( + "[BRIDGE MANAGER] You must retrieve a valid quote first." + ) + + if self.data.last_requested_quote_bundle["id"] != quote_bundle_id: + raise RuntimeError( + f"[BRIDGE MANAGER] Id {quote_bundle_id} does not match latest requested quote bundle id {self.data.last_requested_quote_bundle['id']}." + ) + + from_ = self.data.last_requested_quote_bundle["from"] + to = self.data.last_requested_quote_bundle["to"] + + reqs = self.bridge_refill_requirements(from_, to) + + if reqs["is_refill_required"]: + raise RuntimeError( + f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bindle id {quote_bundle_id}." + ) + + print("[BRIDGE MANAGER] Executing quotes") From 1c90c4730211960dd950b699548f30b7227cddf2 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 26 Mar 2025 23:30:12 +0100 Subject: [PATCH 006/173] chore: update --- operate/bridge/bridge.py | 104 +++++++++++++++++++++++++++++++++++++-- operate/cli.py | 1 + 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 76f3bf108..5849b27b7 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -33,8 +33,10 @@ import requests from aea.helpers.logging import setup_logger from deepdiff import DeepDiff +from web3 import Web3 from operate.constants import ZERO_ADDRESS +from operate.ledger import get_default_rpc from operate.ledger.profiles import get_target_chain_asset_address from operate.operate_types import Chain from operate.resource import LocalResource @@ -43,13 +45,17 @@ DEFAULT_MAX_RETRIES = 3 -QUOTE_VALIDITY_PERIOD = 3 * 60 +DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" class Bridge: """Abstract Bridge""" + def __init__(self, wallet_manager: MasterWalletManager) -> None: + """Initialize the bridge""" + self.wallet_manager = wallet_manager + @abstractmethod def get_quote( self, @@ -107,6 +113,7 @@ def get_quotes(self, from_chain: Chain, from_address: str, to: dict) -> dict: return quotes + # TODO gas fees ! def get_bridge_requirements(self, quotes: dict) -> dict: """Get bridge requirements given a collection of quotes""" bridge_requirements: defaultdict = defaultdict(int) @@ -121,6 +128,11 @@ def get_bridge_requirements(self, quotes: dict) -> dict: return dict(bridge_requirements) + @abstractmethod + def execute_quote(self, quote: dict) -> None: + """Execute the quote""" + raise NotImplementedError() + class LiFiBridge(Bridge): """LI.FI Bridge""" @@ -182,6 +194,80 @@ def get_transaction_value(self, quote: dict) -> int: """Get the transaction value to execute a quote""" return int(quote["transactionRequest"]["value"], 16) + def execute_quote(self, quote) -> None: + """Execute the quote""" + + print("Execute_quote") + from_token = quote["action"]["fromToken"]["address"] + from_amount = int(quote["action"]["fromAmount"]) + + transaction_request = quote["transactionRequest"] + from_chain = Chain.from_id(transaction_request["chainId"]) + wallet = self.wallet_manager.load(from_chain.ledger_type) + + # TODO rewrite with framework methods + private_key = wallet.crypto.private_key + w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) + account = w3.eth.account.from_key(private_key) + + if from_token != ZERO_ADDRESS: + print(f"Approve transaction for token {from_token}") + from_token_contract = w3.eth.contract( + address=from_token, + abi=[ + { + "constant": False, + "inputs": [ + {"name": "spender", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + "name": "approve", + "outputs": [{"name": "", "type": "bool"}], + "payable": False, + "stateMutability": "nonpayable", + "type": "function", + } + ], + ) + + transaction = from_token_contract.functions.approve( + transaction_request["to"], from_amount + ).build_transaction( + { + "from": account.address, + "nonce": w3.eth.get_transaction_count(account.address), + "gasPrice": w3.to_wei("20", "gwei"), + } + ) + + gas_estimate = w3.eth.estimate_gas(transaction) + transaction["gas"] = gas_estimate + signed_transaction = w3.eth.account.sign_transaction( + transaction, private_key + ) + tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) + print(f"Approve transaction {tx_hash=}") + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + print(f"Approve transaction {receipt=}") + + transaction = { + "value": w3.to_wei(int(transaction_request["value"], 16), "wei"), + "to": transaction_request["to"], + "data": bytes.fromhex(transaction_request["data"][2:]), + "from": account.address, + "gasPrice": w3.to_wei(int(transaction_request["gasPrice"], 16), "wei"), + "chainId": transaction_request["chainId"], + "nonce": w3.eth.get_transaction_count(account.address), + } + + gas_estimate = w3.eth.estimate_gas(transaction) + transaction["gas"] = gas_estimate + signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) + tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) + print(f"Quote transaction {tx_hash=}") + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + print(f"Quote transaction {receipt=}") + class BridgeManagerState(Enum): """BridgeManagerState""" @@ -211,12 +297,17 @@ def __init__( wallet_manager: MasterWalletManager, logger: t.Optional[logging.Logger] = None, bridge: t.Optional[Bridge] = None, + quote_validity_period: t.Optional[int] = None, ) -> None: """Initialize bridge manager.""" self.path = path self.wallet_manager = wallet_manager self.logger = logger or setup_logger(name="operate.master_wallet_manager") - self.bridge = bridge or LiFiBridge() + self.bridge = bridge or LiFiBridge(wallet_manager) + self.quote_validity_period = ( + quote_validity_period or DEFAULT_QUOTE_VALIDITY_PERIOD + ) + self.path.mkdir(exist_ok=True) # TODO Migrate to LocalResource @@ -292,7 +383,7 @@ def _get_valid_quote_bundle(self, from_: dict, to: dict) -> dict: quote_bundle = {} quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" quote_bundle["timestamp"] = now - quote_bundle["expiration_timestamp"] = now + QUOTE_VALIDITY_PERIOD + quote_bundle["expiration_timestamp"] = now + self.quote_validity_period quote_bundle["quotes"] = {} quote_bundle["from"] = from_ quote_bundle["to"] = to @@ -321,6 +412,8 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: from_chain = Chain(from_chain) wallet = self.wallet_manager.load(from_chain.ledger_type) + # TODO Purge empty addresses on 'to' + quote_bundle = self._get_valid_quote_bundle(from_, to) balances = get_assets_balances( @@ -378,7 +471,10 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: if reqs["is_refill_required"]: raise RuntimeError( - f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bindle id {quote_bundle_id}." + f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bundle id {quote_bundle_id}." ) + for _, quote in self.data.last_requested_quote_bundle["quotes"].items(): + self.bridge.execute_quote(quote) + print("[BRIDGE MANAGER] Executing quotes") diff --git a/operate/cli.py b/operate/cli.py index 6de1c7507..47be20993 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -171,6 +171,7 @@ def bridge_manager(self) -> BridgeManager: manager = BridgeManager( path=self._path / "bridge", wallet_manager=self.wallet_manager, + quote_validity_period=24 * 60 * 60, # TODO remove ) return manager From ec81e99f6787583b9644b63bed570b65232b1d60 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 27 Mar 2025 13:11:40 +0100 Subject: [PATCH 007/173] fix: docs --- docs/api.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/api.md b/docs/api.md index 59992606e..6b9238530 100644 --- a/docs/api.md +++ b/docs/api.md @@ -338,7 +338,8 @@ Creates a Gnosis safe for given chain. ```js { - "chain": Chain, + "chain": "gnosis", + "backup_owner": "0x46eC2E77Fe3E367252f1A8a77470CE8eEd2A985b" "fund_amount": 10000000000000000 } ``` @@ -352,15 +353,8 @@ Creates a Gnosis safe for given chain. ```json { - "address": "0xFafd5cb31a611C5e5aa65ea8c6226EB4328175E7", - "safe_chains": [ - "gnosis" - ], - "ledger_type": 0, - "safes": { - "gnosis": "0xd56fb274ce2C66008D5c4C09980c4f36Ab81ff23" - }, - "safe_nonce": 110558881674480320952254000342160989674913430251257716940579305238321962891821 + "safe": "0x29e23F7705d849F368855947691cB133CD770752", + "message": "Safe created!" } ``` From 0e55e21572424d8b7d89d6395be2e86b8275a162 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 28 Mar 2025 18:30:58 +0100 Subject: [PATCH 008/173] chore: refactor --- operate/bridge/bridge.py | 353 ++++++++++++++++++++++----------------- operate/cli.py | 2 +- 2 files changed, 200 insertions(+), 155 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 5849b27b7..680bcbdfa 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -20,15 +20,16 @@ """Bridge manager.""" +import json import logging import time -import typing as t import uuid from abc import abstractmethod from collections import defaultdict from dataclasses import dataclass, field from enum import Enum from pathlib import Path +from typing import cast import requests from aea.helpers.logging import setup_logger @@ -41,6 +42,7 @@ from operate.operate_types import Chain from operate.resource import LocalResource from operate.services.manage import get_assets_balances +from operate.utils.gnosis import get_asset_balance from operate.wallet.master import MasterWalletManager @@ -70,63 +72,49 @@ def get_quote( """Get bridge quote""" raise NotImplementedError() - @abstractmethod - def get_from_token(self, quote: dict) -> int: - """Get the from_token of a quote""" - raise NotImplementedError() - - @abstractmethod - def get_from_amount(self, quote: dict) -> int: - """Get the from_amount of a quote""" - raise NotImplementedError() - - @abstractmethod - def get_transaction_value(self, quote: dict) -> int: - """Get the transaction value to execute a quote""" - raise NotImplementedError() - - def get_quotes(self, from_chain: Chain, from_address: str, to: dict) -> dict: + def get_quotes(self, quote_requests: list) -> dict: """Get bridge quotes (destinations specified in `to` dict)""" quotes = {} - for to_chain_str in to: - for to_address in to[to_chain_str]: - for to_token, to_amount in to[to_chain_str][to_address].items(): - to_chain = Chain(to_chain_str) - from_token = get_target_chain_asset_address( - source_chain=to_chain, - source_asset_address=to_token, - target_chain=from_chain, - ) - quote = self.get_quote( - from_chain=from_chain, - from_address=from_address, - from_token=from_token, - to_chain=to_chain, - to_address=to_address, - to_token=to_token, - to_amount=to_amount, - ) + for quote_request in quote_requests: + quote = self.get_quote( + from_chain=Chain(quote_request["from_chain"]), + from_address=quote_request["from_address"], + from_token=quote_request["from_token"], + to_chain=Chain(quote_request["to_chain"]), + to_address=quote_request["to_address"], + to_token=quote_request["to_token"], + to_amount=quote_request["to_amount"], + ) - if quote: - quotes[quote["id"]] = quote + if quote: + quotes[quote["id"]] = quote return quotes - # TODO gas fees ! - def get_bridge_requirements(self, quotes: dict) -> dict: + @abstractmethod + def get_quote_requirements(self, quote: dict) -> dict: """Get bridge requirements given a collection of quotes""" - bridge_requirements: defaultdict = defaultdict(int) + raise NotImplementedError() - for _, quote in quotes.items(): - from_token = self.get_from_token(quote) - from_amount = self.get_from_amount(quote) - transaction_value = self.get_transaction_value(quote) - if from_token != ZERO_ADDRESS: - bridge_requirements[from_token] += from_amount - bridge_requirements[ZERO_ADDRESS] += transaction_value + def sum_quotes_requirements(self, quotes: dict) -> dict: + """Get bridge requirements given a collection of quotes""" - return dict(bridge_requirements) + bridge_requirements = {} + + for _, quote in quotes.items(): + req = self.get_quote_requirements(quote) + for from_chain, from_addresses in req.items(): + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + bridge_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ).setdefault(from_token, 0) + bridge_requirements[from_chain][from_address][ + from_token + ] += from_amount + + return bridge_requirements @abstractmethod def execute_quote(self, quote: dict) -> None: @@ -182,17 +170,31 @@ def get_quote( raise return {} - def get_from_token(self, quote: dict) -> int: - """Get the from_token of a quote""" - return quote["action"]["fromToken"]["address"] + # TODO gas fees ! + def get_quote_requirements(self, quote: dict) -> dict: + """Get bridge requirements given a collection of quotes""" - def get_from_amount(self, quote: dict) -> int: - """Get the from_amount of a quote""" - return int(quote["action"]["fromAmount"]) + from_chain = Chain.from_id(quote["action"]["fromChainId"]) + from_address = quote["action"]["fromAddress"] + from_token = quote["action"]["fromToken"]["address"] + from_amount = int(quote["action"]["fromAmount"]) + transaction_value = int(quote["transactionRequest"]["value"], 16) - def get_transaction_value(self, quote: dict) -> int: - """Get the transaction value to execute a quote""" - return int(quote["transactionRequest"]["value"], 16) + if from_token == ZERO_ADDRESS: + return { + from_chain.value: { + from_address: {from_token: from_amount + transaction_value} + } + } + else: + return { + from_chain.value: { + from_address: { + ZERO_ADDRESS: transaction_value, + from_token: from_amount, + } + } + } def execute_quote(self, quote) -> None: """Execute the quote""" @@ -269,21 +271,13 @@ def execute_quote(self, quote) -> None: print(f"Quote transaction {receipt=}") -class BridgeManagerState(Enum): - """BridgeManagerState""" - - QUOTE_BUNDLE_NOT_REQUESTED = 0 - QUOTE_BUNDLE_UP_TO_DATE = 1 - - @dataclass class BridgeManagerData(LocalResource): """BridgeManagerData""" path: Path version: int = 1 - last_requested_quote_bundle: t.Dict = field(default_factory=dict) # type: ignore - state: BridgeManagerState = BridgeManagerState.QUOTE_BUNDLE_NOT_REQUESTED + requested_quote_bundles: dict = field(default_factory=dict) _file = "bridge.json" @@ -295,9 +289,9 @@ def __init__( self, path: Path, wallet_manager: MasterWalletManager, - logger: t.Optional[logging.Logger] = None, - bridge: t.Optional[Bridge] = None, - quote_validity_period: t.Optional[int] = None, + logger: logging.Logger | None = None, + bridge: Bridge | None = None, + quote_validity_period: int | None = None, ) -> None: """Initialize bridge manager.""" self.path = path @@ -317,10 +311,9 @@ def __init__( data.store() # End migrate - self.data: BridgeManagerData = t.cast( + self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) ) - self.data.state = BridgeManagerState.QUOTE_BUNDLE_NOT_REQUESTED self.data.store() @staticmethod @@ -338,41 +331,33 @@ def _get_from_tokens(from_chain: Chain, to: dict) -> set: ) ) return from_tokens + + @staticmethod + def _get_quote_bundle_id(quote_requests: list) -> str + """Generate a deterministic id based on the content of quote_requests.""" - def _get_valid_quote_bundle(self, from_: dict, to: dict) -> dict: - """Ensures to return a valid (non expired) quote bundle for the given inputs.""" - from_chain, from_address = next(iter(from_.items())) - from_chain = Chain(from_chain) + json_list = [json.dumps(obj, sort_keys=True, separators=(",", ":")) for obj in quote_requests] + + # Sort the JSON string representations + json_list.sort() - wallet = self.wallet_manager.load(from_chain.ledger_type) - from_safe = False - if ( - wallet.safes - and from_chain in wallet.safes - and from_address == wallet.safes[from_chain] - ): - from_safe = True - elif from_address == wallet.address: - from_safe = False - else: - raise ValueError( - f"[BRIDGE MANAGER] Invalid 'from_' input: Address does not match Master Safe nor Master EOA. {from_=}" - ) - quote_bundle = self.data.last_requested_quote_bundle + + def _get_valid_quote_bundle(self, quote_requests: list) -> dict: + """Ensures to return a valid (non expired) quote bundle for the given inputs.""" + quote_bundle = self.data.requested_quote_bundles now = int(time.time()) refresh_quote_bundle = False if not quote_bundle: self.logger.info("[BRIDGE MANAGER] No last_requested_quote_bundle.") refresh_quote_bundle = True - elif DeepDiff(quote_bundle.get("from", {}), from_) or DeepDiff( - quote_bundle.get("to", {}), to - ): - self.logger.info( - "[BRIDGE MANAGER] Different quote bundle input parameters." - ) + quote_bundle = {} + quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" + quote_bundle["quote_requests"] = quote_requests + elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", {})): + self.logger.info("[BRIDGE MANAGER] Different quote requests.") refresh_quote_bundle = True elif now > quote_bundle.get("expiration_timestamp", 0): self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") @@ -380,73 +365,141 @@ def _get_valid_quote_bundle(self, from_: dict, to: dict) -> dict: if refresh_quote_bundle: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") - quote_bundle = {} - quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + self.quote_validity_period - quote_bundle["quotes"] = {} - quote_bundle["from"] = from_ - quote_bundle["to"] = to - quote_bundle["from_safe"] = from_safe - quote_bundle["quotes"] = self.bridge.get_quotes( - from_chain=from_chain, from_address=from_address, to=to - ) - quote_bundle["bridge_requirements"] = self.bridge.get_bridge_requirements( + quote_bundle["quotes"] = self.bridge.get_quotes(quote_requests) + quote_bundle["bridge_requirements"] = self.bridge.sum_quotes_requirements( quote_bundle["quotes"] ) - self.data.last_requested_quote_bundle = quote_bundle + self.data.requested_quote_bundles[quote_bundle["id"]] = quote_bundle self.data.state = BridgeManagerState.QUOTE_BUNDLE_UP_TO_DATE self.data.store() return quote_bundle - def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: - """Get bridge refill requirements.""" - - if not isinstance(from_, dict) or len(from_) != 1: - raise ValueError( - f"[BRIDGE MANAGER] Invalid 'from_' input: Must contain exactly one chain mapping. {from_=}" + @staticmethod + def _has_duplicates(quote_requests: list) -> bool: + """Check if there are duplicate quote requests (excluding to_amount value).""" + + seen = set() + for request in quote_requests: + key = ( + request["from_chain"], + request["from_address"], + request["from_token"], + request["to_chain"], + request["to_address"], + request["to_token"], ) - from_chain, from_address = next(iter(from_.items())) - from_chain = Chain(from_chain) - wallet = self.wallet_manager.load(from_chain.ledger_type) + if key in seen: + return True + seen.add(key) + + return False + + @staticmethod + def _flatten_quote_requests(quote_requests: list) -> list: + """Flatten quote requests into an internal format. + + { + from_chain: value, + from_address: value, + from_token: value, + to_chain: value, + to_address: value, + to_token: value, + to_amount: value + } + """ + flattened = [] + + for request in quote_requests: + if len(request["from"]) != 1: + raise ValueError( + "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one sender and one token." + ) + + from_chain, from_addresses = next(iter(request["from"].items())) + + if len(from_addresses) != 1: + raise ValueError( + "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one sender and one token." + ) + + from_address, from_token = next(iter(from_addresses.items())) + + for to_chain, to_details in request["to"].items(): + to_address, to_address_details = next(iter(to_details.items())) + for to_token, amount in to_address_details.items(): + flattened.append( + { + "from_address": from_address, + "from_token": from_token, + "from_chain": from_chain, + "to_address": to_address, + "to_token": to_token, + "to_chain": to_chain, + "to_amount": amount, + } + ) + + return flattened + + def bridge_refill_requirements(self, request: dict) -> dict: + """Get bridge refill requirements.""" # TODO Purge empty addresses on 'to' + # TODO store flattened vs user input + + quote_requests = self._flatten_quote_requests(request["quote_requests"]) - quote_bundle = self._get_valid_quote_bundle(from_, to) + if self._has_duplicates(quote_requests): + raise ValueError( + "[BRIDGE MANAGER] Input contains duplicate quote requests." + ) + + self.logger.info(f"[BRIDGE MANAGER] {len(quote_requests)} quotes requested.") + quote_bundle = self._get_valid_quote_bundle(quote_requests) - balances = get_assets_balances( - ledger_api=wallet.ledger_api(chain=from_chain), - addresses={from_address}, - asset_addresses={ZERO_ADDRESS} - | self._get_from_tokens(from_chain=from_chain, to=to), - raise_on_invalid_address=False, + bridge_requirements = self.bridge.sum_quotes_requirements( + quote_bundle["quotes"] ) + balances = {} bridge_refill_requirements = {} - bridge_refill_requirements[from_address] = {} - for from_token, amount in quote_bundle["bridge_requirements"].items(): - bridge_refill_requirements[from_address][from_token] = max( - amount - balances[from_address][from_token], 0 - ) + for from_chain, from_addresses in bridge_requirements.items(): + ledger_api = self.wallet_manager.load( + Chain(from_chain).ledger_type + ).ledger_api(Chain(from_chain)) + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + balance = get_asset_balance( + ledger_api=ledger_api, + address=from_address, + asset_address=from_token, + ) + balances.setdefault(from_chain, {}).setdefault(from_address, {}) + balances[from_chain][from_address][from_token] = balance + bridge_refill_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ) + bridge_refill_requirements[from_chain][from_address][ + from_token + ] = max(from_amount - balance, 0) - print(bridge_refill_requirements) is_refill_required = any( amount > 0 - for asset in bridge_refill_requirements.values() - for amount in asset.values() + for from_addresses in bridge_refill_requirements.values() + for from_tokens in from_addresses.values() + for amount in from_tokens.values() ) return { "id": quote_bundle["id"], - "balances": {from_chain.value: balances}, - "bridge_requirements": { - from_chain.value: quote_bundle["bridge_requirements"] - }, - "bridge_refill_requirements": { - from_chain.value: dict(bridge_refill_requirements) - }, + "balances": balances, + "bridge_requirements": bridge_requirements, + "bridge_refill_requirements": bridge_refill_requirements, "expiration_timestamp": quote_bundle["expiration_timestamp"], "is_refill_required": is_refill_required, } @@ -454,27 +507,19 @@ def bridge_refill_requirements(self, from_: dict, to: dict) -> dict: def execute_quote_bundle(self, quote_bundle_id: str) -> None: """Execute quote bundle""" - if self.data.state != BridgeManagerState.QUOTE_BUNDLE_UP_TO_DATE: - raise RuntimeError( - "[BRIDGE MANAGER] You must retrieve a valid quote first." - ) - - if self.data.last_requested_quote_bundle["id"] != quote_bundle_id: - raise RuntimeError( - f"[BRIDGE MANAGER] Id {quote_bundle_id} does not match latest requested quote bundle id {self.data.last_requested_quote_bundle['id']}." - ) + quote_bundle = self.data.requested_quote_bundles.get(quote_bundle_id) + if not quote_bundle: + raise ValueError(f"[BRIDGE MANAGER] Id {quote_bundle_id} not found.") - from_ = self.data.last_requested_quote_bundle["from"] - to = self.data.last_requested_quote_bundle["to"] + quote_requests = quote_bundle["quote_requests"] - reqs = self.bridge_refill_requirements(from_, to) + requirements = self.bridge_refill_requirements(quote_requests) - if reqs["is_refill_required"]: + if requirements["is_refill_required"]: raise RuntimeError( f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bundle id {quote_bundle_id}." ) - for _, quote in self.data.last_requested_quote_bundle["quotes"].items(): - self.bridge.execute_quote(quote) - print("[BRIDGE MANAGER] Executing quotes") + for _, quote in self.data.requested_quote_bundles["quotes"].items(): + self.bridge.execute_quote(quote) diff --git a/operate/cli.py b/operate/cli.py index 47be20993..4d472e5a4 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1003,7 +1003,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: data = await request.json() return JSONResponse( content=operate.bridge_manager().bridge_refill_requirements( - from_=data["from"], to=data["to"] + quote_requests=data["quote_requests"] ) ) From bd4c277976d0d2201ae09a2317621427e38a0729 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 31 Mar 2025 19:01:43 +0200 Subject: [PATCH 009/173] chore: update --- operate/bridge/bridge.py | 323 ++++++++++++++++++++++++--------------- operate/cli.py | 4 +- 2 files changed, 200 insertions(+), 127 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 680bcbdfa..19e5a9320 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -20,14 +20,11 @@ """Bridge manager.""" -import json import logging import time import uuid from abc import abstractmethod -from collections import defaultdict from dataclasses import dataclass, field -from enum import Enum from pathlib import Path from typing import cast @@ -42,11 +39,10 @@ from operate.operate_types import Chain from operate.resource import LocalResource from operate.services.manage import get_assets_balances -from operate.utils.gnosis import get_asset_balance from operate.wallet.master import MasterWalletManager -DEFAULT_MAX_RETRIES = 3 +DEFAULT_MAX_RETRIES = 1 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" @@ -54,9 +50,14 @@ class Bridge: """Abstract Bridge""" - def __init__(self, wallet_manager: MasterWalletManager) -> None: + def __init__( + self, + wallet_manager: MasterWalletManager, + logger: logging.Logger | None = None, + ) -> None: """Initialize the bridge""" self.wallet_manager = wallet_manager + self.logger = logger or setup_logger(name="operate.bridge.Bridge") @abstractmethod def get_quote( @@ -72,12 +73,12 @@ def get_quote( """Get bridge quote""" raise NotImplementedError() - def get_quotes(self, quote_requests: list) -> dict: + def get_quote_responses(self, quote_requests: list) -> list: """Get bridge quotes (destinations specified in `to` dict)""" - quotes = {} + bridge_quote_responses = [] for quote_request in quote_requests: - quote = self.get_quote( + bridge_quote_response = self.get_quote( from_chain=Chain(quote_request["from_chain"]), from_address=quote_request["from_address"], from_token=quote_request["from_token"], @@ -87,23 +88,27 @@ def get_quotes(self, quote_requests: list) -> dict: to_amount=quote_request["to_amount"], ) - if quote: - quotes[quote["id"]] = quote + # TODO remove 0 - transfer quotes on sanitize input? + bridge_quote_responses.append(bridge_quote_response) - return quotes + return bridge_quote_responses @abstractmethod - def get_quote_requirements(self, quote: dict) -> dict: + def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: """Get bridge requirements given a collection of quotes""" raise NotImplementedError() - def sum_quotes_requirements(self, quotes: dict) -> dict: + def sum_quotes_requirements(self, bridge_quote_responses: list) -> dict: """Get bridge requirements given a collection of quotes""" - bridge_requirements = {} + bridge_requirements: dict = {} + + for bridge_quote_response in bridge_quote_responses: + req = self.get_quote_requirements(bridge_quote_response) + + if not req: + continue - for _, quote in quotes.items(): - req = self.get_quote_requirements(quote) for from_chain, from_addresses in req.items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): @@ -137,8 +142,18 @@ def get_quote( ) -> dict: """Get bridge quote""" + now = int(time.time()) if to_amount == 0: - return {} + self.logger.info("[BRIDGE] Zero-amount quote requested") + return { + "quote": {}, + "metadata": { + "error": False, + "message": "Zero-amount quote requested", + "request_status": 0, + "timestamp": now, + }, + } url = "https://li.quest/v1/quote/toAmount" headers = {"accept": "application/json"} @@ -157,23 +172,49 @@ def get_quote( url=url, headers=headers, params=params, timeout=30 ) response.raise_for_status() - return response.json() + return { + "quote": response.json(), + "metadata": { + "error": False, + "message": "", + "request_status": response.status_code, + "timestamp": now, + }, + } except requests.RequestException as e: - print( - f"[BRIDGE MANAGER] Request quote failed with code {response.status_code} (attempt {attempt}/{DEFAULT_MAX_RETRIES}): {e}" + self.logger.warning( + f"[BRIDGE] Request quote failed with code {response.status_code} (attempt {attempt}/{DEFAULT_MAX_RETRIES}): {e}" ) if attempt >= DEFAULT_MAX_RETRIES: - print( - f"[BRIDGE MANAGER]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" + self.logger.error( + f"[BRIDGE]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" ) - raise + response_json = response.json() + return { + "quote": response_json, + "metadata": { + "error": True, + "message": response_json["message"], + "request_status": response.status_code, + "timestamp": now, + }, + } + else: + time.sleep(2) + return {} # TODO gas fees ! - def get_quote_requirements(self, quote: dict) -> dict: + def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: """Get bridge requirements given a collection of quotes""" + quote = bridge_quote_response["quote"] + metadata = bridge_quote_response["metadata"] + + if metadata.get("error", False) or "action" not in quote: + return None + from_chain = Chain.from_id(quote["action"]["fromChainId"]) from_address = quote["action"]["fromAddress"] from_token = quote["action"]["fromToken"]["address"] @@ -196,10 +237,10 @@ def get_quote_requirements(self, quote: dict) -> dict: } } - def execute_quote(self, quote) -> None: + def execute_quote(self, quote: dict) -> None: """Execute the quote""" - print("Execute_quote") + self.logger.info("[BRIDGE] Execute_quote") from_token = quote["action"]["fromToken"]["address"] from_amount = int(quote["action"]["fromAmount"]) @@ -213,7 +254,7 @@ def execute_quote(self, quote) -> None: account = w3.eth.account.from_key(private_key) if from_token != ZERO_ADDRESS: - print(f"Approve transaction for token {from_token}") + self.logger.info(f"[BRIDGE] Approve transaction for token {from_token}") from_token_contract = w3.eth.contract( address=from_token, abi=[ @@ -248,9 +289,9 @@ def execute_quote(self, quote) -> None: transaction, private_key ) tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - print(f"Approve transaction {tx_hash=}") + self.logger.info(f"[BRIDGE] Approve transaction {tx_hash=}") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - print(f"Approve transaction {receipt=}") + self.logger.info(f"[BRIDGE] Approve transaction {receipt=}") transaction = { "value": w3.to_wei(int(transaction_request["value"], 16), "wei"), @@ -278,6 +319,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 requested_quote_bundles: dict = field(default_factory=dict) + last_requested_quote_bundle_id: str | None = None _file = "bridge.json" @@ -285,6 +327,8 @@ class BridgeManagerData(LocalResource): class BridgeManager: """BridgeManager""" + # TODO singleton + def __init__( self, path: Path, @@ -296,7 +340,7 @@ def __init__( """Initialize bridge manager.""" self.path = path self.wallet_manager = wallet_manager - self.logger = logger or setup_logger(name="operate.master_wallet_manager") + self.logger = logger or setup_logger(name="operate.bridge.BridgeManager") self.bridge = bridge or LiFiBridge(wallet_manager) self.quote_validity_period = ( quote_validity_period or DEFAULT_QUOTE_VALIDITY_PERIOD @@ -331,76 +375,58 @@ def _get_from_tokens(from_chain: Chain, to: dict) -> set: ) ) return from_tokens - - @staticmethod - def _get_quote_bundle_id(quote_requests: list) -> str - """Generate a deterministic id based on the content of quote_requests.""" - - json_list = [json.dumps(obj, sort_keys=True, separators=(",", ":")) for obj in quote_requests] - - # Sort the JSON string representations - json_list.sort() - - - def _get_valid_quote_bundle(self, quote_requests: list) -> dict: """Ensures to return a valid (non expired) quote bundle for the given inputs.""" - quote_bundle = self.data.requested_quote_bundles + + # TODO store only one? + + quote_bundle = self.data.requested_quote_bundles.get( + self.data.last_requested_quote_bundle_id, {} + ) now = int(time.time()) - refresh_quote_bundle = False + create_new_quote_bundle = False + refresh_quotes = False + if not quote_bundle: self.logger.info("[BRIDGE MANAGER] No last_requested_quote_bundle.") - refresh_quote_bundle = True - quote_bundle = {} - quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" - quote_bundle["quote_requests"] = quote_requests - elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", {})): + create_new_quote_bundle = True + elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", [])): self.logger.info("[BRIDGE MANAGER] Different quote requests.") - refresh_quote_bundle = True + create_new_quote_bundle = True elif now > quote_bundle.get("expiration_timestamp", 0): - self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") - refresh_quote_bundle = True + self.logger.info("[BRIDGE MANAGER] Quotes expired.") + refresh_quotes = True + + if create_new_quote_bundle: + quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" + quote_bundle["quote_requests"] = quote_requests + quote_bundle["executions"] = [] + quote_bundle["execution_status"] = [] + self.data.requested_quote_bundles[quote_bundle["id"]] = quote_bundle + refresh_quotes = True - if refresh_quote_bundle: + if refresh_quotes: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + self.quote_validity_period - quote_bundle["quotes"] = self.bridge.get_quotes(quote_requests) - quote_bundle["bridge_requirements"] = self.bridge.sum_quotes_requirements( - quote_bundle["quotes"] + quote_bundle["bridge_quote_responses"] = self.bridge.get_quote_responses( + quote_requests ) - self.data.requested_quote_bundles[quote_bundle["id"]] = quote_bundle - - self.data.state = BridgeManagerState.QUOTE_BUNDLE_UP_TO_DATE - self.data.store() - return quote_bundle - - @staticmethod - def _has_duplicates(quote_requests: list) -> bool: - """Check if there are duplicate quote requests (excluding to_amount value).""" - - seen = set() - for request in quote_requests: - key = ( - request["from_chain"], - request["from_address"], - request["from_token"], - request["to_chain"], - request["to_address"], - request["to_token"], + quote_bundle["bridge_requirements"] = self.bridge.sum_quotes_requirements( + quote_bundle["bridge_quote_responses"] ) - if key in seen: - return True - seen.add(key) + if create_new_quote_bundle or refresh_quotes: + self.data.last_requested_quote_bundle_id = quote_bundle["id"] + self.data.store() - return False + return quote_bundle @staticmethod - def _flatten_quote_requests(quote_requests: list) -> list: - """Flatten quote requests into an internal format. + def _preprocess_input(client_requests: list) -> list: + """Preprocess quote requests into an internal format. { from_chain: value, @@ -412,75 +438,115 @@ def _flatten_quote_requests(quote_requests: list) -> list: to_amount: value } """ - flattened = [] - - for request in quote_requests: - if len(request["from"]) != 1: + output = [] + + for request in client_requests: + if ( + not isinstance(request, dict) + or "from" not in request + or "to" not in request + or len(request["from"]) != 1 + or len(request["to"]) != 1 + ): raise ValueError( - "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one sender and one token." + "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one 'from' and one 'to' sender." ) from_chain, from_addresses = next(iter(request["from"].items())) - - if len(from_addresses) != 1: + if not isinstance(from_addresses, dict) or len(from_addresses) != 1: raise ValueError( - "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one sender and one token." + "[BRIDGE MANAGER] Invalid 'from' structure: must have exactly one address mapping." ) from_address, from_token = next(iter(from_addresses.items())) - for to_chain, to_details in request["to"].items(): - to_address, to_address_details = next(iter(to_details.items())) - for to_token, amount in to_address_details.items(): - flattened.append( - { - "from_address": from_address, - "from_token": from_token, - "from_chain": from_chain, - "to_address": to_address, - "to_token": to_token, - "to_chain": to_chain, - "to_amount": amount, - } - ) - - return flattened + to_chain, to_addresses = next(iter(request["to"].items())) + if not isinstance(to_addresses, dict) or len(to_addresses) != 1: + raise ValueError( + "[BRIDGE MANAGER] Invalid 'to' structure: must have exactly one address mapping." + ) - def bridge_refill_requirements(self, request: dict) -> dict: - """Get bridge refill requirements.""" + to_address, to_tokens = next(iter(to_addresses.items())) + if not isinstance(to_tokens, dict) or len(to_tokens) != 1: + raise ValueError( + "[BRIDGE MANAGER] Invalid 'to' structure: address mapping must have exactly one token entry." + ) - # TODO Purge empty addresses on 'to' - # TODO store flattened vs user input + to_token, to_value = next(iter(to_tokens.items())) - quote_requests = self._flatten_quote_requests(request["quote_requests"]) + output.append( + { + "from_address": from_address, + "from_token": from_token, + "from_chain": from_chain, + "to_address": to_address, + "to_token": to_token, + "to_chain": to_chain, + "to_amount": to_value, + } + ) - if self._has_duplicates(quote_requests): - raise ValueError( - "[BRIDGE MANAGER] Input contains duplicate quote requests." + seen: set = set() + for request in output: + key = ( + request["from_chain"], + request["from_address"], + request["from_token"], + request["to_chain"], + request["to_address"], + request["to_token"], ) + if key in seen: + raise ValueError( + "[BRIDGE MANAGER] Request contains duplicate entries with same 'from' and 'to'." + ) + + return output + + def bridge_refill_requirements(self, bridge_requests: dict) -> dict: + """Get bridge refill requirements.""" + + # TODO store flattened vs user input + # TODO check if destination is EOA or Safe. + + quote_requests = self._preprocess_input(bridge_requests["bridge_requests"]) self.logger.info(f"[BRIDGE MANAGER] {len(quote_requests)} quotes requested.") quote_bundle = self._get_valid_quote_bundle(quote_requests) bridge_requirements = self.bridge.sum_quotes_requirements( - quote_bundle["quotes"] + quote_bundle["bridge_quote_responses"] ) + chains = [request["from_chain"] for request in quote_requests] balances = {} - bridge_refill_requirements = {} + for chain in chains: + ledger_api = self.wallet_manager.load(Chain(chain).ledger_type).ledger_api( + Chain(chain) + ) + balances[chain] = get_assets_balances( + ledger_api=ledger_api, + asset_addresses={ZERO_ADDRESS} + | { + request["from_token"] + for request in quote_requests + if request["from_chain"] == chain + }, + addresses={ + request["from_address"] + for request in quote_requests + if request["from_chain"] == chain + }, + ) + + bridge_refill_requirements: dict = {} for from_chain, from_addresses in bridge_requirements.items(): ledger_api = self.wallet_manager.load( Chain(from_chain).ledger_type ).ledger_api(Chain(from_chain)) for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): - balance = get_asset_balance( - ledger_api=ledger_api, - address=from_address, - asset_address=from_token, - ) - balances.setdefault(from_chain, {}).setdefault(from_address, {}) - balances[from_chain][from_address][from_token] = balance + balance = balances[from_chain][from_address][from_token] bridge_refill_requirements.setdefault(from_chain, {}).setdefault( from_address, {} ) @@ -495,6 +561,11 @@ def bridge_refill_requirements(self, request: dict) -> dict: for amount in from_tokens.values() ) + quote_response_status = [ + response["metadata"] for response in quote_bundle["bridge_quote_responses"] + ] + errors = any(status["error"] for status in quote_response_status) + return { "id": quote_bundle["id"], "balances": balances, @@ -502,6 +573,8 @@ def bridge_refill_requirements(self, request: dict) -> dict: "bridge_refill_requirements": bridge_refill_requirements, "expiration_timestamp": quote_bundle["expiration_timestamp"], "is_refill_required": is_refill_required, + "quote_response_status": quote_response_status, + "errors": errors, } def execute_quote_bundle(self, quote_bundle_id: str) -> None: @@ -520,6 +593,6 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bundle id {quote_bundle_id}." ) - print("[BRIDGE MANAGER] Executing quotes") - for _, quote in self.data.requested_quote_bundles["quotes"].items(): + self.logger.info("[BRIDGE MANAGER] Executing quotes") + for quote in self.data.requested_quote_bundles["bridge_quote_responses"]: self.bridge.execute_quote(quote) diff --git a/operate/cli.py b/operate/cli.py index 4d472e5a4..b6bdac426 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -171,7 +171,7 @@ def bridge_manager(self) -> BridgeManager: manager = BridgeManager( path=self._path / "bridge", wallet_manager=self.wallet_manager, - quote_validity_period=24 * 60 * 60, # TODO remove + # remove: quote_validity_period=24 * 60 * 60, # TODO remove ) return manager @@ -1003,7 +1003,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: data = await request.json() return JSONResponse( content=operate.bridge_manager().bridge_refill_requirements( - quote_requests=data["quote_requests"] + bridge_requests=data["bridge_requests"] ) ) From 4b307a09446fe8b8ff4415f12c0bbfc1c0fa4769 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 31 Mar 2025 19:03:21 +0200 Subject: [PATCH 010/173] chore: remove unused code --- operate/bridge/bridge.py | 17 ----------------- operate/ledger/profiles.py | 19 ------------------- 2 files changed, 36 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 19e5a9320..b8fd6408d 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -35,7 +35,6 @@ from operate.constants import ZERO_ADDRESS from operate.ledger import get_default_rpc -from operate.ledger.profiles import get_target_chain_asset_address from operate.operate_types import Chain from operate.resource import LocalResource from operate.services.manage import get_assets_balances @@ -360,22 +359,6 @@ def __init__( ) self.data.store() - @staticmethod - def _get_from_tokens(from_chain: Chain, to: dict) -> set: - from_tokens = set() - for to_chain_str in to: - for to_address in to[to_chain_str]: - for to_token in to[to_chain_str][to_address]: - to_chain = Chain(to_chain_str) - from_tokens.add( - get_target_chain_asset_address( - source_chain=to_chain, - source_asset_address=to_token, - target_chain=from_chain, - ) - ) - return from_tokens - def _get_valid_quote_bundle(self, quote_requests: list) -> dict: """Ensures to return a valid (non expired) quote bundle for the given inputs.""" diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index aa5823e01..f7b6bf43b 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -189,22 +189,3 @@ def get_staking_contract( staking_program_id, ) - -def get_target_chain_asset_address( - source_chain: Chain, source_asset_address: str, target_chain: Chain -) -> str: - """Get the corresponding token address on the target chain.""" - if source_asset_address == ZERO_ADDRESS: - return ZERO_ADDRESS - - target_chain_token_address = None - for token_dict in ERC20_TOKENS: - if source_asset_address == token_dict.get(source_chain): - target_chain_token_address = token_dict.get(target_chain) - - if not target_chain_token_address: - raise ValueError( - f"Token {source_asset_address=} on {source_chain=} not found for {target_chain=}." - ) - - return target_chain_token_address From a211075f80c73a0cf2ee022b8dca8045a6cd8b3c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 31 Mar 2025 19:03:46 +0200 Subject: [PATCH 011/173] fix: linters --- operate/ledger/profiles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index f7b6bf43b..d2e4b6789 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -188,4 +188,3 @@ def get_staking_contract( staking_program_id, staking_program_id, ) - From c849d5c8b47029f7ee9c4278f6a1b87babd2e24b Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 31 Mar 2025 22:13:07 +0200 Subject: [PATCH 012/173] chore: minor change --- operate/bridge/bridge.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index b8fd6408d..30dc5862d 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -41,7 +41,7 @@ from operate.wallet.master import MasterWalletManager -DEFAULT_MAX_RETRIES = 1 +DEFAULT_MAX_RETRIES = 3 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" @@ -373,13 +373,13 @@ def _get_valid_quote_bundle(self, quote_requests: list) -> dict: refresh_quotes = False if not quote_bundle: - self.logger.info("[BRIDGE MANAGER] No last_requested_quote_bundle.") + self.logger.info("[BRIDGE MANAGER] No last quote bundle.") create_new_quote_bundle = True elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", [])): - self.logger.info("[BRIDGE MANAGER] Different quote requests.") + self.logger.info("[BRIDGE MANAGER] Different bridging requests.") create_new_quote_bundle = True elif now > quote_bundle.get("expiration_timestamp", 0): - self.logger.info("[BRIDGE MANAGER] Quotes expired.") + self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") refresh_quotes = True if create_new_quote_bundle: From b9762de34cf2f5d192be90e0be669eee72d097ef Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 31 Mar 2025 22:45:56 +0200 Subject: [PATCH 013/173] chore: change input --- docs/api.md | 84 +++++++++++++++++++++++++++++++------- operate/bridge/bridge.py | 84 +++++++++++++++++++++++--------------- operate/ledger/profiles.py | 1 - 3 files changed, 119 insertions(+), 50 deletions(-) diff --git a/docs/api.md b/docs/api.md index 6b9238530..d3c02ba58 100644 --- a/docs/api.md +++ b/docs/api.md @@ -898,21 +898,60 @@ Returns the refill requirements on the source chain for bridging assets to targe ```json { - "from": { - "ethereum": "0xDe6B572A049B27D349e89aD0cBEF102227e31473" - }, - "to": { - "gnosis": { - "0xDe6B572A049B27D349e89aD0cBEF102227e31473": { - "0x0000000000000000000000000000000000000000": 10000000000000000000, - "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": 0 + "bridge_requests": [ + { + "from": { + "chain": "ethereum", + "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", + "token": "0x0000000000000000000000000000000000000000" }, - "0x28580196F52DB3C95C3d40Df88426e251d115842": { - "0x0000000000000000000000000000000000000000": 10000000000000000000, - "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": 60000000000000000000 + "to": { + "chain": "gnosis", + "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", + "token": "0x0000000000000000000000000000000000000000", + "amount": 10000000000000000000 }, - } - } + }, + { + "from": { + "chain": "ethereum", + "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", + "token": "0x0000000000000000000000000000000000000000" + }, + "to": { + "chain": "gnosis", + "address": "0x28580196F52DB3C95C3d40Df88426e251d115842", + "token": "0x0000000000000000000000000000000000000000", + "amount": 10000000000000000000 + }, + }, + { + "from": { + "chain": "ethereum", + "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0" + }, + "to": { + "chain": "gnosis", + "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", + "token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", + "amount": 0 + }, + }, + { + "from": { + "chain": "ethereum", + "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0" + }, + "to": { + "chain": "gnosis", + "address": "0x28580196F52DB3C95C3d40Df88426e251d115842", + "token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", + "amount": 60000000000000000000 + }, + }, + ] } ``` @@ -946,8 +985,23 @@ Returns the refill requirements on the source chain for bridging assets to targe } } }, - "expiration_timestamp": 1743007255, - "is_refill_required": true + "expiration_timestamp": 1743000251, + "is_refill_required": true, + "quote_response_status": [ + { + "error": false, + "message": "", + "request_status": 200, + "timestamp": 1743000000 + }, + { + "error": false, + "message": "", + "request_status": 200, + "timestamp": 1743000010 + } + ], + "errors": false } ``` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 30dc5862d..5e29bb8d2 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -20,6 +20,7 @@ """Bridge manager.""" +import json import logging import time import uuid @@ -147,6 +148,7 @@ def get_quote( return { "quote": {}, "metadata": { + "attempts": 0, "error": False, "message": "Zero-amount quote requested", "request_status": 0, @@ -174,6 +176,7 @@ def get_quote( return { "quote": response.json(), "metadata": { + "attempts": attempt, "error": False, "message": "", "request_status": response.status_code, @@ -193,6 +196,7 @@ def get_quote( return { "quote": response_json, "metadata": { + "attempts": attempt, "error": True, "message": response_json["message"], "request_status": response.status_code, @@ -322,6 +326,24 @@ class BridgeManagerData(LocalResource): _file = "bridge.json" + # TODO Migrate to LocalResource? + @classmethod + def load(cls, path: Path) -> "LocalResource": + """Load BridgeManagerData""" + + file = path / cls._file + if not file.exists(): + BridgeManagerData(path=path).store() + + try: + json.loads(file.read_text(encoding="utf-8")) + except json.JSONDecodeError: + new_file = path / f"invalid_{int(time.time())}_{cls._file}" + file.rename(new_file) + BridgeManagerData(path=path).store() + + return super().load(path) + class BridgeManager: """BridgeManager""" @@ -340,20 +362,12 @@ def __init__( self.path = path self.wallet_manager = wallet_manager self.logger = logger or setup_logger(name="operate.bridge.BridgeManager") - self.bridge = bridge or LiFiBridge(wallet_manager) + self.bridge = bridge or LiFiBridge(wallet_manager, logger) self.quote_validity_period = ( quote_validity_period or DEFAULT_QUOTE_VALIDITY_PERIOD ) self.path.mkdir(exist_ok=True) - - # TODO Migrate to LocalResource - data_file = path / BridgeManagerData._file - if not data_file.exists(): - data = BridgeManagerData(path=path) - data.store() - # End migrate - self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) ) @@ -428,44 +442,44 @@ def _preprocess_input(client_requests: list) -> list: not isinstance(request, dict) or "from" not in request or "to" not in request - or len(request["from"]) != 1 - or len(request["to"]) != 1 ): raise ValueError( "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one 'from' and one 'to' sender." ) - from_chain, from_addresses = next(iter(request["from"].items())) - if not isinstance(from_addresses, dict) or len(from_addresses) != 1: - raise ValueError( - "[BRIDGE MANAGER] Invalid 'from' structure: must have exactly one address mapping." - ) + from_ = request["from"] + to = request["to"] - from_address, from_token = next(iter(from_addresses.items())) - - to_chain, to_addresses = next(iter(request["to"].items())) - if not isinstance(to_addresses, dict) or len(to_addresses) != 1: + if ( + not isinstance(from_, dict) + or "chain" not in from_ + or "address" not in from_ + or "token" not in from_ + ): raise ValueError( - "[BRIDGE MANAGER] Invalid 'to' structure: must have exactly one address mapping." + "[BRIDGE MANAGER] Invalid input: 'from' must contain 'chain', 'address', and 'token'." ) - to_address, to_tokens = next(iter(to_addresses.items())) - if not isinstance(to_tokens, dict) or len(to_tokens) != 1: + if ( + not isinstance(to, dict) + or "chain" not in to + or "address" not in to + or "token" not in to + or "amount" not in to + ): raise ValueError( - "[BRIDGE MANAGER] Invalid 'to' structure: address mapping must have exactly one token entry." + "[BRIDGE MANAGER] Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) - to_token, to_value = next(iter(to_tokens.items())) - output.append( { - "from_address": from_address, - "from_token": from_token, - "from_chain": from_chain, - "to_address": to_address, - "to_token": to_token, - "to_chain": to_chain, - "to_amount": to_value, + "from_chain": from_["chain"], + "from_address": from_["address"], + "from_token": from_["token"], + "to_chain": to["chain"], + "to_address": to["address"], + "to_token": to["token"], + "to_amount": to["amount"], } ) @@ -494,7 +508,9 @@ def bridge_refill_requirements(self, bridge_requests: dict) -> dict: # TODO check if destination is EOA or Safe. quote_requests = self._preprocess_input(bridge_requests["bridge_requests"]) - self.logger.info(f"[BRIDGE MANAGER] {len(quote_requests)} quotes requested.") + self.logger.info( + f"[BRIDGE MANAGER] Num. bridge requests: {len(quote_requests)}." + ) quote_bundle = self._get_valid_quote_bundle(quote_requests) bridge_requirements = self.bridge.sum_quotes_requirements( diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index 7dad08b92..5238b4b84 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -21,7 +21,6 @@ import typing as t -from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain, ContractAddresses From 7beebc0c0ca3d95730dfbefc66a0c16f2b3654a2 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 1 Apr 2025 10:56:27 +0200 Subject: [PATCH 014/173] chore: update --- operate/bridge/bridge.py | 191 +++++++++++++++++++++------------------ operate/cli.py | 2 +- 2 files changed, 103 insertions(+), 90 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 5e29bb8d2..85a1de121 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -22,6 +22,7 @@ import json import logging +import shutil import time import uuid from abc import abstractmethod @@ -47,17 +48,17 @@ QUOTE_BUNDLE_PREFIX = "qb-" -class Bridge: - """Abstract Bridge""" +class BridgeProvider: + """Abstract BridgeProvider.""" def __init__( self, wallet_manager: MasterWalletManager, logger: logging.Logger | None = None, ) -> None: - """Initialize the bridge""" + """Initialize the bridge provider.""" self.wallet_manager = wallet_manager - self.logger = logger or setup_logger(name="operate.bridge.Bridge") + self.logger = logger or setup_logger(name="operate.bridge.BridgeProvider") @abstractmethod def get_quote( @@ -70,22 +71,22 @@ def get_quote( to_address: str, to_amount: int, ) -> dict: - """Get bridge quote""" + """Get bridge quote.""" raise NotImplementedError() def get_quote_responses(self, quote_requests: list) -> list: - """Get bridge quotes (destinations specified in `to` dict)""" + """Get bridge quotes.""" bridge_quote_responses = [] for quote_request in quote_requests: bridge_quote_response = self.get_quote( - from_chain=Chain(quote_request["from_chain"]), - from_address=quote_request["from_address"], - from_token=quote_request["from_token"], - to_chain=Chain(quote_request["to_chain"]), - to_address=quote_request["to_address"], - to_token=quote_request["to_token"], - to_amount=quote_request["to_amount"], + from_chain=Chain(quote_request["from"]["chain"]), + from_address=quote_request["from"]["address"], + from_token=quote_request["from"]["token"], + to_chain=Chain(quote_request["to"]["chain"]), + to_address=quote_request["to"]["address"], + to_token=quote_request["to"]["token"], + to_amount=quote_request["to"]["amount"], ) # TODO remove 0 - transfer quotes on sanitize input? @@ -95,11 +96,11 @@ def get_quote_responses(self, quote_requests: list) -> list: @abstractmethod def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: - """Get bridge requirements given a collection of quotes""" + """Get bridge requirements for a single quote.""" raise NotImplementedError() def sum_quotes_requirements(self, bridge_quote_responses: list) -> dict: - """Get bridge requirements given a collection of quotes""" + """Get bridge requirements for a list of quotes.""" bridge_requirements: dict = {} @@ -123,12 +124,12 @@ def sum_quotes_requirements(self, bridge_quote_responses: list) -> dict: @abstractmethod def execute_quote(self, quote: dict) -> None: - """Execute the quote""" + """Execute the quote.""" raise NotImplementedError() -class LiFiBridge(Bridge): - """LI.FI Bridge""" +class LiFiBridgeProvider(BridgeProvider): + """LI.FI Bridge provider.""" def get_quote( self, @@ -140,7 +141,7 @@ def get_quote( to_address: str, to_amount: int, ) -> dict: - """Get bridge quote""" + """Get bridge quote.""" now = int(time.time()) if to_amount == 0: @@ -210,7 +211,7 @@ def get_quote( # TODO gas fees ! def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: - """Get bridge requirements given a collection of quotes""" + """Get bridge requirements for a quote.""" quote = bridge_quote_response["quote"] metadata = bridge_quote_response["metadata"] @@ -241,9 +242,9 @@ def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: } def execute_quote(self, quote: dict) -> None: - """Execute the quote""" + """Execute the quote.""" - self.logger.info("[BRIDGE] Execute_quote") + self.logger.info("[BRIDGE] Execute quote.") from_token = quote["action"]["fromToken"]["address"] from_amount = int(quote["action"]["fromAmount"]) @@ -257,7 +258,7 @@ def execute_quote(self, quote: dict) -> None: account = w3.eth.account.from_key(private_key) if from_token != ZERO_ADDRESS: - self.logger.info(f"[BRIDGE] Approve transaction for token {from_token}") + self.logger.info(f"[BRIDGE] Approve transaction for token {from_token}.") from_token_contract = w3.eth.contract( address=from_token, abi=[ @@ -292,9 +293,9 @@ def execute_quote(self, quote: dict) -> None: transaction, private_key ) tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - self.logger.info(f"[BRIDGE] Approve transaction {tx_hash=}") + self.logger.info(f"[BRIDGE] Approve transaction {tx_hash=}.") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.logger.info(f"[BRIDGE] Approve transaction {receipt=}") + self.logger.info(f"[BRIDGE] Approve transaction {receipt=}.") transaction = { "value": w3.to_wei(int(transaction_request["value"], 16), "wei"), @@ -327,9 +328,33 @@ class BridgeManagerData(LocalResource): _file = "bridge.json" # TODO Migrate to LocalResource? - @classmethod + def store(self) -> None: + """Store local resource.""" + + file_path = self.path / self._file + backup_path = file_path.with_name(file_path.name + ".bak") + + try: + if file_path.exists(): + json.loads(file_path.read_text(encoding="utf-8")) + if backup_path.exists(): + backup_path.unlink() + shutil.copy2(file_path, backup_path) + except json.JSONDecodeError: + pass + + super().store() + + # TODO Migrate to LocalResource? + # It can be inconvenient that all local resources create an empty resource + # if the file is corrupted. For example, if a service configuration is + # corrupted, we might want to halt execution, because otherwise, the application + # could continue as if the user is creatig a service from scratch. + # For the bridge manager data, it's harmless, because its memory + # is limited to the process of getting and executing a quote. + @classmethod # Overrides from LocalResource def load(cls, path: Path) -> "LocalResource": - """Load BridgeManagerData""" + """Load local resource.""" file = path / cls._file if not file.exists(): @@ -355,14 +380,16 @@ def __init__( path: Path, wallet_manager: MasterWalletManager, logger: logging.Logger | None = None, - bridge: Bridge | None = None, + bridge_provider: BridgeProvider | None = None, quote_validity_period: int | None = None, ) -> None: """Initialize bridge manager.""" self.path = path self.wallet_manager = wallet_manager self.logger = logger or setup_logger(name="operate.bridge.BridgeManager") - self.bridge = bridge or LiFiBridge(wallet_manager, logger) + self.bridge_provider = bridge_provider or LiFiBridgeProvider( + wallet_manager, logger + ) self.quote_validity_period = ( quote_validity_period or DEFAULT_QUOTE_VALIDITY_PERIOD ) @@ -373,7 +400,9 @@ def __init__( ) self.data.store() - def _get_valid_quote_bundle(self, quote_requests: list) -> dict: + def _get_valid_quote_bundle( + self, quote_requests: list, force_update_quotes: bool + ) -> dict: """Ensures to return a valid (non expired) quote bundle for the given inputs.""" # TODO store only one? @@ -384,17 +413,20 @@ def _get_valid_quote_bundle(self, quote_requests: list) -> dict: now = int(time.time()) create_new_quote_bundle = False - refresh_quotes = False + update_quotes = False if not quote_bundle: self.logger.info("[BRIDGE MANAGER] No last quote bundle.") create_new_quote_bundle = True elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", [])): - self.logger.info("[BRIDGE MANAGER] Different bridging requests.") + self.logger.info("[BRIDGE MANAGER] Different quote requests.") create_new_quote_bundle = True + elif force_update_quotes: + self.logger.info("[BRIDGE MANAGER] Force quote update.") + update_quotes = True elif now > quote_bundle.get("expiration_timestamp", 0): self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") - refresh_quotes = True + update_quotes = True if create_new_quote_bundle: quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" @@ -402,42 +434,34 @@ def _get_valid_quote_bundle(self, quote_requests: list) -> dict: quote_bundle["executions"] = [] quote_bundle["execution_status"] = [] self.data.requested_quote_bundles[quote_bundle["id"]] = quote_bundle - refresh_quotes = True + update_quotes = True - if refresh_quotes: + if update_quotes: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + self.quote_validity_period - quote_bundle["bridge_quote_responses"] = self.bridge.get_quote_responses( - quote_requests - ) - quote_bundle["bridge_requirements"] = self.bridge.sum_quotes_requirements( + quote_bundle[ + "bridge_quote_responses" + ] = self.bridge_provider.get_quote_responses(quote_requests) + quote_bundle[ + "bridge_requirements" + ] = self.bridge_provider.sum_quotes_requirements( quote_bundle["bridge_quote_responses"] ) - if create_new_quote_bundle or refresh_quotes: + if create_new_quote_bundle or update_quotes: self.data.last_requested_quote_bundle_id = quote_bundle["id"] self.data.store() return quote_bundle @staticmethod - def _preprocess_input(client_requests: list) -> list: - """Preprocess quote requests into an internal format. - - { - from_chain: value, - from_address: value, - from_token: value, - to_chain: value, - to_address: value, - to_token: value, - to_amount: value - } - """ - output = [] + def _validate_input(bridge_requests: list) -> None: + """Preprocess quote requests.""" + + seen: set = set() - for request in client_requests: + for request in bridge_requests: if ( not isinstance(request, dict) or "from" not in request @@ -471,27 +495,13 @@ def _preprocess_input(client_requests: list) -> list: "[BRIDGE MANAGER] Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) - output.append( - { - "from_chain": from_["chain"], - "from_address": from_["address"], - "from_token": from_["token"], - "to_chain": to["chain"], - "to_address": to["address"], - "to_token": to["token"], - "to_amount": to["amount"], - } - ) - - seen: set = set() - for request in output: key = ( - request["from_chain"], - request["from_address"], - request["from_token"], - request["to_chain"], - request["to_address"], - request["to_token"], + request["from"]["chain"], + request["from"]["address"], + request["from"]["token"], + request["to"]["chain"], + request["to"]["address"], + request["to"]["token"], ) if key in seen: @@ -499,25 +509,25 @@ def _preprocess_input(client_requests: list) -> list: "[BRIDGE MANAGER] Request contains duplicate entries with same 'from' and 'to'." ) - return output - - def bridge_refill_requirements(self, bridge_requests: dict) -> dict: + def bridge_refill_requirements(self, client_input: dict) -> dict: """Get bridge refill requirements.""" # TODO store flattened vs user input # TODO check if destination is EOA or Safe. - quote_requests = self._preprocess_input(bridge_requests["bridge_requests"]) + quote_requests = client_input.get("quote_requests", []) + force_update_quotes = client_input.get("force_update_quotes", False) + self._validate_input(quote_requests) self.logger.info( - f"[BRIDGE MANAGER] Num. bridge requests: {len(quote_requests)}." + f"[BRIDGE MANAGER] Num. quote requests: {len(quote_requests)}." ) - quote_bundle = self._get_valid_quote_bundle(quote_requests) + quote_bundle = self._get_valid_quote_bundle(quote_requests, force_update_quotes) - bridge_requirements = self.bridge.sum_quotes_requirements( + bridge_requirements = self.bridge_provider.sum_quotes_requirements( quote_bundle["bridge_quote_responses"] ) - chains = [request["from_chain"] for request in quote_requests] + chains = [request["from"]["chain"] for request in quote_requests] balances = {} for chain in chains: ledger_api = self.wallet_manager.load(Chain(chain).ledger_type).ledger_api( @@ -527,14 +537,14 @@ def bridge_refill_requirements(self, bridge_requests: dict) -> dict: ledger_api=ledger_api, asset_addresses={ZERO_ADDRESS} | { - request["from_token"] + request["from"]["token"] for request in quote_requests - if request["from_chain"] == chain + if request["from"]["chain"] == chain }, addresses={ - request["from_address"] + request["from"]["address"] for request in quote_requests - if request["from_chain"] == chain + if request["from"]["chain"] == chain }, ) @@ -571,6 +581,9 @@ def bridge_refill_requirements(self, bridge_requests: dict) -> dict: "bridge_requirements": bridge_requirements, "bridge_refill_requirements": bridge_refill_requirements, "expiration_timestamp": quote_bundle["expiration_timestamp"], + "expiration_timeout": int( + quote_bundle["expiration_timestamp"] - time.time() + ), "is_refill_required": is_refill_required, "quote_response_status": quote_response_status, "errors": errors, @@ -592,6 +605,6 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bundle id {quote_bundle_id}." ) - self.logger.info("[BRIDGE MANAGER] Executing quotes") + self.logger.info("[BRIDGE MANAGER] Executing quotes.") for quote in self.data.requested_quote_bundles["bridge_quote_responses"]: - self.bridge.execute_quote(quote) + self.bridge_provider.execute_quote(quote) diff --git a/operate/cli.py b/operate/cli.py index b6bdac426..a7e54e9c9 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1003,7 +1003,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: data = await request.json() return JSONResponse( content=operate.bridge_manager().bridge_refill_requirements( - bridge_requests=data["bridge_requests"] + client_input=data ) ) From 396c1fbc4e4c4f419aaf28eae18eaf6eccfabef9 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 1 Apr 2025 11:35:57 +0200 Subject: [PATCH 015/173] chore: update --- operate/bridge/bridge.py | 44 ++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 85a1de121..22582dfb4 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -26,7 +26,7 @@ import time import uuid from abc import abstractmethod -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import cast @@ -60,6 +60,10 @@ def __init__( self.wallet_manager = wallet_manager self.logger = logger or setup_logger(name="operate.bridge.BridgeProvider") + def name(self) -> str: + """Get the name of the bridge provider.""" + return self.__class__.__name__ + @abstractmethod def get_quote( self, @@ -145,7 +149,7 @@ def get_quote( now = int(time.time()) if to_amount == 0: - self.logger.info("[BRIDGE] Zero-amount quote requested") + self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested") return { "quote": {}, "metadata": { @@ -186,12 +190,12 @@ def get_quote( } except requests.RequestException as e: self.logger.warning( - f"[BRIDGE] Request quote failed with code {response.status_code} (attempt {attempt}/{DEFAULT_MAX_RETRIES}): {e}" + f"[LI.FI BRIDGE] Request quote failed with code {response.status_code} (attempt {attempt}/{DEFAULT_MAX_RETRIES}): {e}" ) if attempt >= DEFAULT_MAX_RETRIES: self.logger.error( - f"[BRIDGE]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" + f"[LI.FI BRIDGE]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" ) response_json = response.json() return { @@ -244,7 +248,7 @@ def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: def execute_quote(self, quote: dict) -> None: """Execute the quote.""" - self.logger.info("[BRIDGE] Execute quote.") + self.logger.info("[LI.FI BRIDGE] Execute quote.") from_token = quote["action"]["fromToken"]["address"] from_amount = int(quote["action"]["fromAmount"]) @@ -258,7 +262,7 @@ def execute_quote(self, quote: dict) -> None: account = w3.eth.account.from_key(private_key) if from_token != ZERO_ADDRESS: - self.logger.info(f"[BRIDGE] Approve transaction for token {from_token}.") + self.logger.info(f"[LI.FI BRIDGE] Approve transaction for token {from_token}.") from_token_contract = w3.eth.contract( address=from_token, abi=[ @@ -293,9 +297,9 @@ def execute_quote(self, quote: dict) -> None: transaction, private_key ) tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - self.logger.info(f"[BRIDGE] Approve transaction {tx_hash=}.") + self.logger.info(f"[LI.FI BRIDGE] Approve transaction {tx_hash=}.") receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.logger.info(f"[BRIDGE] Approve transaction {receipt=}.") + self.logger.info(f"[LI.FI BRIDGE] Approve transaction {receipt=}.") transaction = { "value": w3.to_wei(int(transaction_request["value"], 16), "wei"), @@ -322,8 +326,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 - requested_quote_bundles: dict = field(default_factory=dict) - last_requested_quote_bundle_id: str | None = None + last_requested_quote_bundle: dict | None = None _file = "bridge.json" @@ -361,8 +364,8 @@ def load(cls, path: Path) -> "LocalResource": BridgeManagerData(path=path).store() try: - json.loads(file.read_text(encoding="utf-8")) - except json.JSONDecodeError: + super().load(path=file) + except (json.JSONDecodeError, KeyError): new_file = path / f"invalid_{int(time.time())}_{cls._file}" file.rename(new_file) BridgeManagerData(path=path).store() @@ -405,13 +408,8 @@ def _get_valid_quote_bundle( ) -> dict: """Ensures to return a valid (non expired) quote bundle for the given inputs.""" - # TODO store only one? - - quote_bundle = self.data.requested_quote_bundles.get( - self.data.last_requested_quote_bundle_id, {} - ) now = int(time.time()) - + quote_bundle = self.data.last_requested_quote_bundle or {} create_new_quote_bundle = False update_quotes = False @@ -433,13 +431,13 @@ def _get_valid_quote_bundle( quote_bundle["quote_requests"] = quote_requests quote_bundle["executions"] = [] quote_bundle["execution_status"] = [] - self.data.requested_quote_bundles[quote_bundle["id"]] = quote_bundle update_quotes = True if update_quotes: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + self.quote_validity_period + quote_bundle["bridge_provider"] = self.bridge_provider.name() quote_bundle[ "bridge_quote_responses" ] = self.bridge_provider.get_quote_responses(quote_requests) @@ -449,8 +447,7 @@ def _get_valid_quote_bundle( quote_bundle["bridge_quote_responses"] ) - if create_new_quote_bundle or update_quotes: - self.data.last_requested_quote_bundle_id = quote_bundle["id"] + self.data.last_requested_quote_bundle = quote_bundle self.data.store() return quote_bundle @@ -512,7 +509,6 @@ def _validate_input(bridge_requests: list) -> None: def bridge_refill_requirements(self, client_input: dict) -> dict: """Get bridge refill requirements.""" - # TODO store flattened vs user input # TODO check if destination is EOA or Safe. quote_requests = client_input.get("quote_requests", []) @@ -592,7 +588,7 @@ def bridge_refill_requirements(self, client_input: dict) -> dict: def execute_quote_bundle(self, quote_bundle_id: str) -> None: """Execute quote bundle""" - quote_bundle = self.data.requested_quote_bundles.get(quote_bundle_id) + quote_bundle = self.data.last_requested_quote_bundle if not quote_bundle: raise ValueError(f"[BRIDGE MANAGER] Id {quote_bundle_id} not found.") @@ -606,5 +602,5 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") - for quote in self.data.requested_quote_bundles["bridge_quote_responses"]: + for quote in quote_bundle["bridge_quote_responses"]: self.bridge_provider.execute_quote(quote) From a5f309232c89293fa26c30470c05dd010f3c4efd Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 1 Apr 2025 12:31:14 +0200 Subject: [PATCH 016/173] chore: update --- operate/bridge/bridge.py | 26 +++++++++++++++++++------- operate/cli.py | 18 +++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 22582dfb4..7c890b74d 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -93,7 +93,6 @@ def get_quote_responses(self, quote_requests: list) -> list: to_amount=quote_request["to"]["amount"], ) - # TODO remove 0 - transfer quotes on sanitize input? bridge_quote_responses.append(bridge_quote_response) return bridge_quote_responses @@ -262,7 +261,9 @@ def execute_quote(self, quote: dict) -> None: account = w3.eth.account.from_key(private_key) if from_token != ZERO_ADDRESS: - self.logger.info(f"[LI.FI BRIDGE] Approve transaction for token {from_token}.") + self.logger.info( + f"[LI.FI BRIDGE] Approve transaction for token {from_token}." + ) from_token_contract = w3.eth.contract( address=from_token, abi=[ @@ -452,8 +453,7 @@ def _get_valid_quote_bundle( return quote_bundle - @staticmethod - def _validate_input(bridge_requests: list) -> None: + def _raise_if_invalid(self, bridge_requests: list) -> None: """Preprocess quote requests.""" seen: set = set() @@ -492,6 +492,20 @@ def _validate_input(bridge_requests: list) -> None: "[BRIDGE MANAGER] Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) + from_chain = request["from"]["chain"] + from_address = request["from"]["address"] + + wallet = self.wallet_manager.load(Chain(from_chain).ledger_type) + wallet_address = wallet.address + safe_address = wallet.safes.get(from_chain) + + if from_address is None or not ( + from_address == wallet_address or from_address == safe_address + ): + raise ValueError( + f"[BRIDGE MANAGER] Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}." + ) + key = ( request["from"]["chain"], request["from"]["address"], @@ -509,11 +523,9 @@ def _validate_input(bridge_requests: list) -> None: def bridge_refill_requirements(self, client_input: dict) -> dict: """Get bridge refill requirements.""" - # TODO check if destination is EOA or Safe. - quote_requests = client_input.get("quote_requests", []) force_update_quotes = client_input.get("force_update_quotes", False) - self._validate_input(quote_requests) + self._raise_if_invalid(quote_requests) self.logger.info( f"[BRIDGE MANAGER] Num. quote requests: {len(quote_requests)}." ) diff --git a/operate/cli.py b/operate/cli.py index a7e54e9c9..f881eb875 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1000,12 +1000,20 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: if operate.password is None: return USER_NOT_LOGGED_IN_ERROR - data = await request.json() - return JSONResponse( - content=operate.bridge_manager().bridge_refill_requirements( - client_input=data + try: + data = await request.json() + return JSONResponse( + content=operate.bridge_manager().bridge_refill_requirements( + client_input=data + ) + ) + except ValueError as e: + return JSONResponse(content={"error": str(e)}, status_code=400) + except Exception as e: # pylint: disable=broad-except + return JSONResponse( + content={"error": str(e), "traceback": traceback.format_exc()}, + status_code=500, ) - ) return app From 0d58179f75f32b8127847affcbecbba6c5072e48 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 1 Apr 2025 17:36:00 +0200 Subject: [PATCH 017/173] chore: update --- docs/api.md | 3 +- operate/bridge/bridge.py | 100 +++++++++++++++++++++++++-------------- operate/cli.py | 18 +++++-- 3 files changed, 80 insertions(+), 41 deletions(-) diff --git a/docs/api.md b/docs/api.md index d3c02ba58..df1ad96a3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -951,7 +951,8 @@ Returns the refill requirements on the source chain for bridging assets to targe "amount": 60000000000000000000 }, }, - ] + ], + "force_update": false } ``` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 7c890b74d..88a77ab4d 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -26,9 +26,11 @@ import time import uuid from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field +from http import HTTPStatus from pathlib import Path from typing import cast +from urllib.parse import urlencode import requests from aea.helpers.logging import setup_logger @@ -153,9 +155,10 @@ def get_quote( "quote": {}, "metadata": { "attempts": 0, + "elapsed_time": 0, "error": False, "message": "Zero-amount quote requested", - "request_status": 0, + "response_status": HTTPStatus.OK, "timestamp": now, }, } @@ -172,7 +175,10 @@ def get_quote( "toAmount": to_amount, } for attempt in range(1, DEFAULT_MAX_RETRIES + 1): + start = time.time() + response = None try: + self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") response = requests.get( url=url, headers=headers, params=params, timeout=30 ) @@ -181,34 +187,54 @@ def get_quote( "quote": response.json(), "metadata": { "attempts": attempt, + "elapsed_time": time.time() - start, "error": False, "message": "", - "request_status": response.status_code, + "response_status": response.status_code, + "timestamp": now, + }, + } + except requests.Timeout as e: + self.logger.warning( + f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." + ) + output = { + "quote": {}, + "metadata": { + "attempts": attempt, + "elapsed_time": time.time() - start, + "error": True, + "message": str(e), + "response_status": HTTPStatus.GATEWAY_TIMEOUT, "timestamp": now, }, } except requests.RequestException as e: self.logger.warning( - f"[LI.FI BRIDGE] Request quote failed with code {response.status_code} (attempt {attempt}/{DEFAULT_MAX_RETRIES}): {e}" + f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." ) + response_json = response.json() if response else {} + output = { + "quote": response_json, + "metadata": { + "attempts": attempt, + "elapsed_time": time.time() - start, + "error": True, + "message": response_json.get("message") or str(e), + "request_status": response.status_code + if response + else HTTPStatus.INTERNAL_SERVER_ERROR, + "timestamp": now, + }, + } - if attempt >= DEFAULT_MAX_RETRIES: - self.logger.error( - f"[LI.FI BRIDGE]Request quote failed with code {response.status_code} after {DEFAULT_MAX_RETRIES} attempts: {e}" - ) - response_json = response.json() - return { - "quote": response_json, - "metadata": { - "attempts": attempt, - "error": True, - "message": response_json["message"], - "request_status": response.status_code, - "timestamp": now, - }, - } - else: - time.sleep(2) + if attempt >= DEFAULT_MAX_RETRIES: + self.logger.error( + f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_RETRIES} attempts." + ) + return output + + time.sleep(2) return {} @@ -327,7 +353,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 - last_requested_quote_bundle: dict | None = None + last_requested_quote_bundle: dict = field(default_factory=dict) _file = "bridge.json" @@ -379,6 +405,8 @@ class BridgeManager: # TODO singleton + _quote_bundle_updated_on_session = False + def __init__( self, path: Path, @@ -404,9 +432,9 @@ def __init__( ) self.data.store() - def _get_valid_quote_bundle( - self, quote_requests: list, force_update_quotes: bool - ) -> dict: + def _refresh_latest_requested_quote_bundle( + self, quote_requests: list, force_update: bool + ) -> None: """Ensures to return a valid (non expired) quote bundle for the given inputs.""" now = int(time.time()) @@ -420,7 +448,7 @@ def _get_valid_quote_bundle( elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", [])): self.logger.info("[BRIDGE MANAGER] Different quote requests.") create_new_quote_bundle = True - elif force_update_quotes: + elif force_update: self.logger.info("[BRIDGE MANAGER] Force quote update.") update_quotes = True elif now > quote_bundle.get("expiration_timestamp", 0): @@ -451,8 +479,6 @@ def _get_valid_quote_bundle( self.data.last_requested_quote_bundle = quote_bundle self.data.store() - return quote_bundle - def _raise_if_invalid(self, bridge_requests: list) -> None: """Preprocess quote requests.""" @@ -524,16 +550,14 @@ def bridge_refill_requirements(self, client_input: dict) -> dict: """Get bridge refill requirements.""" quote_requests = client_input.get("quote_requests", []) - force_update_quotes = client_input.get("force_update_quotes", False) + force_update = client_input.get("force_update", False) self._raise_if_invalid(quote_requests) self.logger.info( f"[BRIDGE MANAGER] Num. quote requests: {len(quote_requests)}." ) - quote_bundle = self._get_valid_quote_bundle(quote_requests, force_update_quotes) + self._refresh_latest_requested_quote_bundle(quote_requests, force_update) - bridge_requirements = self.bridge_provider.sum_quotes_requirements( - quote_bundle["bridge_quote_responses"] - ) + quote_bundle = self.data.last_requested_quote_bundle chains = [request["from"]["chain"] for request in quote_requests] balances = {} @@ -557,7 +581,7 @@ def bridge_refill_requirements(self, client_input: dict) -> dict: ) bridge_refill_requirements: dict = {} - for from_chain, from_addresses in bridge_requirements.items(): + for from_chain, from_addresses in quote_bundle["bridge_requirements"].items(): ledger_api = self.wallet_manager.load( Chain(from_chain).ledger_type ).ledger_api(Chain(from_chain)) @@ -582,11 +606,12 @@ def bridge_refill_requirements(self, client_input: dict) -> dict: response["metadata"] for response in quote_bundle["bridge_quote_responses"] ] errors = any(status["error"] for status in quote_response_status) + self._quote_bundle_updated_on_session = True return { "id": quote_bundle["id"], "balances": balances, - "bridge_requirements": bridge_requirements, + "bridge_requirements": quote_bundle["bridge_requirements"], "bridge_refill_requirements": bridge_refill_requirements, "expiration_timestamp": quote_bundle["expiration_timestamp"], "expiration_timeout": int( @@ -600,6 +625,11 @@ def bridge_refill_requirements(self, client_input: dict) -> dict: def execute_quote_bundle(self, quote_bundle_id: str) -> None: """Execute quote bundle""" + if not self._quote_bundle_updated_on_session: + raise RuntimeError( + "[BRIDGE MANAGER] Cannot call 'execute_quote_bundle' before 'bridge_refill_requirements'." + ) + quote_bundle = self.data.last_requested_quote_bundle if not quote_bundle: raise ValueError(f"[BRIDGE MANAGER] Id {quote_bundle_id} not found.") diff --git a/operate/cli.py b/operate/cli.py index f881eb875..eeaf27e9c 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -26,6 +26,7 @@ import typing as t import uuid from concurrent.futures import ThreadPoolExecutor +from http import HTTPStatus from pathlib import Path from types import FrameType @@ -1002,17 +1003,24 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: try: data = await request.json() + output = operate.bridge_manager().bridge_refill_requirements( + client_input=data + ) + return JSONResponse( - content=operate.bridge_manager().bridge_refill_requirements( - client_input=data - ) + content=output, + status_code=HTTPStatus.BAD_GATEWAY + if output["errors"] + else HTTPStatus.OK, ) except ValueError as e: - return JSONResponse(content={"error": str(e)}, status_code=400) + return JSONResponse( + content={"error": str(e)}, status_code=HTTPStatus.BAD_REQUEST + ) except Exception as e: # pylint: disable=broad-except return JSONResponse( content={"error": str(e), "traceback": traceback.format_exc()}, - status_code=500, + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, ) return app From 4083f84ea415c8a9c3a59a67c99ecd6be2fe23e1 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 2 Apr 2025 11:09:25 +0200 Subject: [PATCH 018/173] chore: update safe creation transfer --- docs/api.md | 9 ++++-- operate/bridge/bridge.py | 63 ++++++++++++++++++++++---------------- operate/cli.py | 21 ++++++++----- operate/ledger/profiles.py | 23 ++++++++++---- operate/wallet/master.py | 38 +++++++++++++++++++++-- 5 files changed, 109 insertions(+), 45 deletions(-) diff --git a/docs/api.md b/docs/api.md index df1ad96a3..028db6eb1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -336,11 +336,14 @@ Creates a Gnosis safe for given chain.
Request -```js +```json { "chain": "gnosis", - "backup_owner": "0x46eC2E77Fe3E367252f1A8a77470CE8eEd2A985b" - "fund_amount": 10000000000000000 + "backup_owner": "0x46eC2E77Fe3E367252f1A8a77470CE8eEd2A985b", + "initial_funds": { + "0x0000000000000000000000000000000000000000": 1000000000000000000, + "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": 0 + } } ``` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 88a77ab4d..45e5a63e0 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -45,7 +45,7 @@ from operate.wallet.master import MasterWalletManager -DEFAULT_MAX_RETRIES = 3 +DEFAULT_MAX_RETRIES = 1 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" @@ -128,7 +128,7 @@ def sum_quotes_requirements(self, bridge_quote_responses: list) -> dict: return bridge_requirements @abstractmethod - def execute_quote(self, quote: dict) -> None: + def execute_quote(self, bridge_quote_response: dict) -> None: """Execute the quote.""" raise NotImplementedError() @@ -148,7 +148,6 @@ def get_quote( ) -> dict: """Get bridge quote.""" - now = int(time.time()) if to_amount == 0: self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested") return { @@ -158,8 +157,8 @@ def get_quote( "elapsed_time": 0, "error": False, "message": "Zero-amount quote requested", - "response_status": HTTPStatus.OK, - "timestamp": now, + "status": HTTPStatus.OK, + "timestamp": int(time.time()), }, } @@ -176,7 +175,6 @@ def get_quote( } for attempt in range(1, DEFAULT_MAX_RETRIES + 1): start = time.time() - response = None try: self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") response = requests.get( @@ -190,8 +188,8 @@ def get_quote( "elapsed_time": time.time() - start, "error": False, "message": "", - "response_status": response.status_code, - "timestamp": now, + "status": response.status_code, + "timestamp": int(time.time()), }, } except requests.Timeout as e: @@ -205,15 +203,15 @@ def get_quote( "elapsed_time": time.time() - start, "error": True, "message": str(e), - "response_status": HTTPStatus.GATEWAY_TIMEOUT, - "timestamp": now, + "status": HTTPStatus.GATEWAY_TIMEOUT, + "timestamp": int(time.time()), }, } except requests.RequestException as e: self.logger.warning( f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." ) - response_json = response.json() if response else {} + response_json = response.json() output = { "quote": response_json, "metadata": { @@ -221,10 +219,8 @@ def get_quote( "elapsed_time": time.time() - start, "error": True, "message": response_json.get("message") or str(e), - "request_status": response.status_code - if response - else HTTPStatus.INTERNAL_SERVER_ERROR, - "timestamp": now, + "status": response.status_code, + "timestamp": int(time.time()), }, } @@ -270,10 +266,19 @@ def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: } } - def execute_quote(self, quote: dict) -> None: + def execute_quote(self, bridge_quote_response: dict) -> None: """Execute the quote.""" - self.logger.info("[LI.FI BRIDGE] Execute quote.") + quote = bridge_quote_response["quote"] + metadata = bridge_quote_response["metadata"] + + if not quote or metadata.get("error", False): + self.logger.info( + "[LI.FI BRIDGE] Skipping quote execution (no quote or request error)." + ) + return None + + self.logger.info(f"[LI.FI BRIDGE] Execute quote {quote.get('id')}.") from_token = quote["action"]["fromToken"]["address"] from_amount = int(quote["action"]["fromAmount"]) @@ -354,6 +359,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 last_requested_quote_bundle: dict = field(default_factory=dict) + executed_quotes: dict = field(default_factory=dict) _file = "bridge.json" @@ -458,8 +464,7 @@ def _refresh_latest_requested_quote_bundle( if create_new_quote_bundle: quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" quote_bundle["quote_requests"] = quote_requests - quote_bundle["executions"] = [] - quote_bundle["execution_status"] = [] + quote_bundle["quote_executions"] = [] update_quotes = True if update_quotes: @@ -546,11 +551,11 @@ def _raise_if_invalid(self, bridge_requests: list) -> None: "[BRIDGE MANAGER] Request contains duplicate entries with same 'from' and 'to'." ) - def bridge_refill_requirements(self, client_input: dict) -> dict: + def bridge_refill_requirements( + self, quote_requests: list, force_update: bool = False + ) -> dict: """Get bridge refill requirements.""" - quote_requests = client_input.get("quote_requests", []) - force_update = client_input.get("force_update", False) self._raise_if_invalid(quote_requests) self.logger.info( f"[BRIDGE MANAGER] Num. quote requests: {len(quote_requests)}." @@ -632,11 +637,14 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: quote_bundle = self.data.last_requested_quote_bundle if not quote_bundle: - raise ValueError(f"[BRIDGE MANAGER] Id {quote_bundle_id} not found.") + raise RuntimeError("[BRIDGE MANAGER] No quote bundle.") - quote_requests = quote_bundle["quote_requests"] + if quote_bundle.get("id") != quote_bundle_id: + raise RuntimeError( + f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} does not match last requested quote bundle id {quote_bundle.get('id')}." + ) - requirements = self.bridge_refill_requirements(quote_requests) + requirements = self.bridge_refill_requirements(quote_bundle["quote_requests"]) if requirements["is_refill_required"]: raise RuntimeError( @@ -644,5 +652,6 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") - for quote in quote_bundle["bridge_quote_responses"]: - self.bridge_provider.execute_quote(quote) + for bridge_quote_response in quote_bundle["bridge_quote_responses"]: + quote_execution = self.bridge_provider.execute_quote(bridge_quote_response) + quote_bundle["quote_executions"].append(quote_execution) diff --git a/operate/cli.py b/operate/cli.py index eeaf27e9c..9ae860584 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -633,12 +633,19 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: chain=chain, backup_owner=backup_owner, ) - wallet.transfer( - to=t.cast(str, safes.get(chain)), - amount=int(data.get("fund_amount", DEFAULT_NEW_SAFE_FUNDS_AMOUNT[chain])), - chain=chain, - from_safe=False, - ) + + safe_address = t.cast(str, safes.get(chain)) + initial_funds = data.get("initial_funds", DEFAULT_NEW_SAFE_FUNDS_AMOUNT[chain]) + + for asset, amount in initial_funds.items(): + wallet.transfer_asset( + to=safe_address, + amount=amount, + chain=chain, + asset=asset, + from_safe=False, + ) + return JSONResponse( content={"safe": safes.get(chain), "message": "Safe created!"} ) @@ -1004,7 +1011,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: try: data = await request.json() output = operate.bridge_manager().bridge_refill_requirements( - client_input=data + quote_requests=data ) return JSONResponse( diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index 5238b4b84..32ebebeda 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -21,6 +21,7 @@ import typing as t +from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain, ContractAddresses @@ -140,12 +141,22 @@ DEFAULT_MECH_MARKETPLACE_PRIORITY_MECH = "0x552cEA7Bc33CbBEb9f1D90c1D11D2C6daefFd053" -DEFAULT_NEW_SAFE_FUNDS_AMOUNT: t.Dict[Chain, int] = { - Chain.GNOSIS: 1e18, - Chain.OPTIMISTIC: 1e15 / 4, - Chain.BASE: 1e15 / 4, - Chain.ETHEREUM: 1e15 / 4, - Chain.MODE: 1e15 / 4, +DEFAULT_NEW_SAFE_FUNDS_AMOUNT: t.Dict[Chain, t.Dict[str, int]] = { + Chain.GNOSIS: { + ZERO_ADDRESS: int(1e18), + }, + Chain.OPTIMISTIC: { + ZERO_ADDRESS: int(1e15 / 4), + }, + Chain.BASE: { + ZERO_ADDRESS: int(1e15 / 4), + }, + Chain.ETHEREUM: { + ZERO_ADDRESS: int(1e15 / 4), + }, + Chain.MODE: { + ZERO_ADDRESS: int(1e15 / 4), + }, } # ERC20 token addresses diff --git a/operate/wallet/master.py b/operate/wallet/master.py index 7cace9db6..6950ba31d 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -303,7 +303,7 @@ def _transfer_erc20_from_safe( chain: Chain, rpc: t.Optional[str] = None, ) -> None: - """Transfer funds from safe wallet.""" + """Transfer erc20 from safe wallet.""" transfer_erc20_from_safe( ledger_api=self.ledger_api(chain=chain, rpc=rpc), crypto=self.crypto, @@ -321,7 +321,41 @@ def _transfer_erc20_from_eoa( chain: Chain, rpc: t.Optional[str] = None, ) -> None: - raise NotImplementedError() + """Transfer erc20 from EOA wallet.""" + wallet_address = self.address + ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc)) + tx_settler = TxSettler( + ledger_api=ledger_api, + crypto=self.crypto, + chain_type=ChainProfile.CUSTOM, + timeout=ON_CHAIN_INTERACT_TIMEOUT, + retries=ON_CHAIN_INTERACT_RETRIES, + sleep=ON_CHAIN_INTERACT_SLEEP, + ) + + def _build_transfer_tx( # pylint: disable=unused-argument + *args: t.Any, **kargs: t.Any + ) -> t.Dict: + # TODO Backport to OpenAEA + instance = registry_contracts.erc20.get_instance( + ledger_api=ledger_api, + contract_address=token, + ) + tx = instance.functions.transfer(to, amount).build_transaction( + { + "from": wallet_address, + "gas": 1, + "gasPrice": ledger_api.api.eth.gas_price, + "nonce": ledger_api.api.eth.get_transaction_count(wallet_address), + } + ) + return ledger_api.update_with_gas_estimate( + transaction=tx, + raise_on_try=False, + ) + + setattr(tx_settler, "build", _build_transfer_tx) # noqa: B010 + tx_settler.transact(lambda x: x, "", kwargs={}) def transfer( self, From 0b3109fb70822f3d9fd0c216b85ea7cb0606574a Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 2 Apr 2025 19:53:34 +0200 Subject: [PATCH 019/173] feat: added execution and status --- operate/bridge/bridge.py | 445 +++++++++++++++++++++++++++------------ operate/cli.py | 59 +++++- 2 files changed, 364 insertions(+), 140 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 45e5a63e0..179e7c8fe 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -20,6 +20,7 @@ """Bridge manager.""" +import enum import json import logging import shutil @@ -45,7 +46,7 @@ from operate.wallet.master import MasterWalletManager -DEFAULT_MAX_RETRIES = 1 +DEFAULT_MAX_RETRIES = 3 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" @@ -67,50 +68,22 @@ def name(self) -> str: return self.__class__.__name__ @abstractmethod - def get_quote( - self, - from_chain: Chain, - from_address: str, - from_token: str, - to_chain: Chain, - to_token: str, - to_address: str, - to_amount: int, - ) -> dict: - """Get bridge quote.""" + def update_with_quote(self, bridge_workflow: dict) -> None: + """Update the request with the quote.""" raise NotImplementedError() - def get_quote_responses(self, quote_requests: list) -> list: - """Get bridge quotes.""" - bridge_quote_responses = [] - - for quote_request in quote_requests: - bridge_quote_response = self.get_quote( - from_chain=Chain(quote_request["from"]["chain"]), - from_address=quote_request["from"]["address"], - from_token=quote_request["from"]["token"], - to_chain=Chain(quote_request["to"]["chain"]), - to_address=quote_request["to"]["address"], - to_token=quote_request["to"]["token"], - to_amount=quote_request["to"]["amount"], - ) - - bridge_quote_responses.append(bridge_quote_response) - - return bridge_quote_responses - @abstractmethod - def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: + def get_quote_requirements(self, bridge_workflow: dict) -> dict | None: """Get bridge requirements for a single quote.""" raise NotImplementedError() - def sum_quotes_requirements(self, bridge_quote_responses: list) -> dict: + def sum_quotes_requirements(self, bridge_workflows: list) -> dict: """Get bridge requirements for a list of quotes.""" bridge_requirements: dict = {} - for bridge_quote_response in bridge_quote_responses: - req = self.get_quote_requirements(bridge_quote_response) + for workflow in bridge_workflows: + req = self.get_quote_requirements(workflow) if not req: continue @@ -128,47 +101,60 @@ def sum_quotes_requirements(self, bridge_quote_responses: list) -> dict: return bridge_requirements @abstractmethod - def execute_quote(self, bridge_quote_response: dict) -> None: + def update_with_execution(self, bridge_workflow: dict) -> None: """Execute the quote.""" raise NotImplementedError() + @abstractmethod + def update_execution_status(self, bridge_workflow: dict) -> None: + """Update the execution status.""" + raise NotImplementedError() + + @abstractmethod + def is_execution_finished(self, bridge_workflow: dict) -> bool: + """Check if the execution is finished.""" + raise NotImplementedError() + class LiFiBridgeProvider(BridgeProvider): """LI.FI Bridge provider.""" - def get_quote( - self, - from_chain: Chain, - from_address: str, - from_token: str, - to_chain: Chain, - to_token: str, - to_address: str, - to_amount: int, - ) -> dict: - """Get bridge quote.""" + def update_with_quote(self, bridge_workflow: dict) -> None: + """Update the request with the quote.""" + + if "execution" in bridge_workflow: + raise ValueError( + "[LI.FI BRIDGE] Cannot update workflow with quote: execution already present." + ) + + from_chain = bridge_workflow["request"]["from"]["chain"] + from_address = bridge_workflow["request"]["from"]["address"] + from_token = bridge_workflow["request"]["from"]["token"] + to_chain = bridge_workflow["request"]["to"]["chain"] + to_address = bridge_workflow["request"]["to"]["address"] + to_token = bridge_workflow["request"]["to"]["token"] + to_amount = bridge_workflow["request"]["to"]["amount"] if to_amount == 0: self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested") - return { - "quote": {}, - "metadata": { - "attempts": 0, - "elapsed_time": 0, - "error": False, - "message": "Zero-amount quote requested", - "status": HTTPStatus.OK, - "timestamp": int(time.time()), - }, + bridge_workflow["quote"] = { + "response": {}, + "attempts": 0, + "elapsed_time": 0, + "error": False, + "message": "Zero-amount quote requested", + "status": HTTPStatus.OK, + "timestamp": int(time.time()), } + return url = "https://li.quest/v1/quote/toAmount" headers = {"accept": "application/json"} params = { - "fromChain": from_chain.id, + "fromChain": Chain(from_chain).id, "fromAddress": from_address, "fromToken": from_token, - "toChain": to_chain.id, + "toChain": Chain(to_chain).id, "toAddress": to_address, "toToken": to_token, "toAmount": to_amount, @@ -181,31 +167,28 @@ def get_quote( url=url, headers=headers, params=params, timeout=30 ) response.raise_for_status() - return { - "quote": response.json(), - "metadata": { - "attempts": attempt, - "elapsed_time": time.time() - start, - "error": False, - "message": "", - "status": response.status_code, - "timestamp": int(time.time()), - }, + bridge_workflow["quote"] = { + "response": response.json(), + "attempts": attempt, + "elapsed_time": time.time() - start, + "error": False, + "message": "", + "status": response.status_code, + "timestamp": int(time.time()), } + return except requests.Timeout as e: self.logger.warning( f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." ) output = { - "quote": {}, - "metadata": { - "attempts": attempt, - "elapsed_time": time.time() - start, - "error": True, - "message": str(e), - "status": HTTPStatus.GATEWAY_TIMEOUT, - "timestamp": int(time.time()), - }, + "response": {}, + "attempts": attempt, + "elapsed_time": time.time() - start, + "error": True, + "message": str(e), + "status": HTTPStatus.GATEWAY_TIMEOUT, + "timestamp": int(time.time()), } except requests.RequestException as e: self.logger.warning( @@ -213,35 +196,32 @@ def get_quote( ) response_json = response.json() output = { - "quote": response_json, - "metadata": { - "attempts": attempt, - "elapsed_time": time.time() - start, - "error": True, - "message": response_json.get("message") or str(e), - "status": response.status_code, - "timestamp": int(time.time()), - }, + "response": response_json, + "attempts": attempt, + "elapsed_time": time.time() - start, + "error": True, + "message": response_json.get("message") or str(e), + "status": response.status_code, + "timestamp": int(time.time()), } if attempt >= DEFAULT_MAX_RETRIES: self.logger.error( f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_RETRIES} attempts." ) - return output + bridge_workflow["quote"] = output + return time.sleep(2) - return {} - # TODO gas fees ! - def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: + def get_quote_requirements(self, bridge_workflow: dict) -> dict | None: """Get bridge requirements for a quote.""" - quote = bridge_quote_response["quote"] - metadata = bridge_quote_response["metadata"] + quote = bridge_workflow["quote"]["response"] + error = bridge_workflow["quote"].get("error", False) - if metadata.get("error", False) or "action" not in quote: + if error or "action" not in quote: return None from_chain = Chain.from_id(quote["action"]["fromChainId"]) @@ -266,19 +246,48 @@ def get_quote_requirements(self, bridge_quote_response: dict) -> dict | None: } } - def execute_quote(self, bridge_quote_response: dict) -> None: + def update_with_execution(self, bridge_workflow: dict) -> None: """Execute the quote.""" - quote = bridge_quote_response["quote"] - metadata = bridge_quote_response["metadata"] + if "quote" not in bridge_workflow: + raise ValueError( + "[LI.FI BRIDGE] Cannot update workflow with execution: quote not present." + ) - if not quote or metadata.get("error", False): - self.logger.info( - "[LI.FI BRIDGE] Skipping quote execution (no quote or request error)." + if "execution" in bridge_workflow: + raise ValueError( + "[LI.FI BRIDGE] Cannot update workflow with quote: execution already present." ) - return None - self.logger.info(f"[LI.FI BRIDGE] Execute quote {quote.get('id')}.") + quote = bridge_workflow["quote"]["response"] + error = bridge_workflow["quote"].get("error", False) + + if error: + self.logger.info("[LI.FI BRIDGE] Skipping quote execution (quote error).") + bridge_workflow["execution"] = { + "error": True, + "explorer_link": None, + "message": "Skipped quote execution (quote error).", + "status": None, + "timestamp": int(time.time()), + "tx_hash": None, + "tx_status": None, + } + return + if not quote: + self.logger.info("[LI.FI BRIDGE] Skipping quote execution (empty quote).") + bridge_workflow["execution"] = { + "error": False, + "explorer_link": None, + "message": "Skipped quote execution (empty quote).", + "status": None, + "timestamp": int(time.time()), + "tx_hash": None, + "tx_status": None, + } + return + + self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") from_token = quote["action"]["fromToken"]["address"] from_amount = int(quote["action"]["fromAmount"]) @@ -347,9 +356,78 @@ def execute_quote(self, bridge_quote_response: dict) -> None: transaction["gas"] = gas_estimate signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - print(f"Quote transaction {tx_hash=}") + self.logger.info(f"[LI.FI BRIDGE] Quote {quote['id']} tx_hash={tx_hash.hex()}.") + + # TODO remove? receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - print(f"Quote transaction {receipt=}") + self.logger.info(f"[LI.FI BRIDGE] Quote {quote['id']} executed.") + + bridge_workflow["execution"] = { + "error": receipt.get("status", 0) == 0, + "explorer_link": f"https://scan.li.fi/tx/{tx_hash.hex()}", + "message": "", + "status": None, + "timestamp": int(time.time()), + "tx_hash": tx_hash.hex(), + "tx_status": receipt.get("status", 0), + } + + def update_execution_status(self, bridge_workflow: dict) -> None: + """Update the execution status.""" + + print("xxxxxx") + + if "execution" not in bridge_workflow: + raise ValueError( + "[LI.FI BRIDGE] Cannot update workflow execution: execution not present." + ) + + execution = bridge_workflow["execution"] + tx_hash = execution["tx_hash"] + + if not tx_hash: + execution["status"] = None + return + + url = "https://li.quest/v1/status" + headers = {"accept": "application/json"} + params = { + "txHash": tx_hash, + } + response = requests.get(url=url, headers=headers, params=params, timeout=30) + response.raise_for_status() + response_json = response.json() + status = response_json.get("status", "unknown").lower() + execution["status"] = status + + def is_execution_finished(self, bridge_workflow: dict) -> bool: + """Check if the execution is finished.""" + + if "execution" not in bridge_workflow: + raise ValueError( + "[LI.FI BRIDGE] Cannot update workflow execution: execution not present." + ) + + execution = bridge_workflow["execution"] + if "status" in execution and execution["status"] in (None, "done", "failed"): + return True + + self.update_execution_status(bridge_workflow) + + execution = bridge_workflow["execution"] + if execution.get("status") in (None, "done", "failed"): + return True + + return False + + +class QuoteBundleStatus(str, enum.Enum): + """Quote bundle status.""" + + CREATED = "created" + QUOTED = "quoted" + EXECUTED = "executed" + FINISHED = "finished" # All requests in the bundle are either done or failed. @dataclass @@ -358,7 +436,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 - last_requested_quote_bundle: dict = field(default_factory=dict) + last_requested_quote_bundle: dict | None = None executed_quotes: dict = field(default_factory=dict) _file = "bridge.json" @@ -438,52 +516,85 @@ def __init__( ) self.data.store() - def _refresh_latest_requested_quote_bundle( - self, quote_requests: list, force_update: bool - ) -> None: + def _get_updated_quote_bundle( + self, bridge_requests: list, force_update: bool + ) -> dict: """Ensures to return a valid (non expired) quote bundle for the given inputs.""" now = int(time.time()) quote_bundle = self.data.last_requested_quote_bundle or {} + quote_bundle_id = quote_bundle.get("id") create_new_quote_bundle = False - update_quotes = False if not quote_bundle: self.logger.info("[BRIDGE MANAGER] No last quote bundle.") create_new_quote_bundle = True - elif DeepDiff(quote_requests, quote_bundle.get("quote_requests", [])): + quote_bundle_id = None + elif quote_bundle.get("status") != QuoteBundleStatus.QUOTED: + raise RuntimeError("[BRIDGE MANAGER] Quote bundle inconsistent status.") + elif DeepDiff(bridge_requests, quote_bundle.get("bridge_requests", [])): self.logger.info("[BRIDGE MANAGER] Different quote requests.") create_new_quote_bundle = True + quote_bundle_id = None elif force_update: self.logger.info("[BRIDGE MANAGER] Force quote update.") - update_quotes = True - elif now > quote_bundle.get("expiration_timestamp", 0): + create_new_quote_bundle = True + elif now > quote_bundle.get("timestamp", 0) + self.quote_validity_period: self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") - update_quotes = True + create_new_quote_bundle = True if create_new_quote_bundle: - quote_bundle["id"] = f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" - quote_bundle["quote_requests"] = quote_requests - quote_bundle["quote_executions"] = [] - update_quotes = True - - if update_quotes: self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") + quote_bundle = {} + quote_bundle["id"] = ( + quote_bundle_id or f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" + ) + quote_bundle["bridge_provider"] = self.bridge_provider.name() + quote_bundle["status"] = QuoteBundleStatus.CREATED.value + quote_bundle["bridge_requests"] = bridge_requests + quote_bundle["bridge_workflows"] = [ + {"request": request} for request in bridge_requests + ] quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + self.quote_validity_period - quote_bundle["bridge_provider"] = self.bridge_provider.name() - quote_bundle[ - "bridge_quote_responses" - ] = self.bridge_provider.get_quote_responses(quote_requests) + + for workflow in quote_bundle["bridge_workflows"]: + self.bridge_provider.update_with_quote(workflow) + + quote_bundle["status"] = QuoteBundleStatus.QUOTED.value quote_bundle[ "bridge_requirements" ] = self.bridge_provider.sum_quotes_requirements( - quote_bundle["bridge_quote_responses"] + quote_bundle["bridge_workflows"] ) self.data.last_requested_quote_bundle = quote_bundle self.data.store() + return quote_bundle + + def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: + quote_bundle = self.data.executed_quotes.get(quote_bundle_id) + + if not quote_bundle: + raise ValueError( + f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} not found." + ) + + initial_status = quote_bundle["status"] + is_execution_finished = all( + self.bridge_provider.is_execution_finished(workflow) + for workflow in quote_bundle["bridge_workflows"] + ) + + if is_execution_finished: + quote_bundle["status"] = QuoteBundleStatus.FINISHED.value + else: + quote_bundle["status"] = QuoteBundleStatus.EXECUTED.value + + if initial_status != quote_bundle["status"]: + self.data.store() + def _raise_if_invalid(self, bridge_requests: list) -> None: """Preprocess quote requests.""" @@ -552,19 +663,18 @@ def _raise_if_invalid(self, bridge_requests: list) -> None: ) def bridge_refill_requirements( - self, quote_requests: list, force_update: bool = False + self, bridge_requests: list, force_update: bool = False ) -> dict: """Get bridge refill requirements.""" - self._raise_if_invalid(quote_requests) + self._raise_if_invalid(bridge_requests) self.logger.info( - f"[BRIDGE MANAGER] Num. quote requests: {len(quote_requests)}." + f"[BRIDGE MANAGER] Quote requests count: {len(bridge_requests)}." ) - self._refresh_latest_requested_quote_bundle(quote_requests, force_update) - quote_bundle = self.data.last_requested_quote_bundle + quote_bundle = self._get_updated_quote_bundle(bridge_requests, force_update) - chains = [request["from"]["chain"] for request in quote_requests] + chains = [request["from"]["chain"] for request in bridge_requests] balances = {} for chain in chains: ledger_api = self.wallet_manager.load(Chain(chain).ledger_type).ledger_api( @@ -575,12 +685,12 @@ def bridge_refill_requirements( asset_addresses={ZERO_ADDRESS} | { request["from"]["token"] - for request in quote_requests + for request in bridge_requests if request["from"]["chain"] == chain }, addresses={ request["from"]["address"] - for request in quote_requests + for request in bridge_requests if request["from"]["chain"] == chain }, ) @@ -608,9 +718,15 @@ def bridge_refill_requirements( ) quote_response_status = [ - response["metadata"] for response in quote_bundle["bridge_quote_responses"] + { + "message": response["quote"]["message"], + "error": response["quote"]["error"], + } + for response in quote_bundle["bridge_workflows"] ] - errors = any(status["error"] for status in quote_response_status) + errors = any( + workflow["quote"]["error"] for workflow in quote_bundle["bridge_workflows"] + ) self._quote_bundle_updated_on_session = True return { @@ -627,7 +743,7 @@ def bridge_refill_requirements( "errors": errors, } - def execute_quote_bundle(self, quote_bundle_id: str) -> None: + def execute_quote_bundle(self, quote_bundle_id: str) -> dict: """Execute quote bundle""" if not self._quote_bundle_updated_on_session: @@ -636,6 +752,7 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: ) quote_bundle = self.data.last_requested_quote_bundle + if not quote_bundle: raise RuntimeError("[BRIDGE MANAGER] No quote bundle.") @@ -644,7 +761,7 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} does not match last requested quote bundle id {quote_bundle.get('id')}." ) - requirements = self.bridge_refill_requirements(quote_bundle["quote_requests"]) + requirements = self.bridge_refill_requirements(quote_bundle["bridge_requests"]) if requirements["is_refill_required"]: raise RuntimeError( @@ -652,6 +769,56 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> None: ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") - for bridge_quote_response in quote_bundle["bridge_quote_responses"]: - quote_execution = self.bridge_provider.execute_quote(bridge_quote_response) - quote_bundle["quote_executions"].append(quote_execution) + for bridge_workflow in quote_bundle["bridge_workflows"]: + self.bridge_provider.update_with_execution(bridge_workflow) + + quote_bundle["status"] = QuoteBundleStatus.EXECUTED.value + + self.data.last_requested_quote_bundle = None + self.data.executed_quotes[quote_bundle["id"]] = quote_bundle + self.data.store() + self._quote_bundle_updated_on_session = False + self.logger.info( + f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} executed." + ) + + executions = [ + workflow["execution"] for workflow in quote_bundle["bridge_workflows"] + ] + errors = any( + workflow["execution"]["error"] + for workflow in quote_bundle["bridge_workflows"] + ) + + return { + "id": quote_bundle["id"], + "executions": executions, + "errors": errors, + } + + def get_execution_status(self, quote_bundle_id: str) -> dict: + """Get execution status of quote bundle.""" + + quote_bundle = self.data.executed_quotes.get(quote_bundle_id) + + if not quote_bundle: + raise ValueError( + f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} not found." + ) + + self._update_quote_bundle_status(quote_bundle_id) + + executions = [ + workflow["execution"] for workflow in quote_bundle["bridge_workflows"] + ] + errors = any( + workflow["execution"]["error"] + for workflow in quote_bundle["bridge_workflows"] + ) + + return { + "id": quote_bundle["id"], + "status": quote_bundle["status"], + "executions": executions, + "errors": errors, + } diff --git a/operate/cli.py b/operate/cli.py index 9ae860584..d26440ea2 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1011,7 +1011,64 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: try: data = await request.json() output = operate.bridge_manager().bridge_refill_requirements( - quote_requests=data + bridge_requests=data + ) + + return JSONResponse( + content=output, + status_code=HTTPStatus.BAD_GATEWAY + if output["errors"] + else HTTPStatus.OK, + ) + except ValueError as e: + return JSONResponse( + content={"error": str(e)}, status_code=HTTPStatus.BAD_REQUEST + ) + except Exception as e: # pylint: disable=broad-except + return JSONResponse( + content={"error": str(e), "traceback": traceback.format_exc()}, + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @app.post("/api/bridge/execute") + @with_retries + async def _bridge_execute(request: Request) -> JSONResponse: + """Get the bridge refill requirements.""" + if operate.password is None: + return USER_NOT_LOGGED_IN_ERROR + + try: + data = await request.json() + output = operate.bridge_manager().execute_quote_bundle( + quote_bundle_id=data["id"] + ) + + return JSONResponse( + content=output, + status_code=HTTPStatus.BAD_GATEWAY + if output["errors"] + else HTTPStatus.OK, + ) + except ValueError as e: + return JSONResponse( + content={"error": str(e)}, status_code=HTTPStatus.BAD_REQUEST + ) + except Exception as e: # pylint: disable=broad-except + return JSONResponse( + content={"error": str(e), "traceback": traceback.format_exc()}, + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @app.get(f"/api/bridge/status/{id}") + @with_retries + async def _bridge_status(request: Request) -> JSONResponse: + """Get the bridge refill requirements.""" + + quote_bundle_id = request.path_params["id"] + + try: + output = operate.bridge_manager().get_execution_status( + quote_bundle_id=quote_bundle_id ) return JSONResponse( From 00ae72cf8f5f2899ae3106763977b5c2bddf3785 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 2 Apr 2025 23:02:03 +0200 Subject: [PATCH 020/173] chore: update --- docs/api.md | 103 +++++++++++++++++++++++++- operate/bridge/bridge.py | 151 ++++++++++++++++++++++----------------- 2 files changed, 186 insertions(+), 68 deletions(-) diff --git a/docs/api.md b/docs/api.md index 028db6eb1..b2bb30c15 100644 --- a/docs/api.md +++ b/docs/api.md @@ -894,7 +894,10 @@ The refill requirements are computed based on the fund requirements present on t ### `POST /api/bridge/bridge_refill_requirements` -Returns the refill requirements on the source chain for bridging assets to target chains. +Creates a quote bundle to fulfill the bridge requests and returns + +- the refill requirements on the source chain for bridging assets to target chains, +- the quote bundle id to execute the request.
Request @@ -964,9 +967,9 @@ Returns the refill requirements on the source chain for bridging assets to targe
Response -```json + ```json { - "id": "e05509f3-d153-4b69-94cf-f82324f8c226", + "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", "balances": { "ethereum": { "0xDe6B572A049B27D349e89aD0cBEF102227e31473": { @@ -1007,10 +1010,104 @@ Returns the refill requirements on the source chain for bridging assets to targe ], "errors": false } + ``` + +
+ +--- + +### `POST /api/bridge/execute` + +Executes a quote bundle. + +
+ Request + +```json + { + "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d" + } ```
+
+ Response + + ```json + { + "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", + "status": "submitted", + "executions": [ + { + "error": false, + "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", + "message": "", + "status": "done", + "timestamp": 1743626170, + "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", + "tx_status": 1 + }, + { + "error": false, + "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", + "message": "", + "status": "pending", + "timestamp": 1743626185, + "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", + "tx_status": 1 + } + ], + "errors": false + } + ``` + +
+ +--- + +### `GET /api/bridge/status/{quote_bundle_id}` + +Gets the status of a quote bundle. The attribute `status` can take the following values sequentially: + +- `created`: The quote bundle internal data structure has been created, but no quotes have been requested yet. +- `quoted`: A quote is available. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. +- `submitted`: The quote bundle has been submitted for execution. +- `finished`: All the quote executions in the bundle have reached their final state (either done or failed). No more updates are expected for this quote bundle. + +
+ Response + + ```json + { + "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", + "status": "finished", + "executions": [ + { + "error": false, + "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", + "message": "", + "status": "done", + "timestamp": 1743626170, + "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", + "tx_status": 1 + }, + { + "error": false, + "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", + "message": "", + "status": "done", + "timestamp": 1743626185, + "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", + "tx_status": 1 + } + ], + "errors": false + } + ``` + +
+ --- ## Unused endpoints diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 179e7c8fe..09c2911ab 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -25,6 +25,7 @@ import logging import shutil import time +import typing as t import uuid from abc import abstractmethod from dataclasses import dataclass, field @@ -35,10 +36,17 @@ import requests from aea.helpers.logging import setup_logger +from autonomy.chain.base import registry_contracts +from autonomy.chain.tx import TxSettler from deepdiff import DeepDiff from web3 import Web3 -from operate.constants import ZERO_ADDRESS +from operate.constants import ( + ON_CHAIN_INTERACT_RETRIES, + ON_CHAIN_INTERACT_SLEEP, + ON_CHAIN_INTERACT_TIMEOUT, + ZERO_ADDRESS, +) from operate.ledger import get_default_rpc from operate.operate_types import Chain from operate.resource import LocalResource @@ -116,6 +124,20 @@ def is_execution_finished(self, bridge_workflow: dict) -> bool: raise NotImplementedError() +class LiFiTransactionStatus(str, enum.Enum): + """LI.FI transaction status.""" + + NOT_FOUND = "not_found" + INVALID = "invalid" + PENDING = "pending" + DONE = "done" + FAILED = "failed" + + def __str__(self) -> str: + """__str__""" + return self.value + + class LiFiBridgeProvider(BridgeProvider): """LI.FI Bridge provider.""" @@ -295,52 +317,50 @@ def update_with_execution(self, bridge_workflow: dict) -> None: from_chain = Chain.from_id(transaction_request["chainId"]) wallet = self.wallet_manager.load(from_chain.ledger_type) - # TODO rewrite with framework methods - private_key = wallet.crypto.private_key - w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) - account = w3.eth.account.from_key(private_key) - + # Bridges from an asset other than native require an approval transaction. if from_token != ZERO_ADDRESS: self.logger.info( - f"[LI.FI BRIDGE] Approve transaction for token {from_token}." - ) - from_token_contract = w3.eth.contract( - address=from_token, - abi=[ - { - "constant": False, - "inputs": [ - {"name": "spender", "type": "address"}, - {"name": "amount", "type": "uint256"}, - ], - "name": "approve", - "outputs": [{"name": "", "type": "bool"}], - "payable": False, - "stateMutability": "nonpayable", - "type": "function", - } - ], + f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token}=)." ) - transaction = from_token_contract.functions.approve( - transaction_request["to"], from_amount - ).build_transaction( - { - "from": account.address, - "nonce": w3.eth.get_transaction_count(account.address), - "gasPrice": w3.to_wei("20", "gwei"), - } + # TODO Approval is done on several places. Consider exporting to a + # higher-level layer (e.g., wallet?) + tx_settler = TxSettler( + ledger_api=wallet.ledger_api(from_chain), + crypto=wallet.crypto, + chain_type=from_chain, + timeout=ON_CHAIN_INTERACT_TIMEOUT, + retries=ON_CHAIN_INTERACT_RETRIES, + sleep=ON_CHAIN_INTERACT_SLEEP, ) - gas_estimate = w3.eth.estimate_gas(transaction) - transaction["gas"] = gas_estimate - signed_transaction = w3.eth.account.sign_transaction( - transaction, private_key + def _build_approval_tx( # pylint: disable=unused-argument + *args: t.Any, **kargs: t.Any + ) -> dict: + return registry_contracts.erc20.get_approve_tx( + ledger_api=wallet.ledger_api(from_chain), + contract_address=from_token, + spender=transaction_request["to"], + sender=wallet.address, + amount=from_amount, + ) + + setattr(tx_settler, "build", _build_approval_tx) # noqa: B010 + tx_settler.transact( + method=lambda: {}, + contract="", + kwargs={}, + dry_run=False, ) - tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - self.logger.info(f"[LI.FI BRIDGE] Approve transaction {tx_hash=}.") - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.logger.info(f"[LI.FI BRIDGE] Approve transaction {receipt=}.") + self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") + + # TODO rewrite with framework methods + self.logger.info( + f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." + ) + private_key = wallet.crypto.private_key + w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) + account = w3.eth.account.from_key(private_key) transaction = { "value": w3.to_wei(int(transaction_request["value"], 16), "wei"), @@ -356,11 +376,11 @@ def update_with_execution(self, bridge_workflow: dict) -> None: transaction["gas"] = gas_estimate signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - self.logger.info(f"[LI.FI BRIDGE] Quote {quote['id']} tx_hash={tx_hash.hex()}.") + self.logger.info(f"[LI.FI BRIDGE] Bridge transaction tx_hash={tx_hash.hex()}.") # TODO remove? receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - self.logger.info(f"[LI.FI BRIDGE] Quote {quote['id']} executed.") + self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") bridge_workflow["execution"] = { "error": receipt.get("status", 0) == 0, @@ -375,8 +395,6 @@ def update_with_execution(self, bridge_workflow: dict) -> None: def update_execution_status(self, bridge_workflow: dict) -> None: """Update the execution status.""" - print("xxxxxx") - if "execution" not in bridge_workflow: raise ValueError( "[LI.FI BRIDGE] Cannot update workflow execution: execution not present." @@ -409,13 +427,24 @@ def is_execution_finished(self, bridge_workflow: dict) -> bool: ) execution = bridge_workflow["execution"] - if "status" in execution and execution["status"] in (None, "done", "failed"): + + if execution["tx_hash"] is None: + execution["status"] = None + return True + + if execution["status"] in ( + LiFiTransactionStatus.DONE, + LiFiTransactionStatus.FAILED, + ): return True self.update_execution_status(bridge_workflow) execution = bridge_workflow["execution"] - if execution.get("status") in (None, "done", "failed"): + if execution["status"] in ( + LiFiTransactionStatus.DONE, + LiFiTransactionStatus.FAILED, + ): return True return False @@ -426,9 +455,13 @@ class QuoteBundleStatus(str, enum.Enum): CREATED = "created" QUOTED = "quoted" - EXECUTED = "executed" + SUBMITTED = "submitted" FINISHED = "finished" # All requests in the bundle are either done or failed. + def __str__(self) -> str: + """__str__""" + return self.value + @dataclass class BridgeManagerData(LocalResource): @@ -550,7 +583,7 @@ def _get_updated_quote_bundle( quote_bundle_id or f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" ) quote_bundle["bridge_provider"] = self.bridge_provider.name() - quote_bundle["status"] = QuoteBundleStatus.CREATED.value + quote_bundle["status"] = str(QuoteBundleStatus.CREATED) quote_bundle["bridge_requests"] = bridge_requests quote_bundle["bridge_workflows"] = [ {"request": request} for request in bridge_requests @@ -561,7 +594,7 @@ def _get_updated_quote_bundle( for workflow in quote_bundle["bridge_workflows"]: self.bridge_provider.update_with_quote(workflow) - quote_bundle["status"] = QuoteBundleStatus.QUOTED.value + quote_bundle["status"] = str(QuoteBundleStatus.QUOTED) quote_bundle[ "bridge_requirements" ] = self.bridge_provider.sum_quotes_requirements( @@ -588,9 +621,9 @@ def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: ) if is_execution_finished: - quote_bundle["status"] = QuoteBundleStatus.FINISHED.value + quote_bundle["status"] = str(QuoteBundleStatus.FINISHED) else: - quote_bundle["status"] = QuoteBundleStatus.EXECUTED.value + quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) if initial_status != quote_bundle["status"]: self.data.store() @@ -772,7 +805,7 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> dict: for bridge_workflow in quote_bundle["bridge_workflows"]: self.bridge_provider.update_with_execution(bridge_workflow) - quote_bundle["status"] = QuoteBundleStatus.EXECUTED.value + quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) self.data.last_requested_quote_bundle = None self.data.executed_quotes[quote_bundle["id"]] = quote_bundle @@ -782,19 +815,7 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> dict: f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} executed." ) - executions = [ - workflow["execution"] for workflow in quote_bundle["bridge_workflows"] - ] - errors = any( - workflow["execution"]["error"] - for workflow in quote_bundle["bridge_workflows"] - ) - - return { - "id": quote_bundle["id"], - "executions": executions, - "errors": errors, - } + return self.get_execution_status(quote_bundle_id) def get_execution_status(self, quote_bundle_id: str) -> dict: """Get execution status of quote bundle.""" From bb6ac79994a00b420347178e5c20fbf1a2b78591 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 00:21:10 +0200 Subject: [PATCH 021/173] chore: uppercase enums --- docs/api.md | 8 ++++---- operate/bridge/bridge.py | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/api.md b/docs/api.md index b2bb30c15..78fa8ab5f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1070,10 +1070,10 @@ Executes a quote bundle. Gets the status of a quote bundle. The attribute `status` can take the following values sequentially: -- `created`: The quote bundle internal data structure has been created, but no quotes have been requested yet. -- `quoted`: A quote is available. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. -- `submitted`: The quote bundle has been submitted for execution. -- `finished`: All the quote executions in the bundle have reached their final state (either done or failed). No more updates are expected for this quote bundle. +- `CREATED`: The quote bundle internal data structure has been created, but no quotes have been requested yet. +- `QUOTED`: A quote is available. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. +- `SUBMITTED`: The quote bundle has been submitted for execution. +- `FINISHED`: All the quote executions in the bundle have reached their final state (either done or failed). No more updates are expected for this quote bundle.
Response diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 09c2911ab..4d78e2db8 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -127,11 +127,12 @@ def is_execution_finished(self, bridge_workflow: dict) -> bool: class LiFiTransactionStatus(str, enum.Enum): """LI.FI transaction status.""" - NOT_FOUND = "not_found" - INVALID = "invalid" - PENDING = "pending" - DONE = "done" - FAILED = "failed" + NOT_FOUND = "NOT_FOUND" + INVALID = "INVALID" + PENDING = "PENDING" + DONE = "DONE" + FAILED = "FAILED" + UNKNOWN = "UNKNOWN" def __str__(self) -> str: """__str__""" @@ -415,7 +416,7 @@ def update_execution_status(self, bridge_workflow: dict) -> None: response = requests.get(url=url, headers=headers, params=params, timeout=30) response.raise_for_status() response_json = response.json() - status = response_json.get("status", "unknown").lower() + status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) execution["status"] = status def is_execution_finished(self, bridge_workflow: dict) -> bool: @@ -453,10 +454,10 @@ def is_execution_finished(self, bridge_workflow: dict) -> bool: class QuoteBundleStatus(str, enum.Enum): """Quote bundle status.""" - CREATED = "created" - QUOTED = "quoted" - SUBMITTED = "submitted" - FINISHED = "finished" # All requests in the bundle are either done or failed. + CREATED = "CREATED" + QUOTED = "QUOTED" + SUBMITTED = "SUBMITTED" + FINISHED = "FINISHED" # All requests in the bundle are either done or failed. def __str__(self) -> str: """__str__""" @@ -563,7 +564,7 @@ def _get_updated_quote_bundle( self.logger.info("[BRIDGE MANAGER] No last quote bundle.") create_new_quote_bundle = True quote_bundle_id = None - elif quote_bundle.get("status") != QuoteBundleStatus.QUOTED: + elif quote_bundle.get("status") not in (QuoteBundleStatus.CREATED, QuoteBundleStatus.QUOTED): raise RuntimeError("[BRIDGE MANAGER] Quote bundle inconsistent status.") elif DeepDiff(bridge_requests, quote_bundle.get("bridge_requests", [])): self.logger.info("[BRIDGE MANAGER] Different quote requests.") From 103e4fc6940a502e44a49e6a0ec9a77c1af1e358 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 10:29:48 +0200 Subject: [PATCH 022/173] chore: update --- operate/bridge/bridge.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 4d78e2db8..60f470ba0 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -147,7 +147,7 @@ def update_with_quote(self, bridge_workflow: dict) -> None: if "execution" in bridge_workflow: raise ValueError( - "[LI.FI BRIDGE] Cannot update workflow with quote: execution already present." + f"[LI.FI BRIDGE] Cannot update workflow {bridge_workflow['id']} with quote: execution already present." ) from_chain = bridge_workflow["request"]["from"]["chain"] @@ -159,13 +159,13 @@ def update_with_quote(self, bridge_workflow: dict) -> None: to_amount = bridge_workflow["request"]["to"]["amount"] if to_amount == 0: - self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested") + self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested.") bridge_workflow["quote"] = { "response": {}, "attempts": 0, "elapsed_time": 0, "error": False, - "message": "Zero-amount quote requested", + "message": "Zero-amount quote requested.", "status": HTTPStatus.OK, "timestamp": int(time.time()), } @@ -278,9 +278,10 @@ def update_with_execution(self, bridge_workflow: dict) -> None: ) if "execution" in bridge_workflow: - raise ValueError( - "[LI.FI BRIDGE] Cannot update workflow with quote: execution already present." + self.logger.warning( + f"[LI.FI BRIDGE] Execution already present on bridge workflow {bridge_workflow['id']}." ) + return quote = bridge_workflow["quote"]["response"] error = bridge_workflow["quote"].get("error", False) @@ -471,7 +472,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 last_requested_quote_bundle: dict | None = None - executed_quotes: dict = field(default_factory=dict) + executed_quote_bundles: dict = field(default_factory=dict) _file = "bridge.json" @@ -564,7 +565,10 @@ def _get_updated_quote_bundle( self.logger.info("[BRIDGE MANAGER] No last quote bundle.") create_new_quote_bundle = True quote_bundle_id = None - elif quote_bundle.get("status") not in (QuoteBundleStatus.CREATED, QuoteBundleStatus.QUOTED): + elif quote_bundle.get("status") not in ( + QuoteBundleStatus.CREATED, + QuoteBundleStatus.QUOTED, + ): raise RuntimeError("[BRIDGE MANAGER] Quote bundle inconsistent status.") elif DeepDiff(bridge_requests, quote_bundle.get("bridge_requests", [])): self.logger.info("[BRIDGE MANAGER] Different quote requests.") @@ -587,7 +591,8 @@ def _get_updated_quote_bundle( quote_bundle["status"] = str(QuoteBundleStatus.CREATED) quote_bundle["bridge_requests"] = bridge_requests quote_bundle["bridge_workflows"] = [ - {"request": request} for request in bridge_requests + {"id": f"{uuid.uuid4()}", "request": request} + for request in bridge_requests ] quote_bundle["timestamp"] = now quote_bundle["expiration_timestamp"] = now + self.quote_validity_period @@ -608,7 +613,7 @@ def _get_updated_quote_bundle( return quote_bundle def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: - quote_bundle = self.data.executed_quotes.get(quote_bundle_id) + quote_bundle = self.data.executed_quote_bundles.get(quote_bundle_id) if not quote_bundle: raise ValueError( @@ -755,6 +760,7 @@ def bridge_refill_requirements( { "message": response["quote"]["message"], "error": response["quote"]["error"], + "status": response["quote"]["status"], } for response in quote_bundle["bridge_workflows"] ] @@ -805,11 +811,12 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> dict: self.logger.info("[BRIDGE MANAGER] Executing quotes.") for bridge_workflow in quote_bundle["bridge_workflows"]: self.bridge_provider.update_with_execution(bridge_workflow) + self.data.store() quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) self.data.last_requested_quote_bundle = None - self.data.executed_quotes[quote_bundle["id"]] = quote_bundle + self.data.executed_quote_bundles[quote_bundle["id"]] = quote_bundle self.data.store() self._quote_bundle_updated_on_session = False self.logger.info( @@ -821,7 +828,7 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> dict: def get_execution_status(self, quote_bundle_id: str) -> dict: """Get execution status of quote bundle.""" - quote_bundle = self.data.executed_quotes.get(quote_bundle_id) + quote_bundle = self.data.executed_quote_bundles.get(quote_bundle_id) if not quote_bundle: raise ValueError( From 765a3e2ad5b18a088bb1f562032d16ed03e89219 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 10:32:05 +0200 Subject: [PATCH 023/173] doc: update --- docs/api.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api.md b/docs/api.md index 78fa8ab5f..3db91030a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1043,7 +1043,7 @@ Executes a quote bundle. "error": false, "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", "message": "", - "status": "done", + "status": "DONE", "timestamp": 1743626170, "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", "tx_status": 1 @@ -1052,7 +1052,7 @@ Executes a quote bundle. "error": false, "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", "message": "", - "status": "pending", + "status": "PENDING", "timestamp": 1743626185, "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", "tx_status": 1 @@ -1081,13 +1081,13 @@ Gets the status of a quote bundle. The attribute `status` can take the following ```json { "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", - "status": "finished", + "status": "FINISHED", "executions": [ { "error": false, "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", "message": "", - "status": "done", + "status": "DONE", "timestamp": 1743626170, "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", "tx_status": 1 @@ -1096,7 +1096,7 @@ Gets the status of a quote bundle. The attribute `status` can take the following "error": false, "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", "message": "", - "status": "done", + "status": "DONE", "timestamp": 1743626185, "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", "tx_status": 1 From 2ed3c1fd4a9a51e49c639400e166cea2da0255f0 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 13:38:00 +0200 Subject: [PATCH 024/173] chore: api updates --- docs/api.md | 56 ++++--------------------------- operate/bridge/bridge.py | 71 ++++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 84 deletions(-) diff --git a/docs/api.md b/docs/api.md index 3db91030a..598d4ef89 100644 --- a/docs/api.md +++ b/docs/api.md @@ -929,34 +929,8 @@ Creates a quote bundle to fulfill the bridge requests and returns "address": "0x28580196F52DB3C95C3d40Df88426e251d115842", "token": "0x0000000000000000000000000000000000000000", "amount": 10000000000000000000 - }, - }, - { - "from": { - "chain": "ethereum", - "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", - "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0" - }, - "to": { - "chain": "gnosis", - "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", - "token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", - "amount": 0 - }, - }, - { - "from": { - "chain": "ethereum", - "address": "0xDe6B572A049B27D349e89aD0cBEF102227e31473", - "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0" - }, - "to": { - "chain": "gnosis", - "address": "0x28580196F52DB3C95C3d40Df88426e251d115842", - "token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", - "amount": 60000000000000000000 - }, - }, + } + } ], "force_update": false } @@ -978,7 +952,7 @@ Creates a quote bundle to fulfill the bridge requests and returns } } }, - "bridge_requirements": { + "bridge_total_requirements": { "ethereum": { "0x0000000000000000000000000000000000000000": 10073082159280405, "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0": 61944358967139717502 @@ -998,17 +972,13 @@ Creates a quote bundle to fulfill the bridge requests and returns { "error": false, "message": "", - "request_status": 200, - "timestamp": 1743000000 }, { "error": false, "message": "", - "request_status": 200, - "timestamp": 1743000010 } ], - "errors": false + "error": false } ``` @@ -1037,28 +1007,22 @@ Executes a quote bundle. ```json { "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", - "status": "submitted", + "status": "SUBMITTED", "executions": [ { - "error": false, "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", "message": "", "status": "DONE", - "timestamp": 1743626170, "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", - "tx_status": 1 }, { - "error": false, "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", "message": "", "status": "PENDING", - "timestamp": 1743626185, "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", - "tx_status": 1 } ], - "errors": false + "error": false } ``` @@ -1084,25 +1048,19 @@ Gets the status of a quote bundle. The attribute `status` can take the following "status": "FINISHED", "executions": [ { - "error": false, "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", "message": "", "status": "DONE", - "timestamp": 1743626170, "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", - "tx_status": 1 }, { - "error": false, "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", "message": "", "status": "DONE", - "timestamp": 1743626185, "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", - "tx_status": 1 } ], - "errors": false + "error": false } ``` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 60f470ba0..302fd1970 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -81,32 +81,28 @@ def update_with_quote(self, bridge_workflow: dict) -> None: raise NotImplementedError() @abstractmethod - def get_quote_requirements(self, bridge_workflow: dict) -> dict | None: + def get_quote_requirements(self, bridge_workflow: dict) -> dict: """Get bridge requirements for a single quote.""" raise NotImplementedError() def sum_quotes_requirements(self, bridge_workflows: list) -> dict: """Get bridge requirements for a list of quotes.""" - bridge_requirements: dict = {} + bridge_total_requirements: dict = {} for workflow in bridge_workflows: req = self.get_quote_requirements(workflow) - - if not req: - continue - for from_chain, from_addresses in req.items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): - bridge_requirements.setdefault(from_chain, {}).setdefault( + bridge_total_requirements.setdefault(from_chain, {}).setdefault( from_address, {} ).setdefault(from_token, 0) - bridge_requirements[from_chain][from_address][ + bridge_total_requirements[from_chain][from_address][ from_token ] += from_amount - return bridge_requirements + return bridge_total_requirements @abstractmethod def update_with_execution(self, bridge_workflow: dict) -> None: @@ -238,30 +234,34 @@ def update_with_quote(self, bridge_workflow: dict) -> None: time.sleep(2) # TODO gas fees ! - def get_quote_requirements(self, bridge_workflow: dict) -> dict | None: + def get_quote_requirements(self, bridge_workflow: dict) -> dict: """Get bridge requirements for a quote.""" quote = bridge_workflow["quote"]["response"] error = bridge_workflow["quote"].get("error", False) if error or "action" not in quote: - return None - - from_chain = Chain.from_id(quote["action"]["fromChainId"]) - from_address = quote["action"]["fromAddress"] - from_token = quote["action"]["fromToken"]["address"] - from_amount = int(quote["action"]["fromAmount"]) - transaction_value = int(quote["transactionRequest"]["value"], 16) + from_chain = bridge_workflow["request"]["from"]["chain"] + from_address = bridge_workflow["request"]["from"]["address"] + from_token = bridge_workflow["request"]["from"]["token"] + from_amount = 0 + transaction_value = 0 + else: + from_chain = Chain.from_id(quote["action"]["fromChainId"]).value + from_address = quote["action"]["fromAddress"] + from_token = quote["action"]["fromToken"]["address"] + from_amount = int(quote["action"]["fromAmount"]) + transaction_value = int(quote["transactionRequest"]["value"], 16) if from_token == ZERO_ADDRESS: return { - from_chain.value: { + from_chain: { from_address: {from_token: from_amount + transaction_value} } } else: return { - from_chain.value: { + from_chain: { from_address: { ZERO_ADDRESS: transaction_value, from_token: from_amount, @@ -322,7 +322,7 @@ def update_with_execution(self, bridge_workflow: dict) -> None: # Bridges from an asset other than native require an approval transaction. if from_token != ZERO_ADDRESS: self.logger.info( - f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token}=)." + f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." ) # TODO Approval is done on several places. Consider exporting to a @@ -602,7 +602,7 @@ def _get_updated_quote_bundle( quote_bundle["status"] = str(QuoteBundleStatus.QUOTED) quote_bundle[ - "bridge_requirements" + "bridge_total_requirements" ] = self.bridge_provider.sum_quotes_requirements( quote_bundle["bridge_workflows"] ) @@ -735,10 +735,9 @@ def bridge_refill_requirements( ) bridge_refill_requirements: dict = {} - for from_chain, from_addresses in quote_bundle["bridge_requirements"].items(): - ledger_api = self.wallet_manager.load( - Chain(from_chain).ledger_type - ).ledger_api(Chain(from_chain)) + for from_chain, from_addresses in quote_bundle[ + "bridge_total_requirements" + ].items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): balance = balances[from_chain][from_address][from_token] @@ -760,11 +759,10 @@ def bridge_refill_requirements( { "message": response["quote"]["message"], "error": response["quote"]["error"], - "status": response["quote"]["status"], } for response in quote_bundle["bridge_workflows"] ] - errors = any( + error = any( workflow["quote"]["error"] for workflow in quote_bundle["bridge_workflows"] ) self._quote_bundle_updated_on_session = True @@ -772,15 +770,12 @@ def bridge_refill_requirements( return { "id": quote_bundle["id"], "balances": balances, - "bridge_requirements": quote_bundle["bridge_requirements"], + "bridge_total_requirements": quote_bundle["bridge_total_requirements"], "bridge_refill_requirements": bridge_refill_requirements, "expiration_timestamp": quote_bundle["expiration_timestamp"], - "expiration_timeout": int( - quote_bundle["expiration_timestamp"] - time.time() - ), "is_refill_required": is_refill_required, "quote_response_status": quote_response_status, - "errors": errors, + "error": error, } def execute_quote_bundle(self, quote_bundle_id: str) -> dict: @@ -838,9 +833,15 @@ def get_execution_status(self, quote_bundle_id: str) -> dict: self._update_quote_bundle_status(quote_bundle_id) executions = [ - workflow["execution"] for workflow in quote_bundle["bridge_workflows"] + { + "explorer_link": workflow["execution"]["explorer_link"], + "message": workflow["execution"]["message"], + "status": workflow["execution"]["status"], + "tx_hash": workflow["execution"]["tx_hash"], + } + for workflow in quote_bundle["bridge_workflows"] ] - errors = any( + error = any( workflow["execution"]["error"] for workflow in quote_bundle["bridge_workflows"] ) @@ -849,5 +850,5 @@ def get_execution_status(self, quote_bundle_id: str) -> dict: "id": quote_bundle["id"], "status": quote_bundle["status"], "executions": executions, - "errors": errors, + "error": error, } From e69b963b39d345fed32142dfae03c20cf6b252df Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 15:45:30 +0200 Subject: [PATCH 025/173] chore: update API output --- operate/bridge/bridge.py | 78 ++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 302fd1970..86bdc2cb7 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -110,7 +110,7 @@ def update_with_execution(self, bridge_workflow: dict) -> None: raise NotImplementedError() @abstractmethod - def update_execution_status(self, bridge_workflow: dict) -> None: + def update_execution_status(self, bridge_workflow: dict) -> bool: """Update the execution status.""" raise NotImplementedError() @@ -191,7 +191,7 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "attempts": attempt, "elapsed_time": time.time() - start, "error": False, - "message": "", + "message": None, "status": response.status_code, "timestamp": int(time.time()), } @@ -387,15 +387,15 @@ def _build_approval_tx( # pylint: disable=unused-argument bridge_workflow["execution"] = { "error": receipt.get("status", 0) == 0, "explorer_link": f"https://scan.li.fi/tx/{tx_hash.hex()}", - "message": "", + "message": None, "status": None, "timestamp": int(time.time()), "tx_hash": tx_hash.hex(), "tx_status": receipt.get("status", 0), } - def update_execution_status(self, bridge_workflow: dict) -> None: - """Update the execution status.""" + def update_execution_status(self, bridge_workflow: dict) -> bool: + """Update the execution status. Returns `True` if the status changed.""" if "execution" not in bridge_workflow: raise ValueError( @@ -406,19 +406,34 @@ def update_execution_status(self, bridge_workflow: dict) -> None: tx_hash = execution["tx_hash"] if not tx_hash: - execution["status"] = None - return + if execution["status"] is not None: + execution["status"] = None + return True + return False + + if execution["status"] in ( + LiFiTransactionStatus.DONE, + LiFiTransactionStatus.FAILED, + ): + return False url = "https://li.quest/v1/status" headers = {"accept": "application/json"} params = { "txHash": tx_hash, } + self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") response = requests.get(url=url, headers=headers, params=params, timeout=30) response.raise_for_status() response_json = response.json() status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) - execution["status"] = status + execution["message"] = response_json.get("substatusMessage") + + if execution["status"] != status: + execution["status"] = status + return True + + return False def is_execution_finished(self, bridge_workflow: dict) -> bool: """Check if the execution is finished.""" @@ -428,28 +443,18 @@ def is_execution_finished(self, bridge_workflow: dict) -> bool: "[LI.FI BRIDGE] Cannot update workflow execution: execution not present." ) - execution = bridge_workflow["execution"] + self.update_execution_status(bridge_workflow) - if execution["tx_hash"] is None: - execution["status"] = None - return True + execution = bridge_workflow["execution"] + tx_hash = execution["tx_hash"] - if execution["status"] in ( - LiFiTransactionStatus.DONE, - LiFiTransactionStatus.FAILED, - ): + if not tx_hash: return True - self.update_execution_status(bridge_workflow) - - execution = bridge_workflow["execution"] - if execution["status"] in ( + return execution["status"] in ( LiFiTransactionStatus.DONE, LiFiTransactionStatus.FAILED, - ): - return True - - return False + ) class QuoteBundleStatus(str, enum.Enum): @@ -549,6 +554,9 @@ def __init__( self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) ) + + def _store_data(self) -> None: + self.logger.info("[BRIDGE MANAGER] Storing data to file.") self.data.store() def _get_updated_quote_bundle( @@ -608,7 +616,7 @@ def _get_updated_quote_bundle( ) self.data.last_requested_quote_bundle = quote_bundle - self.data.store() + self._store_data() return quote_bundle @@ -621,8 +629,16 @@ def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: ) initial_status = quote_bundle["status"] + execution_status_changed = [ + self.bridge_provider.update_execution_status(workflow) + for workflow in quote_bundle["bridge_workflows"] + ] + + if any(execution_status_changed): + self._store_data() + is_execution_finished = all( - self.bridge_provider.is_execution_finished(workflow) + self.bridge_provider.update_execution_status(workflow) for workflow in quote_bundle["bridge_workflows"] ) @@ -632,7 +648,7 @@ def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) if initial_status != quote_bundle["status"]: - self.data.store() + self._store_data() def _raise_if_invalid(self, bridge_requests: list) -> None: """Preprocess quote requests.""" @@ -804,15 +820,15 @@ def execute_quote_bundle(self, quote_bundle_id: str) -> dict: ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") + quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) + for bridge_workflow in quote_bundle["bridge_workflows"]: self.bridge_provider.update_with_execution(bridge_workflow) - self.data.store() - - quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) + self._store_data() self.data.last_requested_quote_bundle = None self.data.executed_quote_bundles[quote_bundle["id"]] = quote_bundle - self.data.store() + self._store_data() self._quote_bundle_updated_on_session = False self.logger.info( f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} executed." From e0dfd170371a0ebdf67082bbcebea7516fca0e11 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 16:28:32 +0200 Subject: [PATCH 026/173] chore: refactor with framework methods --- operate/bridge/bridge.py | 69 ++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 86bdc2cb7..47e996da9 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -319,6 +319,15 @@ def update_with_execution(self, bridge_workflow: dict) -> None: from_chain = Chain.from_id(transaction_request["chainId"]) wallet = self.wallet_manager.load(from_chain.ledger_type) + tx_settler = TxSettler( + ledger_api=wallet.ledger_api(from_chain), + crypto=wallet.crypto, + chain_type=from_chain, + timeout=ON_CHAIN_INTERACT_TIMEOUT, + retries=ON_CHAIN_INTERACT_RETRIES, + sleep=ON_CHAIN_INTERACT_SLEEP, + ) + # Bridges from an asset other than native require an approval transaction. if from_token != ZERO_ADDRESS: self.logger.info( @@ -327,15 +336,6 @@ def update_with_execution(self, bridge_workflow: dict) -> None: # TODO Approval is done on several places. Consider exporting to a # higher-level layer (e.g., wallet?) - tx_settler = TxSettler( - ledger_api=wallet.ledger_api(from_chain), - crypto=wallet.crypto, - chain_type=from_chain, - timeout=ON_CHAIN_INTERACT_TIMEOUT, - retries=ON_CHAIN_INTERACT_RETRIES, - sleep=ON_CHAIN_INTERACT_SLEEP, - ) - def _build_approval_tx( # pylint: disable=unused-argument *args: t.Any, **kargs: t.Any ) -> dict: @@ -356,42 +356,43 @@ def _build_approval_tx( # pylint: disable=unused-argument ) self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") - # TODO rewrite with framework methods self.logger.info( f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." ) - private_key = wallet.crypto.private_key - w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) - account = w3.eth.account.from_key(private_key) - - transaction = { - "value": w3.to_wei(int(transaction_request["value"], 16), "wei"), - "to": transaction_request["to"], - "data": bytes.fromhex(transaction_request["data"][2:]), - "from": account.address, - "gasPrice": w3.to_wei(int(transaction_request["gasPrice"], 16), "wei"), - "chainId": transaction_request["chainId"], - "nonce": w3.eth.get_transaction_count(account.address), - } - gas_estimate = w3.eth.estimate_gas(transaction) - transaction["gas"] = gas_estimate - signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) - tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) - self.logger.info(f"[LI.FI BRIDGE] Bridge transaction tx_hash={tx_hash.hex()}.") + def _build_bridge_tx( # pylint: disable=unused-argument + *args: t.Any, **kargs: t.Any + ) -> dict: + w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) + return { + "value": int(transaction_request["value"], 16), + "to": transaction_request["to"], + "data": bytes.fromhex(transaction_request["data"][2:]), + "from": wallet.address, + "chainId": transaction_request["chainId"], + "gasPrice": int(transaction_request["gasPrice"], 16), + "gas": int(transaction_request["gasLimit"], 16), + "nonce": w3.eth.get_transaction_count(wallet.address), + } - # TODO remove? - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + setattr(tx_settler, "build", _build_bridge_tx) # noqa: B010 + tx_receipt = tx_settler.transact( + method=lambda: {}, + contract="", + kwargs={}, + dry_run=False, + ) self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") + tx_hash = tx_receipt.get("transactionHash", "").hex() bridge_workflow["execution"] = { - "error": receipt.get("status", 0) == 0, - "explorer_link": f"https://scan.li.fi/tx/{tx_hash.hex()}", + "error": tx_receipt.get("status", 0) == 0, + "explorer_link": f"https://scan.li.fi/tx/{tx_hash}", "message": None, "status": None, "timestamp": int(time.time()), - "tx_hash": tx_hash.hex(), - "tx_status": receipt.get("status", 0), + "tx_hash": tx_hash, + "tx_status": tx_receipt.get("status", 0), } def update_execution_status(self, bridge_workflow: dict) -> bool: From 1546de715e52a35e17bfe6c9faeb83d6fbbcca1c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 3 Apr 2025 16:34:44 +0200 Subject: [PATCH 027/173] chore: update --- operate/bridge/bridge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 47e996da9..09ecc2786 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -343,7 +343,7 @@ def _build_approval_tx( # pylint: disable=unused-argument ledger_api=wallet.ledger_api(from_chain), contract_address=from_token, spender=transaction_request["to"], - sender=wallet.address, + sender=transaction_request["from"], amount=from_amount, ) @@ -368,11 +368,11 @@ def _build_bridge_tx( # pylint: disable=unused-argument "value": int(transaction_request["value"], 16), "to": transaction_request["to"], "data": bytes.fromhex(transaction_request["data"][2:]), - "from": wallet.address, + "from": transaction_request["from"], "chainId": transaction_request["chainId"], "gasPrice": int(transaction_request["gasPrice"], 16), "gas": int(transaction_request["gasLimit"], 16), - "nonce": w3.eth.get_transaction_count(wallet.address), + "nonce": w3.eth.get_transaction_count(transaction_request["from"]), } setattr(tx_settler, "build", _build_bridge_tx) # noqa: B010 From f67eb6d33857d2b7b0b3f8d1598619c8cab28695 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 4 Apr 2025 12:30:27 +0200 Subject: [PATCH 028/173] chore: added status to all API endpoints --- docs/api.md | 16 +-- operate/bridge/bridge.py | 228 ++++++++++++++++++++++----------------- 2 files changed, 140 insertions(+), 104 deletions(-) diff --git a/docs/api.md b/docs/api.md index 598d4ef89..b7ff01484 100644 --- a/docs/api.md +++ b/docs/api.md @@ -968,14 +968,14 @@ Creates a quote bundle to fulfill the bridge requests and returns }, "expiration_timestamp": 1743000251, "is_refill_required": true, - "quote_response_status": [ + "bridge_request_status": [ { - "error": false, "message": "", + "status": "QUOTE_DONE", }, { - "error": false, "message": "", + "status": "QUOTE_DONE", } ], "error": false @@ -1008,17 +1008,17 @@ Executes a quote bundle. { "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", "status": "SUBMITTED", - "executions": [ + "bridge_request_status": [ { "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", - "message": "", - "status": "DONE", + "message": null, + "status": "EXECUTION_DONE", "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", }, { "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", - "message": "", - "status": "PENDING", + "message": null, + "status": "EXECUTION_PENDING", "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", } ], diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 09ecc2786..b724d5c66 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -58,6 +58,22 @@ DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" +class BridgeWorkflowStatus(str, enum.Enum): + """Bridge workflow status.""" + + WORKFLOW_CREATED = "WORKFLOW_CREATED" + QUOTE_DONE = "QUOTE_DONE" + QUOTE_FAILED = "QUOTE_FAILED" + EXECUTION_PENDING = "EXECUTION_PENDING" + EXECUTION_DONE = "EXECUTION_DONE" + EXECUTION_FAILED = "EXECUTION_FAILED" + EXECUTION_NA = "EXECUTION_NA" + + + def __str__(self) -> str: + """__str__""" + return self.value + class BridgeProvider: """Abstract BridgeProvider.""" @@ -162,7 +178,8 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "elapsed_time": 0, "error": False, "message": "Zero-amount quote requested.", - "status": HTTPStatus.OK, + "response_status": 0, + "status": BridgeWorkflowStatus.QUOTE_DONE, "timestamp": int(time.time()), } return @@ -192,7 +209,8 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "elapsed_time": time.time() - start, "error": False, "message": None, - "status": response.status_code, + "response_status": response.status_code, + "status": BridgeWorkflowStatus.QUOTE_DONE, "timestamp": int(time.time()), } return @@ -206,7 +224,8 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "elapsed_time": time.time() - start, "error": True, "message": str(e), - "status": HTTPStatus.GATEWAY_TIMEOUT, + "response_status": HTTPStatus.GATEWAY_TIMEOUT, + "status": BridgeWorkflowStatus.QUOTE_FAILED, "timestamp": int(time.time()), } except requests.RequestException as e: @@ -220,7 +239,8 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "elapsed_time": time.time() - start, "error": True, "message": response_json.get("message") or str(e), - "status": response.status_code, + "response_status": response.status_code, + "status": BridgeWorkflowStatus.QUOTE_FAILED, "timestamp": int(time.time()), } @@ -291,8 +311,9 @@ def update_with_execution(self, bridge_workflow: dict) -> None: bridge_workflow["execution"] = { "error": True, "explorer_link": None, - "message": "Skipped quote execution (quote error).", - "status": None, + "message": "Skipped execution (quote error).", + "lifi_status": None, + "status": BridgeWorkflowStatus.EXECUTION_DONE, # TODO alternative state? "timestamp": int(time.time()), "tx_hash": None, "tx_status": None, @@ -303,118 +324,128 @@ def update_with_execution(self, bridge_workflow: dict) -> None: bridge_workflow["execution"] = { "error": False, "explorer_link": None, - "message": "Skipped quote execution (empty quote).", - "status": None, + "message": "Skipped execution (empty quote).", + "lifi_status": None, + "status": BridgeWorkflowStatus.EXECUTION_DONE, # TODO alternative state? "timestamp": int(time.time()), "tx_hash": None, "tx_status": None, } return - self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") - from_token = quote["action"]["fromToken"]["address"] - from_amount = int(quote["action"]["fromAmount"]) - - transaction_request = quote["transactionRequest"] - from_chain = Chain.from_id(transaction_request["chainId"]) - wallet = self.wallet_manager.load(from_chain.ledger_type) - - tx_settler = TxSettler( - ledger_api=wallet.ledger_api(from_chain), - crypto=wallet.crypto, - chain_type=from_chain, - timeout=ON_CHAIN_INTERACT_TIMEOUT, - retries=ON_CHAIN_INTERACT_RETRIES, - sleep=ON_CHAIN_INTERACT_SLEEP, - ) + try: + self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") + from_token = quote["action"]["fromToken"]["address"] + from_amount = int(quote["action"]["fromAmount"]) + + transaction_request = quote["transactionRequest"] + from_chain = Chain.from_id(transaction_request["chainId"]) + wallet = self.wallet_manager.load(from_chain.ledger_type) + + tx_settler = TxSettler( + ledger_api=wallet.ledger_api(from_chain), + crypto=wallet.crypto, + chain_type=from_chain, + timeout=ON_CHAIN_INTERACT_TIMEOUT, + retries=ON_CHAIN_INTERACT_RETRIES, + sleep=ON_CHAIN_INTERACT_SLEEP, + ) + + # Bridges from an asset other than native require an approval transaction. + if from_token != ZERO_ADDRESS: + self.logger.info( + f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." + ) + + # TODO Approval is done on several places. Consider exporting to a + # higher-level layer (e.g., wallet?) + def _build_approval_tx( # pylint: disable=unused-argument + *args: t.Any, **kargs: t.Any + ) -> dict: + return registry_contracts.erc20.get_approve_tx( + ledger_api=wallet.ledger_api(from_chain), + contract_address=from_token, + spender=transaction_request["to"], + sender=transaction_request["from"], + amount=from_amount, + ) + + setattr(tx_settler, "build", _build_approval_tx) # noqa: B010 + tx_settler.transact( + method=lambda: {}, + contract="", + kwargs={}, + dry_run=False, + ) + self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") - # Bridges from an asset other than native require an approval transaction. - if from_token != ZERO_ADDRESS: self.logger.info( - f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." + f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." ) - # TODO Approval is done on several places. Consider exporting to a - # higher-level layer (e.g., wallet?) - def _build_approval_tx( # pylint: disable=unused-argument + def _build_bridge_tx( # pylint: disable=unused-argument *args: t.Any, **kargs: t.Any ) -> dict: - return registry_contracts.erc20.get_approve_tx( - ledger_api=wallet.ledger_api(from_chain), - contract_address=from_token, - spender=transaction_request["to"], - sender=transaction_request["from"], - amount=from_amount, - ) + w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) + return { + "value": int(transaction_request["value"], 16), + "to": transaction_request["to"], + "data": bytes.fromhex(transaction_request["data"][2:]), + "from": transaction_request["from"], + "chainId": transaction_request["chainId"], + "gasPrice": int(transaction_request["gasPrice"], 16), + "gas": int(transaction_request["gasLimit"], 16), + "nonce": w3.eth.get_transaction_count(transaction_request["from"]), + } - setattr(tx_settler, "build", _build_approval_tx) # noqa: B010 - tx_settler.transact( + setattr(tx_settler, "build", _build_bridge_tx) # noqa: B010 + tx_receipt = tx_settler.transact( method=lambda: {}, contract="", kwargs={}, dry_run=False, ) - self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") - - self.logger.info( - f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." - ) + self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") + tx_hash = tx_receipt.get("transactionHash", "").hex() - def _build_bridge_tx( # pylint: disable=unused-argument - *args: t.Any, **kargs: t.Any - ) -> dict: - w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) - return { - "value": int(transaction_request["value"], 16), - "to": transaction_request["to"], - "data": bytes.fromhex(transaction_request["data"][2:]), - "from": transaction_request["from"], - "chainId": transaction_request["chainId"], - "gasPrice": int(transaction_request["gasPrice"], 16), - "gas": int(transaction_request["gasLimit"], 16), - "nonce": w3.eth.get_transaction_count(transaction_request["from"]), + bridge_workflow["execution"] = { + "error": tx_receipt.get("status", 0) == 0, + "explorer_link": f"https://scan.li.fi/tx/{tx_hash}", + "message": None, + "lifi_status": LiFiTransactionStatus.NOT_FOUND, + "status": BridgeWorkflowStatus.EXECUTION_PENDING if tx_hash else BridgeWorkflowStatus.EXECUTION_FAILED, + "timestamp": int(time.time()), + "tx_hash": tx_hash, + "tx_status": tx_receipt.get("status", 0), + } + except Exception as e: # pylint: disable=broad-except + bridge_workflow["execution"] = { + "error": True, + "explorer_link": None, + "message": f"Error executing quote: {str(e)}", + "lifi_status": LiFiTransactionStatus.FAILED, + "status": BridgeWorkflowStatus.EXECUTION_FAILED, + "timestamp": int(time.time()), + "tx_hash": None, + "tx_status": None, } - - setattr(tx_settler, "build", _build_bridge_tx) # noqa: B010 - tx_receipt = tx_settler.transact( - method=lambda: {}, - contract="", - kwargs={}, - dry_run=False, - ) - self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") - tx_hash = tx_receipt.get("transactionHash", "").hex() - - bridge_workflow["execution"] = { - "error": tx_receipt.get("status", 0) == 0, - "explorer_link": f"https://scan.li.fi/tx/{tx_hash}", - "message": None, - "status": None, - "timestamp": int(time.time()), - "tx_hash": tx_hash, - "tx_status": tx_receipt.get("status", 0), - } def update_execution_status(self, bridge_workflow: dict) -> bool: """Update the execution status. Returns `True` if the status changed.""" + if "execution" not in bridge_workflow: - raise ValueError( - "[LI.FI BRIDGE] Cannot update workflow execution: execution not present." - ) + return False execution = bridge_workflow["execution"] tx_hash = execution["tx_hash"] - if not tx_hash: - if execution["status"] is not None: - execution["status"] = None - return True - return False + print("update_execution_status") + print(f"{execution=}") if execution["status"] in ( - LiFiTransactionStatus.DONE, - LiFiTransactionStatus.FAILED, + BridgeWorkflowStatus.EXECUTION_DONE, + BridgeWorkflowStatus.EXECUTION_FAILED, ): return False @@ -427,11 +458,16 @@ def update_execution_status(self, bridge_workflow: dict) -> bool: response = requests.get(url=url, headers=headers, params=params, timeout=30) response.raise_for_status() response_json = response.json() - status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) + lifi_status = response_json.get("lifi_status", str(LiFiTransactionStatus.UNKNOWN)) execution["message"] = response_json.get("substatusMessage") - if execution["status"] != status: - execution["status"] = status + if execution["lifi_status"] != lifi_status: + execution["lifi_status"] = lifi_status + if lifi_status in ( + LiFiTransactionStatus.DONE, + LiFiTransactionStatus.FAILED, + ): + execution["status"] = BridgeWorkflowStatus.EXECUTION_DONE return True return False @@ -639,7 +675,7 @@ def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: self._store_data() is_execution_finished = all( - self.bridge_provider.update_execution_status(workflow) + self.bridge_provider.is_execution_finished(workflow) for workflow in quote_bundle["bridge_workflows"] ) @@ -772,10 +808,10 @@ def bridge_refill_requirements( for amount in from_tokens.values() ) - quote_response_status = [ + bridge_request_status = [ { "message": response["quote"]["message"], - "error": response["quote"]["error"], + "status": response["quote"]["status"], } for response in quote_bundle["bridge_workflows"] ] @@ -791,7 +827,7 @@ def bridge_refill_requirements( "bridge_refill_requirements": bridge_refill_requirements, "expiration_timestamp": quote_bundle["expiration_timestamp"], "is_refill_required": is_refill_required, - "quote_response_status": quote_response_status, + "bridge_request_status": bridge_request_status, "error": error, } @@ -849,7 +885,7 @@ def get_execution_status(self, quote_bundle_id: str) -> dict: self._update_quote_bundle_status(quote_bundle_id) - executions = [ + bridge_request_status = [ { "explorer_link": workflow["execution"]["explorer_link"], "message": workflow["execution"]["message"], @@ -866,6 +902,6 @@ def get_execution_status(self, quote_bundle_id: str) -> dict: return { "id": quote_bundle["id"], "status": quote_bundle["status"], - "executions": executions, + "bridge_request_status": bridge_request_status, "error": error, } From 6924d4d2aae813bee84754c22c910a72cf008b62 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 5 Apr 2025 21:07:19 +0200 Subject: [PATCH 029/173] chore: refactor --- operate/bridge/bridge.py | 94 ++++++++++++++++++++++++++++++---------- operate/resource.py | 13 ++++++ 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index b724d5c66..a39f8eb78 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -58,6 +58,7 @@ DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 QUOTE_BUNDLE_PREFIX = "qb-" + class BridgeWorkflowStatus(str, enum.Enum): """Bridge workflow status.""" @@ -75,6 +76,57 @@ def __str__(self) -> str: return self.value + +@dataclass +class RequestData(LocalResource): + from_chain: Chain + from_address: str + from_token: str + to_chain: Chain + to_address: str + to_token: str + to_amount: int + + +@dataclass +class QuoteData(LocalResource): + """Bridge quote data.""" + + attempts: int = 0 + elapsed_time: int = 0 + error: bool = False + message: str | None = None + response: dict | None = None + status: int = 0 + timestamp: int = int(time.time()) + + +@dataclass +class ExecutionData(LocalResource): + "Bridge execution data." + + error: bool = False + explorer_link: str | None = None + message: str | None = None + status: str | None = None + timestamp: int = int(time.time()) + tx_hash: str | None = None + tx_status: str | None = None + + +@dataclass +class BridgeWorkflowData(LocalResource): + """Bridge request""" + path: Path + quote: QuoteData | None + _file = "bridge_workflow.json" + + request: RequestData + id: str = f"{uuid.uuid4()}" + status: BridgeWorkflowStatus = BridgeWorkflowStatus.WORKFLOW_CREATED + execution: ExecutionData | None = None + + class BridgeProvider: """Abstract BridgeProvider.""" @@ -101,25 +153,6 @@ def get_quote_requirements(self, bridge_workflow: dict) -> dict: """Get bridge requirements for a single quote.""" raise NotImplementedError() - def sum_quotes_requirements(self, bridge_workflows: list) -> dict: - """Get bridge requirements for a list of quotes.""" - - bridge_total_requirements: dict = {} - - for workflow in bridge_workflows: - req = self.get_quote_requirements(workflow) - for from_chain, from_addresses in req.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - bridge_total_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - ).setdefault(from_token, 0) - bridge_total_requirements[from_chain][from_address][ - from_token - ] += from_amount - - return bridge_total_requirements - @abstractmethod def update_with_execution(self, bridge_workflow: dict) -> None: """Execute the quote.""" @@ -596,6 +629,25 @@ def _store_data(self) -> None: self.logger.info("[BRIDGE MANAGER] Storing data to file.") self.data.store() + def _sum_quotes_requirements(self, bridge_workflows: list) -> dict: + """Get bridge requirements for a list of quotes.""" + + bridge_total_requirements: dict = {} + + for workflow in bridge_workflows: + req = self.bridge_provider.get_quote_requirements(workflow) + for from_chain, from_addresses in req.items(): + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + bridge_total_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ).setdefault(from_token, 0) + bridge_total_requirements[from_chain][from_address][ + from_token + ] += from_amount + + return bridge_total_requirements + def _get_updated_quote_bundle( self, bridge_requests: list, force_update: bool ) -> dict: @@ -648,9 +700,7 @@ def _get_updated_quote_bundle( quote_bundle["status"] = str(QuoteBundleStatus.QUOTED) quote_bundle[ "bridge_total_requirements" - ] = self.bridge_provider.sum_quotes_requirements( - quote_bundle["bridge_workflows"] - ) + ] = self._sum_quotes_requirements(quote_bundle["bridge_workflows"]) self.data.last_requested_quote_bundle = quote_bundle self._store_data() diff --git a/operate/resource.py b/operate/resource.py index b0ddc6909..390e159da 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -21,6 +21,7 @@ import enum import json +import types import typing as t from dataclasses import asdict, is_dataclass from pathlib import Path @@ -46,6 +47,18 @@ def serialize(obj: t.Any) -> t.Any: def deserialize(obj: t.Any, otype: t.Any) -> t.Any: """Desrialize a json object.""" + + if obj is None: + return None + + if getattr(otype, '__origin__', None) is t.Union or type(otype) == types.UnionType: + for arg in otype.__args__: + try: + return deserialize(obj, arg) + except Exception: + continue + return None + base = getattr(otype, "__class__") # noqa: B009 if base.__name__ == "_GenericAlias": # type: ignore args = otype.__args__ # type: ignore From 0ee280bcd7be6b02adc66e347954ab57afc1d3aa Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 5 Apr 2025 21:09:44 +0200 Subject: [PATCH 030/173] chore: revert --- operate/bridge/bridge.py | 51 ---------------------------------------- operate/resource.py | 12 ---------- 2 files changed, 63 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index a39f8eb78..74cbaa04b 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -76,57 +76,6 @@ def __str__(self) -> str: return self.value - -@dataclass -class RequestData(LocalResource): - from_chain: Chain - from_address: str - from_token: str - to_chain: Chain - to_address: str - to_token: str - to_amount: int - - -@dataclass -class QuoteData(LocalResource): - """Bridge quote data.""" - - attempts: int = 0 - elapsed_time: int = 0 - error: bool = False - message: str | None = None - response: dict | None = None - status: int = 0 - timestamp: int = int(time.time()) - - -@dataclass -class ExecutionData(LocalResource): - "Bridge execution data." - - error: bool = False - explorer_link: str | None = None - message: str | None = None - status: str | None = None - timestamp: int = int(time.time()) - tx_hash: str | None = None - tx_status: str | None = None - - -@dataclass -class BridgeWorkflowData(LocalResource): - """Bridge request""" - path: Path - quote: QuoteData | None - _file = "bridge_workflow.json" - - request: RequestData - id: str = f"{uuid.uuid4()}" - status: BridgeWorkflowStatus = BridgeWorkflowStatus.WORKFLOW_CREATED - execution: ExecutionData | None = None - - class BridgeProvider: """Abstract BridgeProvider.""" diff --git a/operate/resource.py b/operate/resource.py index 390e159da..1d7f7981b 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -21,7 +21,6 @@ import enum import json -import types import typing as t from dataclasses import asdict, is_dataclass from pathlib import Path @@ -48,17 +47,6 @@ def serialize(obj: t.Any) -> t.Any: def deserialize(obj: t.Any, otype: t.Any) -> t.Any: """Desrialize a json object.""" - if obj is None: - return None - - if getattr(otype, '__origin__', None) is t.Union or type(otype) == types.UnionType: - for arg in otype.__args__: - try: - return deserialize(obj, arg) - except Exception: - continue - return None - base = getattr(otype, "__class__") # noqa: B009 if base.__name__ == "_GenericAlias": # type: ignore args = otype.__args__ # type: ignore From ca55aea49a0081108fc0ad4796a2d9491fec7604 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 15 Apr 2025 17:39:38 +0200 Subject: [PATCH 031/173] fix: resource --- operate/bridge/bridge.py | 18 ------------------ operate/resource.py | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 74cbaa04b..1491d0690 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -500,24 +500,6 @@ class BridgeManagerData(LocalResource): _file = "bridge.json" - # TODO Migrate to LocalResource? - def store(self) -> None: - """Store local resource.""" - - file_path = self.path / self._file - backup_path = file_path.with_name(file_path.name + ".bak") - - try: - if file_path.exists(): - json.loads(file_path.read_text(encoding="utf-8")) - if backup_path.exists(): - backup_path.unlink() - shutil.copy2(file_path, backup_path) - except json.JSONDecodeError: - pass - - super().store() - # TODO Migrate to LocalResource? # It can be inconvenient that all local resources create an empty resource # if the file is corrupted. For example, if a service configuration is diff --git a/operate/resource.py b/operate/resource.py index 3300c5927..926da7aa6 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -130,7 +130,7 @@ def store(self) -> None: encoding="utf-8", ) - self.load(path) # Validate before making backup + self.load(self.path) # Validate before making backup for i in reversed(range(N_BACKUPS - 1)): older = path.with_name(f"{path.name}.bak.{i}") From f60acac59ff1732bf622f4242f5bc01dee212006 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 15 Apr 2025 18:23:06 +0200 Subject: [PATCH 032/173] doc: update doc --- docs/api.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/api.md b/docs/api.md index b7ff01484..3d1cd56b2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -988,7 +988,7 @@ Creates a quote bundle to fulfill the bridge requests and returns ### `POST /api/bridge/execute` -Executes a quote bundle. +Executes a quote bundle. See [GET /api/bridge/status/{quote_bundle_id}](#get-apibridgestatusquote_bundle_id) for status values.
Request @@ -1039,24 +1039,32 @@ Gets the status of a quote bundle. The attribute `status` can take the following - `SUBMITTED`: The quote bundle has been submitted for execution. - `FINISHED`: All the quote executions in the bundle have reached their final state (either done or failed). No more updates are expected for this quote bundle. +Individual bridge request status: + +- `QUOTE_DONE`: A quote is available. +- `QUOTE_FAILED`: Failed to request a quote. +- `EXECUTION_PENDING`: Execution submitted and pending to be finalized. +- `EXECUTION_DONE`: Execution finalized successfully. +- `EXECUTION_FAILED`: Execution failed. +
Response ```json { "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", - "status": "FINISHED", - "executions": [ + "status": "SUBMITTED", + "bridge_request_status": [ { "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", - "message": "", - "status": "DONE", + "message": null, + "status": "EXECUTION_DONE", "tx_hash": "0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", }, { "explorer_link": "https://scan.li.fi/tx/0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", - "message": "", - "status": "DONE", + "message": null, + "status": "EXECUTION_PENDING", "tx_hash": "0x0e53f1b6aa5552f2d4cfe8e623dd95e54ca079c4b23b89d0c0aa6ed4a6442384", } ], From a2889392301b87e330e0bb8cbfceb046e5533fcf Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 16 Apr 2025 10:19:13 +0200 Subject: [PATCH 033/173] chore: update --- operate/bridge/bridge.py | 406 ++++++++++++++++++++++----------------- operate/cli.py | 6 +- 2 files changed, 234 insertions(+), 178 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 1491d0690..e9adeb588 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -56,13 +56,15 @@ DEFAULT_MAX_RETRIES = 3 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 -QUOTE_BUNDLE_PREFIX = "qb-" +BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" +BRIDGE_REQUEST_PREFIX = "br-" -class BridgeWorkflowStatus(str, enum.Enum): - """Bridge workflow status.""" - WORKFLOW_CREATED = "WORKFLOW_CREATED" +class BridgeRequestStatus(str, enum.Enum): + """Bridge request status.""" + + REQUEST_CREATED = "REQUEST_CREATED" QUOTE_DONE = "QUOTE_DONE" QUOTE_FAILED = "QUOTE_FAILED" EXECUTION_PENDING = "EXECUTION_PENDING" @@ -75,6 +77,15 @@ def __str__(self) -> str: """__str__""" return self.value +@dataclass +class BridgeRequest(LocalResource): + """BridgeRequest.""" + + request: dict + id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" + status: BridgeRequestStatus = BridgeRequestStatus.REQUEST_CREATED + quote: dict | None = None + execution: dict | None = None class BridgeProvider: """Abstract BridgeProvider.""" @@ -93,27 +104,27 @@ def name(self) -> str: return self.__class__.__name__ @abstractmethod - def update_with_quote(self, bridge_workflow: dict) -> None: + def update_with_quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" raise NotImplementedError() @abstractmethod - def get_quote_requirements(self, bridge_workflow: dict) -> dict: + def get_quote_requirements(self, bridge_request: BridgeRequest) -> dict: """Get bridge requirements for a single quote.""" raise NotImplementedError() @abstractmethod - def update_with_execution(self, bridge_workflow: dict) -> None: + def update_with_execution(self, bridge_request: dict) -> None: """Execute the quote.""" raise NotImplementedError() @abstractmethod - def update_execution_status(self, bridge_workflow: dict) -> bool: + def update_execution_status(self, bridge_request: dict) -> bool: """Update the execution status.""" raise NotImplementedError() @abstractmethod - def is_execution_finished(self, bridge_workflow: dict) -> bool: + def is_execution_finished(self, bridge_request: dict) -> bool: """Check if the execution is finished.""" raise NotImplementedError() @@ -136,34 +147,28 @@ def __str__(self) -> str: class LiFiBridgeProvider(BridgeProvider): """LI.FI Bridge provider.""" - def update_with_quote(self, bridge_workflow: dict) -> None: + def update_with_quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" - if "execution" in bridge_workflow: + if bridge_request.execution: raise ValueError( - f"[LI.FI BRIDGE] Cannot update workflow {bridge_workflow['id']} with quote: execution already present." + f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id} with quote: execution already present." ) - from_chain = bridge_workflow["request"]["from"]["chain"] - from_address = bridge_workflow["request"]["from"]["address"] - from_token = bridge_workflow["request"]["from"]["token"] - to_chain = bridge_workflow["request"]["to"]["chain"] - to_address = bridge_workflow["request"]["to"]["address"] - to_token = bridge_workflow["request"]["to"]["token"] - to_amount = bridge_workflow["request"]["to"]["amount"] + from_chain = bridge_request.request["from"]["chain"] + from_address = bridge_request.request["from"]["address"] + from_token = bridge_request.request["from"]["token"] + to_chain = bridge_request.request["to"]["chain"] + to_address = bridge_request.request["to"]["address"] + to_token = bridge_request.request["to"]["token"] + to_amount = bridge_request.request["to"]["amount"] if to_amount == 0: self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested.") - bridge_workflow["quote"] = { - "response": {}, - "attempts": 0, - "elapsed_time": 0, - "error": False, - "message": "Zero-amount quote requested.", - "response_status": 0, - "status": BridgeWorkflowStatus.QUOTE_DONE, - "timestamp": int(time.time()), - } + bridge_request.quote = new_quote( + message="Zero-amount quote requested.", + ) + bridge_request.status = BridgeRequestStatus.QUOTE_DONE return url = "https://li.quest/v1/quote/toAmount" @@ -185,16 +190,16 @@ def update_with_quote(self, bridge_workflow: dict) -> None: url=url, headers=headers, params=params, timeout=30 ) response.raise_for_status() - bridge_workflow["quote"] = { + bridge_request.quote = { "response": response.json(), "attempts": attempt, "elapsed_time": time.time() - start, "error": False, "message": None, "response_status": response.status_code, - "status": BridgeWorkflowStatus.QUOTE_DONE, "timestamp": int(time.time()), } + bridge_request.status = BridgeRequestStatus.QUOTE_DONE return except requests.Timeout as e: self.logger.warning( @@ -207,9 +212,9 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "error": True, "message": str(e), "response_status": HTTPStatus.GATEWAY_TIMEOUT, - "status": BridgeWorkflowStatus.QUOTE_FAILED, "timestamp": int(time.time()), } + bridge_request.status = BridgeRequestStatus.QUOTE_FAILED except requests.RequestException as e: self.logger.warning( f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." @@ -222,33 +227,31 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "error": True, "message": response_json.get("message") or str(e), "response_status": response.status_code, - "status": BridgeWorkflowStatus.QUOTE_FAILED, "timestamp": int(time.time()), } + bridge_request.status = BridgeRequestStatus.QUOTE_FAILED if attempt >= DEFAULT_MAX_RETRIES: self.logger.error( f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_RETRIES} attempts." ) - bridge_workflow["quote"] = output + bridge_request.quote = output return time.sleep(2) # TODO gas fees ! - def get_quote_requirements(self, bridge_workflow: dict) -> dict: + def get_quote_requirements(self, bridge_request: BridgeRequest) -> dict: """Get bridge requirements for a quote.""" - quote = bridge_workflow["quote"]["response"] - error = bridge_workflow["quote"].get("error", False) - - if error or "action" not in quote: - from_chain = bridge_workflow["request"]["from"]["chain"] - from_address = bridge_workflow["request"]["from"]["address"] - from_token = bridge_workflow["request"]["from"]["token"] + if not bridge_request.quote or not bridge_request.quote["response"] or not "action" in bridge_request.quote["response"]: + from_chain = bridge_request.request["from"]["chain"] + from_address = bridge_request.request["from"]["address"] + from_token = bridge_request.request["from"]["token"] from_amount = 0 transaction_value = 0 else: + quote = bridge_request.quote["response"] from_chain = Chain.from_id(quote["action"]["fromChainId"]).value from_address = quote["action"]["fromAddress"] from_token = quote["action"]["fromToken"]["address"] @@ -271,48 +274,48 @@ def get_quote_requirements(self, bridge_workflow: dict) -> dict: } } - def update_with_execution(self, bridge_workflow: dict) -> None: + def update_with_execution(self, bridge_request: dict) -> None: """Execute the quote.""" - if "quote" not in bridge_workflow: + if "quote" not in bridge_request: raise ValueError( - "[LI.FI BRIDGE] Cannot update workflow with execution: quote not present." + "[LI.FI BRIDGE] Cannot update bridge request with execution: quote not present." ) - if "execution" in bridge_workflow: + if "execution" in bridge_request: self.logger.warning( - f"[LI.FI BRIDGE] Execution already present on bridge workflow {bridge_workflow['id']}." + f"[LI.FI BRIDGE] Execution already present on bridge request {bridge_request['id']}." ) return - quote = bridge_workflow["quote"]["response"] - error = bridge_workflow["quote"].get("error", False) + quote = bridge_request["quote"]["response"] + error = bridge_request["quote"].get("error", False) if error: self.logger.info("[LI.FI BRIDGE] Skipping quote execution (quote error).") - bridge_workflow["execution"] = { + bridge_request["execution"] = { "error": True, "explorer_link": None, "message": "Skipped execution (quote error).", "lifi_status": None, - "status": BridgeWorkflowStatus.EXECUTION_DONE, # TODO alternative state? "timestamp": int(time.time()), "tx_hash": None, "tx_status": None, } + bridge_request["status"] = BridgeRequestStatus.EXECUTION_DONE # TODO alternative state? return if not quote: self.logger.info("[LI.FI BRIDGE] Skipping quote execution (empty quote).") - bridge_workflow["execution"] = { + bridge_request["execution"] = { "error": False, "explorer_link": None, "message": "Skipped execution (empty quote).", "lifi_status": None, - "status": BridgeWorkflowStatus.EXECUTION_DONE, # TODO alternative state? "timestamp": int(time.time()), "tx_hash": None, "tx_status": None, } + bridge_request["status"] = BridgeRequestStatus.EXECUTION_DONE # TODO alternative state? return try: @@ -390,44 +393,46 @@ def _build_bridge_tx( # pylint: disable=unused-argument self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") tx_hash = tx_receipt.get("transactionHash", "").hex() - bridge_workflow["execution"] = { + bridge_request["execution"] = { "error": tx_receipt.get("status", 0) == 0, "explorer_link": f"https://scan.li.fi/tx/{tx_hash}", "message": None, "lifi_status": LiFiTransactionStatus.NOT_FOUND, - "status": BridgeWorkflowStatus.EXECUTION_PENDING if tx_hash else BridgeWorkflowStatus.EXECUTION_FAILED, "timestamp": int(time.time()), "tx_hash": tx_hash, "tx_status": tx_receipt.get("status", 0), } + bridge_request["status"] = BridgeRequestStatus.EXECUTION_PENDING if tx_hash else BridgeRequestStatus.EXECUTION_FAILED + except Exception as e: # pylint: disable=broad-except - bridge_workflow["execution"] = { + bridge_request["execution"] = { "error": True, "explorer_link": None, "message": f"Error executing quote: {str(e)}", "lifi_status": LiFiTransactionStatus.FAILED, - "status": BridgeWorkflowStatus.EXECUTION_FAILED, + "status": BridgeRequestStatus.EXECUTION_FAILED, "timestamp": int(time.time()), "tx_hash": None, "tx_status": None, } + bridge_request["status"] = BridgeRequestStatus.EXECUTION_FAILED - def update_execution_status(self, bridge_workflow: dict) -> bool: + def update_execution_status(self, bridge_request: dict) -> bool: """Update the execution status. Returns `True` if the status changed.""" - if "execution" not in bridge_workflow: + if "execution" not in bridge_request: return False - execution = bridge_workflow["execution"] + execution = bridge_request["execution"] tx_hash = execution["tx_hash"] print("update_execution_status") print(f"{execution=}") if execution["status"] in ( - BridgeWorkflowStatus.EXECUTION_DONE, - BridgeWorkflowStatus.EXECUTION_FAILED, + BridgeRequestStatus.EXECUTION_DONE, + BridgeRequestStatus.EXECUTION_FAILED, ): return False @@ -449,22 +454,22 @@ def update_execution_status(self, bridge_workflow: dict) -> bool: LiFiTransactionStatus.DONE, LiFiTransactionStatus.FAILED, ): - execution["status"] = BridgeWorkflowStatus.EXECUTION_DONE + execution["status"] = BridgeRequestStatus.EXECUTION_DONE return True return False - def is_execution_finished(self, bridge_workflow: dict) -> bool: + def is_execution_finished(self, bridge_request: dict) -> bool: """Check if the execution is finished.""" - if "execution" not in bridge_workflow: + if "execution" not in bridge_request: raise ValueError( - "[LI.FI BRIDGE] Cannot update workflow execution: execution not present." + "[LI.FI BRIDGE] Cannot update bridge request execution: execution not present." ) - self.update_execution_status(bridge_workflow) + self.update_execution_status(bridge_request) - execution = bridge_workflow["execution"] + execution = bridge_request["execution"] tx_hash = execution["tx_hash"] if not tx_hash: @@ -476,8 +481,8 @@ def is_execution_finished(self, bridge_workflow: dict) -> bool: ) -class QuoteBundleStatus(str, enum.Enum): - """Quote bundle status.""" +class BridgeRequestBundleStatus(str, enum.Enum): + """Bridge request bundle status.""" CREATED = "CREATED" QUOTED = "QUOTED" @@ -495,8 +500,8 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 - last_requested_quote_bundle: dict | None = None - executed_quote_bundles: dict = field(default_factory=dict) + last_requested_bundle: dict[str, BridgeRequest] | None = None + executed_bundles: dict = field(default_factory=dict) _file = "bridge.json" @@ -525,12 +530,63 @@ def load(cls, path: Path) -> "LocalResource": return super().load(path) +def new_bridge_request(request: dict) -> dict: + """Create a new bridge request.""" + return { + "id": f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", + "status": BridgeRequestStatus.REQUEST_CREATED, + "request": request, + "quote": None, + "execution": None, + } + + +def new_quote( # pylint: disable=too-many-positional-arguments + attempts: int = 0, + elapsed_time: int = 0, + message: str | None = None, + response: dict | None = None, + response_status: int = 0, + timestamp: int = int(time.time()), +) -> dict: + """Create a new quote.""" + return { + "attempts": attempts, + "elapsed_time": elapsed_time, + "message": message, + "response": response, + "response_status": response_status, + "timestamp": timestamp, + } + + +def new_execution( # pylint: disable=too-many-positional-arguments + bridge_status: enum.Enum | None = None, + elapsed_time: int = 0, + explorer_link: str | None = None, + message: str | None = None, + timestamp: int = int(time.time()), + tx_hash: str | None = None, + tx_status: str | None = None, +) -> dict: + """Create a new execution.""" + return { + "bridge_status": bridge_status, + "elapsed_time": elapsed_time, + "explorer_link": explorer_link, + "message": message, + "timestamp": timestamp, + "tx_hash": tx_hash, + "tx_status": tx_status, + } + + class BridgeManager: """BridgeManager""" # TODO singleton - _quote_bundle_updated_on_session = False + _bundle_updated_on_session = False def __init__( self, @@ -560,13 +616,13 @@ def _store_data(self) -> None: self.logger.info("[BRIDGE MANAGER] Storing data to file.") self.data.store() - def _sum_quotes_requirements(self, bridge_workflows: list) -> dict: + def _sum_quotes_requirements(self, bridge_requests: list) -> dict: """Get bridge requirements for a list of quotes.""" bridge_total_requirements: dict = {} - for workflow in bridge_workflows: - req = self.bridge_provider.get_quote_requirements(workflow) + for request in bridge_requests: + req = self.bridge_provider.get_quote_requirements(request) for from_chain, from_addresses in req.items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): @@ -579,93 +635,93 @@ def _sum_quotes_requirements(self, bridge_workflows: list) -> dict: return bridge_total_requirements - def _get_updated_quote_bundle( - self, bridge_requests: list, force_update: bool + def _get_updated_bundle( + self, user_bridge_requests: list, force_update: bool ) -> dict: - """Ensures to return a valid (non expired) quote bundle for the given inputs.""" + """Ensures to return a valid (non expired) bundle for the given inputs.""" now = int(time.time()) - quote_bundle = self.data.last_requested_quote_bundle or {} - quote_bundle_id = quote_bundle.get("id") - create_new_quote_bundle = False - - if not quote_bundle: - self.logger.info("[BRIDGE MANAGER] No last quote bundle.") - create_new_quote_bundle = True - quote_bundle_id = None - elif quote_bundle.get("status") not in ( - QuoteBundleStatus.CREATED, - QuoteBundleStatus.QUOTED, + bundle = self.data.last_requested_bundle or {} + bundle_id = bundle.get("id") + create_new_bundle = False + + if not bundle: + self.logger.info("[BRIDGE MANAGER] No last bundle.") + create_new_bundle = True + bundle_id = None + elif bundle.get("status") not in ( + BridgeRequestBundleStatus.CREATED, + BridgeRequestBundleStatus.QUOTED, ): - raise RuntimeError("[BRIDGE MANAGER] Quote bundle inconsistent status.") - elif DeepDiff(bridge_requests, quote_bundle.get("bridge_requests", [])): + raise RuntimeError("[BRIDGE MANAGER] Bundle inconsistent status.") + elif DeepDiff(user_bridge_requests, bundle.get("bridge_requests", [])): self.logger.info("[BRIDGE MANAGER] Different quote requests.") - create_new_quote_bundle = True - quote_bundle_id = None + create_new_bundle = True + bundle_id = None elif force_update: self.logger.info("[BRIDGE MANAGER] Force quote update.") - create_new_quote_bundle = True - elif now > quote_bundle.get("timestamp", 0) + self.quote_validity_period: - self.logger.info("[BRIDGE MANAGER] Quote bundle expired.") - create_new_quote_bundle = True - - if create_new_quote_bundle: - self.logger.info("[BRIDGE MANAGER] Requesting new quote bundle.") - quote_bundle = {} - quote_bundle["id"] = ( - quote_bundle_id or f"{QUOTE_BUNDLE_PREFIX}{uuid.uuid4()}" + create_new_bundle = True + elif now > bundle.get("timestamp", 0) + self.quote_validity_period: + self.logger.info("[BRIDGE MANAGER] Bundle expired.") + create_new_bundle = True + + if create_new_bundle: + self.logger.info("[BRIDGE MANAGER] Requesting new bundle.") + bundle = {} + bundle["id"] = ( + bundle_id or f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}" ) - quote_bundle["bridge_provider"] = self.bridge_provider.name() - quote_bundle["status"] = str(QuoteBundleStatus.CREATED) - quote_bundle["bridge_requests"] = bridge_requests - quote_bundle["bridge_workflows"] = [ - {"id": f"{uuid.uuid4()}", "request": request} - for request in bridge_requests + bundle["bridge_provider"] = self.bridge_provider.name() + bundle["status"] = str(BridgeRequestBundleStatus.CREATED) + bundle["user_bridge_requests"] = user_bridge_requests + bundle["bridge_requests"] = [ + BridgeRequest(request=request) + for request in user_bridge_requests ] - quote_bundle["timestamp"] = now - quote_bundle["expiration_timestamp"] = now + self.quote_validity_period + bundle["timestamp"] = now + bundle["expiration_timestamp"] = now + self.quote_validity_period - for workflow in quote_bundle["bridge_workflows"]: - self.bridge_provider.update_with_quote(workflow) + for request in bundle["bridge_requests"]: + self.bridge_provider.update_with_quote(request) - quote_bundle["status"] = str(QuoteBundleStatus.QUOTED) - quote_bundle[ + bundle["status"] = str(BridgeRequestBundleStatus.QUOTED) + bundle[ "bridge_total_requirements" - ] = self._sum_quotes_requirements(quote_bundle["bridge_workflows"]) + ] = self._sum_quotes_requirements(bundle["bridge_requests"]) - self.data.last_requested_quote_bundle = quote_bundle + self.data.last_requested_bundle = bundle self._store_data() - return quote_bundle + return bundle - def _update_quote_bundle_status(self, quote_bundle_id: str) -> None: - quote_bundle = self.data.executed_quote_bundles.get(quote_bundle_id) + def _update_bundle_status(self, bundle_id: str) -> None: + bundle = self.data.executed_bundles.get(bundle_id) - if not quote_bundle: + if not bundle: raise ValueError( - f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} not found." + f"[BRIDGE MANAGER] Bundle id {bundle_id} not found." ) - initial_status = quote_bundle["status"] + initial_status = bundle["status"] execution_status_changed = [ - self.bridge_provider.update_execution_status(workflow) - for workflow in quote_bundle["bridge_workflows"] + self.bridge_provider.update_execution_status(request) + for request in bundle["bridge_requests"] ] if any(execution_status_changed): self._store_data() is_execution_finished = all( - self.bridge_provider.is_execution_finished(workflow) - for workflow in quote_bundle["bridge_workflows"] + self.bridge_provider.is_execution_finished(request) + for request in bundle["bridge_requests"] ) if is_execution_finished: - quote_bundle["status"] = str(QuoteBundleStatus.FINISHED) + bundle["status"] = str(BridgeRequestBundleStatus.FINISHED) else: - quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) + bundle["status"] = str(BridgeRequestBundleStatus.SUBMITTED) - if initial_status != quote_bundle["status"]: + if initial_status != bundle["status"]: self._store_data() def _raise_if_invalid(self, bridge_requests: list) -> None: @@ -745,7 +801,7 @@ def bridge_refill_requirements( f"[BRIDGE MANAGER] Quote requests count: {len(bridge_requests)}." ) - quote_bundle = self._get_updated_quote_bundle(bridge_requests, force_update) + bundle = self._get_updated_bundle(bridge_requests, force_update) chains = [request["from"]["chain"] for request in bridge_requests] balances = {} @@ -769,7 +825,7 @@ def bridge_refill_requirements( ) bridge_refill_requirements: dict = {} - for from_chain, from_addresses in quote_bundle[ + for from_chain, from_addresses in bundle[ "bridge_total_requirements" ].items(): for from_address, from_tokens in from_addresses.items(): @@ -791,98 +847,98 @@ def bridge_refill_requirements( bridge_request_status = [ { - "message": response["quote"]["message"], - "status": response["quote"]["status"], + "message": request.quote["message"], + "status": request.status, } - for response in quote_bundle["bridge_workflows"] + for request in bundle["bridge_requests"] ] error = any( - workflow["quote"]["error"] for workflow in quote_bundle["bridge_workflows"] + request.status in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) for request in bundle["bridge_requests"] ) - self._quote_bundle_updated_on_session = True + self._bundle_updated_on_session = True return { - "id": quote_bundle["id"], + "id": bundle["id"], "balances": balances, - "bridge_total_requirements": quote_bundle["bridge_total_requirements"], + "bridge_total_requirements": bundle["bridge_total_requirements"], "bridge_refill_requirements": bridge_refill_requirements, - "expiration_timestamp": quote_bundle["expiration_timestamp"], + "expiration_timestamp": bundle["expiration_timestamp"], "is_refill_required": is_refill_required, "bridge_request_status": bridge_request_status, "error": error, } - def execute_quote_bundle(self, quote_bundle_id: str) -> dict: - """Execute quote bundle""" + def execute_bundle(self, bundle_id: str) -> dict: + """Execute the bundle""" - if not self._quote_bundle_updated_on_session: + if not self._bundle_updated_on_session: raise RuntimeError( - "[BRIDGE MANAGER] Cannot call 'execute_quote_bundle' before 'bridge_refill_requirements'." + "[BRIDGE MANAGER] Cannot execute bundle if not updated on session." ) - quote_bundle = self.data.last_requested_quote_bundle + bundle = self.data.last_requested_bundle - if not quote_bundle: - raise RuntimeError("[BRIDGE MANAGER] No quote bundle.") + if not bundle: + raise RuntimeError("[BRIDGE MANAGER] No bundle.") - if quote_bundle.get("id") != quote_bundle_id: + if bundle.get("id") != bundle_id: raise RuntimeError( - f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} does not match last requested quote bundle id {quote_bundle.get('id')}." + f"[BRIDGE MANAGER] Quote bundle id {bundle_id} does not match last requested bundle id {bundle.get('id')}." ) - requirements = self.bridge_refill_requirements(quote_bundle["bridge_requests"]) + requirements = self.bridge_refill_requirements(bundle["bridge_requests"]) if requirements["is_refill_required"]: raise RuntimeError( - f"[BRIDGE MANAGER] Refill requirements not satisfied for quote bundle id {quote_bundle_id}." + f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}." ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") - quote_bundle["status"] = str(QuoteBundleStatus.SUBMITTED) + bundle["status"] = str(BridgeRequestBundleStatus.SUBMITTED) - for bridge_workflow in quote_bundle["bridge_workflows"]: - self.bridge_provider.update_with_execution(bridge_workflow) + for request in bundle["bridge_requests"]: + self.bridge_provider.update_with_execution(request) self._store_data() - self.data.last_requested_quote_bundle = None - self.data.executed_quote_bundles[quote_bundle["id"]] = quote_bundle + self.data.last_requested_bundle = None + self.data.executed_bundles[bundle["id"]] = bundle self._store_data() - self._quote_bundle_updated_on_session = False + self._bundle_updated_on_session = False self.logger.info( - f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} executed." + f"[BRIDGE MANAGER] Bundle id {bundle_id} executed." ) - return self.get_execution_status(quote_bundle_id) + return self.get_execution_status(bundle_id) - def get_execution_status(self, quote_bundle_id: str) -> dict: - """Get execution status of quote bundle.""" + def get_execution_status(self, bundle_id: str) -> dict: + """Get execution status of bundle.""" - quote_bundle = self.data.executed_quote_bundles.get(quote_bundle_id) + bundle = self.data.executed_bundles.get(bundle_id) - if not quote_bundle: + if not bundle: raise ValueError( - f"[BRIDGE MANAGER] Quote bundle id {quote_bundle_id} not found." + f"[BRIDGE MANAGER] Bundle id {bundle_id} not found." ) - self._update_quote_bundle_status(quote_bundle_id) + self._update_bundle_status(bundle_id) bridge_request_status = [ { - "explorer_link": workflow["execution"]["explorer_link"], - "message": workflow["execution"]["message"], - "status": workflow["execution"]["status"], - "tx_hash": workflow["execution"]["tx_hash"], + "explorer_link": request["execution"]["explorer_link"], + "message": request["execution"]["message"], + "status": request["execution"]["status"], + "tx_hash": request["execution"]["tx_hash"], } - for workflow in quote_bundle["bridge_workflows"] + for request in bundle["bridge_requests"] ] error = any( - workflow["execution"]["error"] - for workflow in quote_bundle["bridge_workflows"] + request["execution"]["error"] + for request in bundle["bridge_requests"] ) return { - "id": quote_bundle["id"], - "status": quote_bundle["status"], + "id": bundle["id"], + "status": bundle["status"], "bridge_request_status": bridge_request_status, "error": error, } diff --git a/operate/cli.py b/operate/cli.py index d26440ea2..8fb1a4ad8 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1039,8 +1039,8 @@ async def _bridge_execute(request: Request) -> JSONResponse: try: data = await request.json() - output = operate.bridge_manager().execute_quote_bundle( - quote_bundle_id=data["id"] + output = operate.bridge_manager().execute_bundle( + bundle_id=data["id"] ) return JSONResponse( @@ -1068,7 +1068,7 @@ async def _bridge_status(request: Request) -> JSONResponse: try: output = operate.bridge_manager().get_execution_status( - quote_bundle_id=quote_bundle_id + bundle_id=quote_bundle_id ) return JSONResponse( From 43b6c4ce97c286d2c6dae047b48515f9caffa924 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 16 Apr 2025 10:23:55 +0200 Subject: [PATCH 034/173] chore: keep placeholder for master safe --- operate/services/manage.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/operate/services/manage.py b/operate/services/manage.py index f1ada9dd5..5d445d8f5 100644 --- a/operate/services/manage.py +++ b/operate/services/manage.py @@ -2352,9 +2352,6 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, ): allow_start_agent = False - # Remove placeholder value - refill_requirements[chain].pop("master_safe", None) - # Refill requirements for Master EOA eoa_funding_values = self._get_master_eoa_native_funding_values( master_safe_exists=master_safe_exists, From 30dbddfdb8f0aa2852a3be614cc7889dc2ef7aaa Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 16 Apr 2025 13:04:53 +0200 Subject: [PATCH 035/173] fix: endpoint --- operate/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operate/cli.py b/operate/cli.py index d26440ea2..4e886ac85 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1011,7 +1011,8 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: try: data = await request.json() output = operate.bridge_manager().bridge_refill_requirements( - bridge_requests=data + bridge_requests=data["bridge_requests"], + force_update=data.get("force_update", False) ) return JSONResponse( From 2f01ddefb7f36c6a22251cb63db09b2585319fa8 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 16 Apr 2025 13:20:11 +0200 Subject: [PATCH 036/173] fix: cli --- operate/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/cli.py b/operate/cli.py index 4e886ac85..4bc38f9ef 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1018,7 +1018,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: return JSONResponse( content=output, status_code=HTTPStatus.BAD_GATEWAY - if output["errors"] + if output["error"] else HTTPStatus.OK, ) except ValueError as e: From b0eb0ccbf6f722d974f794264e1e733c886b9a72 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 16 Apr 2025 16:22:16 +0200 Subject: [PATCH 037/173] chore: maxPriceImpact --- operate/bridge/bridge.py | 1 + 1 file changed, 1 insertion(+) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 1491d0690..94afa7b45 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -176,6 +176,7 @@ def update_with_quote(self, bridge_workflow: dict) -> None: "toAddress": to_address, "toToken": to_token, "toAmount": to_amount, + "maxPriceImpact": 0.20, # TODO determine correct value } for attempt in range(1, DEFAULT_MAX_RETRIES + 1): start = time.time() From df08baf19ed717e8e7edb737a0e63dbec552ea1a Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 17 Apr 2025 12:01:48 +0200 Subject: [PATCH 038/173] chore: update --- operate/bridge/bridge.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index fa3fb0eef..3a6976f83 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -81,12 +81,18 @@ def __str__(self) -> str: class BridgeRequest(LocalResource): """BridgeRequest.""" - request: dict + params: dict id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" status: BridgeRequestStatus = BridgeRequestStatus.REQUEST_CREATED quote: dict | None = None execution: dict | None = None + +@dataclass +class BridgeRequestBundle(LocalResource): + pass + + class BridgeProvider: """Abstract BridgeProvider.""" @@ -155,13 +161,13 @@ def update_with_quote(self, bridge_request: BridgeRequest) -> None: f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id} with quote: execution already present." ) - from_chain = bridge_request.request["from"]["chain"] - from_address = bridge_request.request["from"]["address"] - from_token = bridge_request.request["from"]["token"] - to_chain = bridge_request.request["to"]["chain"] - to_address = bridge_request.request["to"]["address"] - to_token = bridge_request.request["to"]["token"] - to_amount = bridge_request.request["to"]["amount"] + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] if to_amount == 0: self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested.") @@ -246,9 +252,9 @@ def get_quote_requirements(self, bridge_request: BridgeRequest) -> dict: """Get bridge requirements for a quote.""" if not bridge_request.quote or not bridge_request.quote["response"] or not "action" in bridge_request.quote["response"]: - from_chain = bridge_request.request["from"]["chain"] - from_address = bridge_request.request["from"]["address"] - from_token = bridge_request.request["from"]["token"] + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] from_amount = 0 transaction_value = 0 else: @@ -676,7 +682,7 @@ def _get_updated_bundle( bundle["status"] = str(BridgeRequestBundleStatus.CREATED) bundle["user_bridge_requests"] = user_bridge_requests bundle["bridge_requests"] = [ - BridgeRequest(request=request) + BridgeRequest(params=request) for request in user_bridge_requests ] bundle["timestamp"] = now From 91e7b0f22ffa39000a05a9eb7de49efb33ae3ecd Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 22 Apr 2025 09:55:29 +0200 Subject: [PATCH 039/173] chore: refactor bridge --- operate/bridge/bridge.py | 797 +++++++++++++++++++-------------------- operate/cli.py | 2 +- operate/resource.py | 27 ++ 3 files changed, 421 insertions(+), 405 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 3a6976f83..f4ee050e7 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -23,7 +23,6 @@ import enum import json import logging -import shutil import time import typing as t import uuid @@ -54,47 +53,163 @@ from operate.wallet.master import MasterWalletManager -DEFAULT_MAX_RETRIES = 3 +DEFAULT_MAX_QUOTE_RETRIES = 3 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" BRIDGE_REQUEST_PREFIX = "br-" +MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." +MESSAGE_EXECUTION_SKIPPED = "Execution skipped." +@dataclass +class QuoteData(LocalResource): + """QuoteData""" + attempts: int + requirements: dict + elapsed_time: float + message: str | None + response: dict | None + response_status: int + timestamp: int + + +@dataclass +class ExecutionData(LocalResource): + """ExecutionData""" + bridge_status: enum.Enum | None + elapsed_time: float + explorer_link: str | None + message: str | None + timestamp: int + tx_hash: str | None + tx_status: int -class BridgeRequestStatus(str, enum.Enum): - """Bridge request status.""" - REQUEST_CREATED = "REQUEST_CREATED" +class BridgeRequestStatus(str, enum.Enum): + """BridgeRequestStatus""" + CREATED = "CREATED" QUOTE_DONE = "QUOTE_DONE" QUOTE_FAILED = "QUOTE_FAILED" EXECUTION_PENDING = "EXECUTION_PENDING" EXECUTION_DONE = "EXECUTION_DONE" EXECUTION_FAILED = "EXECUTION_FAILED" - EXECUTION_NA = "EXECUTION_NA" - def __str__(self) -> str: """__str__""" return self.value + @dataclass class BridgeRequest(LocalResource): - """BridgeRequest.""" - + """BridgeRequest""" params: dict id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" - status: BridgeRequestStatus = BridgeRequestStatus.REQUEST_CREATED - quote: dict | None = None - execution: dict | None = None + status: BridgeRequestStatus = BridgeRequestStatus.CREATED + quote_data: QuoteData | None = None + execution_data: ExecutionData | None = None + + def get_status_json(self) -> dict: + """JSON representation of the status.""" + if self.execution_data: + return { + "explorer_link": self.execution_data.explorer_link, + "message": self.execution_data.message, + "status": self.status.value, + "tx_hash": self.execution_data.tx_hash, + } + if self.quote_data: + return { + "message": self.quote_data.message, + "status": self.status.value + } + + return { + "message": None, + "status": self.status.value + } + + +class BridgeRequestBundleStatus(str, enum.Enum): + """BridgeRequestBundleStatus""" + + CREATED = "CREATED" + QUOTE_DONE = "QUOTE_DONE" + QUOTE_FAILED = "QUOTE_FAILED" + EXECUTION_PENDING = "EXECUTION_PENDING" + EXECUTION_DONE = "EXECUTION_DONE" + EXECUTION_FAILED = "EXECUTION_FAILED" + + # CREATED = "CREATED" + # QUOTED = "QUOTED" + # QUOTED2 = "QUOTED" + # SUBMITTED = "SUBMITTED" + # FINISHED = "FINISHED" # All requests in the bundle are either done or failed. + + def __str__(self) -> str: + """__str__""" + return self.value @dataclass class BridgeRequestBundle(LocalResource): - pass + """BridgeRequestBundle""" + bridge_provider: str + status: BridgeRequestBundleStatus + requests_params: list[dict] + bridge_requests: list[BridgeRequest] + timestamp: int + id: str + + def get_from_chains(self) -> set[Chain]: + """Get 'from' chains.""" + return { + Chain(request.params["from"]["chain"]) + for request in self.bridge_requests + } + + def get_from_addresses(self, chain: Chain) -> set[str]: + """Get 'from' addresses.""" + chain_str = chain.value + return { + request.params["from"]["address"] + for request in self.bridge_requests + if request.params["from"]["chain"] == chain_str + } + + def get_from_tokens(self, chain: Chain) -> set[str]: + """Get 'from' tokens.""" + chain_str = chain.value + return { + request.params["from"]["token"] + for request in self.bridge_requests + if request.params["from"]["chain"] == chain_str + } + + def sum_bridge_requirements(self) -> dict: + """Sum bridge requirements.""" + + bridge_total_requirements: dict = {} + + for request in self.bridge_requests: + if not request.quote_data: + continue + + bridge_requirements = request.quote_data.requirements + for from_chain, from_addresses in bridge_requirements.items(): + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + bridge_total_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ).setdefault(from_token, 0) + bridge_total_requirements[from_chain][from_address][ + from_token + ] += from_amount + + return bridge_total_requirements class BridgeProvider: - """Abstract BridgeProvider.""" + """(Abstract) BridgeProvider""" def __init__( self, @@ -110,29 +225,31 @@ def name(self) -> str: return self.__class__.__name__ @abstractmethod - def update_with_quote(self, bridge_request: BridgeRequest) -> None: + def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" raise NotImplementedError() @abstractmethod - def get_quote_requirements(self, bridge_request: BridgeRequest) -> dict: - """Get bridge requirements for a single quote.""" - raise NotImplementedError() - - @abstractmethod - def update_with_execution(self, bridge_request: dict) -> None: + def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" raise NotImplementedError() @abstractmethod - def update_execution_status(self, bridge_request: dict) -> bool: + def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status.""" raise NotImplementedError() - @abstractmethod - def is_execution_finished(self, bridge_request: dict) -> bool: - """Check if the execution is finished.""" - raise NotImplementedError() + def quote_bundle(self, bundle: BridgeRequestBundle) -> None: + """Update the bundle with the quotes.""" + for bridge_request in bundle.bridge_requests: + self.quote(bridge_request=bridge_request) + + bundle.timestamp = int(time.time()) + + def execute_bundle(self, bundle: BridgeRequestBundle) -> None: + """Update the bundle with the quotes.""" + for bridge_request in bundle.bridge_requests: + self.execute(bridge_request=bridge_request) class LiFiTransactionStatus(str, enum.Enum): @@ -153,12 +270,17 @@ def __str__(self) -> str: class LiFiBridgeProvider(BridgeProvider): """LI.FI Bridge provider.""" - def update_with_quote(self, bridge_request: BridgeRequest) -> None: + def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" - if bridge_request.execution: - raise ValueError( - f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id} with quote: execution already present." + if bridge_request.status not in (BridgeRequestStatus.CREATED, BridgeRequestStatus.QUOTE_DONE, BridgeRequestStatus.QUOTE_FAILED): + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." + ) + + if bridge_request.execution_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id}: execution already present." ) from_chain = bridge_request.params["from"]["chain"] @@ -168,12 +290,27 @@ def update_with_quote(self, bridge_request: BridgeRequest) -> None: to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] + zero_requirements = { + from_chain: { + from_address: { + ZERO_ADDRESS: 0, + from_token: 0, + } + } + } if to_amount == 0: - self.logger.info("[LI.FI BRIDGE] Zero-amount quote requested.") - bridge_request.quote = new_quote( - message="Zero-amount quote requested.", + self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") + quote_data = QuoteData( + attempts=0, + requirements=zero_requirements, + elapsed_time=0, + message=MESSAGE_QUOTE_ZERO, + response=None, + response_status=0, + timestamp=int(time.time()) ) + bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE return @@ -189,7 +326,7 @@ def update_with_quote(self, bridge_request: BridgeRequest) -> None: "toAmount": to_amount, "maxPriceImpact": 0.20, # TODO determine correct value } - for attempt in range(1, DEFAULT_MAX_RETRIES + 1): + for attempt in range(1, DEFAULT_MAX_QUOTE_RETRIES + 1): start = time.time() try: self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") @@ -197,132 +334,114 @@ def update_with_quote(self, bridge_request: BridgeRequest) -> None: url=url, headers=headers, params=params, timeout=30 ) response.raise_for_status() - bridge_request.quote = { - "response": response.json(), - "attempts": attempt, - "elapsed_time": time.time() - start, - "error": False, - "message": None, - "response_status": response.status_code, - "timestamp": int(time.time()), - } + response_json = response.json() + from_amount = int(response_json["action"]["fromAmount"]) + transaction_value = int(response_json["transactionRequest"]["value"], 16) + + # TODO: gas fees! + if from_token == ZERO_ADDRESS: + requirements = { + from_chain: { + from_address: {from_token: from_amount + transaction_value} + } + } + else: + requirements = { + from_chain: { + from_address: { + ZERO_ADDRESS: transaction_value, + from_token: from_amount, + } + } + } + + quote_data = QuoteData( + attempts=attempt, + requirements=requirements, + elapsed_time=time.time() - start, + message=None, + response=response_json, + response_status=response.status_code, + timestamp=int(time.time()) + ) + bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE return except requests.Timeout as e: self.logger.warning( - f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." + f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." + ) + quote_data = QuoteData( + attempts=attempt, + requirements=zero_requirements, + elapsed_time=time.time() - start, + message=str(e), + response=None, + response_status=HTTPStatus.GATEWAY_TIMEOUT, + timestamp=int(time.time()) ) - output = { - "response": {}, - "attempts": attempt, - "elapsed_time": time.time() - start, - "error": True, - "message": str(e), - "response_status": HTTPStatus.GATEWAY_TIMEOUT, - "timestamp": int(time.time()), - } - bridge_request.status = BridgeRequestStatus.QUOTE_FAILED except requests.RequestException as e: self.logger.warning( - f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_RETRIES}: {e}." + f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." ) response_json = response.json() - output = { - "response": response_json, - "attempts": attempt, - "elapsed_time": time.time() - start, - "error": True, - "message": response_json.get("message") or str(e), - "response_status": response.status_code, - "timestamp": int(time.time()), - } - bridge_request.status = BridgeRequestStatus.QUOTE_FAILED - - if attempt >= DEFAULT_MAX_RETRIES: + quote_data = QuoteData( + attempts=attempt, + requirements=zero_requirements, + elapsed_time=time.time() - start, + message=response_json.get("message") or str(e), + response=response_json, + response_status=getattr(response, "status_code", HTTPStatus.BAD_GATEWAY), + timestamp=int(time.time()) + ) + if attempt >= DEFAULT_MAX_QUOTE_RETRIES: self.logger.error( - f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_RETRIES} attempts." + f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_QUOTE_RETRIES} attempts." ) - bridge_request.quote = output + bridge_request.quote_data = quote_data + bridge_request.status = BridgeRequestStatus.QUOTE_FAILED return time.sleep(2) - # TODO gas fees ! - def get_quote_requirements(self, bridge_request: BridgeRequest) -> dict: - """Get bridge requirements for a quote.""" - - if not bridge_request.quote or not bridge_request.quote["response"] or not "action" in bridge_request.quote["response"]: - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] - from_token = bridge_request.params["from"]["token"] - from_amount = 0 - transaction_value = 0 - else: - quote = bridge_request.quote["response"] - from_chain = Chain.from_id(quote["action"]["fromChainId"]).value - from_address = quote["action"]["fromAddress"] - from_token = quote["action"]["fromToken"]["address"] - from_amount = int(quote["action"]["fromAmount"]) - transaction_value = int(quote["transactionRequest"]["value"], 16) + def execute(self, bridge_request: BridgeRequest) -> None: + """Execute the quote.""" - if from_token == ZERO_ADDRESS: - return { - from_chain: { - from_address: {from_token: from_amount + transaction_value} - } - } - else: - return { - from_chain: { - from_address: { - ZERO_ADDRESS: transaction_value, - from_token: from_amount, - } - } - } + if bridge_request.status not in (BridgeRequestStatus.QUOTE_DONE, BridgeRequestStatus.QUOTE_FAILED): + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." + ) - def update_with_execution(self, bridge_request: dict) -> None: - """Execute the quote.""" + if not bridge_request.quote_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: quote data not present." + ) - if "quote" not in bridge_request: - raise ValueError( - "[LI.FI BRIDGE] Cannot update bridge request with execution: quote not present." + if bridge_request.execution_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: execution data already present." ) - if "execution" in bridge_request: - self.logger.warning( - f"[LI.FI BRIDGE] Execution already present on bridge request {bridge_request['id']}." + timestamp = time.time() + quote = bridge_request.quote_data.response + + if not quote or "action" not in quote: + self.logger.info("[LI.FI BRIDGE] Skipping quote execution.") + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=0, + explorer_link=None, + message=MESSAGE_EXECUTION_SKIPPED, + timestamp=int(timestamp), + tx_hash=None, + tx_status=0 ) - return + bridge_request.execution_data = execution_data - quote = bridge_request["quote"]["response"] - error = bridge_request["quote"].get("error", False) - - if error: - self.logger.info("[LI.FI BRIDGE] Skipping quote execution (quote error).") - bridge_request["execution"] = { - "error": True, - "explorer_link": None, - "message": "Skipped execution (quote error).", - "lifi_status": None, - "timestamp": int(time.time()), - "tx_hash": None, - "tx_status": None, - } - bridge_request["status"] = BridgeRequestStatus.EXECUTION_DONE # TODO alternative state? - return - if not quote: - self.logger.info("[LI.FI BRIDGE] Skipping quote execution (empty quote).") - bridge_request["execution"] = { - "error": False, - "explorer_link": None, - "message": "Skipped execution (empty quote).", - "lifi_status": None, - "timestamp": int(time.time()), - "tx_hash": None, - "tx_status": None, - } - bridge_request["status"] = BridgeRequestStatus.EXECUTION_DONE # TODO alternative state? + if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return try: @@ -400,48 +519,52 @@ def _build_bridge_tx( # pylint: disable=unused-argument self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") tx_hash = tx_receipt.get("transactionHash", "").hex() - bridge_request["execution"] = { - "error": tx_receipt.get("status", 0) == 0, - "explorer_link": f"https://scan.li.fi/tx/{tx_hash}", - "message": None, - "lifi_status": LiFiTransactionStatus.NOT_FOUND, - "timestamp": int(time.time()), - "tx_hash": tx_hash, - "tx_status": tx_receipt.get("status", 0), - } - bridge_request["status"] = BridgeRequestStatus.EXECUTION_PENDING if tx_hash else BridgeRequestStatus.EXECUTION_FAILED + execution_data = ExecutionData( + bridge_status=LiFiTransactionStatus.NOT_FOUND, + elapsed_time=time.time() - timestamp, + explorer_link=f"https://scan.li.fi/tx/{tx_hash}", + message=None, + timestamp=int(timestamp), + tx_hash=tx_hash, + tx_status=tx_receipt.get("status", 0), + ) + bridge_request.execution_data = execution_data + if tx_hash: + bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED except Exception as e: # pylint: disable=broad-except - bridge_request["execution"] = { - "error": True, - "explorer_link": None, - "message": f"Error executing quote: {str(e)}", - "lifi_status": LiFiTransactionStatus.FAILED, - "status": BridgeRequestStatus.EXECUTION_FAILED, - "timestamp": int(time.time()), - "tx_hash": None, - "tx_status": None, - } - bridge_request["status"] = BridgeRequestStatus.EXECUTION_FAILED + execution_data = ExecutionData( + bridge_status=LiFiTransactionStatus.UNKNOWN, + elapsed_time=time.time() - timestamp, + explorer_link=None, + message=f"Error executing quote: {str(e)}", + timestamp=int(timestamp), + tx_hash=None, + tx_status=0, + ) + bridge_request.execution_data = execution_data + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - def update_execution_status(self, bridge_request: dict) -> bool: + def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" + if bridge_request.status in (BridgeRequestStatus.EXECUTION_DONE, BridgeRequestStatus.EXECUTION_FAILED): + return - if "execution" not in bridge_request: - return False - - execution = bridge_request["execution"] - tx_hash = execution["tx_hash"] + if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id} with status {bridge_request.status}." + ) - print("update_execution_status") - print(f"{execution=}") + if not bridge_request.execution_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id}: execution data not present." + ) - if execution["status"] in ( - BridgeRequestStatus.EXECUTION_DONE, - BridgeRequestStatus.EXECUTION_FAILED, - ): - return False + execution = bridge_request.execution_data + tx_hash = execution.tx_hash url = "https://li.quest/v1/status" headers = {"accept": "application/json"} @@ -452,54 +575,15 @@ def update_execution_status(self, bridge_request: dict) -> bool: response = requests.get(url=url, headers=headers, params=params, timeout=30) response.raise_for_status() response_json = response.json() - lifi_status = response_json.get("lifi_status", str(LiFiTransactionStatus.UNKNOWN)) - execution["message"] = response_json.get("substatusMessage") - - if execution["lifi_status"] != lifi_status: - execution["lifi_status"] = lifi_status - if lifi_status in ( - LiFiTransactionStatus.DONE, - LiFiTransactionStatus.FAILED, - ): - execution["status"] = BridgeRequestStatus.EXECUTION_DONE - return True - - return False - - def is_execution_finished(self, bridge_request: dict) -> bool: - """Check if the execution is finished.""" - - if "execution" not in bridge_request: - raise ValueError( - "[LI.FI BRIDGE] Cannot update bridge request execution: execution not present." - ) - - self.update_execution_status(bridge_request) - - execution = bridge_request["execution"] - tx_hash = execution["tx_hash"] - - if not tx_hash: - return True - - return execution["status"] in ( - LiFiTransactionStatus.DONE, - LiFiTransactionStatus.FAILED, - ) - - -class BridgeRequestBundleStatus(str, enum.Enum): - """Bridge request bundle status.""" - - CREATED = "CREATED" - QUOTED = "QUOTED" - SUBMITTED = "SUBMITTED" - FINISHED = "FINISHED" # All requests in the bundle are either done or failed. - - def __str__(self) -> str: - """__str__""" - return self.value + lifi_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) + execution.message = response_json.get("substatusMessage") + if execution.bridge_status != lifi_status: + execution.bridge_status = lifi_status + if lifi_status == LiFiTransactionStatus.DONE: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + elif lifi_status == LiFiTransactionStatus.FAILED: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @dataclass class BridgeManagerData(LocalResource): @@ -507,8 +591,8 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 - last_requested_bundle: dict[str, BridgeRequest] | None = None - executed_bundles: dict = field(default_factory=dict) + last_requested_bundle: BridgeRequestBundle | None = None + executed_bundles: dict[str, BridgeRequestBundle] = field(default_factory=dict[str, BridgeRequestBundle]) _file = "bridge.json" @@ -537,55 +621,8 @@ def load(cls, path: Path) -> "LocalResource": return super().load(path) -def new_bridge_request(request: dict) -> dict: - """Create a new bridge request.""" - return { - "id": f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", - "status": BridgeRequestStatus.REQUEST_CREATED, - "request": request, - "quote": None, - "execution": None, - } - - -def new_quote( # pylint: disable=too-many-positional-arguments - attempts: int = 0, - elapsed_time: int = 0, - message: str | None = None, - response: dict | None = None, - response_status: int = 0, - timestamp: int = int(time.time()), -) -> dict: - """Create a new quote.""" - return { - "attempts": attempts, - "elapsed_time": elapsed_time, - "message": message, - "response": response, - "response_status": response_status, - "timestamp": timestamp, - } - - -def new_execution( # pylint: disable=too-many-positional-arguments - bridge_status: enum.Enum | None = None, - elapsed_time: int = 0, - explorer_link: str | None = None, - message: str | None = None, - timestamp: int = int(time.time()), - tx_hash: str | None = None, - tx_status: str | None = None, -) -> dict: - """Create a new execution.""" - return { - "bridge_status": bridge_status, - "elapsed_time": elapsed_time, - "explorer_link": explorer_link, - "message": message, - "timestamp": timestamp, - "tx_hash": tx_hash, - "tx_status": tx_status, - } + + class BridgeManager: @@ -593,8 +630,6 @@ class BridgeManager: # TODO singleton - _bundle_updated_on_session = False - def __init__( self, path: Path, @@ -623,80 +658,53 @@ def _store_data(self) -> None: self.logger.info("[BRIDGE MANAGER] Storing data to file.") self.data.store() - def _sum_quotes_requirements(self, bridge_requests: list) -> dict: - """Get bridge requirements for a list of quotes.""" - - bridge_total_requirements: dict = {} - - for request in bridge_requests: - req = self.bridge_provider.get_quote_requirements(request) - for from_chain, from_addresses in req.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - bridge_total_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - ).setdefault(from_token, 0) - bridge_total_requirements[from_chain][from_address][ - from_token - ] += from_amount - - return bridge_total_requirements - def _get_updated_bundle( - self, user_bridge_requests: list, force_update: bool - ) -> dict: + self, requests_params: list[dict], force_update: bool + ) -> BridgeRequestBundle: """Ensures to return a valid (non expired) bundle for the given inputs.""" now = int(time.time()) - bundle = self.data.last_requested_bundle or {} - bundle_id = bundle.get("id") + bundle = self.data.last_requested_bundle create_new_bundle = False if not bundle: self.logger.info("[BRIDGE MANAGER] No last bundle.") create_new_bundle = True - bundle_id = None - elif bundle.get("status") not in ( + elif bundle.status not in ( BridgeRequestBundleStatus.CREATED, - BridgeRequestBundleStatus.QUOTED, + BridgeRequestBundleStatus.QUOTE_DONE, + BridgeRequestBundleStatus.QUOTE_FAILED, ): raise RuntimeError("[BRIDGE MANAGER] Bundle inconsistent status.") - elif DeepDiff(user_bridge_requests, bundle.get("bridge_requests", [])): - self.logger.info("[BRIDGE MANAGER] Different quote requests.") + elif DeepDiff(requests_params, bundle.requests_params): + self.logger.info("[BRIDGE MANAGER] Different requests params.") create_new_bundle = True - bundle_id = None elif force_update: - self.logger.info("[BRIDGE MANAGER] Force quote update.") - create_new_bundle = True - elif now > bundle.get("timestamp", 0) + self.quote_validity_period: + self.logger.info("[BRIDGE MANAGER] Force bundle update.") + self.bridge_provider.quote_bundle(bundle) + self._store_data() + elif now > bundle.timestamp + self.quote_validity_period: self.logger.info("[BRIDGE MANAGER] Bundle expired.") - create_new_bundle = True + self.bridge_provider.quote_bundle(bundle) + self._store_data() - if create_new_bundle: - self.logger.info("[BRIDGE MANAGER] Requesting new bundle.") - bundle = {} - bundle["id"] = ( - bundle_id or f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}" + if not bundle or create_new_bundle: + self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.") + + bundle = BridgeRequestBundle( + id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}", + bridge_provider=self.bridge_provider.name(), + status=BridgeRequestBundleStatus.CREATED, + requests_params=requests_params, + bridge_requests=[ + BridgeRequest(params=params) + for params in requests_params + ], + timestamp=now, ) - bundle["bridge_provider"] = self.bridge_provider.name() - bundle["status"] = str(BridgeRequestBundleStatus.CREATED) - bundle["user_bridge_requests"] = user_bridge_requests - bundle["bridge_requests"] = [ - BridgeRequest(params=request) - for request in user_bridge_requests - ] - bundle["timestamp"] = now - bundle["expiration_timestamp"] = now + self.quote_validity_period - - for request in bundle["bridge_requests"]: - self.bridge_provider.update_with_quote(request) - - bundle["status"] = str(BridgeRequestBundleStatus.QUOTED) - bundle[ - "bridge_total_requirements" - ] = self._sum_quotes_requirements(bundle["bridge_requests"]) self.data.last_requested_bundle = bundle + self.bridge_provider.quote_bundle(bundle) self._store_data() return bundle @@ -708,27 +716,36 @@ def _update_bundle_status(self, bundle_id: str) -> None: raise ValueError( f"[BRIDGE MANAGER] Bundle id {bundle_id} not found." ) + + if bundle.status in (BridgeRequestBundleStatus.EXECUTION_DONE, BridgeRequestBundleStatus.EXECUTION_FAILED): + return - initial_status = bundle["status"] - execution_status_changed = [ - self.bridge_provider.update_execution_status(request) - for request in bundle["bridge_requests"] + initial_bundle_status = bundle.status + initial_status = [ + request.status for request in bundle.bridge_requests ] - if any(execution_status_changed): - self._store_data() + for request in bundle.bridge_requests: + self.bridge_provider.update_execution_status(request) - is_execution_finished = all( - self.bridge_provider.is_execution_finished(request) - for request in bundle["bridge_requests"] - ) + status = [ + request.status for request in bundle.bridge_requests + ] - if is_execution_finished: - bundle["status"] = str(BridgeRequestBundleStatus.FINISHED) + if all( + request.status in (BridgeRequestStatus.EXECUTION_DONE) + for request in bundle.bridge_requests + ): + bundle.status = BridgeRequestBundleStatus.EXECUTION_DONE + elif all( + request.status in (BridgeRequestStatus.EXECUTION_DONE, BridgeRequestStatus.EXECUTION_FAILED) + for request in bundle.bridge_requests + ): + bundle.status = BridgeRequestBundleStatus.EXECUTION_FAILED else: - bundle["status"] = str(BridgeRequestBundleStatus.SUBMITTED) + bundle.status = BridgeRequestBundleStatus.EXECUTION_PENDING - if initial_status != bundle["status"]: + if initial_bundle_status != bundle.status or initial_status != status: self._store_data() def _raise_if_invalid(self, bridge_requests: list) -> None: @@ -799,51 +816,38 @@ def _raise_if_invalid(self, bridge_requests: list) -> None: ) def bridge_refill_requirements( - self, bridge_requests: list, force_update: bool = False + self, requests_params: list[dict], force_update: bool = False ) -> dict: """Get bridge refill requirements.""" - self._raise_if_invalid(bridge_requests) + self._raise_if_invalid(requests_params) self.logger.info( - f"[BRIDGE MANAGER] Quote requests count: {len(bridge_requests)}." + f"[BRIDGE MANAGER] Quote requests count: {len(requests_params)}." ) - bundle = self._get_updated_bundle(bridge_requests, force_update) + bundle = self._get_updated_bundle(requests_params, force_update) - chains = [request["from"]["chain"] for request in bridge_requests] balances = {} - for chain in chains: - ledger_api = self.wallet_manager.load(Chain(chain).ledger_type).ledger_api( - Chain(chain) + for chain in bundle.get_from_chains(): + ledger_api = self.wallet_manager.load(chain.ledger_type).ledger_api( + chain ) - balances[chain] = get_assets_balances( + balances[chain.value] = get_assets_balances( ledger_api=ledger_api, - asset_addresses={ZERO_ADDRESS} - | { - request["from"]["token"] - for request in bridge_requests - if request["from"]["chain"] == chain - }, - addresses={ - request["from"]["address"] - for request in bridge_requests - if request["from"]["chain"] == chain - }, + asset_addresses={ZERO_ADDRESS} | bundle.get_from_tokens(chain), + addresses=bundle.get_from_addresses(chain), ) + bridge_total_requirements = bundle.sum_bridge_requirements() + bridge_refill_requirements: dict = {} - for from_chain, from_addresses in bundle[ - "bridge_total_requirements" - ].items(): + for from_chain, from_addresses in bridge_total_requirements.items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): balance = balances[from_chain][from_address][from_token] bridge_refill_requirements.setdefault(from_chain, {}).setdefault( from_address, {} - ) - bridge_refill_requirements[from_chain][from_address][ - from_token - ] = max(from_amount - balance, 0) + )[from_token] = max(from_amount - balance, 0) is_refill_required = any( amount > 0 @@ -853,47 +857,38 @@ def bridge_refill_requirements( ) bridge_request_status = [ - { - "message": request.quote["message"], - "status": request.status, - } - for request in bundle["bridge_requests"] + request.get_status_json() + for request in bundle.bridge_requests ] error = any( - request.status in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) for request in bundle["bridge_requests"] + request.status in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) for request in bundle.bridge_requests ) - self._bundle_updated_on_session = True - return { - "id": bundle["id"], + return dict({ + "id": bundle.id, "balances": balances, - "bridge_total_requirements": bundle["bridge_total_requirements"], "bridge_refill_requirements": bridge_refill_requirements, - "expiration_timestamp": bundle["expiration_timestamp"], - "is_refill_required": is_refill_required, "bridge_request_status": bridge_request_status, + "bridge_total_requirements": bridge_total_requirements, "error": error, - } + "expiration_timestamp": bundle.timestamp + self.quote_validity_period, + "is_refill_required": is_refill_required, + }) def execute_bundle(self, bundle_id: str) -> dict: """Execute the bundle""" - if not self._bundle_updated_on_session: - raise RuntimeError( - "[BRIDGE MANAGER] Cannot execute bundle if not updated on session." - ) - bundle = self.data.last_requested_bundle if not bundle: raise RuntimeError("[BRIDGE MANAGER] No bundle.") - if bundle.get("id") != bundle_id: + if bundle.id != bundle_id: raise RuntimeError( f"[BRIDGE MANAGER] Quote bundle id {bundle_id} does not match last requested bundle id {bundle.get('id')}." ) - requirements = self.bridge_refill_requirements(bundle["bridge_requests"]) + requirements = self.bridge_refill_requirements(bundle.requests_params) if requirements["is_refill_required"]: raise RuntimeError( @@ -901,16 +896,15 @@ def execute_bundle(self, bundle_id: str) -> dict: ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") - bundle["status"] = str(BridgeRequestBundleStatus.SUBMITTED) + bundle.status = BridgeRequestBundleStatus.EXECUTION_PENDING - for request in bundle["bridge_requests"]: - self.bridge_provider.update_with_execution(request) + for request in bundle.bridge_requests: + self.bridge_provider.execute(request) self._store_data() self.data.last_requested_bundle = None - self.data.executed_bundles[bundle["id"]] = bundle + self.data.executed_bundles[bundle.id] = bundle self._store_data() - self._bundle_updated_on_session = False self.logger.info( f"[BRIDGE MANAGER] Bundle id {bundle_id} executed." ) @@ -930,22 +924,17 @@ def get_execution_status(self, bundle_id: str) -> dict: self._update_bundle_status(bundle_id) bridge_request_status = [ - { - "explorer_link": request["execution"]["explorer_link"], - "message": request["execution"]["message"], - "status": request["execution"]["status"], - "tx_hash": request["execution"]["tx_hash"], - } - for request in bundle["bridge_requests"] + request.get_status_json() + for request in bundle.bridge_requests ] error = any( - request["execution"]["error"] - for request in bundle["bridge_requests"] + request.status in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) + for request in bundle.bridge_requests ) return { - "id": bundle["id"], - "status": bundle["status"], + "id": bundle.id, + "status": bundle.status, "bridge_request_status": bridge_request_status, "error": error, } diff --git a/operate/cli.py b/operate/cli.py index c37fe0cba..8a89d00c7 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1015,7 +1015,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: try: data = await request.json() output = operate.bridge_manager().bridge_refill_requirements( - bridge_requests=data["bridge_requests"], + requests_params=data["bridge_requests"], force_update=data.get("force_update", False) ) diff --git a/operate/resource.py b/operate/resource.py index a6fe3c7c7..8ffdbfe7d 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -22,6 +22,7 @@ import enum import json import shutil +import types import typing as t from dataclasses import asdict, is_dataclass from pathlib import Path @@ -51,6 +52,32 @@ def serialize(obj: t.Any) -> t.Any: def deserialize(obj: t.Any, otype: t.Any) -> t.Any: """Desrialize a json object.""" + origin = getattr(otype, '__origin__', None) + + # Handle Union and Optional + if origin is t.Union or isinstance(otype, types.UnionType): + for arg in t.get_args(otype): + if arg is type(None): + continue + try: + return deserialize(obj, arg) + except Exception: + continue + return None + + # Handle list[T] + if origin is list: + (item_type,) = t.get_args(otype) + return [deserialize(item, item_type) for item in obj] + + # Handle dict[K, V] + if origin is dict: + key_type, val_type = t.get_args(otype) + return { + deserialize(k, key_type): deserialize(v, val_type) + for k, v in obj.items() + } + base = getattr(otype, "__class__") # noqa: B009 if base.__name__ == "_GenericAlias": # type: ignore args = otype.__args__ # type: ignore From 8e72e2c089499f87531bc57824d8a5f084212e79 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 22 Apr 2025 09:55:52 +0200 Subject: [PATCH 040/173] chore: tests --- tests/test_bridge_bridge.py | 566 ++++++++++++++++++++++++++++++++++++ 1 file changed, 566 insertions(+) create mode 100644 tests/test_bridge_bridge.py diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py new file mode 100644 index 000000000..6944309f6 --- /dev/null +++ b/tests/test_bridge_bridge.py @@ -0,0 +1,566 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for bridge.bridge module.""" + + +import os +from pathlib import Path +import time + +from operate.constants import ZERO_ADDRESS +import pytest +from deepdiff import DeepDiff + +from operate import wallet +from operate.bridge.bridge import ( + MESSAGE_EXECUTION_SKIPPED, + MESSAGE_QUOTE_ZERO, + BridgeRequestStatus, + BridgeRequest, + LiFiBridgeProvider, + # MESSAGE_EXECUTION_SKIPPED, + # MESSAGE_QUOTE_ZERO, +) +from operate.cli import OperateApp +from operate.operate_types import Chain, LedgerType +from operate.ledger.profiles import OLAS + +ROOT_PATH = Path(__file__).resolve().parent +OPERATE = ".operate_test" + + +class TestLiFiBridge: + """Tests for bridge.bridge.BridgeWorkflow class.""" + + def test_bridge_zero( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_zero""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 0, + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + with pytest.raises(RuntimeError): + bridge.update_execution_status(bridge_request) + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts == 0, "Wrong quote data." + assert qd.elapsed_time == 0, "Wrong quote data." + assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." + assert qd.response is None, "Wrong quote data." + assert timestamp <= qd.timestamp and qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + sj = bridge_request.get_status_json() + expected_sj = { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + br = bridge_request.quote_data.requirements + expected_br = { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: 0, + OLAS[Chain.GNOSIS]: 0 + } + } + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts == 0, "Wrong quote data." + assert qd.elapsed_time == 0, "Wrong quote data." + assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." + assert qd.response is None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + with pytest.raises(RuntimeError): + bridge.update_execution_status(bridge_request) + + timestamp = int(time.time()) + bridge.execute(bridge_request=bridge_request) + ed = bridge_request.execution_data + assert ed is not None, "Missing execution data." + assert ed.bridge_status is None, "Wrong execution data." + assert ed.elapsed_time == 0, "Wrong execution data." + assert ed.explorer_link is None, "Wrong execution data." + assert ed.message == MESSAGE_EXECUTION_SKIPPED, "Wrong execution data." + assert timestamp <= ed.timestamp and ed.timestamp <= int(time.time()), "Wrong execution data." + assert ed.tx_hash is None, "Wrong execution data." + assert ed.tx_status == 0, "Wrong execution data." + assert bridge_request.status == BridgeRequestStatus.EXECUTION_DONE, "Wrong status." + + bridge.update_execution_status(bridge_request) + assert bridge_request.status == BridgeRequestStatus.EXECUTION_DONE, "Wrong status." + + sj = bridge_request.get_status_json() + expected_sj = { + "message": MESSAGE_EXECUTION_SKIPPED, + "status": BridgeRequestStatus.EXECUTION_DONE.value + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + def test_bridge_error( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_error""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1, # This will cause a quote error + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + with pytest.raises(RuntimeError): + bridge.update_execution_status(bridge_request) + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is not None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert timestamp <= qd.timestamp and qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + + assert bridge_request.quote_data is not None, "Wrong quote data." + sj = bridge_request.get_status_json() + expected_sj = { + "message": bridge_request.quote_data.message, + "status": BridgeRequestStatus.QUOTE_FAILED + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + br = bridge_request.quote_data.requirements + expected_br = { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: 0, + OLAS[Chain.GNOSIS]: 0 + } + } + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is not None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + + with pytest.raises(RuntimeError): + bridge.update_execution_status(bridge_request) + + timestamp = int(time.time()) + bridge.execute(bridge_request=bridge_request) + ed = bridge_request.execution_data + assert ed is not None, "Missing execution data." + assert ed.bridge_status is None, "Wrong execution data." + assert ed.elapsed_time == 0, "Wrong execution data." + assert ed.explorer_link is None, "Wrong execution data." + assert ed.message == MESSAGE_EXECUTION_SKIPPED, "Wrong execution data." + assert timestamp <= ed.timestamp and ed.timestamp <= int(time.time()), "Wrong execution data." + assert ed.tx_hash is None, "Wrong execution data." + assert ed.tx_status == 0, "Wrong execution data." + assert bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED, "Wrong status." + + bridge.update_execution_status(bridge_request) + assert bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED, "Wrong status." + + sj = bridge_request.get_status_json() + expected_sj = { + "message": ed.message, + "status": BridgeRequestStatus.EXECUTION_FAILED + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip test on CI.") + def test_bridge_quote( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_quote""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1_000_000_000_000_000_000, + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + with pytest.raises(RuntimeError): + bridge.update_execution_status(bridge_request) + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert timestamp <= qd.timestamp and qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + assert bridge_request.quote_data is not None, "Wrong quote data." + sj = bridge_request.get_status_json() + expected_sj = { + "message": bridge_request.quote_data.message, + "status": BridgeRequestStatus.QUOTE_DONE + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + quote = bridge_request.quote_data.response + br = bridge_request.quote_data.requirements + expected_br = { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: int(quote["transactionRequest"]["value"], 16), + OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]) + } + } + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + with pytest.raises(RuntimeError): + bridge.update_execution_status(bridge_request) + + def test_bundle_zero( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bundle""" + + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + bridge_manager = operate.bridge_manager() + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = [ + { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": ZERO_ADDRESS, + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": ZERO_ADDRESS, + "amount": 0, #1_000_000_000_000_000, + }, + }, + { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 0, # 1_000_000_000_000_000_000, + }, + } + ] + + timestamp1 = time.time() + brr = bridge_manager.bridge_refill_requirements( + requests_params=params, + force_update=False + ) + timestamp2 = time.time() + expected_brr = { + "id": brr["id"], + "balances": { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: 0, + OLAS[Chain.GNOSIS]: 0 + } + } + }, + "bridge_refill_requirements": brr["bridge_refill_requirements"], + "bridge_request_status": [ + { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value + }, + { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value + } + ], + "bridge_total_requirements": brr["bridge_total_requirements"], + "error": False, + "expiration_timestamp": brr["expiration_timestamp"], + "is_refill_required": False, + } + + assert brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0, "Wrong bridge refill requirements." + assert brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0, "Wrong bridge refill requirements." + assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0, "Wrong bridge refill requirements." + assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0, "Wrong bridge refill requirements." + assert not DeepDiff(brr["bridge_refill_requirements"], brr["bridge_total_requirements"]), "Wrong bridge refill requirements." + assert brr["expiration_timestamp"] >= timestamp1, "Wrong bridge refill requirements." + assert brr["expiration_timestamp"] <= timestamp2 + bridge_manager.quote_validity_period, "Wrong bridge refill requirements." + + diff = DeepDiff(brr, expected_brr) + if diff: + print(diff) + + assert not diff, "Wrong bridge refill requirements." + + @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip test on CI.") + def test_bundle( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bundle""" + + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + bridge_manager = operate.bridge_manager() + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = [ + { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": ZERO_ADDRESS, + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": ZERO_ADDRESS, + "amount": 1_000_000_000_000_000, + }, + }, + { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1_000_000_000_000_000_000, + }, + } + ] + + timestamp1 = time.time() + brr = bridge_manager.bridge_refill_requirements( + requests_params=params, + force_update=False + ) + timestamp2 = time.time() + expected_brr = { + "id": brr["id"], + "balances": { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: 0, + OLAS[Chain.GNOSIS]: 0 + } + } + }, + "bridge_refill_requirements": brr["bridge_refill_requirements"], + "bridge_request_status": [ + { + "message": None, + "status": BridgeRequestStatus.QUOTE_DONE.value + }, + { + "message": None, + "status": BridgeRequestStatus.QUOTE_DONE.value + } + ], + "bridge_total_requirements": brr["bridge_total_requirements"], + "error": False, + "expiration_timestamp": brr["expiration_timestamp"], + "is_refill_required": True, + } + + assert brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0, "Wrong bridge refill requirements." + assert brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0, "Wrong bridge refill requirements." + assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] > 0, "Wrong bridge refill requirements." + assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] > 0, "Wrong bridge refill requirements." + assert not DeepDiff(brr["bridge_refill_requirements"], brr["bridge_total_requirements"]), "Wrong bridge refill requirements." + assert brr["expiration_timestamp"] >= timestamp1, "Wrong bridge refill requirements." + assert brr["expiration_timestamp"] <= timestamp2 + bridge_manager.quote_validity_period, "Wrong bridge refill requirements." + + diff = DeepDiff(brr, expected_brr) + if diff: + print(diff) + + assert not diff, "Wrong bridge refill requirements." + From bf8d44c2fb2f8780d0f0c3f97abe31e630deb24c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 22 Apr 2025 10:13:08 +0200 Subject: [PATCH 041/173] chore: linters --- operate/bridge/bridge.py | 138 ++++++++++++------------ operate/cli.py | 6 +- operate/resource.py | 9 +- tests/test_bridge_bridge.py | 202 +++++++++++++++++++++--------------- 4 files changed, 195 insertions(+), 160 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index f4ee050e7..d22613114 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -64,6 +64,7 @@ @dataclass class QuoteData(LocalResource): """QuoteData""" + attempts: int requirements: dict elapsed_time: float @@ -76,6 +77,7 @@ class QuoteData(LocalResource): @dataclass class ExecutionData(LocalResource): """ExecutionData""" + bridge_status: enum.Enum | None elapsed_time: float explorer_link: str | None @@ -87,6 +89,7 @@ class ExecutionData(LocalResource): class BridgeRequestStatus(str, enum.Enum): """BridgeRequestStatus""" + CREATED = "CREATED" QUOTE_DONE = "QUOTE_DONE" QUOTE_FAILED = "QUOTE_FAILED" @@ -102,6 +105,7 @@ def __str__(self) -> str: @dataclass class BridgeRequest(LocalResource): """BridgeRequest""" + params: dict id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" status: BridgeRequestStatus = BridgeRequestStatus.CREATED @@ -118,15 +122,9 @@ def get_status_json(self) -> dict: "tx_hash": self.execution_data.tx_hash, } if self.quote_data: - return { - "message": self.quote_data.message, - "status": self.status.value - } + return {"message": self.quote_data.message, "status": self.status.value} - return { - "message": None, - "status": self.status.value - } + return {"message": None, "status": self.status.value} class BridgeRequestBundleStatus(str, enum.Enum): @@ -153,6 +151,7 @@ def __str__(self) -> str: @dataclass class BridgeRequestBundle(LocalResource): """BridgeRequestBundle""" + bridge_provider: str status: BridgeRequestBundleStatus requests_params: list[dict] @@ -163,8 +162,7 @@ class BridgeRequestBundle(LocalResource): def get_from_chains(self) -> set[Chain]: """Get 'from' chains.""" return { - Chain(request.params["from"]["chain"]) - for request in self.bridge_requests + Chain(request.params["from"]["chain"]) for request in self.bridge_requests } def get_from_addresses(self, chain: Chain) -> set[str]: @@ -273,7 +271,11 @@ class LiFiBridgeProvider(BridgeProvider): def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" - if bridge_request.status not in (BridgeRequestStatus.CREATED, BridgeRequestStatus.QUOTE_DONE, BridgeRequestStatus.QUOTE_FAILED): + if bridge_request.status not in ( + BridgeRequestStatus.CREATED, + BridgeRequestStatus.QUOTE_DONE, + BridgeRequestStatus.QUOTE_FAILED, + ): raise RuntimeError( f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." ) @@ -308,7 +310,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: message=MESSAGE_QUOTE_ZERO, response=None, response_status=0, - timestamp=int(time.time()) + timestamp=int(time.time()), ) bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE @@ -336,7 +338,9 @@ def quote(self, bridge_request: BridgeRequest) -> None: response.raise_for_status() response_json = response.json() from_amount = int(response_json["action"]["fromAmount"]) - transaction_value = int(response_json["transactionRequest"]["value"], 16) + transaction_value = int( + response_json["transactionRequest"]["value"], 16 + ) # TODO: gas fees! if from_token == ZERO_ADDRESS: @@ -362,7 +366,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: message=None, response=response_json, response_status=response.status_code, - timestamp=int(time.time()) + timestamp=int(time.time()), ) bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE @@ -378,7 +382,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: message=str(e), response=None, response_status=HTTPStatus.GATEWAY_TIMEOUT, - timestamp=int(time.time()) + timestamp=int(time.time()), ) except requests.RequestException as e: self.logger.warning( @@ -391,8 +395,10 @@ def quote(self, bridge_request: BridgeRequest) -> None: elapsed_time=time.time() - start, message=response_json.get("message") or str(e), response=response_json, - response_status=getattr(response, "status_code", HTTPStatus.BAD_GATEWAY), - timestamp=int(time.time()) + response_status=getattr( + response, "status_code", HTTPStatus.BAD_GATEWAY + ), + timestamp=int(time.time()), ) if attempt >= DEFAULT_MAX_QUOTE_RETRIES: self.logger.error( @@ -407,7 +413,10 @@ def quote(self, bridge_request: BridgeRequest) -> None: def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" - if bridge_request.status not in (BridgeRequestStatus.QUOTE_DONE, BridgeRequestStatus.QUOTE_FAILED): + if bridge_request.status not in ( + BridgeRequestStatus.QUOTE_DONE, + BridgeRequestStatus.QUOTE_FAILED, + ): raise RuntimeError( f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." ) @@ -434,7 +443,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: message=MESSAGE_EXECUTION_SKIPPED, timestamp=int(timestamp), tx_hash=None, - tx_status=0 + tx_status=0, ) bridge_request.execution_data = execution_data @@ -550,7 +559,10 @@ def _build_bridge_tx( # pylint: disable=unused-argument def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" - if bridge_request.status in (BridgeRequestStatus.EXECUTION_DONE, BridgeRequestStatus.EXECUTION_FAILED): + if bridge_request.status in ( + BridgeRequestStatus.EXECUTION_DONE, + BridgeRequestStatus.EXECUTION_FAILED, + ): return if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): @@ -585,6 +597,7 @@ def update_execution_status(self, bridge_request: BridgeRequest) -> None: elif lifi_status == LiFiTransactionStatus.FAILED: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + @dataclass class BridgeManagerData(LocalResource): """BridgeManagerData""" @@ -592,7 +605,9 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 last_requested_bundle: BridgeRequestBundle | None = None - executed_bundles: dict[str, BridgeRequestBundle] = field(default_factory=dict[str, BridgeRequestBundle]) + executed_bundles: dict[str, BridgeRequestBundle] = field( + default_factory=dict[str, BridgeRequestBundle] + ) _file = "bridge.json" @@ -621,10 +636,6 @@ def load(cls, path: Path) -> "LocalResource": return super().load(path) - - - - class BridgeManager: """BridgeManager""" @@ -697,8 +708,7 @@ def _get_updated_bundle( status=BridgeRequestBundleStatus.CREATED, requests_params=requests_params, bridge_requests=[ - BridgeRequest(params=params) - for params in requests_params + BridgeRequest(params=params) for params in requests_params ], timestamp=now, ) @@ -713,24 +723,21 @@ def _update_bundle_status(self, bundle_id: str) -> None: bundle = self.data.executed_bundles.get(bundle_id) if not bundle: - raise ValueError( - f"[BRIDGE MANAGER] Bundle id {bundle_id} not found." - ) - - if bundle.status in (BridgeRequestBundleStatus.EXECUTION_DONE, BridgeRequestBundleStatus.EXECUTION_FAILED): + raise ValueError(f"[BRIDGE MANAGER] Bundle id {bundle_id} not found.") + + if bundle.status in ( + BridgeRequestBundleStatus.EXECUTION_DONE, + BridgeRequestBundleStatus.EXECUTION_FAILED, + ): return initial_bundle_status = bundle.status - initial_status = [ - request.status for request in bundle.bridge_requests - ] + initial_status = [request.status for request in bundle.bridge_requests] for request in bundle.bridge_requests: self.bridge_provider.update_execution_status(request) - status = [ - request.status for request in bundle.bridge_requests - ] + status = [request.status for request in bundle.bridge_requests] if all( request.status in (BridgeRequestStatus.EXECUTION_DONE) @@ -738,7 +745,11 @@ def _update_bundle_status(self, bundle_id: str) -> None: ): bundle.status = BridgeRequestBundleStatus.EXECUTION_DONE elif all( - request.status in (BridgeRequestStatus.EXECUTION_DONE, BridgeRequestStatus.EXECUTION_FAILED) + request.status + in ( + BridgeRequestStatus.EXECUTION_DONE, + BridgeRequestStatus.EXECUTION_FAILED, + ) for request in bundle.bridge_requests ): bundle.status = BridgeRequestBundleStatus.EXECUTION_FAILED @@ -829,9 +840,7 @@ def bridge_refill_requirements( balances = {} for chain in bundle.get_from_chains(): - ledger_api = self.wallet_manager.load(chain.ledger_type).ledger_api( - chain - ) + ledger_api = self.wallet_manager.load(chain.ledger_type).ledger_api(chain) balances[chain.value] = get_assets_balances( ledger_api=ledger_api, asset_addresses={ZERO_ADDRESS} | bundle.get_from_tokens(chain), @@ -857,23 +866,26 @@ def bridge_refill_requirements( ) bridge_request_status = [ - request.get_status_json() - for request in bundle.bridge_requests + request.get_status_json() for request in bundle.bridge_requests ] error = any( - request.status in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) for request in bundle.bridge_requests + request.status + in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) + for request in bundle.bridge_requests ) - return dict({ - "id": bundle.id, - "balances": balances, - "bridge_refill_requirements": bridge_refill_requirements, - "bridge_request_status": bridge_request_status, - "bridge_total_requirements": bridge_total_requirements, - "error": error, - "expiration_timestamp": bundle.timestamp + self.quote_validity_period, - "is_refill_required": is_refill_required, - }) + return dict( + { + "id": bundle.id, + "balances": balances, + "bridge_refill_requirements": bridge_refill_requirements, + "bridge_request_status": bridge_request_status, + "bridge_total_requirements": bridge_total_requirements, + "error": error, + "expiration_timestamp": bundle.timestamp + self.quote_validity_period, + "is_refill_required": is_refill_required, + } + ) def execute_bundle(self, bundle_id: str) -> dict: """Execute the bundle""" @@ -885,7 +897,7 @@ def execute_bundle(self, bundle_id: str) -> dict: if bundle.id != bundle_id: raise RuntimeError( - f"[BRIDGE MANAGER] Quote bundle id {bundle_id} does not match last requested bundle id {bundle.get('id')}." + f"[BRIDGE MANAGER] Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}." ) requirements = self.bridge_refill_requirements(bundle.requests_params) @@ -905,9 +917,7 @@ def execute_bundle(self, bundle_id: str) -> dict: self.data.last_requested_bundle = None self.data.executed_bundles[bundle.id] = bundle self._store_data() - self.logger.info( - f"[BRIDGE MANAGER] Bundle id {bundle_id} executed." - ) + self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.") return self.get_execution_status(bundle_id) @@ -917,18 +927,16 @@ def get_execution_status(self, bundle_id: str) -> dict: bundle = self.data.executed_bundles.get(bundle_id) if not bundle: - raise ValueError( - f"[BRIDGE MANAGER] Bundle id {bundle_id} not found." - ) + raise ValueError(f"[BRIDGE MANAGER] Bundle id {bundle_id} not found.") self._update_bundle_status(bundle_id) bridge_request_status = [ - request.get_status_json() - for request in bundle.bridge_requests + request.get_status_json() for request in bundle.bridge_requests ] error = any( - request.status in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) + request.status + in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) for request in bundle.bridge_requests ) diff --git a/operate/cli.py b/operate/cli.py index 8a89d00c7..7765c5d7d 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1016,7 +1016,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: data = await request.json() output = operate.bridge_manager().bridge_refill_requirements( requests_params=data["bridge_requests"], - force_update=data.get("force_update", False) + force_update=data.get("force_update", False), ) return JSONResponse( @@ -1044,9 +1044,7 @@ async def _bridge_execute(request: Request) -> JSONResponse: try: data = await request.json() - output = operate.bridge_manager().execute_bundle( - bundle_id=data["id"] - ) + output = operate.bridge_manager().execute_bundle(bundle_id=data["id"]) return JSONResponse( content=output, diff --git a/operate/resource.py b/operate/resource.py index 8ffdbfe7d..e7dfa48c1 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -52,16 +52,16 @@ def serialize(obj: t.Any) -> t.Any: def deserialize(obj: t.Any, otype: t.Any) -> t.Any: """Desrialize a json object.""" - origin = getattr(otype, '__origin__', None) + origin = getattr(otype, "__origin__", None) # Handle Union and Optional if origin is t.Union or isinstance(otype, types.UnionType): for arg in t.get_args(otype): - if arg is type(None): + if arg is type(None): # noqa: E721 continue try: return deserialize(obj, arg) - except Exception: + except Exception: # pylint: disable=broad-except continue return None @@ -74,8 +74,7 @@ def deserialize(obj: t.Any, otype: t.Any) -> t.Any: if origin is dict: key_type, val_type = t.get_args(otype) return { - deserialize(k, key_type): deserialize(v, val_type) - for k, v in obj.items() + deserialize(k, key_type): deserialize(v, val_type) for k, v in obj.items() } base = getattr(otype, "__class__") # noqa: B009 diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 6944309f6..0a2a6b22f 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -21,26 +21,24 @@ import os -from pathlib import Path import time +from pathlib import Path -from operate.constants import ZERO_ADDRESS import pytest from deepdiff import DeepDiff -from operate import wallet -from operate.bridge.bridge import ( - MESSAGE_EXECUTION_SKIPPED, - MESSAGE_QUOTE_ZERO, - BridgeRequestStatus, +from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, + BridgeRequestStatus, LiFiBridgeProvider, - # MESSAGE_EXECUTION_SKIPPED, - # MESSAGE_QUOTE_ZERO, + MESSAGE_EXECUTION_SKIPPED, + MESSAGE_QUOTE_ZERO, ) from operate.cli import OperateApp -from operate.operate_types import Chain, LedgerType +from operate.constants import ZERO_ADDRESS from operate.ledger.profiles import OLAS +from operate.operate_types import Chain, LedgerType + ROOT_PATH = Path(__file__).resolve().parent OPERATE = ".operate_test" @@ -98,28 +96,27 @@ def test_bridge_zero( assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.response is None, "Wrong quote data." - assert timestamp <= qd.timestamp and qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_DONE + ), "Wrong status." sj = bridge_request.get_status_json() expected_sj = { "message": MESSAGE_QUOTE_ZERO, - "status": BridgeRequestStatus.QUOTE_DONE.value + "status": BridgeRequestStatus.QUOTE_DONE.value, } diff = DeepDiff(sj, expected_sj) if diff: print(diff) assert not diff, "Wrong status." + assert bridge_request.quote_data is not None, "Missing quote data." br = bridge_request.quote_data.requirements expected_br = { - "gnosis": { - wallet_address: { - ZERO_ADDRESS: 0, - OLAS[Chain.GNOSIS]: 0 - } - } + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} } diff = DeepDiff(br, expected_br) if diff: @@ -147,18 +144,23 @@ def test_bridge_zero( assert ed.elapsed_time == 0, "Wrong execution data." assert ed.explorer_link is None, "Wrong execution data." assert ed.message == MESSAGE_EXECUTION_SKIPPED, "Wrong execution data." - assert timestamp <= ed.timestamp and ed.timestamp <= int(time.time()), "Wrong execution data." + assert timestamp <= ed.timestamp, "Wrong quote data." + assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hash is None, "Wrong execution data." assert ed.tx_status == 0, "Wrong execution data." - assert bridge_request.status == BridgeRequestStatus.EXECUTION_DONE, "Wrong status." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong status." bridge.update_execution_status(bridge_request) - assert bridge_request.status == BridgeRequestStatus.EXECUTION_DONE, "Wrong status." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong status." sj = bridge_request.get_status_json() expected_sj = { "message": MESSAGE_EXECUTION_SKIPPED, - "status": BridgeRequestStatus.EXECUTION_DONE.value + "status": BridgeRequestStatus.EXECUTION_DONE.value, } diff = DeepDiff(sj, expected_sj) if diff: @@ -215,14 +217,17 @@ def test_bridge_error( assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." - assert timestamp <= qd.timestamp and qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_FAILED + ), "Wrong status." assert bridge_request.quote_data is not None, "Wrong quote data." sj = bridge_request.get_status_json() expected_sj = { "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_FAILED + "status": BridgeRequestStatus.QUOTE_FAILED, } diff = DeepDiff(sj, expected_sj) if diff: @@ -232,12 +237,7 @@ def test_bridge_error( br = bridge_request.quote_data.requirements expected_br = { - "gnosis": { - wallet_address: { - ZERO_ADDRESS: 0, - OLAS[Chain.GNOSIS]: 0 - } - } + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} } diff = DeepDiff(br, expected_br) if diff: @@ -252,7 +252,9 @@ def test_bridge_error( assert qd.message is not None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_FAILED + ), "Wrong status." with pytest.raises(RuntimeError): bridge.update_execution_status(bridge_request) @@ -265,18 +267,23 @@ def test_bridge_error( assert ed.elapsed_time == 0, "Wrong execution data." assert ed.explorer_link is None, "Wrong execution data." assert ed.message == MESSAGE_EXECUTION_SKIPPED, "Wrong execution data." - assert timestamp <= ed.timestamp and ed.timestamp <= int(time.time()), "Wrong execution data." + assert timestamp <= ed.timestamp, "Wrong quote data." + assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hash is None, "Wrong execution data." assert ed.tx_status == 0, "Wrong execution data." - assert bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED, "Wrong status." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong status." bridge.update_execution_status(bridge_request) - assert bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED, "Wrong status." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong status." sj = bridge_request.get_status_json() expected_sj = { "message": ed.message, - "status": BridgeRequestStatus.EXECUTION_FAILED + "status": BridgeRequestStatus.EXECUTION_FAILED, } diff = DeepDiff(sj, expected_sj) if diff: @@ -334,20 +341,24 @@ def test_bridge_quote( assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." - assert timestamp <= qd.timestamp and qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_DONE + ), "Wrong status." assert bridge_request.quote_data is not None, "Wrong quote data." sj = bridge_request.get_status_json() expected_sj = { "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_DONE + "status": BridgeRequestStatus.QUOTE_DONE, } diff = DeepDiff(sj, expected_sj) if diff: print(diff) assert not diff, "Wrong status." + assert bridge_request.quote_data.response is not None, "Missing quote data." quote = bridge_request.quote_data.response br = bridge_request.quote_data.requirements @@ -355,7 +366,7 @@ def test_bridge_quote( "gnosis": { wallet_address: { ZERO_ADDRESS: int(quote["transactionRequest"]["value"], 16), - OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]) + OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore } } } @@ -405,7 +416,7 @@ def test_bundle_zero( "chain": "base", "address": wallet_address, "token": ZERO_ADDRESS, - "amount": 0, #1_000_000_000_000_000, + "amount": 0, # 1_000_000_000_000_000, }, }, { @@ -420,35 +431,29 @@ def test_bundle_zero( "token": OLAS[Chain.BASE], "amount": 0, # 1_000_000_000_000_000_000, }, - } + }, ] timestamp1 = time.time() brr = bridge_manager.bridge_refill_requirements( - requests_params=params, - force_update=False + requests_params=params, force_update=False ) timestamp2 = time.time() expected_brr = { "id": brr["id"], "balances": { - "gnosis": { - wallet_address: { - ZERO_ADDRESS: 0, - OLAS[Chain.GNOSIS]: 0 - } - } + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} }, "bridge_refill_requirements": brr["bridge_refill_requirements"], "bridge_request_status": [ { "message": MESSAGE_QUOTE_ZERO, - "status": BridgeRequestStatus.QUOTE_DONE.value + "status": BridgeRequestStatus.QUOTE_DONE.value, }, { "message": MESSAGE_QUOTE_ZERO, - "status": BridgeRequestStatus.QUOTE_DONE.value - } + "status": BridgeRequestStatus.QUOTE_DONE.value, + }, ], "bridge_total_requirements": brr["bridge_total_requirements"], "error": False, @@ -456,13 +461,32 @@ def test_bundle_zero( "is_refill_required": False, } - assert brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0, "Wrong bridge refill requirements." - assert brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0, "Wrong bridge refill requirements." - assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0, "Wrong bridge refill requirements." - assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0, "Wrong bridge refill requirements." - assert not DeepDiff(brr["bridge_refill_requirements"], brr["bridge_total_requirements"]), "Wrong bridge refill requirements." - assert brr["expiration_timestamp"] >= timestamp1, "Wrong bridge refill requirements." - assert brr["expiration_timestamp"] <= timestamp2 + bridge_manager.quote_validity_period, "Wrong bridge refill requirements." + assert ( + brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] + == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["bridge_refill_requirements"]["gnosis"][wallet_address][ + OLAS[Chain.GNOSIS] + ] + == 0 + ), "Wrong bridge refill requirements." + assert not DeepDiff( + brr["bridge_refill_requirements"], brr["bridge_total_requirements"] + ), "Wrong bridge refill requirements." + assert ( + brr["expiration_timestamp"] >= timestamp1 + ), "Wrong bridge refill requirements." + assert ( + brr["expiration_timestamp"] + <= timestamp2 + bridge_manager.quote_validity_period + ), "Wrong bridge refill requirements." diff = DeepDiff(brr, expected_brr) if diff: @@ -514,35 +538,23 @@ def test_bundle( "token": OLAS[Chain.BASE], "amount": 1_000_000_000_000_000_000, }, - } + }, ] timestamp1 = time.time() brr = bridge_manager.bridge_refill_requirements( - requests_params=params, - force_update=False + requests_params=params, force_update=False ) timestamp2 = time.time() expected_brr = { "id": brr["id"], "balances": { - "gnosis": { - wallet_address: { - ZERO_ADDRESS: 0, - OLAS[Chain.GNOSIS]: 0 - } - } + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} }, "bridge_refill_requirements": brr["bridge_refill_requirements"], "bridge_request_status": [ - { - "message": None, - "status": BridgeRequestStatus.QUOTE_DONE.value - }, - { - "message": None, - "status": BridgeRequestStatus.QUOTE_DONE.value - } + {"message": None, "status": BridgeRequestStatus.QUOTE_DONE.value}, + {"message": None, "status": BridgeRequestStatus.QUOTE_DONE.value}, ], "bridge_total_requirements": brr["bridge_total_requirements"], "error": False, @@ -550,17 +562,35 @@ def test_bundle( "is_refill_required": True, } - assert brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0, "Wrong bridge refill requirements." - assert brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0, "Wrong bridge refill requirements." - assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] > 0, "Wrong bridge refill requirements." - assert brr["bridge_refill_requirements"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] > 0, "Wrong bridge refill requirements." - assert not DeepDiff(brr["bridge_refill_requirements"], brr["bridge_total_requirements"]), "Wrong bridge refill requirements." - assert brr["expiration_timestamp"] >= timestamp1, "Wrong bridge refill requirements." - assert brr["expiration_timestamp"] <= timestamp2 + bridge_manager.quote_validity_period, "Wrong bridge refill requirements." + assert ( + brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] + > 0 + ), "Wrong bridge refill requirements." + assert ( + brr["bridge_refill_requirements"]["gnosis"][wallet_address][ + OLAS[Chain.GNOSIS] + ] + > 0 + ), "Wrong bridge refill requirements." + assert not DeepDiff( + brr["bridge_refill_requirements"], brr["bridge_total_requirements"] + ), "Wrong bridge refill requirements." + assert ( + brr["expiration_timestamp"] >= timestamp1 + ), "Wrong bridge refill requirements." + assert ( + brr["expiration_timestamp"] + <= timestamp2 + bridge_manager.quote_validity_period + ), "Wrong bridge refill requirements." diff = DeepDiff(brr, expected_brr) if diff: print(diff) assert not diff, "Wrong bridge refill requirements." - From cc91e82b0d1fd5e23899e41669d821a89eb5088a Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 10:28:34 +0200 Subject: [PATCH 042/173] chore: update to typing --- operate/bridge/bridge.py | 134 +++++++++++++++++---------------------- operate/resource.py | 22 +++---- 2 files changed, 70 insertions(+), 86 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index d22613114..80f25c2ca 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -33,6 +33,7 @@ from typing import cast from urllib.parse import urlencode +from pytest import param import requests from aea.helpers.logging import setup_logger from autonomy.chain.base import registry_contracts @@ -66,10 +67,10 @@ class QuoteData(LocalResource): """QuoteData""" attempts: int - requirements: dict + requirements: t.Dict elapsed_time: float - message: str | None - response: dict | None + message: t.Optional[str] + response: t.Optional[t.Dict] response_status: int timestamp: int @@ -78,12 +79,12 @@ class QuoteData(LocalResource): class ExecutionData(LocalResource): """ExecutionData""" - bridge_status: enum.Enum | None + bridge_status: t.Optional[enum.Enum] elapsed_time: float - explorer_link: str | None - message: str | None + explorer_link: t.Optional[str] + message: t.Optional[str] timestamp: int - tx_hash: str | None + tx_hash: t.Optional[str] tx_status: int @@ -106,13 +107,13 @@ def __str__(self) -> str: class BridgeRequest(LocalResource): """BridgeRequest""" - params: dict + params: t.Dict id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" status: BridgeRequestStatus = BridgeRequestStatus.CREATED - quote_data: QuoteData | None = None - execution_data: ExecutionData | None = None + quote_data: t.Optional[QuoteData] = None + execution_data: t.Optional[ExecutionData] = None - def get_status_json(self) -> dict: + def get_status_json(self) -> t.Dict: """JSON representation of the status.""" if self.execution_data: return { @@ -136,12 +137,7 @@ class BridgeRequestBundleStatus(str, enum.Enum): EXECUTION_PENDING = "EXECUTION_PENDING" EXECUTION_DONE = "EXECUTION_DONE" EXECUTION_FAILED = "EXECUTION_FAILED" - - # CREATED = "CREATED" - # QUOTED = "QUOTED" - # QUOTED2 = "QUOTED" - # SUBMITTED = "SUBMITTED" - # FINISHED = "FINISHED" # All requests in the bundle are either done or failed. + UNKNOWN = "UNKNOWN" def __str__(self) -> str: """__str__""" @@ -153,12 +149,33 @@ class BridgeRequestBundle(LocalResource): """BridgeRequestBundle""" bridge_provider: str - status: BridgeRequestBundleStatus - requests_params: list[dict] - bridge_requests: list[BridgeRequest] + requests_params: t.List[t.Dict] + bridge_requests: t.List[BridgeRequest] timestamp: int id: str + @property + def status(self) -> BridgeRequestBundleStatus: + """Status""" + statuses = {request.status for request in self.bridge_requests} + + if BridgeRequestStatus.EXECUTION_PENDING in statuses: + return BridgeRequestBundleStatus.EXECUTION_PENDING + + if BridgeRequestStatus.EXECUTION_FAILED in statuses: + return BridgeRequestBundleStatus.EXECUTION_FAILED + + if statuses == {BridgeRequestStatus.EXECUTION_DONE}: + return BridgeRequestBundleStatus.EXECUTION_DONE + + if BridgeRequestStatus.QUOTE_FAILED in statuses: + return BridgeRequestBundleStatus.QUOTE_FAILED + + if statuses == {BridgeRequestStatus.QUOTE_DONE}: + return BridgeRequestBundleStatus.QUOTE_DONE + + return BridgeRequestBundleStatus.CREATED + def get_from_chains(self) -> set[Chain]: """Get 'from' chains.""" return { @@ -183,10 +200,10 @@ def get_from_tokens(self, chain: Chain) -> set[str]: if request.params["from"]["chain"] == chain_str } - def sum_bridge_requirements(self) -> dict: + def sum_bridge_requirements(self) -> t.Dict: """Sum bridge requirements.""" - bridge_total_requirements: dict = {} + bridge_total_requirements: t.Dict = {} for request in self.bridge_requests: if not request.quote_data: @@ -212,7 +229,7 @@ class BridgeProvider: def __init__( self, wallet_manager: MasterWalletManager, - logger: logging.Logger | None = None, + logger: t.Optional[logging.Logger] = None, ) -> None: """Initialize the bridge provider.""" self.wallet_manager = wallet_manager @@ -435,12 +452,12 @@ def execute(self, bridge_request: BridgeRequest) -> None: quote = bridge_request.quote_data.response if not quote or "action" not in quote: - self.logger.info("[LI.FI BRIDGE] Skipping quote execution.") + self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})") execution_data = ExecutionData( bridge_status=None, elapsed_time=0, explorer_link=None, - message=MESSAGE_EXECUTION_SKIPPED, + message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", timestamp=int(timestamp), tx_hash=None, tx_status=0, @@ -481,7 +498,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: # higher-level layer (e.g., wallet?) def _build_approval_tx( # pylint: disable=unused-argument *args: t.Any, **kargs: t.Any - ) -> dict: + ) -> t.Dict: return registry_contracts.erc20.get_approve_tx( ledger_api=wallet.ledger_api(from_chain), contract_address=from_token, @@ -505,7 +522,7 @@ def _build_approval_tx( # pylint: disable=unused-argument def _build_bridge_tx( # pylint: disable=unused-argument *args: t.Any, **kargs: t.Any - ) -> dict: + ) -> t.Dict: w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) return { "value": int(transaction_request["value"], 16), @@ -604,9 +621,9 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 - last_requested_bundle: BridgeRequestBundle | None = None - executed_bundles: dict[str, BridgeRequestBundle] = field( - default_factory=dict[str, BridgeRequestBundle] + last_requested_bundle: t.Optional[BridgeRequestBundle] = None + executed_bundles: t.Dict[str, BridgeRequestBundle] = field( + default_factory=dict ) _file = "bridge.json" @@ -645,9 +662,9 @@ def __init__( self, path: Path, wallet_manager: MasterWalletManager, - logger: logging.Logger | None = None, - bridge_provider: BridgeProvider | None = None, - quote_validity_period: int | None = None, + logger: t.Optional[logging.Logger] = None, + bridge_provider: t.Optional[BridgeProvider] = None, + quote_validity_period: int = DEFAULT_QUOTE_VALIDITY_PERIOD, ) -> None: """Initialize bridge manager.""" self.path = path @@ -656,10 +673,7 @@ def __init__( self.bridge_provider = bridge_provider or LiFiBridgeProvider( wallet_manager, logger ) - self.quote_validity_period = ( - quote_validity_period or DEFAULT_QUOTE_VALIDITY_PERIOD - ) - + self.quote_validity_period = quote_validity_period self.path.mkdir(exist_ok=True) self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) @@ -670,7 +684,7 @@ def _store_data(self) -> None: self.data.store() def _get_updated_bundle( - self, requests_params: list[dict], force_update: bool + self, requests_params: t.List[t.Dict], force_update: bool ) -> BridgeRequestBundle: """Ensures to return a valid (non expired) bundle for the given inputs.""" @@ -705,7 +719,6 @@ def _get_updated_bundle( bundle = BridgeRequestBundle( id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}", bridge_provider=self.bridge_provider.name(), - status=BridgeRequestBundleStatus.CREATED, requests_params=requests_params, bridge_requests=[ BridgeRequest(params=params) for params in requests_params @@ -739,27 +752,10 @@ def _update_bundle_status(self, bundle_id: str) -> None: status = [request.status for request in bundle.bridge_requests] - if all( - request.status in (BridgeRequestStatus.EXECUTION_DONE) - for request in bundle.bridge_requests - ): - bundle.status = BridgeRequestBundleStatus.EXECUTION_DONE - elif all( - request.status - in ( - BridgeRequestStatus.EXECUTION_DONE, - BridgeRequestStatus.EXECUTION_FAILED, - ) - for request in bundle.bridge_requests - ): - bundle.status = BridgeRequestBundleStatus.EXECUTION_FAILED - else: - bundle.status = BridgeRequestBundleStatus.EXECUTION_PENDING - if initial_bundle_status != bundle.status or initial_status != status: self._store_data() - def _raise_if_invalid(self, bridge_requests: list) -> None: + def _raise_if_invalid(self, bridge_requests: t.List) -> None: """Preprocess quote requests.""" seen: set = set() @@ -827,8 +823,8 @@ def _raise_if_invalid(self, bridge_requests: list) -> None: ) def bridge_refill_requirements( - self, requests_params: list[dict], force_update: bool = False - ) -> dict: + self, requests_params: t.List[t.Dict], force_update: bool = False + ) -> t.Dict: """Get bridge refill requirements.""" self._raise_if_invalid(requests_params) @@ -849,7 +845,7 @@ def bridge_refill_requirements( bridge_total_requirements = bundle.sum_bridge_requirements() - bridge_refill_requirements: dict = {} + bridge_refill_requirements = {} for from_chain, from_addresses in bridge_total_requirements.items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): @@ -868,11 +864,6 @@ def bridge_refill_requirements( bridge_request_status = [ request.get_status_json() for request in bundle.bridge_requests ] - error = any( - request.status - in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) - for request in bundle.bridge_requests - ) return dict( { @@ -881,13 +872,13 @@ def bridge_refill_requirements( "bridge_refill_requirements": bridge_refill_requirements, "bridge_request_status": bridge_request_status, "bridge_total_requirements": bridge_total_requirements, - "error": error, "expiration_timestamp": bundle.timestamp + self.quote_validity_period, "is_refill_required": is_refill_required, + "status": bundle.status } ) - def execute_bundle(self, bundle_id: str) -> dict: + def execute_bundle(self, bundle_id: str) -> t.Dict: """Execute the bundle""" bundle = self.data.last_requested_bundle @@ -908,7 +899,6 @@ def execute_bundle(self, bundle_id: str) -> dict: ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") - bundle.status = BridgeRequestBundleStatus.EXECUTION_PENDING for request in bundle.bridge_requests: self.bridge_provider.execute(request) @@ -921,7 +911,7 @@ def execute_bundle(self, bundle_id: str) -> dict: return self.get_execution_status(bundle_id) - def get_execution_status(self, bundle_id: str) -> dict: + def get_execution_status(self, bundle_id: str) -> t.Dict: """Get execution status of bundle.""" bundle = self.data.executed_bundles.get(bundle_id) @@ -934,15 +924,9 @@ def get_execution_status(self, bundle_id: str) -> dict: bridge_request_status = [ request.get_status_json() for request in bundle.bridge_requests ] - error = any( - request.status - in (BridgeRequestStatus.QUOTE_FAILED, BridgeRequestStatus.EXECUTION_FAILED) - for request in bundle.bridge_requests - ) return { "id": bundle.id, "status": bundle.status, "bridge_request_status": bridge_request_status, - "error": error, } diff --git a/operate/resource.py b/operate/resource.py index e7dfa48c1..e49822468 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -65,17 +65,17 @@ def deserialize(obj: t.Any, otype: t.Any) -> t.Any: continue return None - # Handle list[T] - if origin is list: - (item_type,) = t.get_args(otype) - return [deserialize(item, item_type) for item in obj] - - # Handle dict[K, V] - if origin is dict: - key_type, val_type = t.get_args(otype) - return { - deserialize(k, key_type): deserialize(v, val_type) for k, v in obj.items() - } + # # Handle list[T] + # if origin is list: + # (item_type,) = t.get_args(otype) + # return [deserialize(item, item_type) for item in obj] + + # # Handle dict[K, V] + # if origin is dict: + # key_type, val_type = t.get_args(otype) + # return { + # deserialize(k, key_type): deserialize(v, val_type) for k, v in obj.items() + # } base = getattr(otype, "__class__") # noqa: B009 if base.__name__ == "_GenericAlias": # type: ignore From 29db7bb9b6d265ab9eab78bc0b08858a6860fd1e Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 10:28:43 +0200 Subject: [PATCH 043/173] chore: update docs --- docs/api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 3d1cd56b2..ca057d02c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1035,9 +1035,11 @@ Executes a quote bundle. See [GET /api/bridge/status/{quote_bundle_id}](#get-api Gets the status of a quote bundle. The attribute `status` can take the following values sequentially: - `CREATED`: The quote bundle internal data structure has been created, but no quotes have been requested yet. -- `QUOTED`: A quote is available. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. -- `SUBMITTED`: The quote bundle has been submitted for execution. -- `FINISHED`: All the quote executions in the bundle have reached their final state (either done or failed). No more updates are expected for this quote bundle. +- `QUOTE_DONE`: A quote is available for all bridge requests. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. +- `QUOTE_FAILED`: Quote failed for some bridge requests. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. +- `EXECUTION_PENDING`: Execution submitted and pending to be finalized for some bridge request. +- `EXECUTION_DONE`: Execution finalized successfully for all requests. +- `EXECUTION_FAILED`: Execution failed. Individual bridge request status: From 92a10b909c4d1a3fea088cc3c46621ad90cc62b0 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 11:51:31 +0200 Subject: [PATCH 044/173] chore: error codes --- operate/cli.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index 7765c5d7d..f1ed80cf3 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -43,7 +43,7 @@ from operate import services from operate.account.user import UserAccount -from operate.bridge.bridge import BridgeManager +from operate.bridge.bridge import BridgeManager, BridgeRequestBundleStatus from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES from operate.ledger.profiles import DEFAULT_NEW_SAFE_FUNDS_AMOUNT from operate.migration import MigrationManager @@ -1021,9 +1021,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: return JSONResponse( content=output, - status_code=HTTPStatus.BAD_GATEWAY - if output["error"] - else HTTPStatus.OK, + status_code=HTTPStatus.OK, ) except ValueError as e: return JSONResponse( @@ -1048,9 +1046,7 @@ async def _bridge_execute(request: Request) -> JSONResponse: return JSONResponse( content=output, - status_code=HTTPStatus.BAD_GATEWAY - if output["errors"] - else HTTPStatus.OK, + status_code=HTTPStatus.OK, ) except ValueError as e: return JSONResponse( From 58e94e4dcc1129b632d690fbf97a0a8dd623a2ff Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 13:05:15 +0200 Subject: [PATCH 045/173] fix: linters --- operate/bridge/bridge.py | 13 ++--- operate/cli.py | 2 +- operate/resource.py | 12 ---- tests/test_bridge_bridge.py | 106 ++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 20 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 80f25c2ca..cff4e076b 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -33,7 +33,6 @@ from typing import cast from urllib.parse import urlencode -from pytest import param import requests from aea.helpers.logging import setup_logger from autonomy.chain.base import registry_contracts @@ -452,7 +451,9 @@ def execute(self, bridge_request: BridgeRequest) -> None: quote = bridge_request.quote_data.response if not quote or "action" not in quote: - self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})") + self.logger.info( + f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" + ) execution_data = ExecutionData( bridge_status=None, elapsed_time=0, @@ -622,9 +623,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 last_requested_bundle: t.Optional[BridgeRequestBundle] = None - executed_bundles: t.Dict[str, BridgeRequestBundle] = field( - default_factory=dict - ) + executed_bundles: t.Dict[str, BridgeRequestBundle] = field(default_factory=dict) _file = "bridge.json" @@ -845,7 +844,7 @@ def bridge_refill_requirements( bridge_total_requirements = bundle.sum_bridge_requirements() - bridge_refill_requirements = {} + bridge_refill_requirements: t.Dict = {} for from_chain, from_addresses in bridge_total_requirements.items(): for from_address, from_tokens in from_addresses.items(): for from_token, from_amount in from_tokens.items(): @@ -874,7 +873,7 @@ def bridge_refill_requirements( "bridge_total_requirements": bridge_total_requirements, "expiration_timestamp": bundle.timestamp + self.quote_validity_period, "is_refill_required": is_refill_required, - "status": bundle.status + "status": bundle.status, } ) diff --git a/operate/cli.py b/operate/cli.py index f1ed80cf3..0d4c4b663 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -43,7 +43,7 @@ from operate import services from operate.account.user import UserAccount -from operate.bridge.bridge import BridgeManager, BridgeRequestBundleStatus +from operate.bridge.bridge import BridgeManager from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES from operate.ledger.profiles import DEFAULT_NEW_SAFE_FUNDS_AMOUNT from operate.migration import MigrationManager diff --git a/operate/resource.py b/operate/resource.py index e49822468..933e127b3 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -65,18 +65,6 @@ def deserialize(obj: t.Any, otype: t.Any) -> t.Any: continue return None - # # Handle list[T] - # if origin is list: - # (item_type,) = t.get_args(otype) - # return [deserialize(item, item_type) for item in obj] - - # # Handle dict[K, V] - # if origin is dict: - # key_type, val_type = t.get_args(otype) - # return { - # deserialize(k, key_type): deserialize(v, val_type) for k, v in obj.items() - # } - base = getattr(otype, "__class__") # noqa: B009 if base.__name__ == "_GenericAlias": # type: ignore args = otype.__args__ # type: ignore diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 0a2a6b22f..d5b143127 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -494,6 +494,112 @@ def test_bundle_zero( assert not diff, "Wrong bridge refill requirements." + def test_bundle_error( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bundle""" + + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + bridge_manager = operate.bridge_manager() + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = [ + { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": ZERO_ADDRESS, + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": ZERO_ADDRESS, + "amount": 1, + }, + }, + { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 0, # 1_000_000_000_000_000_000, + }, + }, + ] + + timestamp1 = time.time() + brr = bridge_manager.bridge_refill_requirements( + requests_params=params, force_update=False + ) + timestamp2 = time.time() + expected_brr = { + "id": brr["id"], + "balances": { + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + }, + "bridge_refill_requirements": brr["bridge_refill_requirements"], + "bridge_request_status": [ + { + "message": brr["bridge_request_status"][0]["message"], + "status": BridgeRequestStatus.QUOTE_FAILED.value, + }, + { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value, + }, + ], + "bridge_total_requirements": brr["bridge_total_requirements"], + "error": False, + "expiration_timestamp": brr["expiration_timestamp"], + "is_refill_required": False, + } + + assert ( + brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] + == 0 + ), "Wrong bridge refill requirements." + assert ( + brr["bridge_refill_requirements"]["gnosis"][wallet_address][ + OLAS[Chain.GNOSIS] + ] + == 0 + ), "Wrong bridge refill requirements." + assert not DeepDiff( + brr["bridge_refill_requirements"], brr["bridge_total_requirements"] + ), "Wrong bridge refill requirements." + assert ( + brr["expiration_timestamp"] >= timestamp1 + ), "Wrong bridge refill requirements." + assert ( + brr["expiration_timestamp"] + <= timestamp2 + bridge_manager.quote_validity_period + ), "Wrong bridge refill requirements." + + diff = DeepDiff(brr, expected_brr) + if diff: + print(diff) + + assert not diff, "Wrong bridge refill requirements." + @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip test on CI.") def test_bundle( self, From 282c28dd32c639a2a0e41ba3e273fc7bad6f247e Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 13:09:22 +0200 Subject: [PATCH 046/173] fix: linter --- operate/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/resource.py b/operate/resource.py index 933e127b3..deede8e14 100644 --- a/operate/resource.py +++ b/operate/resource.py @@ -61,7 +61,7 @@ def deserialize(obj: t.Any, otype: t.Any) -> t.Any: continue try: return deserialize(obj, arg) - except Exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except # nosec continue return None From 895c22490195d09dfea445f9bbbfcbc9ac97fdd6 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 17:03:03 +0200 Subject: [PATCH 047/173] chore: fix gas fees --- operate/bridge/bridge.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index cff4e076b..8fb838a6c 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -357,19 +357,21 @@ def quote(self, bridge_request: BridgeRequest) -> None: transaction_value = int( response_json["transactionRequest"]["value"], 16 ) + gas_price = int(response_json["gasPrice"], 16) + gas_limit = int(response_json["gasLimit"], 16) + gas_fees = gas_price * gas_limit - # TODO: gas fees! if from_token == ZERO_ADDRESS: requirements = { from_chain: { - from_address: {from_token: from_amount + transaction_value} + from_address: {from_token: transaction_value + gas_fees} } } else: requirements = { from_chain: { from_address: { - ZERO_ADDRESS: transaction_value, + ZERO_ADDRESS: transaction_value + gas_fees, from_token: from_amount, } } From 06805f15fb5e7f5b102009124b7d48d1c49e9017 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 18:58:21 +0200 Subject: [PATCH 048/173] chore: fix gas price --- operate/bridge/bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 8fb838a6c..e10188067 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -357,8 +357,8 @@ def quote(self, bridge_request: BridgeRequest) -> None: transaction_value = int( response_json["transactionRequest"]["value"], 16 ) - gas_price = int(response_json["gasPrice"], 16) - gas_limit = int(response_json["gasLimit"], 16) + gas_price = int(response_json["transactionRequest"]["gasPrice"], 16) + gas_limit = int(response_json["transactionRequest"]["gasLimit"], 16) gas_fees = gas_price * gas_limit if from_token == ZERO_ADDRESS: From 32e6ae6100ceabbacca4b0543e9b12d26b7c47ab Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 21:23:15 +0200 Subject: [PATCH 049/173] chore: update --- operate/bridge/bridge.py | 50 +++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index e10188067..f739570a2 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -353,12 +353,10 @@ def quote(self, bridge_request: BridgeRequest) -> None: ) response.raise_for_status() response_json = response.json() - from_amount = int(response_json["action"]["fromAmount"]) - transaction_value = int( - response_json["transactionRequest"]["value"], 16 - ) - gas_price = int(response_json["transactionRequest"]["gasPrice"], 16) - gas_limit = int(response_json["transactionRequest"]["gasLimit"], 16) + transaction_request = response_json["transactionRequest"] + transaction_value = int(transaction_request["value"], 16) + gas_price = int(transaction_request["gasPrice"], 16) + gas_limit = int(transaction_request["gasLimit"], 16) gas_fees = gas_price * gas_limit if from_token == ZERO_ADDRESS: @@ -368,6 +366,26 @@ def quote(self, bridge_request: BridgeRequest) -> None: } } else: + from_amount = int(response_json["action"]["fromAmount"]) + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + + approve_tx = registry_contracts.erc20.get_approve_tx( + ledger_api=wallet.ledger_api(chain), + contract_address=from_token, + spender=transaction_request["to"], + sender=transaction_request["from"], + amount=from_amount, + ) + approve_tx = ledger_api.update_with_gas_estimate( + transaction=approve_tx, + raise_on_try=True, + ) + gas_price = approve_tx["gas"] + approve_gas_limit = approve_tx["gas"] + gas_fees = (gas_limit + approve_gas_limit) * gas_price + requirements = { from_chain: { from_address: { @@ -479,13 +497,13 @@ def execute(self, bridge_request: BridgeRequest) -> None: from_amount = int(quote["action"]["fromAmount"]) transaction_request = quote["transactionRequest"] - from_chain = Chain.from_id(transaction_request["chainId"]) - wallet = self.wallet_manager.load(from_chain.ledger_type) + chain = Chain.from_id(transaction_request["chainId"]) + wallet = self.wallet_manager.load(chain.ledger_type) tx_settler = TxSettler( - ledger_api=wallet.ledger_api(from_chain), + ledger_api=wallet.ledger_api(chain), crypto=wallet.crypto, - chain_type=from_chain, + chain_type=chain, timeout=ON_CHAIN_INTERACT_TIMEOUT, retries=ON_CHAIN_INTERACT_RETRIES, sleep=ON_CHAIN_INTERACT_SLEEP, @@ -497,20 +515,20 @@ def execute(self, bridge_request: BridgeRequest) -> None: f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." ) - # TODO Approval is done on several places. Consider exporting to a + # TODO Approve is done on several places. Consider exporting to a # higher-level layer (e.g., wallet?) - def _build_approval_tx( # pylint: disable=unused-argument + def _build_approve_tx( # pylint: disable=unused-argument *args: t.Any, **kargs: t.Any ) -> t.Dict: return registry_contracts.erc20.get_approve_tx( - ledger_api=wallet.ledger_api(from_chain), + ledger_api=wallet.ledger_api(chain), contract_address=from_token, spender=transaction_request["to"], sender=transaction_request["from"], amount=from_amount, ) - setattr(tx_settler, "build", _build_approval_tx) # noqa: B010 + setattr(tx_settler, "build", _build_approve_tx) # noqa: B010 tx_settler.transact( method=lambda: {}, contract="", @@ -526,7 +544,7 @@ def _build_approval_tx( # pylint: disable=unused-argument def _build_bridge_tx( # pylint: disable=unused-argument *args: t.Any, **kargs: t.Any ) -> t.Dict: - w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=from_chain))) + w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=chain))) return { "value": int(transaction_request["value"], 16), "to": transaction_request["to"], @@ -800,7 +818,7 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: wallet = self.wallet_manager.load(Chain(from_chain).ledger_type) wallet_address = wallet.address - safe_address = wallet.safes.get(from_chain) + safe_address = wallet.safes.get(Chain(from_chain)) if from_address is None or not ( from_address == wallet_address or from_address == safe_address From 702907404f4fcb1add78af3ccca804a87df123e3 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 23:04:18 +0200 Subject: [PATCH 050/173] fix: tests --- operate/bridge/bridge.py | 4 ++-- tests/test_bridge_bridge.py | 31 ++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index f739570a2..9346cc39d 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -893,7 +893,7 @@ def bridge_refill_requirements( "bridge_total_requirements": bridge_total_requirements, "expiration_timestamp": bundle.timestamp + self.quote_validity_period, "is_refill_required": is_refill_required, - "status": bundle.status, + "status": bundle.status.value, } ) @@ -946,6 +946,6 @@ def get_execution_status(self, bundle_id: str) -> t.Dict: return { "id": bundle.id, - "status": bundle.status, + "status": bundle.status.value, "bridge_request_status": bridge_request_status, } diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index d5b143127..53bfc4fde 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -29,6 +29,7 @@ from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, + BridgeRequestBundleStatus, BridgeRequestStatus, LiFiBridgeProvider, MESSAGE_EXECUTION_SKIPPED, @@ -143,7 +144,8 @@ def test_bridge_zero( assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.explorer_link is None, "Wrong execution data." - assert ed.message == MESSAGE_EXECUTION_SKIPPED, "Wrong execution data." + assert ed.message is not None, "Wrong execution data." + assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hash is None, "Wrong execution data." @@ -158,8 +160,11 @@ def test_bridge_zero( ), "Wrong status." sj = bridge_request.get_status_json() + assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { - "message": MESSAGE_EXECUTION_SKIPPED, + "explorer_link": sj["explorer_link"], + "tx_hash": None, + "message": sj["message"], "status": BridgeRequestStatus.EXECUTION_DONE.value, } diff = DeepDiff(sj, expected_sj) @@ -227,7 +232,7 @@ def test_bridge_error( sj = bridge_request.get_status_json() expected_sj = { "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_FAILED, + "status": BridgeRequestStatus.QUOTE_FAILED.value, } diff = DeepDiff(sj, expected_sj) if diff: @@ -266,7 +271,8 @@ def test_bridge_error( assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.explorer_link is None, "Wrong execution data." - assert ed.message == MESSAGE_EXECUTION_SKIPPED, "Wrong execution data." + assert ed.message is not None, "Wrong execution data." + assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hash is None, "Wrong execution data." @@ -281,9 +287,12 @@ def test_bridge_error( ), "Wrong status." sj = bridge_request.get_status_json() + assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { - "message": ed.message, - "status": BridgeRequestStatus.EXECUTION_FAILED, + "explorer_link": sj["explorer_link"], + "tx_hash": None, + "message": sj["message"], + "status": BridgeRequestStatus.EXECUTION_FAILED.value, } diff = DeepDiff(sj, expected_sj) if diff: @@ -351,7 +360,7 @@ def test_bridge_quote( sj = bridge_request.get_status_json() expected_sj = { "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_DONE, + "status": BridgeRequestStatus.QUOTE_DONE.value, } diff = DeepDiff(sj, expected_sj) if diff: @@ -365,7 +374,7 @@ def test_bridge_quote( expected_br = { "gnosis": { wallet_address: { - ZERO_ADDRESS: int(quote["transactionRequest"]["value"], 16), + ZERO_ADDRESS: br["gnosis"][wallet_address][ZERO_ADDRESS], OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore } } @@ -456,7 +465,7 @@ def test_bundle_zero( }, ], "bridge_total_requirements": brr["bridge_total_requirements"], - "error": False, + "status": BridgeRequestBundleStatus.QUOTE_DONE.value, "expiration_timestamp": brr["expiration_timestamp"], "is_refill_required": False, } @@ -562,7 +571,7 @@ def test_bundle_error( }, ], "bridge_total_requirements": brr["bridge_total_requirements"], - "error": False, + "status": BridgeRequestBundleStatus.QUOTE_FAILED.value, "expiration_timestamp": brr["expiration_timestamp"], "is_refill_required": False, } @@ -663,7 +672,7 @@ def test_bundle( {"message": None, "status": BridgeRequestStatus.QUOTE_DONE.value}, ], "bridge_total_requirements": brr["bridge_total_requirements"], - "error": False, + "status": BridgeRequestBundleStatus.QUOTE_DONE.value, "expiration_timestamp": brr["expiration_timestamp"], "is_refill_required": True, } From 3ad175c9cf507d0ed6c7dbd27bdb85684034bd79 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 23 Apr 2025 23:10:42 +0200 Subject: [PATCH 051/173] fix: linters --- tests/test_bridge_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 53bfc4fde..dfd0486eb 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -163,7 +163,7 @@ def test_bridge_zero( assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], - "tx_hash": None, + "tx_hash": None, # type: ignore "message": sj["message"], "status": BridgeRequestStatus.EXECUTION_DONE.value, } From 3e7b3ad2f4785ba924046d12a801e36a1548ced2 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 13:06:19 +0200 Subject: [PATCH 052/173] chore: add hash to safe creation --- operate/cli.py | 19 ++++++++++---- operate/utils/gnosis.py | 21 +++++++++------ operate/wallet/master.py | 57 +++++++++++++++++++++------------------- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index 0d4c4b663..c00d7b363 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -575,7 +575,7 @@ async def _get_safes(request: Request) -> t.List[t.Dict]: @app.get("/api/wallet/safe/{chain}") @with_retries async def _get_safe(request: Request) -> t.List[t.Dict]: - """Create wallet safe""" + """Get safe address""" chain = Chain.from_string(request.path_params["chain"]) ledger_type = chain.ledger_type manager = operate.wallet_manager @@ -633,27 +633,36 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: if backup_owner: backup_owner = ledger_api.api.to_checksum_address(backup_owner) - wallet.create_safe( # pylint: disable=no-member + tx_hash = wallet.create_safe( # pylint: disable=no-member chain=chain, backup_owner=backup_owner, ) + create_tx_link = tx_hash safe_address = t.cast(str, safes.get(chain)) initial_funds = data.get("initial_funds", DEFAULT_NEW_SAFE_FUNDS_AMOUNT[chain]) + transfer_tx_links = {} for asset, amount in initial_funds.items(): - wallet.transfer_asset( + tx_hash = wallet.transfer_asset( to=safe_address, amount=amount, chain=chain, asset=asset, from_safe=False, ) + transfer_tx_links[asset] = tx_hash return JSONResponse( - content={"safe": safes.get(chain), "message": "Safe created!"} + content={ + "create_tx": create_tx_link, + "transfer_txs": transfer_tx_links, + "safe": safes.get(chain), + "message": "Safe created!" + } ) + # TODO possibly unused endpoint @app.post("/api/wallet/safes") @with_retries async def _create_safes(request: Request) -> t.List[t.Dict]: @@ -730,7 +739,7 @@ async def _update_safe(request: Request) -> t.List[t.Dict]: if "chain" not in data: return JSONResponse( - content={"error": "You need to specify a chain to updae a safe."}, + content={"error": "You need to specify a chain to update a safe."}, status_code=401, ) diff --git a/operate/utils/gnosis.py b/operate/utils/gnosis.py index ca14a78e5..089f3e045 100644 --- a/operate/utils/gnosis.py +++ b/operate/utils/gnosis.py @@ -211,7 +211,7 @@ def create_safe( crypto: Crypto, backup_owner: t.Optional[str] = None, salt_nonce: t.Optional[int] = None, -) -> t.Tuple[str, int]: +) -> t.Tuple[str, int, str]: """Create gnosis safe.""" salt_nonce = salt_nonce or _get_nonce() @@ -248,12 +248,13 @@ def _build( # pylint: disable=unused-argument contract="", kwargs={}, ) + tx_hash = receipt.get("transactionHash", "").hex() instance = registry_contracts.gnosis_safe_proxy_factory.get_instance( ledger_api=ledger_api, contract_address="0xa6b71e26c5e0845f74c812102ca7114b6a896ab2", ) (event,) = instance.events.ProxyCreation().process_receipt(receipt) - return event["args"]["proxy"], salt_nonce + return event["args"]["proxy"], salt_nonce, tx_hash def get_owners(ledger_api: LedgerApi, safe: str) -> t.List[str]: @@ -270,7 +271,7 @@ def send_safe_txs( ledger_api: LedgerApi, crypto: Crypto, to: t.Optional[str] = None, -) -> None: +) -> t.Optional[str]: """Send internal safe transaction.""" owner = ledger_api.api.to_checksum_address( crypto.address, @@ -313,10 +314,12 @@ def _build_and_send_tx() -> t.Optional[str]: crypto.sign_transaction(transaction), ) - settle_raw_transaction( + tx_receipt = settle_raw_transaction( ledger_api=ledger_api, build_and_send_tx=_build_and_send_tx, ) + tx_hash = tx_receipt.get("transactionHash", "").hex() + return tx_hash def add_owner( @@ -428,7 +431,7 @@ def transfer( safe: str, to: str, amount: t.Union[float, int], -) -> None: +) -> t.Optional[str]: """Transfer assets from safe to given address.""" amount = int(amount) owner = ledger_api.api.to_checksum_address( @@ -471,10 +474,12 @@ def _build_and_send_tx() -> t.Optional[str]: crypto.sign_transaction(transaction), ) - settle_raw_transaction( + tx_receipt = settle_raw_transaction( ledger_api=ledger_api, build_and_send_tx=_build_and_send_tx, ) + tx_hash = tx_receipt.get("transactionHash", "").hex() + return tx_hash def transfer_erc20_from_safe( @@ -484,7 +489,7 @@ def transfer_erc20_from_safe( token: str, to: str, amount: t.Union[float, int], -) -> None: +) -> t.Optional[str]: """Transfer ERC20 assets from safe to given address.""" amount = int(amount) instance = registry_contracts.erc20.get_instance( @@ -498,7 +503,7 @@ def transfer_erc20_from_safe( amount, ], ) - send_safe_txs( + return send_safe_txs( txd=bytes.fromhex(txd[2:]), safe=safe, ledger_api=ledger_api, diff --git a/operate/wallet/master.py b/operate/wallet/master.py index b0515e15c..f2aa19f51 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -129,7 +129,7 @@ def transfer( chain: Chain, from_safe: bool = True, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer funds to the given account.""" raise NotImplementedError() @@ -142,7 +142,7 @@ def transfer_erc20( chain: Chain, from_safe: bool = True, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer funds to the given account.""" raise NotImplementedError() @@ -154,7 +154,7 @@ def transfer_asset( asset: str = ZERO_ADDRESS, from_safe: bool = True, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer erc20/native assets to the given account.""" raise NotImplementedError() @@ -178,7 +178,7 @@ def create_safe( chain: Chain, backup_owner: t.Optional[str] = None, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Create safe.""" raise NotImplementedError() @@ -241,7 +241,7 @@ class EthereumMasterWallet(MasterWallet): def _transfer_from_eoa( self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None - ) -> None: + ) -> t.Optional[str]: """Transfer funds from EOA wallet.""" ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc)) tx_helper = TxSettler( @@ -278,22 +278,22 @@ def _build_tx( # pylint: disable=unused-argument ) setattr(tx_helper, "build", _build_tx) # noqa: B010 - tx_helper.transact(lambda x: x, "", kwargs={}) + tx_receipt = tx_helper.transact(lambda x: x, "", kwargs={}) + tx_hash = tx_receipt.get("transactionHash", "").hex() + return tx_hash + def _transfer_from_safe( self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None - ) -> None: + ) -> t.Optional[str]: """Transfer funds from safe wallet.""" - if self.safes is not None: - transfer_from_safe( - ledger_api=self.ledger_api(chain=chain, rpc=rpc), - crypto=self.crypto, - safe=t.cast(str, self.safes[chain]), - to=to, - amount=amount, - ) - else: - raise ValueError("Safes not initialized") + return transfer_from_safe( + ledger_api=self.ledger_api(chain=chain, rpc=rpc), + crypto=self.crypto, + safe=t.cast(str, self.safes[chain]), + to=to, + amount=amount, + ) def _transfer_erc20_from_safe( self, @@ -302,9 +302,9 @@ def _transfer_erc20_from_safe( amount: int, chain: Chain, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer erc20 from safe wallet.""" - transfer_erc20_from_safe( + return transfer_erc20_from_safe( ledger_api=self.ledger_api(chain=chain, rpc=rpc), crypto=self.crypto, token=token, @@ -320,7 +320,7 @@ def _transfer_erc20_from_eoa( amount: int, chain: Chain, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer erc20 from EOA wallet.""" wallet_address = self.address ledger_api = t.cast(EthereumApi, self.ledger_api(chain=chain, rpc=rpc)) @@ -355,7 +355,9 @@ def _build_transfer_tx( # pylint: disable=unused-argument ) setattr(tx_settler, "build", _build_transfer_tx) # noqa: B010 - tx_settler.transact(lambda x: x, "", kwargs={}) + tx_receipt = tx_settler.transact(lambda x: x, "", kwargs={}) + tx_hash = tx_receipt.get("transactionHash", "").hex() + return tx_hash def transfer( self, @@ -364,7 +366,7 @@ def transfer( chain: Chain, from_safe: bool = True, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer funds to the given account.""" if amount <= 0: return None @@ -409,7 +411,7 @@ def transfer_erc20( chain: Chain, from_safe: bool = True, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Transfer funds to the given account.""" if amount <= 0: return None @@ -465,7 +467,7 @@ def transfer_asset( asset: str = ZERO_ADDRESS, from_safe: bool = True, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """ Transfer assets to the given account. @@ -615,11 +617,11 @@ def create_safe( chain: Chain, backup_owner: t.Optional[str] = None, rpc: t.Optional[str] = None, - ) -> None: + ) -> t.Optional[str]: """Create safe.""" if chain in self.safe_chains: - return - safe, self.safe_nonce = create_gnosis_safe( + return None + safe, self.safe_nonce, tx_hash = create_gnosis_safe( ledger_api=self.ledger_api(chain=chain, rpc=rpc), crypto=self.crypto, backup_owner=backup_owner, @@ -630,6 +632,7 @@ def create_safe( self.safes = {} self.safes[chain] = safe self.store() + return tx_hash def update_backup_owner( self, From c33a295349b41a9283200edd1fd150aba429cd3a Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 14:42:46 +0200 Subject: [PATCH 053/173] chore: update --- operate/cli.py | 13 ++++++------- operate/utils/gnosis.py | 2 +- operate/wallet/master.py | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index c00d7b363..b799706b1 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -633,16 +633,15 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: if backup_owner: backup_owner = ledger_api.api.to_checksum_address(backup_owner) - tx_hash = wallet.create_safe( # pylint: disable=no-member + create_tx = wallet.create_safe( # pylint: disable=no-member chain=chain, backup_owner=backup_owner, ) - create_tx_link = tx_hash safe_address = t.cast(str, safes.get(chain)) initial_funds = data.get("initial_funds", DEFAULT_NEW_SAFE_FUNDS_AMOUNT[chain]) - transfer_tx_links = {} + transfer_txs = {} for asset, amount in initial_funds.items(): tx_hash = wallet.transfer_asset( to=safe_address, @@ -651,14 +650,14 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: asset=asset, from_safe=False, ) - transfer_tx_links[asset] = tx_hash + transfer_txs[asset] = tx_hash return JSONResponse( content={ - "create_tx": create_tx_link, - "transfer_txs": transfer_tx_links, + "create_tx": create_tx, + "transfer_txs": transfer_txs, "safe": safes.get(chain), - "message": "Safe created!" + "message": "Safe created!", } ) diff --git a/operate/utils/gnosis.py b/operate/utils/gnosis.py index 089f3e045..1e9fb574c 100644 --- a/operate/utils/gnosis.py +++ b/operate/utils/gnosis.py @@ -479,7 +479,7 @@ def _build_and_send_tx() -> t.Optional[str]: build_and_send_tx=_build_and_send_tx, ) tx_hash = tx_receipt.get("transactionHash", "").hex() - return tx_hash + return tx_hash def transfer_erc20_from_safe( diff --git a/operate/wallet/master.py b/operate/wallet/master.py index f2aa19f51..932cf7488 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -282,7 +282,6 @@ def _build_tx( # pylint: disable=unused-argument tx_hash = tx_receipt.get("transactionHash", "").hex() return tx_hash - def _transfer_from_safe( self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None ) -> t.Optional[str]: From 45003195248a6709c12fb58d36ccd4db77c6acb9 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 15:04:58 +0200 Subject: [PATCH 054/173] chore: fixes --- docs/api.md | 5 +++++ operate/wallet/master.py | 36 +++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/docs/api.md b/docs/api.md index ca057d02c..e94bc756a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -356,6 +356,11 @@ Creates a Gnosis safe for given chain. ```json { + "create_tx": "0xac14dcd5938c71cd97388307c41477dc2f2a4e97b6b2641cef123a769898bd03", + "transfer_txs": { + "0x0000000000000000000000000000000000000000": "0x0036489a4a27b4ee1b7e4a37ae5597b895bf43e4cda25c7e6c0f1d02f8c098aa", + "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f": "0x45304084049916535419d4b175074cdfcaeea59e3a5d34010854b3ac049261b2" + }, "safe": "0x29e23F7705d849F368855947691cB133CD770752", "message": "Safe created!" } diff --git a/operate/wallet/master.py b/operate/wallet/master.py index 932cf7488..e0a5cfe49 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -286,13 +286,16 @@ def _transfer_from_safe( self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None ) -> t.Optional[str]: """Transfer funds from safe wallet.""" - return transfer_from_safe( - ledger_api=self.ledger_api(chain=chain, rpc=rpc), - crypto=self.crypto, - safe=t.cast(str, self.safes[chain]), - to=to, - amount=amount, - ) + if self.safes is not None: + return transfer_from_safe( + ledger_api=self.ledger_api(chain=chain, rpc=rpc), + crypto=self.crypto, + safe=t.cast(str, self.safes[chain]), + to=to, + amount=amount, + ) + else: + raise ValueError("Safes not initialized") def _transfer_erc20_from_safe( self, @@ -303,14 +306,17 @@ def _transfer_erc20_from_safe( rpc: t.Optional[str] = None, ) -> t.Optional[str]: """Transfer erc20 from safe wallet.""" - return transfer_erc20_from_safe( - ledger_api=self.ledger_api(chain=chain, rpc=rpc), - crypto=self.crypto, - token=token, - safe=t.cast(str, self.safes[chain]), # type: ignore - to=to, - amount=amount, - ) + if self.safes is not None: + return transfer_erc20_from_safe( + ledger_api=self.ledger_api(chain=chain, rpc=rpc), + crypto=self.crypto, + token=token, + safe=t.cast(str, self.safes[chain]), # type: ignore + to=to, + amount=amount, + ) + else: + raise ValueError("Safes not initialized") def _transfer_erc20_from_eoa( self, From af0390e68d1d4cf6a0cea012cefaaf00f89bba44 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 15:07:29 +0200 Subject: [PATCH 055/173] chore: fix linters --- operate/wallet/master.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/operate/wallet/master.py b/operate/wallet/master.py index e0a5cfe49..d2bb33ac5 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -286,16 +286,16 @@ def _transfer_from_safe( self, to: str, amount: int, chain: Chain, rpc: t.Optional[str] = None ) -> t.Optional[str]: """Transfer funds from safe wallet.""" - if self.safes is not None: - return transfer_from_safe( - ledger_api=self.ledger_api(chain=chain, rpc=rpc), - crypto=self.crypto, - safe=t.cast(str, self.safes[chain]), - to=to, - amount=amount, - ) - else: + if self.safes is None: raise ValueError("Safes not initialized") + + return transfer_from_safe( + ledger_api=self.ledger_api(chain=chain, rpc=rpc), + crypto=self.crypto, + safe=t.cast(str, self.safes[chain]), + to=to, + amount=amount, + ) def _transfer_erc20_from_safe( self, @@ -306,18 +306,18 @@ def _transfer_erc20_from_safe( rpc: t.Optional[str] = None, ) -> t.Optional[str]: """Transfer erc20 from safe wallet.""" - if self.safes is not None: - return transfer_erc20_from_safe( - ledger_api=self.ledger_api(chain=chain, rpc=rpc), - crypto=self.crypto, - token=token, - safe=t.cast(str, self.safes[chain]), # type: ignore - to=to, - amount=amount, - ) - else: + if self.safes is None: raise ValueError("Safes not initialized") + return transfer_erc20_from_safe( + ledger_api=self.ledger_api(chain=chain, rpc=rpc), + crypto=self.crypto, + token=token, + safe=t.cast(str, self.safes[chain]), # type: ignore + to=to, + amount=amount, + ) + def _transfer_erc20_from_eoa( self, token: str, From 4006b570cca3335843d0ba2aea5557a0419df751 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 15:08:48 +0200 Subject: [PATCH 056/173] fix: linters --- operate/wallet/master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/wallet/master.py b/operate/wallet/master.py index d2bb33ac5..f6df43fcc 100644 --- a/operate/wallet/master.py +++ b/operate/wallet/master.py @@ -288,7 +288,7 @@ def _transfer_from_safe( """Transfer funds from safe wallet.""" if self.safes is None: raise ValueError("Safes not initialized") - + return transfer_from_safe( ledger_api=self.ledger_api(chain=chain, rpc=rpc), crypto=self.crypto, From 2ccfa2bbb87bef84c81a6573d194ce5ea0bc9b82 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 15:32:29 +0200 Subject: [PATCH 057/173] chore: update --- operate/bridge/bridge.py | 18 +++++++++--------- operate/cli.py | 6 ++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 9346cc39d..38e1a49e5 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -755,7 +755,7 @@ def _update_bundle_status(self, bundle_id: str) -> None: bundle = self.data.executed_bundles.get(bundle_id) if not bundle: - raise ValueError(f"[BRIDGE MANAGER] Bundle id {bundle_id} not found.") + raise ValueError(f"Bundle id {bundle_id} not found.") if bundle.status in ( BridgeRequestBundleStatus.EXECUTION_DONE, @@ -786,7 +786,7 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: or "to" not in request ): raise ValueError( - "[BRIDGE MANAGER] Invalid input: All quote requests must contain exactly one 'from' and one 'to' sender." + "Invalid input: All quote requests must contain exactly one 'from' and one 'to' sender." ) from_ = request["from"] @@ -799,7 +799,7 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: or "token" not in from_ ): raise ValueError( - "[BRIDGE MANAGER] Invalid input: 'from' must contain 'chain', 'address', and 'token'." + "Invalid input: 'from' must contain 'chain', 'address', and 'token'." ) if ( @@ -810,7 +810,7 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: or "amount" not in to ): raise ValueError( - "[BRIDGE MANAGER] Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." + "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) from_chain = request["from"]["chain"] @@ -824,7 +824,7 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: from_address == wallet_address or from_address == safe_address ): raise ValueError( - f"[BRIDGE MANAGER] Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}." + f"Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}." ) key = ( @@ -838,7 +838,7 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: if key in seen: raise ValueError( - "[BRIDGE MANAGER] Request contains duplicate entries with same 'from' and 'to'." + "Request contains duplicate entries with same 'from' and 'to'." ) def bridge_refill_requirements( @@ -907,14 +907,14 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: if bundle.id != bundle_id: raise RuntimeError( - f"[BRIDGE MANAGER] Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}." + f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}." ) requirements = self.bridge_refill_requirements(bundle.requests_params) if requirements["is_refill_required"]: raise RuntimeError( - f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}." + f"Refill requirements not satisfied for bundle id {bundle_id}." ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") @@ -936,7 +936,7 @@ def get_execution_status(self, bundle_id: str) -> t.Dict: bundle = self.data.executed_bundles.get(bundle_id) if not bundle: - raise ValueError(f"[BRIDGE MANAGER] Bundle id {bundle_id} not found.") + raise ValueError(f"Bundle id {bundle_id} not found.") self._update_bundle_status(bundle_id) diff --git a/operate/cli.py b/operate/cli.py index b799706b1..903211d74 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1066,7 +1066,7 @@ async def _bridge_execute(request: Request) -> JSONResponse: status_code=HTTPStatus.INTERNAL_SERVER_ERROR, ) - @app.get(f"/api/bridge/status/{id}") + @app.get("/api/bridge/status/{id}") @with_retries async def _bridge_status(request: Request) -> JSONResponse: """Get the bridge refill requirements.""" @@ -1080,9 +1080,7 @@ async def _bridge_status(request: Request) -> JSONResponse: return JSONResponse( content=output, - status_code=HTTPStatus.BAD_GATEWAY - if output["errors"] - else HTTPStatus.OK, + status_code=HTTPStatus.OK, ) except ValueError as e: return JSONResponse( From 27d1d4dd22526e538f934a1a7bd415de18075355 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 15:48:36 +0200 Subject: [PATCH 058/173] fix: tests --- tests/test_bridge_bridge.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index dfd0486eb..fd5f8543f 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -43,6 +43,10 @@ ROOT_PATH = Path(__file__).resolve().parent OPERATE = ".operate_test" +RUNNING_IN_CI = ( + os.getenv("GITHUB_ACTIONS", "").lower() == "true" + or os.getenv("CI", "").lower() == "true" +) class TestLiFiBridge: @@ -300,7 +304,7 @@ def test_bridge_error( assert not diff, "Wrong status." - @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip test on CI.") + @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") def test_bridge_quote( self, tmp_path: Path, @@ -609,8 +613,8 @@ def test_bundle_error( assert not diff, "Wrong bridge refill requirements." - @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip test on CI.") - def test_bundle( + @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") + def test_bundle_quote( self, tmp_path: Path, password: str, From 0e29b02a58292d0be5ae85b4489a82b5344f234c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 17:13:45 +0200 Subject: [PATCH 059/173] chore: added exception on get asset balance --- operate/utils/gnosis.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/operate/utils/gnosis.py b/operate/utils/gnosis.py index 1e9fb574c..98c53ea27 100644 --- a/operate/utils/gnosis.py +++ b/operate/utils/gnosis.py @@ -590,16 +590,21 @@ def get_asset_balance( raise ValueError(f"Invalid address: {address}") return 0 - if asset_address == ZERO_ADDRESS: - return ledger_api.get_balance(address) - return ( - registry_contracts.erc20.get_instance( - ledger_api=ledger_api, - contract_address=asset_address, + try: + if asset_address == ZERO_ADDRESS: + return ledger_api.get_balance(address, raise_on_try=True) + return ( + registry_contracts.erc20.get_instance( + ledger_api=ledger_api, + contract_address=asset_address, + ) + .functions.balanceOf(address) + .call() ) - .functions.balanceOf(address) - .call() - ) + except Exception as e: + raise RuntimeError( + f"Cannot get balance of {address=} {asset_address=} rpc={ledger_api._api.provider.endpoint_uri}." + ) from e def get_assets_balances( From 41c306932e4b277143963a181dae9f3985f14030 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 24 Apr 2025 17:22:07 +0200 Subject: [PATCH 060/173] fix: linters --- operate/utils/gnosis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/utils/gnosis.py b/operate/utils/gnosis.py index 98c53ea27..c70ac326c 100644 --- a/operate/utils/gnosis.py +++ b/operate/utils/gnosis.py @@ -603,7 +603,7 @@ def get_asset_balance( ) except Exception as e: raise RuntimeError( - f"Cannot get balance of {address=} {asset_address=} rpc={ledger_api._api.provider.endpoint_uri}." + f"Cannot get balance of {address=} {asset_address=} rpc={ledger_api._api.provider.endpoint_uri}." # pylint: disable=protected-access ) from e From 41284397f99f8b4f393acc7a20f49481fa9bb033 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 25 Apr 2025 11:38:57 +0200 Subject: [PATCH 061/173] chore: fix test --- tests/test_services_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_services_service.py b/tests/test_services_service.py index b9ff24d11..0b77d08ce 100644 --- a/tests/test_services_service.py +++ b/tests/test_services_service.py @@ -341,8 +341,8 @@ class TestService: @pytest.mark.parametrize( "staking_program_id", ["staking_program_1", "staking_program_2"] ) - @pytest.mark.parametrize("use_mech_marketplace", [True]) - @pytest.mark.parametrize("use_staking", [True]) + @pytest.mark.parametrize("use_mech_marketplace", [True, False]) + @pytest.mark.parametrize("use_staking", [True, False]) @pytest.mark.parametrize( "get_config_json_data", [ From d6a8a950767ca79931a692ff18137bb196c04d0d Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 25 Apr 2025 16:03:44 +0200 Subject: [PATCH 062/173] chore: remove enum --- operate/bridge/bridge.py | 1 - 1 file changed, 1 deletion(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 38e1a49e5..dce3908b1 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -136,7 +136,6 @@ class BridgeRequestBundleStatus(str, enum.Enum): EXECUTION_PENDING = "EXECUTION_PENDING" EXECUTION_DONE = "EXECUTION_DONE" EXECUTION_FAILED = "EXECUTION_FAILED" - UNKNOWN = "UNKNOWN" def __str__(self) -> str: """__str__""" From b68e148645517dd017cbb7cd87b9d0671a334a82 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 27 Apr 2025 23:38:25 +0200 Subject: [PATCH 063/173] feat: improve gas estimation --- operate/bridge/bridge.py | 297 ++++++++++++++++++++++-------------- tests/test_bridge_bridge.py | 6 +- 2 files changed, 186 insertions(+), 117 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index dce3908b1..f5c688d1b 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -34,6 +34,7 @@ from urllib.parse import urlencode import requests +from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger from autonomy.chain.base import registry_contracts from autonomy.chain.tx import TxSettler @@ -66,7 +67,6 @@ class QuoteData(LocalResource): """QuoteData""" attempts: int - requirements: t.Dict elapsed_time: float message: t.Optional[str] response: t.Optional[t.Dict] @@ -198,28 +198,6 @@ def get_from_tokens(self, chain: Chain) -> set[str]: if request.params["from"]["chain"] == chain_str } - def sum_bridge_requirements(self) -> t.Dict: - """Sum bridge requirements.""" - - bridge_total_requirements: t.Dict = {} - - for request in self.bridge_requests: - if not request.quote_data: - continue - - bridge_requirements = request.quote_data.requirements - for from_chain, from_addresses in bridge_requirements.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - bridge_total_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - ).setdefault(from_token, 0) - bridge_total_requirements[from_chain][from_address][ - from_token - ] += from_amount - - return bridge_total_requirements - class BridgeProvider: """(Abstract) BridgeProvider""" @@ -242,6 +220,11 @@ def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" raise NotImplementedError() + @abstractmethod + def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: + """Gets the bridge requirements to execute the quote, with updated gas estimation.""" + raise NotImplementedError() + @abstractmethod def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" @@ -259,6 +242,28 @@ def quote_bundle(self, bundle: BridgeRequestBundle) -> None: bundle.timestamp = int(time.time()) + def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: + """Sum bridge requirements.""" + + bridge_total_requirements: t.Dict = {} + + for request in bundle.bridge_requests: + if not request.quote_data: + continue + + bridge_requirements = self.bridge_requirements(request) + for from_chain, from_addresses in bridge_requirements.items(): + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + bridge_total_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ).setdefault(from_token, 0) + bridge_total_requirements[from_chain][from_address][ + from_token + ] += from_amount + + return bridge_total_requirements + def execute_bundle(self, bundle: BridgeRequestBundle) -> None: """Update the bundle with the quotes.""" for bridge_request in bundle.bridge_requests: @@ -283,6 +288,92 @@ def __str__(self) -> str: class LiFiBridgeProvider(BridgeProvider): """LI.FI Bridge provider.""" + @staticmethod + def _build_approve_tx( + quote_data: QuoteData, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote = quote_data.response + if not quote: + return None + + if "action" not in quote: + return None + + from_token = quote["action"]["fromToken"]["address"] + if from_token == ZERO_ADDRESS: + return None + + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None + + from_amount = int(quote["action"]["fromAmount"]) + + approve_tx = registry_contracts.erc20.get_approve_tx( + ledger_api=ledger_api, + contract_address=from_token, + spender=transaction_request["to"], + sender=transaction_request["from"], + amount=from_amount, + ) + return LiFiBridgeProvider._update_tx_gas_pricing(approve_tx, ledger_api) + + @staticmethod + def _get_bridge_tx( + quote_data: QuoteData, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote = quote_data.response + if not quote: + return None + + if "action" not in quote: + return None + + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None + + bridge_tx = { + "value": int(transaction_request["value"], 16), + "to": transaction_request["to"], + "data": bytes.fromhex(transaction_request["data"][2:]), + "from": transaction_request["from"], + "chainId": transaction_request["chainId"], + "gasPrice": int(transaction_request["gasPrice"], 16), + "gas": int(transaction_request["gasLimit"], 16), + "nonce": ledger_api.api.eth.get_transaction_count( + transaction_request["from"] + ), + } + + return LiFiBridgeProvider._update_tx_gas_pricing(bridge_tx, ledger_api) + + @staticmethod + def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: + output_tx = tx.copy() + output_tx.pop("maxFeePerGas", None) + output_tx.pop("gasPrice", None) + output_tx.pop("maxPriorityFeePerGas", None) + + gas_pricing = ledger_api.try_get_gas_pricing() + if gas_pricing is None: + raise RuntimeError("[LI.FI BRIDGE] Unable to retrieve gas pricing.") + + if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: + output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] + output_tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] + elif "gasPrice" in gas_pricing: + output_tx["gasPrice"] = gas_pricing["gasPrice"] + else: + raise RuntimeError("[LI.FI BRIDGE] Retrieved invalid gas pricing.") + + return output_tx + + @staticmethod + def _calculate_gas_fees(tx: t.Dict) -> int: + gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" + return tx.get(gas_key, 0) * tx["gas"] + def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" @@ -307,20 +398,11 @@ def quote(self, bridge_request: BridgeRequest) -> None: to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - zero_requirements = { - from_chain: { - from_address: { - ZERO_ADDRESS: 0, - from_token: 0, - } - } - } if to_amount == 0: self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") quote_data = QuoteData( attempts=0, - requirements=zero_requirements, elapsed_time=0, message=MESSAGE_QUOTE_ZERO, response=None, @@ -352,51 +434,8 @@ def quote(self, bridge_request: BridgeRequest) -> None: ) response.raise_for_status() response_json = response.json() - transaction_request = response_json["transactionRequest"] - transaction_value = int(transaction_request["value"], 16) - gas_price = int(transaction_request["gasPrice"], 16) - gas_limit = int(transaction_request["gasLimit"], 16) - gas_fees = gas_price * gas_limit - - if from_token == ZERO_ADDRESS: - requirements = { - from_chain: { - from_address: {from_token: transaction_value + gas_fees} - } - } - else: - from_amount = int(response_json["action"]["fromAmount"]) - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - - approve_tx = registry_contracts.erc20.get_approve_tx( - ledger_api=wallet.ledger_api(chain), - contract_address=from_token, - spender=transaction_request["to"], - sender=transaction_request["from"], - amount=from_amount, - ) - approve_tx = ledger_api.update_with_gas_estimate( - transaction=approve_tx, - raise_on_try=True, - ) - gas_price = approve_tx["gas"] - approve_gas_limit = approve_tx["gas"] - gas_fees = (gas_limit + approve_gas_limit) * gas_price - - requirements = { - from_chain: { - from_address: { - ZERO_ADDRESS: transaction_value + gas_fees, - from_token: from_amount, - } - } - } - quote_data = QuoteData( attempts=attempt, - requirements=requirements, elapsed_time=time.time() - start, message=None, response=response_json, @@ -412,7 +451,6 @@ def quote(self, bridge_request: BridgeRequest) -> None: ) quote_data = QuoteData( attempts=attempt, - requirements=zero_requirements, elapsed_time=time.time() - start, message=str(e), response=None, @@ -426,7 +464,6 @@ def quote(self, bridge_request: BridgeRequest) -> None: response_json = response.json() quote_data = QuoteData( attempts=attempt, - requirements=zero_requirements, elapsed_time=time.time() - start, message=response_json.get("message") or str(e), response=response_json, @@ -445,6 +482,59 @@ def quote(self, bridge_request: BridgeRequest) -> None: time.sleep(2) + def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: + """Gets the fund requirements to execute the quote, with updated gas estimation.""" + + quote_data = bridge_request.quote_data + if not quote_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot compute requirements for bridge request {bridge_request.id}: quote not present." + ) + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + + zero_requirements = { + from_chain: { + from_address: { + ZERO_ADDRESS: 0, + from_token: 0, + } + } + } + + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + + approve_tx = self._build_approve_tx(quote_data, ledger_api) + bridge_tx = self._get_bridge_tx(quote_data, ledger_api) + if not bridge_tx: + return zero_requirements + + bridge_tx_value = bridge_tx["value"] + bridge_tx_gas_fees = self._calculate_gas_fees(bridge_tx) + + if approve_tx: + approve_tx_gas_fees = self._calculate_gas_fees(approve_tx) + return { + from_chain: { + from_address: { + ZERO_ADDRESS: bridge_tx_value + + bridge_tx_gas_fees + + approve_tx_gas_fees, + from_token: approve_tx["value"], + } + } + } + + return { + from_chain: { + from_address: {from_token: bridge_tx_value + bridge_tx_gas_fees} + } + } + def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" @@ -498,9 +588,10 @@ def execute(self, bridge_request: BridgeRequest) -> None: transaction_request = quote["transactionRequest"] chain = Chain.from_id(transaction_request["chainId"]) wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) tx_settler = TxSettler( - ledger_api=wallet.ledger_api(chain), + ledger_api=ledger_api, crypto=wallet.crypto, chain_type=chain, timeout=ON_CHAIN_INTERACT_TIMEOUT, @@ -509,25 +600,14 @@ def execute(self, bridge_request: BridgeRequest) -> None: ) # Bridges from an asset other than native require an approval transaction. - if from_token != ZERO_ADDRESS: + approve_tx = self._build_approve_tx(bridge_request.quote_data, ledger_api) + if approve_tx: self.logger.info( f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." ) - - # TODO Approve is done on several places. Consider exporting to a - # higher-level layer (e.g., wallet?) - def _build_approve_tx( # pylint: disable=unused-argument - *args: t.Any, **kargs: t.Any - ) -> t.Dict: - return registry_contracts.erc20.get_approve_tx( - ledger_api=wallet.ledger_api(chain), - contract_address=from_token, - spender=transaction_request["to"], - sender=transaction_request["from"], - amount=from_amount, - ) - - setattr(tx_settler, "build", _build_approve_tx) # noqa: B010 + setattr( + tx_settler, "build", lambda *args, **kwargs: approve_tx + ) # noqa: B010 tx_settler.transact( method=lambda: {}, contract="", @@ -539,23 +619,10 @@ def _build_approve_tx( # pylint: disable=unused-argument self.logger.info( f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." ) - - def _build_bridge_tx( # pylint: disable=unused-argument - *args: t.Any, **kargs: t.Any - ) -> t.Dict: - w3 = Web3(Web3.HTTPProvider(get_default_rpc(chain=chain))) - return { - "value": int(transaction_request["value"], 16), - "to": transaction_request["to"], - "data": bytes.fromhex(transaction_request["data"][2:]), - "from": transaction_request["from"], - "chainId": transaction_request["chainId"], - "gasPrice": int(transaction_request["gasPrice"], 16), - "gas": int(transaction_request["gasLimit"], 16), - "nonce": w3.eth.get_transaction_count(transaction_request["from"]), - } - - setattr(tx_settler, "build", _build_bridge_tx) # noqa: B010 + bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) + setattr( + tx_settler, "build", lambda *args, **kwargs: bridge_tx + ) # noqa: B010 tx_receipt = tx_settler.transact( method=lambda: {}, contract="", @@ -861,7 +928,9 @@ def bridge_refill_requirements( addresses=bundle.get_from_addresses(chain), ) - bridge_total_requirements = bundle.sum_bridge_requirements() + bridge_total_requirements = self.bridge_provider.bridge_total_requirements( + bundle + ) bridge_refill_requirements: t.Dict = {} for from_chain, from_addresses in bridge_total_requirements.items(): @@ -912,8 +981,8 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: requirements = self.bridge_refill_requirements(bundle.requests_params) if requirements["is_refill_required"]: - raise RuntimeError( - f"Refill requirements not satisfied for bundle id {bundle_id}." + self.logger.warning( + f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}." ) self.logger.info("[BRIDGE MANAGER] Executing quotes.") diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index fd5f8543f..0bf23a524 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -119,7 +119,7 @@ def test_bridge_zero( assert not diff, "Wrong status." assert bridge_request.quote_data is not None, "Missing quote data." - br = bridge_request.quote_data.requirements + br = bridge.bridge_requirements(bridge_request) expected_br = { "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} } @@ -244,7 +244,7 @@ def test_bridge_error( assert not diff, "Wrong status." - br = bridge_request.quote_data.requirements + br = bridge.bridge_requirements(bridge_request) expected_br = { "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} } @@ -374,7 +374,7 @@ def test_bridge_quote( assert bridge_request.quote_data.response is not None, "Missing quote data." quote = bridge_request.quote_data.response - br = bridge_request.quote_data.requirements + br = bridge.bridge_requirements(bridge_request) expected_br = { "gnosis": { wallet_address: { From a40479005e2815839ddc65719c4cc6b2270e8d47 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 29 Apr 2025 12:08:09 +0200 Subject: [PATCH 064/173] chore: save executed to files --- operate/bridge/bridge.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index f5c688d1b..8dbe25b7f 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -27,7 +27,7 @@ import typing as t import uuid from abc import abstractmethod -from dataclasses import dataclass, field +from dataclasses import dataclass from http import HTTPStatus from pathlib import Path from typing import cast @@ -39,7 +39,6 @@ from autonomy.chain.base import registry_contracts from autonomy.chain.tx import TxSettler from deepdiff import DeepDiff -from web3 import Web3 from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, @@ -47,7 +46,6 @@ ON_CHAIN_INTERACT_TIMEOUT, ZERO_ADDRESS, ) -from operate.ledger import get_default_rpc from operate.operate_types import Chain from operate.resource import LocalResource from operate.services.manage import get_assets_balances @@ -56,6 +54,7 @@ DEFAULT_MAX_QUOTE_RETRIES = 3 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 +EXECUTED_BUNDLES_PATH = "executed" BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" BRIDGE_REQUEST_PREFIX = "br-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." @@ -524,7 +523,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: ZERO_ADDRESS: bridge_tx_value + bridge_tx_gas_fees + approve_tx_gas_fees, - from_token: approve_tx["value"], + from_token: int(quote_data.response["action"]["fromAmount"]), # type: ignore } } } @@ -583,7 +582,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: try: self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") from_token = quote["action"]["fromToken"]["address"] - from_amount = int(quote["action"]["fromAmount"]) transaction_request = quote["transactionRequest"] chain = Chain.from_id(transaction_request["chainId"]) @@ -605,9 +603,9 @@ def execute(self, bridge_request: BridgeRequest) -> None: self.logger.info( f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." ) - setattr( + setattr( # noqa: B010 tx_settler, "build", lambda *args, **kwargs: approve_tx - ) # noqa: B010 + ) tx_settler.transact( method=lambda: {}, contract="", @@ -620,9 +618,9 @@ def execute(self, bridge_request: BridgeRequest) -> None: f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." ) bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) - setattr( + setattr( # noqa: B010 tx_settler, "build", lambda *args, **kwargs: bridge_tx - ) # noqa: B010 + ) tx_receipt = tx_settler.transact( method=lambda: {}, contract="", @@ -709,7 +707,6 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 last_requested_bundle: t.Optional[BridgeRequestBundle] = None - executed_bundles: t.Dict[str, BridgeRequestBundle] = field(default_factory=dict) _file = "bridge.json" @@ -760,6 +757,7 @@ def __init__( ) self.quote_validity_period = quote_validity_period self.path.mkdir(exist_ok=True) + (self.path / EXECUTED_BUNDLES_PATH).mkdir(exist_ok=True) self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) ) @@ -817,12 +815,7 @@ def _get_updated_bundle( return bundle - def _update_bundle_status(self, bundle_id: str) -> None: - bundle = self.data.executed_bundles.get(bundle_id) - - if not bundle: - raise ValueError(f"Bundle id {bundle_id} not found.") - + def _update_bundle_status(self, bundle: BridgeRequestBundle) -> None: if bundle.status in ( BridgeRequestBundleStatus.EXECUTION_DONE, BridgeRequestBundleStatus.EXECUTION_FAILED, @@ -838,7 +831,7 @@ def _update_bundle_status(self, bundle_id: str) -> None: status = [request.status for request in bundle.bridge_requests] if initial_bundle_status != bundle.status or initial_status != status: - self._store_data() + bundle.store() def _raise_if_invalid(self, bridge_requests: t.List) -> None: """Preprocess quote requests.""" @@ -978,6 +971,10 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}." ) + bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json" + bundle.path = bundle_path + bundle.store() + requirements = self.bridge_refill_requirements(bundle.requests_params) if requirements["is_refill_required"]: @@ -992,8 +989,8 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: self._store_data() self.data.last_requested_bundle = None - self.data.executed_bundles[bundle.id] = bundle self._store_data() + bundle.store() self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.") return self.get_execution_status(bundle_id) @@ -1001,12 +998,13 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: def get_execution_status(self, bundle_id: str) -> t.Dict: """Get execution status of bundle.""" - bundle = self.data.executed_bundles.get(bundle_id) + bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json" + bundle = cast(BridgeRequestBundle, BridgeRequestBundle.load(bundle_path)) if not bundle: raise ValueError(f"Bundle id {bundle_id} not found.") - self._update_bundle_status(bundle_id) + self._update_bundle_status(bundle) bridge_request_status = [ request.get_status_json() for request in bundle.bridge_requests From 0cb50e4e1acd030027aa7f69fa21aad493085e0c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 29 Apr 2025 12:31:26 +0200 Subject: [PATCH 065/173] chore: fix --- operate/bridge/bridge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 8dbe25b7f..b2a1d98b3 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -988,7 +988,7 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: self.bridge_provider.execute(request) self._store_data() - self.data.last_requested_bundle = None + self.data.last_requested_bundle = None # TODO Move up after bundle.store() ? self._store_data() bundle.store() self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.") @@ -1000,6 +1000,7 @@ def get_execution_status(self, bundle_id: str) -> t.Dict: bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json" bundle = cast(BridgeRequestBundle, BridgeRequestBundle.load(bundle_path)) + bundle.path = bundle_path # TODO backport to resource.py ? if not bundle: raise ValueError(f"Bundle id {bundle_id} not found.") From da9c7ce174472b6154c39986c362bdebc61e394e Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 29 Apr 2025 18:00:49 +0200 Subject: [PATCH 066/173] chore: fix int --- operate/bridge/bridge.py | 1 + operate/cli.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index b2a1d98b3..8a0ecb11e 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -347,6 +347,7 @@ def _get_bridge_tx( return LiFiBridgeProvider._update_tx_gas_pricing(bridge_tx, ledger_api) + # TODO This gas pricing management should possibly be done at a lower level in the library @staticmethod def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: output_tx = tx.copy() diff --git a/operate/cli.py b/operate/cli.py index 903211d74..1ca70dda4 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -645,7 +645,7 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: for asset, amount in initial_funds.items(): tx_hash = wallet.transfer_asset( to=safe_address, - amount=amount, + amount=int(amount), chain=chain, asset=asset, from_safe=False, From f8b330bd2371072fe81f38b34576b9544bc9819d Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 29 Apr 2025 20:15:56 +0200 Subject: [PATCH 067/173] chore: remove global status --- operate/bridge/bridge.py | 131 +++++++++--------------------------- operate/cli.py | 4 +- tests/test_bridge_bridge.py | 46 ++++++++----- 3 files changed, 64 insertions(+), 117 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 8a0ecb11e..d873cd653 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -56,7 +56,7 @@ DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 EXECUTED_BUNDLES_PATH = "executed" BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" -BRIDGE_REQUEST_PREFIX = "br-" +BRIDGE_REQUEST_PREFIX = "b-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." MESSAGE_EXECUTION_SKIPPED = "Execution skipped." @@ -126,21 +126,6 @@ def get_status_json(self) -> t.Dict: return {"message": None, "status": self.status.value} -class BridgeRequestBundleStatus(str, enum.Enum): - """BridgeRequestBundleStatus""" - - CREATED = "CREATED" - QUOTE_DONE = "QUOTE_DONE" - QUOTE_FAILED = "QUOTE_FAILED" - EXECUTION_PENDING = "EXECUTION_PENDING" - EXECUTION_DONE = "EXECUTION_DONE" - EXECUTION_FAILED = "EXECUTION_FAILED" - - def __str__(self) -> str: - """__str__""" - return self.value - - @dataclass class BridgeRequestBundle(LocalResource): """BridgeRequestBundle""" @@ -151,28 +136,6 @@ class BridgeRequestBundle(LocalResource): timestamp: int id: str - @property - def status(self) -> BridgeRequestBundleStatus: - """Status""" - statuses = {request.status for request in self.bridge_requests} - - if BridgeRequestStatus.EXECUTION_PENDING in statuses: - return BridgeRequestBundleStatus.EXECUTION_PENDING - - if BridgeRequestStatus.EXECUTION_FAILED in statuses: - return BridgeRequestBundleStatus.EXECUTION_FAILED - - if statuses == {BridgeRequestStatus.EXECUTION_DONE}: - return BridgeRequestBundleStatus.EXECUTION_DONE - - if BridgeRequestStatus.QUOTE_FAILED in statuses: - return BridgeRequestBundleStatus.QUOTE_FAILED - - if statuses == {BridgeRequestStatus.QUOTE_DONE}: - return BridgeRequestBundleStatus.QUOTE_DONE - - return BridgeRequestBundleStatus.CREATED - def get_from_chains(self) -> set[Chain]: """Get 'from' chains.""" return { @@ -268,6 +231,27 @@ def execute_bundle(self, bundle: BridgeRequestBundle) -> None: for bridge_request in bundle.bridge_requests: self.execute(bridge_request=bridge_request) + def get_status_json(self, bundle: BridgeRequestBundle) -> t.Dict: + """JSON representation of the status.""" + initial_status = [request.status for request in bundle.bridge_requests] + + for request in bundle.bridge_requests: + self.update_execution_status(request) + + updated_status = [request.status for request in bundle.bridge_requests] + + if initial_status != updated_status and bundle.path is not None: + bundle.store() + + bridge_request_status = [ + request.get_status_json() for request in bundle.bridge_requests + ] + + return { + "id": bundle.id, + "bridge_request_status": bridge_request_status, + } + class LiFiTransactionStatus(str, enum.Enum): """LI.FI transaction status.""" @@ -662,16 +646,8 @@ def execute(self, bridge_request: BridgeRequest) -> None: def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" - if bridge_request.status in ( - BridgeRequestStatus.EXECUTION_DONE, - BridgeRequestStatus.EXECUTION_FAILED, - ): - return - if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id} with status {bridge_request.status}." - ) + return if not bridge_request.execution_data: raise RuntimeError( @@ -779,12 +755,6 @@ def _get_updated_bundle( if not bundle: self.logger.info("[BRIDGE MANAGER] No last bundle.") create_new_bundle = True - elif bundle.status not in ( - BridgeRequestBundleStatus.CREATED, - BridgeRequestBundleStatus.QUOTE_DONE, - BridgeRequestBundleStatus.QUOTE_FAILED, - ): - raise RuntimeError("[BRIDGE MANAGER] Bundle inconsistent status.") elif DeepDiff(requests_params, bundle.requests_params): self.logger.info("[BRIDGE MANAGER] Different requests params.") create_new_bundle = True @@ -816,24 +786,6 @@ def _get_updated_bundle( return bundle - def _update_bundle_status(self, bundle: BridgeRequestBundle) -> None: - if bundle.status in ( - BridgeRequestBundleStatus.EXECUTION_DONE, - BridgeRequestBundleStatus.EXECUTION_FAILED, - ): - return - - initial_bundle_status = bundle.status - initial_status = [request.status for request in bundle.bridge_requests] - - for request in bundle.bridge_requests: - self.bridge_provider.update_execution_status(request) - - status = [request.status for request in bundle.bridge_requests] - - if initial_bundle_status != bundle.status or initial_status != status: - bundle.store() - def _raise_if_invalid(self, bridge_requests: t.List) -> None: """Preprocess quote requests.""" @@ -942,22 +894,17 @@ def bridge_refill_requirements( for amount in from_tokens.values() ) - bridge_request_status = [ - request.get_status_json() for request in bundle.bridge_requests - ] - - return dict( + status_json = self.bridge_provider.get_status_json(bundle) + status_json.update( { - "id": bundle.id, "balances": balances, "bridge_refill_requirements": bridge_refill_requirements, - "bridge_request_status": bridge_request_status, "bridge_total_requirements": bridge_total_requirements, "expiration_timestamp": bundle.timestamp + self.quote_validity_period, "is_refill_required": is_refill_required, - "status": bundle.status.value, } ) + return status_json def execute_bundle(self, bundle_id: str) -> t.Dict: """Execute the bundle""" @@ -972,6 +919,7 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}." ) + self.data.last_requested_bundle = None bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json" bundle.path = bundle_path bundle.store() @@ -989,31 +937,18 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: self.bridge_provider.execute(request) self._store_data() - self.data.last_requested_bundle = None # TODO Move up after bundle.store() ? self._store_data() bundle.store() self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.") + return self.get_status(bundle_id) - return self.get_execution_status(bundle_id) - - def get_execution_status(self, bundle_id: str) -> t.Dict: + def get_status(self, bundle_id: str) -> t.Dict: """Get execution status of bundle.""" + bundle = self.data.last_requested_bundle + if bundle is not None and bundle.id == bundle_id: + return self.bridge_provider.get_status_json(bundle) bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json" bundle = cast(BridgeRequestBundle, BridgeRequestBundle.load(bundle_path)) bundle.path = bundle_path # TODO backport to resource.py ? - - if not bundle: - raise ValueError(f"Bundle id {bundle_id} not found.") - - self._update_bundle_status(bundle) - - bridge_request_status = [ - request.get_status_json() for request in bundle.bridge_requests - ] - - return { - "id": bundle.id, - "status": bundle.status.value, - "bridge_request_status": bridge_request_status, - } + return self.bridge_provider.get_status_json(bundle) diff --git a/operate/cli.py b/operate/cli.py index 1ca70dda4..eea0ceaa3 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1074,9 +1074,7 @@ async def _bridge_status(request: Request) -> JSONResponse: quote_bundle_id = request.path_params["id"] try: - output = operate.bridge_manager().get_execution_status( - bundle_id=quote_bundle_id - ) + output = operate.bridge_manager().get_status(bundle_id=quote_bundle_id) return JSONResponse( content=output, diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 0bf23a524..322aceb79 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -29,7 +29,6 @@ from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, - BridgeRequestBundleStatus, BridgeRequestStatus, LiFiBridgeProvider, MESSAGE_EXECUTION_SKIPPED, @@ -89,8 +88,11 @@ def test_bridge_zero( with pytest.raises(RuntimeError): bridge.execute(bridge_request) - with pytest.raises(RuntimeError): - bridge.update_execution_status(bridge_request) + status1 = bridge_request.status + bridge.update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." for _ in range(2): timestamp = int(time.time()) @@ -138,8 +140,11 @@ def test_bridge_zero( assert qd.timestamp <= int(time.time()), "Wrong quote data." assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - with pytest.raises(RuntimeError): - bridge.update_execution_status(bridge_request) + status1 = bridge_request.status + bridge.update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." timestamp = int(time.time()) bridge.execute(bridge_request=bridge_request) @@ -214,8 +219,11 @@ def test_bridge_error( with pytest.raises(RuntimeError): bridge.execute(bridge_request) - with pytest.raises(RuntimeError): - bridge.update_execution_status(bridge_request) + status1 = bridge_request.status + bridge.update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." for _ in range(2): timestamp = int(time.time()) @@ -265,8 +273,11 @@ def test_bridge_error( bridge_request.status == BridgeRequestStatus.QUOTE_FAILED ), "Wrong status." - with pytest.raises(RuntimeError): - bridge.update_execution_status(bridge_request) + status1 = bridge_request.status + bridge.update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." timestamp = int(time.time()) bridge.execute(bridge_request=bridge_request) @@ -342,8 +353,11 @@ def test_bridge_quote( with pytest.raises(RuntimeError): bridge.execute(bridge_request) - with pytest.raises(RuntimeError): - bridge.update_execution_status(bridge_request) + status1 = bridge_request.status + bridge.update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." for _ in range(2): timestamp = int(time.time()) @@ -398,8 +412,11 @@ def test_bridge_quote( assert qd.timestamp <= int(time.time()), "Wrong quote data." assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - with pytest.raises(RuntimeError): - bridge.update_execution_status(bridge_request) + status1 = bridge_request.status + bridge.update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." def test_bundle_zero( self, @@ -469,7 +486,6 @@ def test_bundle_zero( }, ], "bridge_total_requirements": brr["bridge_total_requirements"], - "status": BridgeRequestBundleStatus.QUOTE_DONE.value, "expiration_timestamp": brr["expiration_timestamp"], "is_refill_required": False, } @@ -575,7 +591,6 @@ def test_bundle_error( }, ], "bridge_total_requirements": brr["bridge_total_requirements"], - "status": BridgeRequestBundleStatus.QUOTE_FAILED.value, "expiration_timestamp": brr["expiration_timestamp"], "is_refill_required": False, } @@ -676,7 +691,6 @@ def test_bundle_quote( {"message": None, "status": BridgeRequestStatus.QUOTE_DONE.value}, ], "bridge_total_requirements": brr["bridge_total_requirements"], - "status": BridgeRequestBundleStatus.QUOTE_DONE.value, "expiration_timestamp": brr["expiration_timestamp"], "is_refill_required": True, } From 70c071aa1abcaeeef52ce94f7453a4cd1684efc3 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 29 Apr 2025 20:17:12 +0200 Subject: [PATCH 068/173] doc: update api.md --- docs/api.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/api.md b/docs/api.md index e94bc756a..b4d040e85 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1012,7 +1012,6 @@ Executes a quote bundle. See [GET /api/bridge/status/{quote_bundle_id}](#get-api ```json { "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", - "status": "SUBMITTED", "bridge_request_status": [ { "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", @@ -1037,16 +1036,7 @@ Executes a quote bundle. See [GET /api/bridge/status/{quote_bundle_id}](#get-api ### `GET /api/bridge/status/{quote_bundle_id}` -Gets the status of a quote bundle. The attribute `status` can take the following values sequentially: - -- `CREATED`: The quote bundle internal data structure has been created, but no quotes have been requested yet. -- `QUOTE_DONE`: A quote is available for all bridge requests. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. -- `QUOTE_FAILED`: Quote failed for some bridge requests. Quote updates are possible in this state if either expired or forced through the [POST /api/bridge/bridge_refill_requirements](#post-apibridgebridge_refill_requirements) endpoint by setting `force_update=true`. -- `EXECUTION_PENDING`: Execution submitted and pending to be finalized for some bridge request. -- `EXECUTION_DONE`: Execution finalized successfully for all requests. -- `EXECUTION_FAILED`: Execution failed. - -Individual bridge request status: +Gets the status of a quote bundle. Individual bridge request status: - `QUOTE_DONE`: A quote is available. - `QUOTE_FAILED`: Failed to request a quote. @@ -1060,7 +1050,6 @@ Individual bridge request status: ```json { "id": "qb-bdaafd7f-0698-4e10-83dd-d742cc0e656d", - "status": "SUBMITTED", "bridge_request_status": [ { "explorer_link": "https://scan.li.fi/tx/0x3795206347eae1537d852bea05e36c3e76b08cefdfa2d772e24bac2e24f31db3", From deda90a2f3b72c5a7da8d50da274346d0c7c8d06 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 29 Apr 2025 22:51:44 +0200 Subject: [PATCH 069/173] chore: update price impact --- operate/bridge/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 8a0ecb11e..11208f4ee 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -423,7 +423,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: "toAddress": to_address, "toToken": to_token, "toAmount": to_amount, - "maxPriceImpact": 0.20, # TODO determine correct value + "maxPriceImpact": 0.50, # TODO determine correct value } for attempt in range(1, DEFAULT_MAX_QUOTE_RETRIES + 1): start = time.time() From 26a7402862b8ef298f30e41e565baffe531ae744 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 1 May 2025 15:02:14 +0200 Subject: [PATCH 070/173] chore: update --- operate/bridge/bridge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 1d6d1ea5b..6c424c5b3 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -919,13 +919,12 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}." ) + requirements = self.bridge_refill_requirements(bundle.requests_params) self.data.last_requested_bundle = None bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json" bundle.path = bundle_path bundle.store() - requirements = self.bridge_refill_requirements(bundle.requests_params) - if requirements["is_refill_required"]: self.logger.warning( f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}." From cb58a62ec58a939700615a268653e0b15a46454c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 1 May 2025 16:28:40 +0200 Subject: [PATCH 071/173] refactor: split files --- operate/bridge/bridge.py | 639 +----------------- operate/bridge/providers/bridge_provider.py | 234 +++++++ .../bridge/providers/lifi_bridge_provider.py | 474 +++++++++++++ tests/test_bridge_bridge.py | 6 +- 4 files changed, 718 insertions(+), 635 deletions(-) create mode 100644 operate/bridge/providers/bridge_provider.py create mode 100644 operate/bridge/providers/lifi_bridge_provider.py diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 6c424c5b3..4aa59a523 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -20,661 +20,34 @@ """Bridge manager.""" -import enum import json import logging import time import typing as t import uuid -from abc import abstractmethod from dataclasses import dataclass -from http import HTTPStatus from pathlib import Path from typing import cast -from urllib.parse import urlencode -import requests -from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger -from autonomy.chain.base import registry_contracts -from autonomy.chain.tx import TxSettler from deepdiff import DeepDiff -from operate.constants import ( - ON_CHAIN_INTERACT_RETRIES, - ON_CHAIN_INTERACT_SLEEP, - ON_CHAIN_INTERACT_TIMEOUT, - ZERO_ADDRESS, +from operate.bridge.providers.bridge_provider import ( + BridgeProvider, + BridgeRequest, + BridgeRequestBundle, ) +from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider +from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain from operate.resource import LocalResource from operate.services.manage import get_assets_balances from operate.wallet.master import MasterWalletManager -DEFAULT_MAX_QUOTE_RETRIES = 3 DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 EXECUTED_BUNDLES_PATH = "executed" BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" -BRIDGE_REQUEST_PREFIX = "b-" -MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." -MESSAGE_EXECUTION_SKIPPED = "Execution skipped." - - -@dataclass -class QuoteData(LocalResource): - """QuoteData""" - - attempts: int - elapsed_time: float - message: t.Optional[str] - response: t.Optional[t.Dict] - response_status: int - timestamp: int - - -@dataclass -class ExecutionData(LocalResource): - """ExecutionData""" - - bridge_status: t.Optional[enum.Enum] - elapsed_time: float - explorer_link: t.Optional[str] - message: t.Optional[str] - timestamp: int - tx_hash: t.Optional[str] - tx_status: int - - -class BridgeRequestStatus(str, enum.Enum): - """BridgeRequestStatus""" - - CREATED = "CREATED" - QUOTE_DONE = "QUOTE_DONE" - QUOTE_FAILED = "QUOTE_FAILED" - EXECUTION_PENDING = "EXECUTION_PENDING" - EXECUTION_DONE = "EXECUTION_DONE" - EXECUTION_FAILED = "EXECUTION_FAILED" - - def __str__(self) -> str: - """__str__""" - return self.value - - -@dataclass -class BridgeRequest(LocalResource): - """BridgeRequest""" - - params: t.Dict - id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" - status: BridgeRequestStatus = BridgeRequestStatus.CREATED - quote_data: t.Optional[QuoteData] = None - execution_data: t.Optional[ExecutionData] = None - - def get_status_json(self) -> t.Dict: - """JSON representation of the status.""" - if self.execution_data: - return { - "explorer_link": self.execution_data.explorer_link, - "message": self.execution_data.message, - "status": self.status.value, - "tx_hash": self.execution_data.tx_hash, - } - if self.quote_data: - return {"message": self.quote_data.message, "status": self.status.value} - - return {"message": None, "status": self.status.value} - - -@dataclass -class BridgeRequestBundle(LocalResource): - """BridgeRequestBundle""" - - bridge_provider: str - requests_params: t.List[t.Dict] - bridge_requests: t.List[BridgeRequest] - timestamp: int - id: str - - def get_from_chains(self) -> set[Chain]: - """Get 'from' chains.""" - return { - Chain(request.params["from"]["chain"]) for request in self.bridge_requests - } - - def get_from_addresses(self, chain: Chain) -> set[str]: - """Get 'from' addresses.""" - chain_str = chain.value - return { - request.params["from"]["address"] - for request in self.bridge_requests - if request.params["from"]["chain"] == chain_str - } - - def get_from_tokens(self, chain: Chain) -> set[str]: - """Get 'from' tokens.""" - chain_str = chain.value - return { - request.params["from"]["token"] - for request in self.bridge_requests - if request.params["from"]["chain"] == chain_str - } - - -class BridgeProvider: - """(Abstract) BridgeProvider""" - - def __init__( - self, - wallet_manager: MasterWalletManager, - logger: t.Optional[logging.Logger] = None, - ) -> None: - """Initialize the bridge provider.""" - self.wallet_manager = wallet_manager - self.logger = logger or setup_logger(name="operate.bridge.BridgeProvider") - - def name(self) -> str: - """Get the name of the bridge provider.""" - return self.__class__.__name__ - - @abstractmethod - def quote(self, bridge_request: BridgeRequest) -> None: - """Update the request with the quote.""" - raise NotImplementedError() - - @abstractmethod - def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: - """Gets the bridge requirements to execute the quote, with updated gas estimation.""" - raise NotImplementedError() - - @abstractmethod - def execute(self, bridge_request: BridgeRequest) -> None: - """Execute the quote.""" - raise NotImplementedError() - - @abstractmethod - def update_execution_status(self, bridge_request: BridgeRequest) -> None: - """Update the execution status.""" - raise NotImplementedError() - - def quote_bundle(self, bundle: BridgeRequestBundle) -> None: - """Update the bundle with the quotes.""" - for bridge_request in bundle.bridge_requests: - self.quote(bridge_request=bridge_request) - - bundle.timestamp = int(time.time()) - - def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: - """Sum bridge requirements.""" - - bridge_total_requirements: t.Dict = {} - - for request in bundle.bridge_requests: - if not request.quote_data: - continue - - bridge_requirements = self.bridge_requirements(request) - for from_chain, from_addresses in bridge_requirements.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - bridge_total_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - ).setdefault(from_token, 0) - bridge_total_requirements[from_chain][from_address][ - from_token - ] += from_amount - - return bridge_total_requirements - - def execute_bundle(self, bundle: BridgeRequestBundle) -> None: - """Update the bundle with the quotes.""" - for bridge_request in bundle.bridge_requests: - self.execute(bridge_request=bridge_request) - - def get_status_json(self, bundle: BridgeRequestBundle) -> t.Dict: - """JSON representation of the status.""" - initial_status = [request.status for request in bundle.bridge_requests] - - for request in bundle.bridge_requests: - self.update_execution_status(request) - - updated_status = [request.status for request in bundle.bridge_requests] - - if initial_status != updated_status and bundle.path is not None: - bundle.store() - - bridge_request_status = [ - request.get_status_json() for request in bundle.bridge_requests - ] - - return { - "id": bundle.id, - "bridge_request_status": bridge_request_status, - } - - -class LiFiTransactionStatus(str, enum.Enum): - """LI.FI transaction status.""" - - NOT_FOUND = "NOT_FOUND" - INVALID = "INVALID" - PENDING = "PENDING" - DONE = "DONE" - FAILED = "FAILED" - UNKNOWN = "UNKNOWN" - - def __str__(self) -> str: - """__str__""" - return self.value - - -class LiFiBridgeProvider(BridgeProvider): - """LI.FI Bridge provider.""" - - @staticmethod - def _build_approve_tx( - quote_data: QuoteData, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: - quote = quote_data.response - if not quote: - return None - - if "action" not in quote: - return None - - from_token = quote["action"]["fromToken"]["address"] - if from_token == ZERO_ADDRESS: - return None - - transaction_request = quote.get("transactionRequest") - if not transaction_request: - return None - - from_amount = int(quote["action"]["fromAmount"]) - - approve_tx = registry_contracts.erc20.get_approve_tx( - ledger_api=ledger_api, - contract_address=from_token, - spender=transaction_request["to"], - sender=transaction_request["from"], - amount=from_amount, - ) - return LiFiBridgeProvider._update_tx_gas_pricing(approve_tx, ledger_api) - - @staticmethod - def _get_bridge_tx( - quote_data: QuoteData, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: - quote = quote_data.response - if not quote: - return None - - if "action" not in quote: - return None - - transaction_request = quote.get("transactionRequest") - if not transaction_request: - return None - - bridge_tx = { - "value": int(transaction_request["value"], 16), - "to": transaction_request["to"], - "data": bytes.fromhex(transaction_request["data"][2:]), - "from": transaction_request["from"], - "chainId": transaction_request["chainId"], - "gasPrice": int(transaction_request["gasPrice"], 16), - "gas": int(transaction_request["gasLimit"], 16), - "nonce": ledger_api.api.eth.get_transaction_count( - transaction_request["from"] - ), - } - - return LiFiBridgeProvider._update_tx_gas_pricing(bridge_tx, ledger_api) - - # TODO This gas pricing management should possibly be done at a lower level in the library - @staticmethod - def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: - output_tx = tx.copy() - output_tx.pop("maxFeePerGas", None) - output_tx.pop("gasPrice", None) - output_tx.pop("maxPriorityFeePerGas", None) - - gas_pricing = ledger_api.try_get_gas_pricing() - if gas_pricing is None: - raise RuntimeError("[LI.FI BRIDGE] Unable to retrieve gas pricing.") - - if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: - output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] - output_tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] - elif "gasPrice" in gas_pricing: - output_tx["gasPrice"] = gas_pricing["gasPrice"] - else: - raise RuntimeError("[LI.FI BRIDGE] Retrieved invalid gas pricing.") - - return output_tx - - @staticmethod - def _calculate_gas_fees(tx: t.Dict) -> int: - gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" - return tx.get(gas_key, 0) * tx["gas"] - - def quote(self, bridge_request: BridgeRequest) -> None: - """Update the request with the quote.""" - - if bridge_request.status not in ( - BridgeRequestStatus.CREATED, - BridgeRequestStatus.QUOTE_DONE, - BridgeRequestStatus.QUOTE_FAILED, - ): - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." - ) - - if bridge_request.execution_data: - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id}: execution already present." - ) - - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] - from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] - to_address = bridge_request.params["to"]["address"] - to_token = bridge_request.params["to"]["token"] - to_amount = bridge_request.params["to"]["amount"] - - if to_amount == 0: - self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") - quote_data = QuoteData( - attempts=0, - elapsed_time=0, - message=MESSAGE_QUOTE_ZERO, - response=None, - response_status=0, - timestamp=int(time.time()), - ) - bridge_request.quote_data = quote_data - bridge_request.status = BridgeRequestStatus.QUOTE_DONE - return - - url = "https://li.quest/v1/quote/toAmount" - headers = {"accept": "application/json"} - params = { - "fromChain": Chain(from_chain).id, - "fromAddress": from_address, - "fromToken": from_token, - "toChain": Chain(to_chain).id, - "toAddress": to_address, - "toToken": to_token, - "toAmount": to_amount, - "maxPriceImpact": 0.50, # TODO determine correct value - } - for attempt in range(1, DEFAULT_MAX_QUOTE_RETRIES + 1): - start = time.time() - try: - self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") - response = requests.get( - url=url, headers=headers, params=params, timeout=30 - ) - response.raise_for_status() - response_json = response.json() - quote_data = QuoteData( - attempts=attempt, - elapsed_time=time.time() - start, - message=None, - response=response_json, - response_status=response.status_code, - timestamp=int(time.time()), - ) - bridge_request.quote_data = quote_data - bridge_request.status = BridgeRequestStatus.QUOTE_DONE - return - except requests.Timeout as e: - self.logger.warning( - f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." - ) - quote_data = QuoteData( - attempts=attempt, - elapsed_time=time.time() - start, - message=str(e), - response=None, - response_status=HTTPStatus.GATEWAY_TIMEOUT, - timestamp=int(time.time()), - ) - except requests.RequestException as e: - self.logger.warning( - f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." - ) - response_json = response.json() - quote_data = QuoteData( - attempts=attempt, - elapsed_time=time.time() - start, - message=response_json.get("message") or str(e), - response=response_json, - response_status=getattr( - response, "status_code", HTTPStatus.BAD_GATEWAY - ), - timestamp=int(time.time()), - ) - if attempt >= DEFAULT_MAX_QUOTE_RETRIES: - self.logger.error( - f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_QUOTE_RETRIES} attempts." - ) - bridge_request.quote_data = quote_data - bridge_request.status = BridgeRequestStatus.QUOTE_FAILED - return - - time.sleep(2) - - def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: - """Gets the fund requirements to execute the quote, with updated gas estimation.""" - - quote_data = bridge_request.quote_data - if not quote_data: - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot compute requirements for bridge request {bridge_request.id}: quote not present." - ) - - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] - from_token = bridge_request.params["from"]["token"] - - zero_requirements = { - from_chain: { - from_address: { - ZERO_ADDRESS: 0, - from_token: 0, - } - } - } - - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - - approve_tx = self._build_approve_tx(quote_data, ledger_api) - bridge_tx = self._get_bridge_tx(quote_data, ledger_api) - if not bridge_tx: - return zero_requirements - - bridge_tx_value = bridge_tx["value"] - bridge_tx_gas_fees = self._calculate_gas_fees(bridge_tx) - - if approve_tx: - approve_tx_gas_fees = self._calculate_gas_fees(approve_tx) - return { - from_chain: { - from_address: { - ZERO_ADDRESS: bridge_tx_value - + bridge_tx_gas_fees - + approve_tx_gas_fees, - from_token: int(quote_data.response["action"]["fromAmount"]), # type: ignore - } - } - } - - return { - from_chain: { - from_address: {from_token: bridge_tx_value + bridge_tx_gas_fees} - } - } - - def execute(self, bridge_request: BridgeRequest) -> None: - """Execute the quote.""" - - if bridge_request.status not in ( - BridgeRequestStatus.QUOTE_DONE, - BridgeRequestStatus.QUOTE_FAILED, - ): - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." - ) - - if not bridge_request.quote_data: - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: quote data not present." - ) - - if bridge_request.execution_data: - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: execution data already present." - ) - - timestamp = time.time() - quote = bridge_request.quote_data.response - - if not quote or "action" not in quote: - self.logger.info( - f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" - ) - execution_data = ExecutionData( - bridge_status=None, - elapsed_time=0, - explorer_link=None, - message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", - timestamp=int(timestamp), - tx_hash=None, - tx_status=0, - ) - bridge_request.execution_data = execution_data - - if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: - bridge_request.status = BridgeRequestStatus.EXECUTION_DONE - else: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - return - - try: - self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") - from_token = quote["action"]["fromToken"]["address"] - - transaction_request = quote["transactionRequest"] - chain = Chain.from_id(transaction_request["chainId"]) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - - tx_settler = TxSettler( - ledger_api=ledger_api, - crypto=wallet.crypto, - chain_type=chain, - timeout=ON_CHAIN_INTERACT_TIMEOUT, - retries=ON_CHAIN_INTERACT_RETRIES, - sleep=ON_CHAIN_INTERACT_SLEEP, - ) - - # Bridges from an asset other than native require an approval transaction. - approve_tx = self._build_approve_tx(bridge_request.quote_data, ledger_api) - if approve_tx: - self.logger.info( - f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." - ) - setattr( # noqa: B010 - tx_settler, "build", lambda *args, **kwargs: approve_tx - ) - tx_settler.transact( - method=lambda: {}, - contract="", - kwargs={}, - dry_run=False, - ) - self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") - - self.logger.info( - f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." - ) - bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) - setattr( # noqa: B010 - tx_settler, "build", lambda *args, **kwargs: bridge_tx - ) - tx_receipt = tx_settler.transact( - method=lambda: {}, - contract="", - kwargs={}, - dry_run=False, - ) - self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") - tx_hash = tx_receipt.get("transactionHash", "").hex() - - execution_data = ExecutionData( - bridge_status=LiFiTransactionStatus.NOT_FOUND, - elapsed_time=time.time() - timestamp, - explorer_link=f"https://scan.li.fi/tx/{tx_hash}", - message=None, - timestamp=int(timestamp), - tx_hash=tx_hash, - tx_status=tx_receipt.get("status", 0), - ) - bridge_request.execution_data = execution_data - if tx_hash: - bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING - else: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - - except Exception as e: # pylint: disable=broad-except - execution_data = ExecutionData( - bridge_status=LiFiTransactionStatus.UNKNOWN, - elapsed_time=time.time() - timestamp, - explorer_link=None, - message=f"Error executing quote: {str(e)}", - timestamp=int(timestamp), - tx_hash=None, - tx_status=0, - ) - bridge_request.execution_data = execution_data - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - - def update_execution_status(self, bridge_request: BridgeRequest) -> None: - """Update the execution status. Returns `True` if the status changed.""" - - if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): - return - - if not bridge_request.execution_data: - raise RuntimeError( - f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id}: execution data not present." - ) - - execution = bridge_request.execution_data - tx_hash = execution.tx_hash - - url = "https://li.quest/v1/status" - headers = {"accept": "application/json"} - params = { - "txHash": tx_hash, - } - self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") - response = requests.get(url=url, headers=headers, params=params, timeout=30) - response.raise_for_status() - response_json = response.json() - lifi_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) - execution.message = response_json.get("substatusMessage") - - if execution.bridge_status != lifi_status: - execution.bridge_status = lifi_status - if lifi_status == LiFiTransactionStatus.DONE: - bridge_request.status = BridgeRequestStatus.EXECUTION_DONE - elif lifi_status == LiFiTransactionStatus.FAILED: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @dataclass diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py new file mode 100644 index 000000000..6a917eb74 --- /dev/null +++ b/operate/bridge/providers/bridge_provider.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Bridge provider.""" + + +import enum +import logging +import time +import typing as t +import uuid +from abc import abstractmethod +from dataclasses import dataclass + +from aea.helpers.logging import setup_logger + +from operate.operate_types import Chain +from operate.resource import LocalResource +from operate.wallet.master import MasterWalletManager + + +DEFAULT_MAX_QUOTE_RETRIES = 3 +DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 +BRIDGE_REQUEST_PREFIX = "b-" +MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." +MESSAGE_EXECUTION_SKIPPED = "Execution skipped." + + +@dataclass +class QuoteData(LocalResource): + """QuoteData""" + + attempts: int + elapsed_time: float + message: t.Optional[str] + response: t.Optional[t.Dict] + response_status: int + timestamp: int + + +@dataclass +class ExecutionData(LocalResource): + """ExecutionData""" + + bridge_status: t.Optional[enum.Enum] + elapsed_time: float + explorer_link: t.Optional[str] + message: t.Optional[str] + timestamp: int + tx_hash: t.Optional[str] + tx_status: int + + +class BridgeRequestStatus(str, enum.Enum): + """BridgeRequestStatus""" + + CREATED = "CREATED" + QUOTE_DONE = "QUOTE_DONE" + QUOTE_FAILED = "QUOTE_FAILED" + EXECUTION_PENDING = "EXECUTION_PENDING" + EXECUTION_DONE = "EXECUTION_DONE" + EXECUTION_FAILED = "EXECUTION_FAILED" + + def __str__(self) -> str: + """__str__""" + return self.value + + +@dataclass +class BridgeRequest(LocalResource): + """BridgeRequest""" + + params: t.Dict + id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" + status: BridgeRequestStatus = BridgeRequestStatus.CREATED + quote_data: t.Optional[QuoteData] = None + execution_data: t.Optional[ExecutionData] = None + + def get_status_json(self) -> t.Dict: + """JSON representation of the status.""" + if self.execution_data: + return { + "explorer_link": self.execution_data.explorer_link, + "message": self.execution_data.message, + "status": self.status.value, + "tx_hash": self.execution_data.tx_hash, + } + if self.quote_data: + return {"message": self.quote_data.message, "status": self.status.value} + + return {"message": None, "status": self.status.value} + + +@dataclass +class BridgeRequestBundle(LocalResource): + """BridgeRequestBundle""" + + bridge_provider: str + requests_params: t.List[t.Dict] + bridge_requests: t.List[BridgeRequest] + timestamp: int + id: str + + def get_from_chains(self) -> set[Chain]: + """Get 'from' chains.""" + return { + Chain(request.params["from"]["chain"]) for request in self.bridge_requests + } + + def get_from_addresses(self, chain: Chain) -> set[str]: + """Get 'from' addresses.""" + chain_str = chain.value + return { + request.params["from"]["address"] + for request in self.bridge_requests + if request.params["from"]["chain"] == chain_str + } + + def get_from_tokens(self, chain: Chain) -> set[str]: + """Get 'from' tokens.""" + chain_str = chain.value + return { + request.params["from"]["token"] + for request in self.bridge_requests + if request.params["from"]["chain"] == chain_str + } + + +class BridgeProvider: + """(Abstract) BridgeProvider""" + + def __init__( + self, + wallet_manager: MasterWalletManager, + logger: t.Optional[logging.Logger] = None, + ) -> None: + """Initialize the bridge provider.""" + self.wallet_manager = wallet_manager + self.logger = logger or setup_logger(name="operate.bridge.BridgeProvider") + + def name(self) -> str: + """Get the name of the bridge provider.""" + return self.__class__.__name__ + + @abstractmethod + def quote(self, bridge_request: BridgeRequest) -> None: + """Update the request with the quote.""" + raise NotImplementedError() + + @abstractmethod + def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: + """Gets the bridge requirements to execute the quote, with updated gas estimation.""" + raise NotImplementedError() + + @abstractmethod + def execute(self, bridge_request: BridgeRequest) -> None: + """Execute the quote.""" + raise NotImplementedError() + + @abstractmethod + def update_execution_status(self, bridge_request: BridgeRequest) -> None: + """Update the execution status.""" + raise NotImplementedError() + + def quote_bundle(self, bundle: BridgeRequestBundle) -> None: + """Update the bundle with the quotes.""" + for bridge_request in bundle.bridge_requests: + self.quote(bridge_request=bridge_request) + + bundle.timestamp = int(time.time()) + + def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: + """Sum bridge requirements.""" + + bridge_total_requirements: t.Dict = {} + + for request in bundle.bridge_requests: + if not request.quote_data: + continue + + bridge_requirements = self.bridge_requirements(request) + for from_chain, from_addresses in bridge_requirements.items(): + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + bridge_total_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ).setdefault(from_token, 0) + bridge_total_requirements[from_chain][from_address][ + from_token + ] += from_amount + + return bridge_total_requirements + + def execute_bundle(self, bundle: BridgeRequestBundle) -> None: + """Update the bundle with the quotes.""" + for bridge_request in bundle.bridge_requests: + self.execute(bridge_request=bridge_request) + + def get_status_json(self, bundle: BridgeRequestBundle) -> t.Dict: + """JSON representation of the status.""" + initial_status = [request.status for request in bundle.bridge_requests] + + for request in bundle.bridge_requests: + self.update_execution_status(request) + + updated_status = [request.status for request in bundle.bridge_requests] + + if initial_status != updated_status and bundle.path is not None: + bundle.store() + + bridge_request_status = [ + request.get_status_json() for request in bundle.bridge_requests + ] + + return { + "id": bundle.id, + "bridge_request_status": bridge_request_status, + } diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py new file mode 100644 index 000000000..7400c07ec --- /dev/null +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""LI.FI Bridge provider.""" + + +import enum +import time +import typing as t +from http import HTTPStatus +from urllib.parse import urlencode + +import requests +from aea.crypto.base import LedgerApi +from autonomy.chain.base import registry_contracts +from autonomy.chain.tx import TxSettler + +from operate.bridge.providers.bridge_provider import ( + BridgeProvider, + BridgeRequest, + BridgeRequestStatus, + DEFAULT_MAX_QUOTE_RETRIES, + ExecutionData, + MESSAGE_EXECUTION_SKIPPED, + MESSAGE_QUOTE_ZERO, + QuoteData, +) +from operate.constants import ( + ON_CHAIN_INTERACT_RETRIES, + ON_CHAIN_INTERACT_SLEEP, + ON_CHAIN_INTERACT_TIMEOUT, + ZERO_ADDRESS, +) +from operate.operate_types import Chain + + +class LiFiTransactionStatus(str, enum.Enum): + """LI.FI transaction status.""" + + NOT_FOUND = "NOT_FOUND" + INVALID = "INVALID" + PENDING = "PENDING" + DONE = "DONE" + FAILED = "FAILED" + UNKNOWN = "UNKNOWN" + + def __str__(self) -> str: + """__str__""" + return self.value + + +class LiFiBridgeProvider(BridgeProvider): + """LI.FI Bridge provider.""" + + @staticmethod + def _build_approve_tx( + quote_data: QuoteData, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote = quote_data.response + if not quote: + return None + + if "action" not in quote: + return None + + from_token = quote["action"]["fromToken"]["address"] + if from_token == ZERO_ADDRESS: + return None + + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None + + from_amount = int(quote["action"]["fromAmount"]) + + approve_tx = registry_contracts.erc20.get_approve_tx( + ledger_api=ledger_api, + contract_address=from_token, + spender=transaction_request["to"], + sender=transaction_request["from"], + amount=from_amount, + ) + return LiFiBridgeProvider._update_tx_gas_pricing(approve_tx, ledger_api) + + @staticmethod + def _get_bridge_tx( + quote_data: QuoteData, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote = quote_data.response + if not quote: + return None + + if "action" not in quote: + return None + + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None + + bridge_tx = { + "value": int(transaction_request["value"], 16), + "to": transaction_request["to"], + "data": bytes.fromhex(transaction_request["data"][2:]), + "from": transaction_request["from"], + "chainId": transaction_request["chainId"], + "gasPrice": int(transaction_request["gasPrice"], 16), + "gas": int(transaction_request["gasLimit"], 16), + "nonce": ledger_api.api.eth.get_transaction_count( + transaction_request["from"] + ), + } + + return LiFiBridgeProvider._update_tx_gas_pricing(bridge_tx, ledger_api) + + # TODO This gas pricing management should possibly be done at a lower level in the library + @staticmethod + def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: + output_tx = tx.copy() + output_tx.pop("maxFeePerGas", None) + output_tx.pop("gasPrice", None) + output_tx.pop("maxPriorityFeePerGas", None) + + gas_pricing = ledger_api.try_get_gas_pricing() + if gas_pricing is None: + raise RuntimeError("[LI.FI BRIDGE] Unable to retrieve gas pricing.") + + if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: + output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] + output_tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] + elif "gasPrice" in gas_pricing: + output_tx["gasPrice"] = gas_pricing["gasPrice"] + else: + raise RuntimeError("[LI.FI BRIDGE] Retrieved invalid gas pricing.") + + return output_tx + + @staticmethod + def _calculate_gas_fees(tx: t.Dict) -> int: + gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" + return tx.get(gas_key, 0) * tx["gas"] + + def quote(self, bridge_request: BridgeRequest) -> None: + """Update the request with the quote.""" + + if bridge_request.status not in ( + BridgeRequestStatus.CREATED, + BridgeRequestStatus.QUOTE_DONE, + BridgeRequestStatus.QUOTE_FAILED, + ): + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." + ) + + if bridge_request.execution_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id}: execution already present." + ) + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] + + if to_amount == 0: + self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") + quote_data = QuoteData( + attempts=0, + elapsed_time=0, + message=MESSAGE_QUOTE_ZERO, + response=None, + response_status=0, + timestamp=int(time.time()), + ) + bridge_request.quote_data = quote_data + bridge_request.status = BridgeRequestStatus.QUOTE_DONE + return + + url = "https://li.quest/v1/quote/toAmount" + headers = {"accept": "application/json"} + params = { + "fromChain": Chain(from_chain).id, + "fromAddress": from_address, + "fromToken": from_token, + "toChain": Chain(to_chain).id, + "toAddress": to_address, + "toToken": to_token, + "toAmount": to_amount, + "maxPriceImpact": 0.50, # TODO determine correct value + } + for attempt in range(1, DEFAULT_MAX_QUOTE_RETRIES + 1): + start = time.time() + try: + self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") + response = requests.get( + url=url, headers=headers, params=params, timeout=30 + ) + response.raise_for_status() + response_json = response.json() + quote_data = QuoteData( + attempts=attempt, + elapsed_time=time.time() - start, + message=None, + response=response_json, + response_status=response.status_code, + timestamp=int(time.time()), + ) + bridge_request.quote_data = quote_data + bridge_request.status = BridgeRequestStatus.QUOTE_DONE + return + except requests.Timeout as e: + self.logger.warning( + f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." + ) + quote_data = QuoteData( + attempts=attempt, + elapsed_time=time.time() - start, + message=str(e), + response=None, + response_status=HTTPStatus.GATEWAY_TIMEOUT, + timestamp=int(time.time()), + ) + except requests.RequestException as e: + self.logger.warning( + f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." + ) + response_json = response.json() + quote_data = QuoteData( + attempts=attempt, + elapsed_time=time.time() - start, + message=response_json.get("message") or str(e), + response=response_json, + response_status=getattr( + response, "status_code", HTTPStatus.BAD_GATEWAY + ), + timestamp=int(time.time()), + ) + if attempt >= DEFAULT_MAX_QUOTE_RETRIES: + self.logger.error( + f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_QUOTE_RETRIES} attempts." + ) + bridge_request.quote_data = quote_data + bridge_request.status = BridgeRequestStatus.QUOTE_FAILED + return + + time.sleep(2) + + def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: + """Gets the fund requirements to execute the quote, with updated gas estimation.""" + + quote_data = bridge_request.quote_data + if not quote_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot compute requirements for bridge request {bridge_request.id}: quote not present." + ) + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + + zero_requirements = { + from_chain: { + from_address: { + ZERO_ADDRESS: 0, + from_token: 0, + } + } + } + + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + + approve_tx = self._build_approve_tx(quote_data, ledger_api) + bridge_tx = self._get_bridge_tx(quote_data, ledger_api) + if not bridge_tx: + return zero_requirements + + bridge_tx_value = bridge_tx["value"] + bridge_tx_gas_fees = self._calculate_gas_fees(bridge_tx) + + if approve_tx: + approve_tx_gas_fees = self._calculate_gas_fees(approve_tx) + return { + from_chain: { + from_address: { + ZERO_ADDRESS: bridge_tx_value + + bridge_tx_gas_fees + + approve_tx_gas_fees, + from_token: int(quote_data.response["action"]["fromAmount"]), # type: ignore + } + } + } + + return { + from_chain: { + from_address: {from_token: bridge_tx_value + bridge_tx_gas_fees} + } + } + + def execute(self, bridge_request: BridgeRequest) -> None: + """Execute the quote.""" + + if bridge_request.status not in ( + BridgeRequestStatus.QUOTE_DONE, + BridgeRequestStatus.QUOTE_FAILED, + ): + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." + ) + + if not bridge_request.quote_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: quote data not present." + ) + + if bridge_request.execution_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: execution data already present." + ) + + timestamp = time.time() + quote = bridge_request.quote_data.response + + if not quote or "action" not in quote: + self.logger.info( + f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" + ) + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=0, + explorer_link=None, + message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", + timestamp=int(timestamp), + tx_hash=None, + tx_status=0, + ) + bridge_request.execution_data = execution_data + + if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return + + try: + self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") + from_token = quote["action"]["fromToken"]["address"] + + transaction_request = quote["transactionRequest"] + chain = Chain.from_id(transaction_request["chainId"]) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + + tx_settler = TxSettler( + ledger_api=ledger_api, + crypto=wallet.crypto, + chain_type=chain, + timeout=ON_CHAIN_INTERACT_TIMEOUT, + retries=ON_CHAIN_INTERACT_RETRIES, + sleep=ON_CHAIN_INTERACT_SLEEP, + ) + + # Bridges from an asset other than native require an approval transaction. + approve_tx = self._build_approve_tx(bridge_request.quote_data, ledger_api) + if approve_tx: + self.logger.info( + f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." + ) + setattr( # noqa: B010 + tx_settler, "build", lambda *args, **kwargs: approve_tx + ) + tx_settler.transact( + method=lambda: {}, + contract="", + kwargs={}, + dry_run=False, + ) + self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") + + self.logger.info( + f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." + ) + bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) + setattr( # noqa: B010 + tx_settler, "build", lambda *args, **kwargs: bridge_tx + ) + tx_receipt = tx_settler.transact( + method=lambda: {}, + contract="", + kwargs={}, + dry_run=False, + ) + self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") + tx_hash = tx_receipt.get("transactionHash", "").hex() + + execution_data = ExecutionData( + bridge_status=LiFiTransactionStatus.NOT_FOUND, + elapsed_time=time.time() - timestamp, + explorer_link=f"https://scan.li.fi/tx/{tx_hash}", + message=None, + timestamp=int(timestamp), + tx_hash=tx_hash, + tx_status=tx_receipt.get("status", 0), + ) + bridge_request.execution_data = execution_data + if tx_hash: + bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + + except Exception as e: # pylint: disable=broad-except + execution_data = ExecutionData( + bridge_status=LiFiTransactionStatus.UNKNOWN, + elapsed_time=time.time() - timestamp, + explorer_link=None, + message=f"Error executing quote: {str(e)}", + timestamp=int(timestamp), + tx_hash=None, + tx_status=0, + ) + bridge_request.execution_data = execution_data + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + + def update_execution_status(self, bridge_request: BridgeRequest) -> None: + """Update the execution status. Returns `True` if the status changed.""" + + if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): + return + + if not bridge_request.execution_data: + raise RuntimeError( + f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id}: execution data not present." + ) + + execution = bridge_request.execution_data + tx_hash = execution.tx_hash + + url = "https://li.quest/v1/status" + headers = {"accept": "application/json"} + params = { + "txHash": tx_hash, + } + self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") + response = requests.get(url=url, headers=headers, params=params, timeout=30) + response.raise_for_status() + response_json = response.json() + lifi_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) + execution.message = response_json.get("substatusMessage") + + if execution.bridge_status != lifi_status: + execution.bridge_status = lifi_status + if lifi_status == LiFiTransactionStatus.DONE: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + elif lifi_status == LiFiTransactionStatus.FAILED: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 322aceb79..a0bebac01 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -29,10 +29,12 @@ from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, - BridgeRequestStatus, LiFiBridgeProvider, +) +from operate.bridge.providers.bridge_provider import ( + BridgeRequestStatus, MESSAGE_EXECUTION_SKIPPED, - MESSAGE_QUOTE_ZERO, + MESSAGE_QUOTE_ZERO ) from operate.cli import OperateApp from operate.constants import ZERO_ADDRESS From 2c3d9a2a710cc94212e6d90a4178bb3ed466da61 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 2 May 2025 11:35:22 +0200 Subject: [PATCH 072/173] chore: refactor --- operate/bridge/bridge.py | 201 ++++++++++-------- operate/bridge/providers/bridge_provider.py | 143 +++++-------- .../bridge/providers/lifi_bridge_provider.py | 4 + operate/cli.py | 2 +- tests/test_bridge_bridge.py | 8 +- 5 files changed, 174 insertions(+), 184 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 4aa59a523..f09fbecb3 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -32,11 +32,7 @@ from aea.helpers.logging import setup_logger from deepdiff import DeepDiff -from operate.bridge.providers.bridge_provider import ( - BridgeProvider, - BridgeRequest, - BridgeRequestBundle, -) +from operate.bridge.providers.bridge_provider import BridgeProvider, BridgeRequest from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain @@ -45,11 +41,45 @@ from operate.wallet.master import MasterWalletManager -DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 +DEFAULT_BUNDLE_VALIDITY_PERIOD = 3 * 60 EXECUTED_BUNDLES_PATH = "executed" BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" +@dataclass +class BridgeRequestBundle(LocalResource): + """BridgeRequestBundle""" + + requests_params: t.List[t.Dict] + bridge_requests: t.List[BridgeRequest] + timestamp: int + id: str + + def get_from_chains(self) -> set[Chain]: + """Get 'from' chains.""" + return { + Chain(request.params["from"]["chain"]) for request in self.bridge_requests + } + + def get_from_addresses(self, chain: Chain) -> set[str]: + """Get 'from' addresses.""" + chain_str = chain.value + return { + request.params["from"]["address"] + for request in self.bridge_requests + if request.params["from"]["chain"] == chain_str + } + + def get_from_tokens(self, chain: Chain) -> set[str]: + """Get 'from' tokens.""" + chain_str = chain.value + return { + request.params["from"]["token"] + for request in self.bridge_requests + if request.params["from"]["chain"] == chain_str + } + + @dataclass class BridgeManagerData(LocalResource): """BridgeManagerData""" @@ -88,29 +118,28 @@ def load(cls, path: Path) -> "LocalResource": class BridgeManager: """BridgeManager""" - # TODO singleton + _bridge_providers: t.Dict[str, BridgeProvider] def __init__( self, path: Path, wallet_manager: MasterWalletManager, logger: t.Optional[logging.Logger] = None, - bridge_provider: t.Optional[BridgeProvider] = None, - quote_validity_period: int = DEFAULT_QUOTE_VALIDITY_PERIOD, + quote_validity_period: int = DEFAULT_BUNDLE_VALIDITY_PERIOD, ) -> None: """Initialize bridge manager.""" self.path = path self.wallet_manager = wallet_manager self.logger = logger or setup_logger(name="operate.bridge.BridgeManager") - self.bridge_provider = bridge_provider or LiFiBridgeProvider( - wallet_manager, logger - ) self.quote_validity_period = quote_validity_period self.path.mkdir(exist_ok=True) (self.path / EXECUTED_BUNDLES_PATH).mkdir(exist_ok=True) self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) ) + self._bridge_providers = { + LiFiBridgeProvider.id(): LiFiBridgeProvider(wallet_manager, logger), + } def _store_data(self) -> None: self.logger.info("[BRIDGE MANAGER] Storing data to file.") @@ -133,73 +162,40 @@ def _get_updated_bundle( create_new_bundle = True elif force_update: self.logger.info("[BRIDGE MANAGER] Force bundle update.") - self.bridge_provider.quote_bundle(bundle) + self.quote_bundle(bundle) self._store_data() elif now > bundle.timestamp + self.quote_validity_period: self.logger.info("[BRIDGE MANAGER] Bundle expired.") - self.bridge_provider.quote_bundle(bundle) + self.quote_bundle(bundle) self._store_data() if not bundle or create_new_bundle: self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.") + bridge_requests = [] + for params in requests_params: + bridge = self._bridge_providers[LiFiBridgeProvider.id()] + bridge_requests.append(bridge.create_request(params=params)) + bundle = BridgeRequestBundle( id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}", - bridge_provider=self.bridge_provider.name(), requests_params=requests_params, - bridge_requests=[ - BridgeRequest(params=params) for params in requests_params - ], + bridge_requests=bridge_requests, timestamp=now, ) self.data.last_requested_bundle = bundle - self.bridge_provider.quote_bundle(bundle) + self.quote_bundle(bundle) self._store_data() return bundle - def _raise_if_invalid(self, bridge_requests: t.List) -> None: + def _raise_if_invalid(self, requests_params: t.List) -> None: """Preprocess quote requests.""" - seen: set = set() - - for request in bridge_requests: - if ( - not isinstance(request, dict) - or "from" not in request - or "to" not in request - ): - raise ValueError( - "Invalid input: All quote requests must contain exactly one 'from' and one 'to' sender." - ) - - from_ = request["from"] - to = request["to"] - - if ( - not isinstance(from_, dict) - or "chain" not in from_ - or "address" not in from_ - or "token" not in from_ - ): - raise ValueError( - "Invalid input: 'from' must contain 'chain', 'address', and 'token'." - ) - - if ( - not isinstance(to, dict) - or "chain" not in to - or "address" not in to - or "token" not in to - or "amount" not in to - ): - raise ValueError( - "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." - ) - - from_chain = request["from"]["chain"] - from_address = request["from"]["address"] + for params in requests_params: + from_chain = params["from"]["chain"] + from_address = params["from"]["address"] wallet = self.wallet_manager.load(Chain(from_chain).ledger_type) wallet_address = wallet.address @@ -212,20 +208,6 @@ def _raise_if_invalid(self, bridge_requests: t.List) -> None: f"Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}." ) - key = ( - request["from"]["chain"], - request["from"]["address"], - request["from"]["token"], - request["to"]["chain"], - request["to"]["address"], - request["to"]["token"], - ) - - if key in seen: - raise ValueError( - "Request contains duplicate entries with same 'from' and 'to'." - ) - def bridge_refill_requirements( self, requests_params: t.List[t.Dict], force_update: bool = False ) -> t.Dict: @@ -247,9 +229,7 @@ def bridge_refill_requirements( addresses=bundle.get_from_addresses(chain), ) - bridge_total_requirements = self.bridge_provider.bridge_total_requirements( - bundle - ) + bridge_total_requirements = self.bridge_total_requirements(bundle) bridge_refill_requirements: t.Dict = {} for from_chain, from_addresses in bridge_total_requirements.items(): @@ -267,7 +247,7 @@ def bridge_refill_requirements( for amount in from_tokens.values() ) - status_json = self.bridge_provider.get_status_json(bundle) + status_json = self.get_status_json(bundle.id) status_json.update( { "balances": balances, @@ -306,21 +286,74 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: self.logger.info("[BRIDGE MANAGER] Executing quotes.") for request in bundle.bridge_requests: - self.bridge_provider.execute(request) + bridge = self._bridge_providers[request.bridge_provider_id] + bridge.execute(request) self._store_data() self._store_data() bundle.store() self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.") - return self.get_status(bundle_id) + return self.get_status_json(bundle_id) - def get_status(self, bundle_id: str) -> t.Dict: + def get_status_json(self, bundle_id: str) -> t.Dict: """Get execution status of bundle.""" bundle = self.data.last_requested_bundle + if bundle is not None and bundle.id == bundle_id: - return self.bridge_provider.get_status_json(bundle) + pass + else: + bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json" + if bundle_path.exists(): + bundle = cast( + BridgeRequestBundle, BridgeRequestBundle.load(bundle_path) + ) + bundle.path = bundle_path # TODO backport to resource.py ? + else: + raise FileNotFoundError(f"Bundle with ID {bundle_id} does not exist.") - bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json" - bundle = cast(BridgeRequestBundle, BridgeRequestBundle.load(bundle_path)) - bundle.path = bundle_path # TODO backport to resource.py ? - return self.bridge_provider.get_status_json(bundle) + initial_status = [request.status for request in bundle.bridge_requests] + + for request in bundle.bridge_requests: + bridge = self._bridge_providers[request.bridge_provider_id] + bridge.update_execution_status(request) + + updated_status = [request.status for request in bundle.bridge_requests] + + if initial_status != updated_status and bundle.path is not None: + bundle.store() + + bridge_request_status = [ + request.get_status_json() for request in bundle.bridge_requests + ] + + return { + "id": bundle.id, + "bridge_request_status": bridge_request_status, + } + + def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: + """Sum bridge requirements.""" + bridge_total_requirements: t.Dict = {} + for request in bundle.bridge_requests: + if not request.quote_data: + continue + bridge = self._bridge_providers[request.bridge_provider_id] + bridge_requirements = bridge.bridge_requirements(request) + for from_chain, from_addresses in bridge_requirements.items(): + for from_address, from_tokens in from_addresses.items(): + for from_token, from_amount in from_tokens.items(): + bridge_total_requirements.setdefault(from_chain, {}).setdefault( + from_address, {} + ).setdefault(from_token, 0) + bridge_total_requirements[from_chain][from_address][ + from_token + ] += from_amount + + return bridge_total_requirements + + def quote_bundle(self, bundle: BridgeRequestBundle) -> None: + """Update the bundle with the quotes.""" + for bridge_request in bundle.bridge_requests: + bridge = self._bridge_providers[bridge_request.bridge_provider_id] + bridge.quote(bridge_request) + bundle.timestamp = int(time.time()) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 6a917eb74..0a823cd2f 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -22,7 +22,6 @@ import enum import logging -import time import typing as t import uuid from abc import abstractmethod @@ -30,7 +29,6 @@ from aea.helpers.logging import setup_logger -from operate.operate_types import Chain from operate.resource import LocalResource from operate.wallet.master import MasterWalletManager @@ -87,6 +85,7 @@ class BridgeRequest(LocalResource): """BridgeRequest""" params: t.Dict + bridge_provider_id: str id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" status: BridgeRequestStatus = BridgeRequestStatus.CREATED quote_data: t.Optional[QuoteData] = None @@ -107,41 +106,6 @@ def get_status_json(self) -> t.Dict: return {"message": None, "status": self.status.value} -@dataclass -class BridgeRequestBundle(LocalResource): - """BridgeRequestBundle""" - - bridge_provider: str - requests_params: t.List[t.Dict] - bridge_requests: t.List[BridgeRequest] - timestamp: int - id: str - - def get_from_chains(self) -> set[Chain]: - """Get 'from' chains.""" - return { - Chain(request.params["from"]["chain"]) for request in self.bridge_requests - } - - def get_from_addresses(self, chain: Chain) -> set[str]: - """Get 'from' addresses.""" - chain_str = chain.value - return { - request.params["from"]["address"] - for request in self.bridge_requests - if request.params["from"]["chain"] == chain_str - } - - def get_from_tokens(self, chain: Chain) -> set[str]: - """Get 'from' tokens.""" - chain_str = chain.value - return { - request.params["from"]["token"] - for request in self.bridge_requests - if request.params["from"]["chain"] == chain_str - } - - class BridgeProvider: """(Abstract) BridgeProvider""" @@ -154,9 +118,53 @@ def __init__( self.wallet_manager = wallet_manager self.logger = logger or setup_logger(name="operate.bridge.BridgeProvider") - def name(self) -> str: - """Get the name of the bridge provider.""" - return self.__class__.__name__ + @classmethod + def id(cls) -> str: + """Get the id of the bridge provider.""" + return f"{cls.__module__}.{cls.__qualname__}" + + def _validate(self, bridge_request: BridgeRequest) -> None: + """Validate the bridge request.""" + if bridge_request.bridge_provider_id != self.id(): + raise ValueError( + f"Bridge request provider id {bridge_request.bridge_provider_id} does not match the bridge provider id {self.id()}" + ) + + def create_request(self, params: t.Dict) -> BridgeRequest: + """Create a bridge request.""" + if "from" not in params or "to" not in params: + raise ValueError( + "Invalid input: All requests must contain exactly one 'from' and one 'to' sender." + ) + + from_ = params["from"] + to = params["to"] + + if ( + not isinstance(from_, t.Dict) + or "chain" not in from_ + or "address" not in from_ + or "token" not in from_ + ): + raise ValueError( + "Invalid input: 'from' must contain 'chain', 'address', and 'token'." + ) + + if ( + not isinstance(to, t.Dict) + or "chain" not in to + or "address" not in to + or "token" not in to + or "amount" not in to + ): + raise ValueError( + "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." + ) + + return BridgeRequest( + params=params, + bridge_provider_id=self.id(), + ) @abstractmethod def quote(self, bridge_request: BridgeRequest) -> None: @@ -177,58 +185,3 @@ def execute(self, bridge_request: BridgeRequest) -> None: def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status.""" raise NotImplementedError() - - def quote_bundle(self, bundle: BridgeRequestBundle) -> None: - """Update the bundle with the quotes.""" - for bridge_request in bundle.bridge_requests: - self.quote(bridge_request=bridge_request) - - bundle.timestamp = int(time.time()) - - def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: - """Sum bridge requirements.""" - - bridge_total_requirements: t.Dict = {} - - for request in bundle.bridge_requests: - if not request.quote_data: - continue - - bridge_requirements = self.bridge_requirements(request) - for from_chain, from_addresses in bridge_requirements.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - bridge_total_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - ).setdefault(from_token, 0) - bridge_total_requirements[from_chain][from_address][ - from_token - ] += from_amount - - return bridge_total_requirements - - def execute_bundle(self, bundle: BridgeRequestBundle) -> None: - """Update the bundle with the quotes.""" - for bridge_request in bundle.bridge_requests: - self.execute(bridge_request=bridge_request) - - def get_status_json(self, bundle: BridgeRequestBundle) -> t.Dict: - """JSON representation of the status.""" - initial_status = [request.status for request in bundle.bridge_requests] - - for request in bundle.bridge_requests: - self.update_execution_status(request) - - updated_status = [request.status for request in bundle.bridge_requests] - - if initial_status != updated_status and bundle.path is not None: - bundle.store() - - bridge_request_status = [ - request.get_status_json() for request in bundle.bridge_requests - ] - - return { - "id": bundle.id, - "bridge_request_status": bridge_request_status, - } diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 7400c07ec..92a474c82 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -157,6 +157,7 @@ def _calculate_gas_fees(tx: t.Dict) -> int: def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" + self._validate(bridge_request) if bridge_request.status not in ( BridgeRequestStatus.CREATED, @@ -265,6 +266,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: """Gets the fund requirements to execute the quote, with updated gas estimation.""" + self._validate(bridge_request) quote_data = bridge_request.quote_data if not quote_data: @@ -318,6 +320,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" + self._validate(bridge_request) if bridge_request.status not in ( BridgeRequestStatus.QUOTE_DONE, @@ -442,6 +445,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" + self._validate(bridge_request) if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): return diff --git a/operate/cli.py b/operate/cli.py index eea0ceaa3..56db8f660 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1074,7 +1074,7 @@ async def _bridge_status(request: Request) -> JSONResponse: quote_bundle_id = request.path_params["id"] try: - output = operate.bridge_manager().get_status(bundle_id=quote_bundle_id) + output = operate.bridge_manager().get_status_json(bundle_id=quote_bundle_id) return JSONResponse( content=output, diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index a0bebac01..cae7e7727 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -34,7 +34,7 @@ from operate.bridge.providers.bridge_provider import ( BridgeRequestStatus, MESSAGE_EXECUTION_SKIPPED, - MESSAGE_QUOTE_ZERO + MESSAGE_QUOTE_ZERO, ) from operate.cli import OperateApp from operate.constants import ZERO_ADDRESS @@ -83,7 +83,7 @@ def test_bridge_zero( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params) + bridge_request = BridgeRequest(params, bridge.id()) assert not bridge_request.quote_data, "Unexpected quote data." @@ -214,7 +214,7 @@ def test_bridge_error( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params) + bridge_request = BridgeRequest(params, bridge.id()) assert not bridge_request.quote_data, "Unexpected quote data." @@ -348,7 +348,7 @@ def test_bridge_quote( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params) + bridge_request = BridgeRequest(params, bridge.id()) assert not bridge_request.quote_data, "Unexpected quote data." From dd69ab6d0e11046ce88ad526e555427142f2e6ec Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 2 May 2025 11:45:20 +0200 Subject: [PATCH 073/173] chore: remove tag from exceptions --- .../bridge/providers/lifi_bridge_provider.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 92a474c82..f87f34784 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -138,7 +138,7 @@ def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: gas_pricing = ledger_api.try_get_gas_pricing() if gas_pricing is None: - raise RuntimeError("[LI.FI BRIDGE] Unable to retrieve gas pricing.") + raise RuntimeError("Unable to retrieve gas pricing.") if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] @@ -146,7 +146,7 @@ def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: elif "gasPrice" in gas_pricing: output_tx["gasPrice"] = gas_pricing["gasPrice"] else: - raise RuntimeError("[LI.FI BRIDGE] Retrieved invalid gas pricing.") + raise RuntimeError("Retrieved invalid gas pricing.") return output_tx @@ -165,12 +165,12 @@ def quote(self, bridge_request: BridgeRequest) -> None: BridgeRequestStatus.QUOTE_FAILED, ): raise RuntimeError( - f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." + f"Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." ) if bridge_request.execution_data: raise RuntimeError( - f"[LI.FI BRIDGE] Cannot quote bridge request {bridge_request.id}: execution already present." + f"Cannot quote bridge request {bridge_request.id}: execution already present." ) from_chain = bridge_request.params["from"]["chain"] @@ -271,7 +271,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: quote_data = bridge_request.quote_data if not quote_data: raise RuntimeError( - f"[LI.FI BRIDGE] Cannot compute requirements for bridge request {bridge_request.id}: quote not present." + f"Cannot compute requirements for bridge request {bridge_request.id}: quote not present." ) from_chain = bridge_request.params["from"]["chain"] @@ -327,17 +327,17 @@ def execute(self, bridge_request: BridgeRequest) -> None: BridgeRequestStatus.QUOTE_FAILED, ): raise RuntimeError( - f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." + f"Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." ) if not bridge_request.quote_data: raise RuntimeError( - f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: quote data not present." + f"Cannot execute bridge request {bridge_request.id}: quote data not present." ) if bridge_request.execution_data: raise RuntimeError( - f"[LI.FI BRIDGE] Cannot execute bridge request {bridge_request.id}: execution data already present." + f"Cannot execute bridge request {bridge_request.id}: execution data already present." ) timestamp = time.time() @@ -452,7 +452,7 @@ def update_execution_status(self, bridge_request: BridgeRequest) -> None: if not bridge_request.execution_data: raise RuntimeError( - f"[LI.FI BRIDGE] Cannot update bridge request {bridge_request.id}: execution data not present." + f"Cannot update bridge request {bridge_request.id}: execution data not present." ) execution = bridge_request.execution_data From cd4b96030aaf5fd4bda6a7205085aa88c939a17f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 2 May 2025 13:05:49 +0200 Subject: [PATCH 074/173] chore: update --- operate/bridge/providers/bridge_provider.py | 4 ++++ operate/bridge/providers/lifi_bridge_provider.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 0a823cd2f..54770f291 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -123,6 +123,10 @@ def id(cls) -> str: """Get the id of the bridge provider.""" return f"{cls.__module__}.{cls.__qualname__}" + def description(self) -> str: + """Get a human-readable description of the bridge provider.""" + return self.__class__.__name__ + def _validate(self, bridge_request: BridgeRequest) -> None: """Validate the bridge request.""" if bridge_request.bridge_provider_id != self.id(): diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index f87f34784..ac09fb871 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -68,6 +68,10 @@ def __str__(self) -> str: class LiFiBridgeProvider(BridgeProvider): """LI.FI Bridge provider.""" + def description(self) -> str: + """Get a human-readable description of the bridge provider.""" + return "LI.FI Bridge & DEX Aggregation Protocol https://li.fi/" + @staticmethod def _build_approve_tx( quote_data: QuoteData, ledger_api: LedgerApi From 0c2bf69c21403d2a3525a7e11ffffcdfe621a378 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 4 May 2025 12:26:20 +0200 Subject: [PATCH 075/173] chore: refactor --- operate/bridge/bridge.py | 7 +- operate/bridge/providers/bridge_provider.py | 246 ++++++++++-- .../bridge/providers/lifi_bridge_provider.py | 358 +++++------------- tests/test_bridge_bridge.py | 55 ++- 4 files changed, 360 insertions(+), 306 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index f09fbecb3..f91f492fa 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -322,9 +322,10 @@ def get_status_json(self, bundle_id: str) -> t.Dict: if initial_status != updated_status and bundle.path is not None: bundle.store() - bridge_request_status = [ - request.get_status_json() for request in bundle.bridge_requests - ] + bridge_request_status = [] + for request in bundle.bridge_requests: + bridge = self._bridge_providers[request.bridge_provider_id] + bridge_request_status.append(bridge.get_status_json(request)) return { "id": bundle.id, diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 54770f291..cd18a80c1 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -22,22 +22,32 @@ import enum import logging +import time import typing as t import uuid from abc import abstractmethod from dataclasses import dataclass +from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger - +from autonomy.chain.tx import TxSettler + +from operate.constants import ( + ON_CHAIN_INTERACT_RETRIES, + ON_CHAIN_INTERACT_SLEEP, + ON_CHAIN_INTERACT_TIMEOUT, + ZERO_ADDRESS, +) +from operate.operate_types import Chain from operate.resource import LocalResource from operate.wallet.master import MasterWalletManager DEFAULT_MAX_QUOTE_RETRIES = 3 -DEFAULT_QUOTE_VALIDITY_PERIOD = 3 * 60 BRIDGE_REQUEST_PREFIX = "b-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." MESSAGE_EXECUTION_SKIPPED = "Execution skipped." +ERC20_APPROVE_SELECTOR = "0x095ea7b3" @dataclass @@ -58,11 +68,10 @@ class ExecutionData(LocalResource): bridge_status: t.Optional[enum.Enum] elapsed_time: float - explorer_link: t.Optional[str] message: t.Optional[str] timestamp: int - tx_hash: t.Optional[str] - tx_status: int + tx_hashes: t.Optional[t.List[str]] + tx_status: t.Optional[t.List[int]] class BridgeRequestStatus(str, enum.Enum): @@ -74,6 +83,7 @@ class BridgeRequestStatus(str, enum.Enum): EXECUTION_PENDING = "EXECUTION_PENDING" EXECUTION_DONE = "EXECUTION_DONE" EXECUTION_FAILED = "EXECUTION_FAILED" + EXECUTION_UNKNOWN = "EXECUTION_UNKNOWN" def __str__(self) -> str: """__str__""" @@ -91,20 +101,6 @@ class BridgeRequest(LocalResource): quote_data: t.Optional[QuoteData] = None execution_data: t.Optional[ExecutionData] = None - def get_status_json(self) -> t.Dict: - """JSON representation of the status.""" - if self.execution_data: - return { - "explorer_link": self.execution_data.explorer_link, - "message": self.execution_data.message, - "status": self.status.value, - "tx_hash": self.execution_data.tx_hash, - } - if self.quote_data: - return {"message": self.quote_data.message, "status": self.status.value} - - return {"message": None, "status": self.status.value} - class BridgeProvider: """(Abstract) BridgeProvider""" @@ -175,17 +171,223 @@ def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" raise NotImplementedError() - @abstractmethod def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: """Gets the bridge requirements to execute the quote, with updated gas estimation.""" - raise NotImplementedError() + self._validate(bridge_request) + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + + transactions = self._get_transactions(bridge_request) + if not transactions: + return { + from_chain: { + from_address: { + ZERO_ADDRESS: 0, + from_token: 0, + } + } + } + + total_native = 0 + total_token = 0 + + for _, tx in transactions: + tx = self._update_with_gas_pricing(tx, ledger_api) + gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" + gas_fees = tx.get(gas_key, 0) * tx["gas"] + tx_value = int(tx.get("value", 0)) + total_native += tx_value + gas_fees + + if tx.get("to", "").lower() == from_token.lower() and tx.get( + "data", "" + ).startswith( + ERC20_APPROVE_SELECTOR + ): # approve(address,uint256) + try: + amount = int(tx["data"][-64:], 16) + total_token += amount + except Exception: + raise RuntimeError("Malformed ERC20 approve transaction.") + + result = { + from_chain: { + from_address: { + ZERO_ADDRESS: total_native, + } + } + } + + if from_token != ZERO_ADDRESS: + result[from_chain][from_address][from_token] = total_token + + return result @abstractmethod + def _get_transactions( + self, bridge_request: BridgeRequest + ) -> t.List[t.Tuple[str, t.Dict]]: + """Get the sorted list of transactions to execute the bridge request.""" + raise NotImplementedError() + def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" - raise NotImplementedError() + self._validate(bridge_request) + + if bridge_request.status not in ( + BridgeRequestStatus.QUOTE_DONE, + BridgeRequestStatus.QUOTE_FAILED, + ): + raise RuntimeError( + f"Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." + ) + if not bridge_request.quote_data: + raise RuntimeError( + f"Cannot execute bridge request {bridge_request.id}: quote data not present." + ) + if bridge_request.execution_data: + raise RuntimeError( + f"Cannot execute bridge request {bridge_request.id}: execution data already present." + ) + + timestamp = time.time() + txs = self._get_transactions(bridge_request) + + if not txs: + self.logger.info( + f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" + ) + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=0, + message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", + timestamp=int(timestamp), + tx_hashes=None, + tx_status=None, + ) + bridge_request.execution_data = execution_data + + if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return + + try: + self.logger.info(f"[BRIDGE] Executing bridge request {bridge_request.id}.") + + chain = Chain(bridge_request.params["from"]["chain"]) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + tx_settler = TxSettler( + ledger_api=ledger_api, + crypto=wallet.crypto, + chain_type=chain, + timeout=ON_CHAIN_INTERACT_TIMEOUT, + retries=ON_CHAIN_INTERACT_RETRIES, + sleep=ON_CHAIN_INTERACT_SLEEP, + ) + tx_hashes = [] + tx_status = [] + + for tx_label, tx in txs: + self.logger.info(f"[BRIDGE] Executing transaction {tx_label}.") + setattr( # noqa: B010 + tx_settler, "build", lambda *args, **kwargs: tx # noqa: B023 + ) + tx_receipt = tx_settler.transact( + method=lambda: {}, + contract="", + kwargs={}, + dry_run=False, + ) + self.logger.info(f"[BRIDGE] Transaction {tx_label} settled.") + tx_hashes.append(tx_receipt.get("transactionHash", "").hex()) + tx_status.append(tx_receipt.get("status", 0)) + + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=time.time() - timestamp, + message=None, + timestamp=int(timestamp), + tx_hashes=tx_hashes, + tx_status=tx_status, + ) + bridge_request.execution_data = execution_data + if len(tx_hashes) == len(txs): + bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + + except Exception as e: # pylint: disable=broad-except + self.logger.error(f"[BRIDGE] Error executing bridge request: {e}") + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=time.time() - timestamp, + message=f"Error executing quote: {str(e)}", + timestamp=int(timestamp), + tx_hashes=None, + tx_status=None, + ) + bridge_request.execution_data = execution_data + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @abstractmethod def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status.""" raise NotImplementedError() + + @abstractmethod + def _get_explorer_link(self, tx_hash: str) -> str: + """Get the explorer link for a transaction.""" + raise NotImplementedError() + + def get_status_json(self, bridge_request: BridgeRequest) -> t.Dict: + """JSON representation of the status.""" + if bridge_request.execution_data: + tx_hash = None + explorer_link = None + if bridge_request.execution_data.tx_hashes: + tx_hash = bridge_request.execution_data.tx_hashes[-1] + explorer_link = self._get_explorer_link(tx_hash) + + return { + "explorer_link": explorer_link, + "message": bridge_request.execution_data.message, + "status": bridge_request.status.value, + "tx_hash": tx_hash, + } + if bridge_request.quote_data: + return { + "message": bridge_request.quote_data.message, + "status": bridge_request.status.value, + } + + return {"message": None, "status": bridge_request.status.value} + + # TODO This gas pricing management should possibly be done at a lower level in the library + @staticmethod + def _update_with_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: + output_tx = tx.copy() + output_tx.pop("maxFeePerGas", None) + output_tx.pop("gasPrice", None) + output_tx.pop("maxPriorityFeePerGas", None) + + gas_pricing = ledger_api.try_get_gas_pricing() + if gas_pricing is None: + raise RuntimeError("Unable to retrieve gas pricing.") + + if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: + output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] + output_tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] + elif "gasPrice" in gas_pricing: + output_tx["gasPrice"] = gas_pricing["gasPrice"] + else: + raise RuntimeError("Retrieved invalid gas pricing.") + + return output_tx diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index ac09fb871..71b6bf1a2 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -29,24 +29,16 @@ import requests from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts -from autonomy.chain.tx import TxSettler from operate.bridge.providers.bridge_provider import ( BridgeProvider, BridgeRequest, BridgeRequestStatus, DEFAULT_MAX_QUOTE_RETRIES, - ExecutionData, - MESSAGE_EXECUTION_SKIPPED, MESSAGE_QUOTE_ZERO, QuoteData, ) -from operate.constants import ( - ON_CHAIN_INTERACT_RETRIES, - ON_CHAIN_INTERACT_SLEEP, - ON_CHAIN_INTERACT_TIMEOUT, - ZERO_ADDRESS, -) +from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain @@ -72,93 +64,6 @@ def description(self) -> str: """Get a human-readable description of the bridge provider.""" return "LI.FI Bridge & DEX Aggregation Protocol https://li.fi/" - @staticmethod - def _build_approve_tx( - quote_data: QuoteData, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: - quote = quote_data.response - if not quote: - return None - - if "action" not in quote: - return None - - from_token = quote["action"]["fromToken"]["address"] - if from_token == ZERO_ADDRESS: - return None - - transaction_request = quote.get("transactionRequest") - if not transaction_request: - return None - - from_amount = int(quote["action"]["fromAmount"]) - - approve_tx = registry_contracts.erc20.get_approve_tx( - ledger_api=ledger_api, - contract_address=from_token, - spender=transaction_request["to"], - sender=transaction_request["from"], - amount=from_amount, - ) - return LiFiBridgeProvider._update_tx_gas_pricing(approve_tx, ledger_api) - - @staticmethod - def _get_bridge_tx( - quote_data: QuoteData, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: - quote = quote_data.response - if not quote: - return None - - if "action" not in quote: - return None - - transaction_request = quote.get("transactionRequest") - if not transaction_request: - return None - - bridge_tx = { - "value": int(transaction_request["value"], 16), - "to": transaction_request["to"], - "data": bytes.fromhex(transaction_request["data"][2:]), - "from": transaction_request["from"], - "chainId": transaction_request["chainId"], - "gasPrice": int(transaction_request["gasPrice"], 16), - "gas": int(transaction_request["gasLimit"], 16), - "nonce": ledger_api.api.eth.get_transaction_count( - transaction_request["from"] - ), - } - - return LiFiBridgeProvider._update_tx_gas_pricing(bridge_tx, ledger_api) - - # TODO This gas pricing management should possibly be done at a lower level in the library - @staticmethod - def _update_tx_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: - output_tx = tx.copy() - output_tx.pop("maxFeePerGas", None) - output_tx.pop("gasPrice", None) - output_tx.pop("maxPriorityFeePerGas", None) - - gas_pricing = ledger_api.try_get_gas_pricing() - if gas_pricing is None: - raise RuntimeError("Unable to retrieve gas pricing.") - - if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: - output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] - output_tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] - elif "gasPrice" in gas_pricing: - output_tx["gasPrice"] = gas_pricing["gasPrice"] - else: - raise RuntimeError("Retrieved invalid gas pricing.") - - return output_tx - - @staticmethod - def _calculate_gas_fees(tx: t.Dict) -> int: - gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" - return tx.get(gas_key, 0) * tx["gas"] - def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" self._validate(bridge_request) @@ -268,184 +173,98 @@ def quote(self, bridge_request: BridgeRequest) -> None: time.sleep(2) - def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: - """Gets the fund requirements to execute the quote, with updated gas estimation.""" - self._validate(bridge_request) + @staticmethod + def _get_bridge_tx( + quote_data: QuoteData, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote = quote_data.response + if not quote: + return None - quote_data = bridge_request.quote_data - if not quote_data: - raise RuntimeError( - f"Cannot compute requirements for bridge request {bridge_request.id}: quote not present." - ) + if "action" not in quote: + return None - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] - from_token = bridge_request.params["from"]["token"] + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None - zero_requirements = { - from_chain: { - from_address: { - ZERO_ADDRESS: 0, - from_token: 0, - } - } + bridge_tx = { + "value": int(transaction_request["value"], 16), + "to": transaction_request["to"], + "data": transaction_request["data"], # TODO remove bytes? + "from": transaction_request["from"], + "chainId": transaction_request["chainId"], + "gasPrice": int(transaction_request["gasPrice"], 16), + "gas": int(transaction_request["gasLimit"], 16), + "nonce": ledger_api.api.eth.get_transaction_count( + transaction_request["from"] + ), } + ledger_api.update_with_gas_estimate(bridge_tx) + return LiFiBridgeProvider._update_with_gas_pricing(bridge_tx, ledger_api) - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - - approve_tx = self._build_approve_tx(quote_data, ledger_api) - bridge_tx = self._get_bridge_tx(quote_data, ledger_api) - if not bridge_tx: - return zero_requirements + @staticmethod + def _get_approve_tx( + quote_data: QuoteData, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote = quote_data.response + if not quote: + return None - bridge_tx_value = bridge_tx["value"] - bridge_tx_gas_fees = self._calculate_gas_fees(bridge_tx) + if "action" not in quote: + return None - if approve_tx: - approve_tx_gas_fees = self._calculate_gas_fees(approve_tx) - return { - from_chain: { - from_address: { - ZERO_ADDRESS: bridge_tx_value - + bridge_tx_gas_fees - + approve_tx_gas_fees, - from_token: int(quote_data.response["action"]["fromAmount"]), # type: ignore - } - } - } - - return { - from_chain: { - from_address: {from_token: bridge_tx_value + bridge_tx_gas_fees} - } - } + from_token = quote["action"]["fromToken"]["address"] + if from_token == ZERO_ADDRESS: + return None - def execute(self, bridge_request: BridgeRequest) -> None: - """Execute the quote.""" - self._validate(bridge_request) + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None - if bridge_request.status not in ( - BridgeRequestStatus.QUOTE_DONE, - BridgeRequestStatus.QUOTE_FAILED, - ): - raise RuntimeError( - f"Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." - ) + from_amount = int(quote["action"]["fromAmount"]) - if not bridge_request.quote_data: - raise RuntimeError( - f"Cannot execute bridge request {bridge_request.id}: quote data not present." - ) + approve_tx = registry_contracts.erc20.get_approve_tx( + ledger_api=ledger_api, + contract_address=from_token, + spender=transaction_request["to"], + sender=transaction_request["from"], + amount=from_amount, + ) + ledger_api.update_with_gas_estimate(approve_tx) + return LiFiBridgeProvider._update_with_gas_pricing(approve_tx, ledger_api) - if bridge_request.execution_data: - raise RuntimeError( - f"Cannot execute bridge request {bridge_request.id}: execution data already present." - ) + def _get_transactions( + self, bridge_request: BridgeRequest + ) -> t.List[t.Tuple[str, t.Dict]]: + """Get the sorted list of transactions to execute the bridge request.""" + self._validate(bridge_request) - timestamp = time.time() - quote = bridge_request.quote_data.response + if not bridge_request.quote_data: + return [] - if not quote or "action" not in quote: - self.logger.info( - f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" - ) - execution_data = ExecutionData( - bridge_status=None, - elapsed_time=0, - explorer_link=None, - message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", - timestamp=int(timestamp), - tx_hash=None, - tx_status=0, - ) - bridge_request.execution_data = execution_data + from_chain = bridge_request.params["from"]["chain"] + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) - if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: - bridge_request.status = BridgeRequestStatus.EXECUTION_DONE - else: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - return + bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) - try: - self.logger.info(f"[LI.FI BRIDGE] Executing quote {quote.get('id')}.") - from_token = quote["action"]["fromToken"]["address"] - - transaction_request = quote["transactionRequest"] - chain = Chain.from_id(transaction_request["chainId"]) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - - tx_settler = TxSettler( - ledger_api=ledger_api, - crypto=wallet.crypto, - chain_type=chain, - timeout=ON_CHAIN_INTERACT_TIMEOUT, - retries=ON_CHAIN_INTERACT_RETRIES, - sleep=ON_CHAIN_INTERACT_SLEEP, - ) + if not bridge_tx: + return [] - # Bridges from an asset other than native require an approval transaction. - approve_tx = self._build_approve_tx(bridge_request.quote_data, ledger_api) - if approve_tx: - self.logger.info( - f"[LI.FI BRIDGE] Preparing approve transaction for for quote {quote['id']} ({from_token=})." - ) - setattr( # noqa: B010 - tx_settler, "build", lambda *args, **kwargs: approve_tx - ) - tx_settler.transact( - method=lambda: {}, - contract="", - kwargs={}, - dry_run=False, - ) - self.logger.info("[LI.FI BRIDGE] Approve transaction settled.") + approve_tx = self._get_approve_tx(bridge_request.quote_data, ledger_api) - self.logger.info( - f"[LI.FI BRIDGE] Preparing bridge transaction for quote {quote['id']}." - ) - bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) - setattr( # noqa: B010 - tx_settler, "build", lambda *args, **kwargs: bridge_tx - ) - tx_receipt = tx_settler.transact( - method=lambda: {}, - contract="", - kwargs={}, - dry_run=False, - ) - self.logger.info("[LI.FI BRIDGE] Bridge transaction settled.") - tx_hash = tx_receipt.get("transactionHash", "").hex() - - execution_data = ExecutionData( - bridge_status=LiFiTransactionStatus.NOT_FOUND, - elapsed_time=time.time() - timestamp, - explorer_link=f"https://scan.li.fi/tx/{tx_hash}", - message=None, - timestamp=int(timestamp), - tx_hash=tx_hash, - tx_status=tx_receipt.get("status", 0), - ) - bridge_request.execution_data = execution_data - if tx_hash: - bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING - else: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + if approve_tx: + bridge_tx["nonce"] = approve_tx["nonce"] + 1 + return [ + ("ERC20 Approve transaction", approve_tx), + ("Bridge transaction", bridge_tx), + ] - except Exception as e: # pylint: disable=broad-except - execution_data = ExecutionData( - bridge_status=LiFiTransactionStatus.UNKNOWN, - elapsed_time=time.time() - timestamp, - explorer_link=None, - message=f"Error executing quote: {str(e)}", - timestamp=int(timestamp), - tx_hash=None, - tx_status=0, - ) - bridge_request.execution_data = execution_data - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return [ + ("Bridge transaction", bridge_tx), + ] def update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" @@ -460,17 +279,30 @@ def update_execution_status(self, bridge_request: BridgeRequest) -> None: ) execution = bridge_request.execution_data - tx_hash = execution.tx_hash + if not execution.tx_hashes: + return + + tx_hash = execution.tx_hashes[-1] url = "https://li.quest/v1/status" headers = {"accept": "application/json"} params = { "txHash": tx_hash, } - self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") - response = requests.get(url=url, headers=headers, params=params, timeout=30) - response.raise_for_status() - response_json = response.json() + try: + self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") + response = requests.get(url=url, headers=headers, params=params, timeout=30) + response.raise_for_status() + response_json = response.json() + except Exception as e: + self.logger.error( + f"[LI.FI BRIDGE] Failed to update bridge status for {tx_hash}: {e}" + ) + bridge_request.status = ( + BridgeRequestStatus.EXECUTION_UNKNOWN + ) # TODO should be EXECUTION_FAILED ? + return + lifi_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) execution.message = response_json.get("substatusMessage") @@ -480,3 +312,7 @@ def update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_request.status = BridgeRequestStatus.EXECUTION_DONE elif lifi_status == LiFiTransactionStatus.FAILED: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + + def _get_explorer_link(self, tx_hash: str) -> str: + """Get the explorer link for a transaction.""" + return f"https://scan.li.fi/tx/{tx_hash}" diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index cae7e7727..36abc2b23 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -111,7 +111,7 @@ def test_bridge_zero( bridge_request.status == BridgeRequestStatus.QUOTE_DONE ), "Wrong status." - sj = bridge_request.get_status_json() + sj = bridge.get_status_json(bridge_request) expected_sj = { "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, @@ -154,13 +154,12 @@ def test_bridge_zero( assert ed is not None, "Missing execution data." assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." - assert ed.explorer_link is None, "Wrong execution data." assert ed.message is not None, "Wrong execution data." assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hash is None, "Wrong execution data." - assert ed.tx_status == 0, "Wrong execution data." + assert ed.tx_hashes is None, "Wrong execution data." + assert ed.tx_status is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE ), "Wrong status." @@ -170,7 +169,7 @@ def test_bridge_zero( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE ), "Wrong status." - sj = bridge_request.get_status_json() + sj = bridge.get_status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], @@ -243,7 +242,7 @@ def test_bridge_error( ), "Wrong status." assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge_request.get_status_json() + sj = bridge.get_status_json(bridge_request) expected_sj = { "message": bridge_request.quote_data.message, "status": BridgeRequestStatus.QUOTE_FAILED.value, @@ -287,13 +286,12 @@ def test_bridge_error( assert ed is not None, "Missing execution data." assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." - assert ed.explorer_link is None, "Wrong execution data." assert ed.message is not None, "Wrong execution data." assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hash is None, "Wrong execution data." - assert ed.tx_status == 0, "Wrong execution data." + assert ed.tx_hashes is None, "Wrong execution data." + assert ed.tx_status is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED ), "Wrong status." @@ -303,7 +301,7 @@ def test_bridge_error( bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED ), "Wrong status." - sj = bridge_request.get_status_json() + sj = bridge.get_status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], @@ -377,7 +375,7 @@ def test_bridge_quote( ), "Wrong status." assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge_request.get_status_json() + sj = bridge.get_status_json(bridge_request) expected_sj = { "message": bridge_request.quote_data.message, "status": BridgeRequestStatus.QUOTE_DONE.value, @@ -651,7 +649,7 @@ def test_bundle_quote( params = [ { "from": { - "chain": "gnosis", + "chain": "ethereum", "address": wallet_address, "token": ZERO_ADDRESS, }, @@ -664,9 +662,9 @@ def test_bundle_quote( }, { "from": { - "chain": "gnosis", + "chain": "ethereum", "address": wallet_address, - "token": OLAS[Chain.GNOSIS], + "token": OLAS[Chain.ETHEREUM], }, "to": { "chain": "base", @@ -677,15 +675,32 @@ def test_bundle_quote( }, ] + bundle = bridge_manager.data.last_requested_bundle + assert bundle is None, "Unexpected bundle." timestamp1 = time.time() brr = bridge_manager.bridge_refill_requirements( requests_params=params, force_update=False ) timestamp2 = time.time() + + bundle = bridge_manager.data.last_requested_bundle + assert bundle is not None, "Unexpected bundle." + + request = bundle.bridge_requests[0] + bridge = bridge_manager._bridge_providers[request.bridge_provider_id] + assert ( + len(bridge._get_transactions(request)) == 1 + ), "Wrong number of transactions." + request = bundle.bridge_requests[1] + bridge = bridge_manager._bridge_providers[request.bridge_provider_id] + assert ( + len(bridge._get_transactions(request)) == 2 + ), "Wrong number of transactions." + expected_brr = { "id": brr["id"], "balances": { - "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + "ethereum": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.ETHEREUM]: 0}} }, "bridge_refill_requirements": brr["bridge_refill_requirements"], "bridge_request_status": [ @@ -698,18 +713,18 @@ def test_bundle_quote( } assert ( - brr["balances"]["gnosis"][wallet_address][ZERO_ADDRESS] == 0 + brr["balances"]["ethereum"][wallet_address][ZERO_ADDRESS] == 0 ), "Wrong bridge refill requirements." assert ( - brr["balances"]["gnosis"][wallet_address][OLAS[Chain.GNOSIS]] == 0 + brr["balances"]["ethereum"][wallet_address][OLAS[Chain.ETHEREUM]] == 0 ), "Wrong bridge refill requirements." assert ( - brr["bridge_refill_requirements"]["gnosis"][wallet_address][ZERO_ADDRESS] + brr["bridge_refill_requirements"]["ethereum"][wallet_address][ZERO_ADDRESS] > 0 ), "Wrong bridge refill requirements." assert ( - brr["bridge_refill_requirements"]["gnosis"][wallet_address][ - OLAS[Chain.GNOSIS] + brr["bridge_refill_requirements"]["ethereum"][wallet_address][ + OLAS[Chain.ETHEREUM] ] > 0 ), "Wrong bridge refill requirements." From bf2a4dde6fceecb2171812f865ab0b5da43449e1 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 4 May 2025 21:33:53 +0200 Subject: [PATCH 076/173] chroe: add file --- tests/test_bridge_providers.py | 419 +++++++++++++++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 tests/test_bridge_providers.py diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py new file mode 100644 index 000000000..5143f89bf --- /dev/null +++ b/tests/test_bridge_providers.py @@ -0,0 +1,419 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for bridge.providers.* module.""" + + +import os +import time +from pathlib import Path + +import pytest +from deepdiff import DeepDiff + +from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, + BridgeRequest, + LiFiBridgeProvider, +) +from operate.bridge.providers.bridge_provider import ( + BridgeRequestStatus, + MESSAGE_EXECUTION_SKIPPED, + MESSAGE_QUOTE_ZERO, +) +from operate.cli import OperateApp +from operate.constants import ZERO_ADDRESS +from operate.ledger.profiles import OLAS +from operate.operate_types import Chain, LedgerType + + +ROOT_PATH = Path(__file__).resolve().parent +OPERATE = ".operate_test" +RUNNING_IN_CI = ( + os.getenv("GITHUB_ACTIONS", "").lower() == "true" + or os.getenv("CI", "").lower() == "true" +) + + +class TestLiFiBridge: + """Tests for bridge.providers.LiFiBridgeProvider class.""" + + def test_bridge_zero( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_zero""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 0, + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params, bridge.id()) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts == 0, "Wrong quote data." + assert qd.elapsed_time == 0, "Wrong quote data." + assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." + assert qd.response is None, "Wrong quote data." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_DONE + ), "Wrong status." + + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + assert bridge_request.quote_data is not None, "Missing quote data." + + br = bridge.bridge_requirements(bridge_request) + expected_br = { + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts == 0, "Wrong quote data." + assert qd.elapsed_time == 0, "Wrong quote data." + assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." + assert qd.response is None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + timestamp = int(time.time()) + bridge.execute(bridge_request=bridge_request) + ed = bridge_request.execution_data + assert ed is not None, "Missing execution data." + assert ed.bridge_status is None, "Wrong execution data." + assert ed.elapsed_time == 0, "Wrong execution data." + assert ed.message is not None, "Wrong execution data." + assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." + assert timestamp <= ed.timestamp, "Wrong quote data." + assert ed.timestamp <= int(time.time()), "Wrong quote data." + assert ed.tx_hashes is None, "Wrong execution data." + assert ed.tx_status is None, "Wrong execution data." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong status." + + bridge._update_execution_status(bridge_request) + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong status." + + sj = bridge.get_status_json(bridge_request) + assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." + expected_sj = { + "explorer_link": sj["explorer_link"], + "tx_hash": None, # type: ignore + "message": sj["message"], + "status": BridgeRequestStatus.EXECUTION_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + def test_bridge_error( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_error""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1, # This will cause a quote error + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params, bridge.id()) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is not None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_FAILED + ), "Wrong status." + + assert bridge_request.quote_data is not None, "Wrong quote data." + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": bridge_request.quote_data.message, + "status": BridgeRequestStatus.QUOTE_FAILED.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + br = bridge.bridge_requirements(bridge_request) + expected_br = { + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is not None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_FAILED + ), "Wrong status." + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + + timestamp = int(time.time()) + bridge.execute(bridge_request=bridge_request) + ed = bridge_request.execution_data + assert ed is not None, "Missing execution data." + assert ed.bridge_status is None, "Wrong execution data." + assert ed.elapsed_time == 0, "Wrong execution data." + assert ed.message is not None, "Wrong execution data." + assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." + assert timestamp <= ed.timestamp, "Wrong quote data." + assert ed.timestamp <= int(time.time()), "Wrong quote data." + assert ed.tx_hashes is None, "Wrong execution data." + assert ed.tx_status is None, "Wrong execution data." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong status." + + bridge._update_execution_status(bridge_request) + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong status." + + sj = bridge.get_status_json(bridge_request) + assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." + expected_sj = { + "explorer_link": sj["explorer_link"], + "tx_hash": None, + "message": sj["message"], + "status": BridgeRequestStatus.EXECUTION_FAILED.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") + def test_bridge_quote( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_quote""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1_000_000_000_000_000_000, + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params, bridge.id()) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_DONE + ), "Wrong status." + + assert bridge_request.quote_data is not None, "Wrong quote data." + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": bridge_request.quote_data.message, + "status": BridgeRequestStatus.QUOTE_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + assert bridge_request.quote_data.response is not None, "Missing quote data." + + quote = bridge_request.quote_data.response + br = bridge.bridge_requirements(bridge_request) + expected_br = { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: br["gnosis"][wallet_address][ZERO_ADDRESS], + OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore + } + } + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." From cd6b2c3a239e3b5195cd1d0bc3380de255efe303 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 4 May 2025 21:34:13 +0200 Subject: [PATCH 077/173] chore: update --- operate/bridge/bridge.py | 8 +- operate/bridge/providers/bridge_provider.py | 31 +- .../bridge/providers/lifi_bridge_provider.py | 31 +- tests/test_bridge_bridge.py | 374 +----------------- 4 files changed, 39 insertions(+), 405 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index f91f492fa..ef37d744f 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -313,20 +313,16 @@ def get_status_json(self, bundle_id: str) -> t.Dict: initial_status = [request.status for request in bundle.bridge_requests] + bridge_request_status = [] for request in bundle.bridge_requests: bridge = self._bridge_providers[request.bridge_provider_id] - bridge.update_execution_status(request) + bridge_request_status.append(bridge.get_status_json(request)) updated_status = [request.status for request in bundle.bridge_requests] if initial_status != updated_status and bundle.path is not None: bundle.store() - bridge_request_status = [] - for request in bundle.bridge_requests: - bridge = self._bridge_providers[request.bridge_provider_id] - bridge_request_status.append(bridge.get_status_json(request)) - return { "id": bundle.id, "bridge_request_status": bridge_request_status, diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index cd18a80c1..2c72c7054 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -25,7 +25,7 @@ import time import typing as t import uuid -from abc import abstractmethod +from abc import ABC, abstractmethod from dataclasses import dataclass from aea.crypto.base import LedgerApi @@ -102,8 +102,16 @@ class BridgeRequest(LocalResource): execution_data: t.Optional[ExecutionData] = None -class BridgeProvider: - """(Abstract) BridgeProvider""" +class BridgeProvider(ABC): + """(Abstract) BridgeProvider. + + Derived classes must iplement the following methods: + - description + - quote + - _get_transactions + - update_execution_status + - _get_explorer_link + """ def __init__( self, @@ -171,6 +179,13 @@ def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" raise NotImplementedError() + @abstractmethod + def _get_transactions( + self, bridge_request: BridgeRequest + ) -> t.List[t.Tuple[str, t.Dict]]: + """Get the sorted list of transactions to execute the bridge request.""" + raise NotImplementedError() + def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: """Gets the bridge requirements to execute the quote, with updated gas estimation.""" self._validate(bridge_request) @@ -228,13 +243,6 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: return result - @abstractmethod - def _get_transactions( - self, bridge_request: BridgeRequest - ) -> t.List[t.Tuple[str, t.Dict]]: - """Get the sorted list of transactions to execute the bridge request.""" - raise NotImplementedError() - def execute(self, bridge_request: BridgeRequest) -> None: """Execute the quote.""" self._validate(bridge_request) @@ -338,7 +346,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @abstractmethod - def update_execution_status(self, bridge_request: BridgeRequest) -> None: + def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status.""" raise NotImplementedError() @@ -350,6 +358,7 @@ def _get_explorer_link(self, tx_hash: str) -> str: def get_status_json(self, bridge_request: BridgeRequest) -> t.Dict: """JSON representation of the status.""" if bridge_request.execution_data: + self._update_execution_status(bridge_request) tx_hash = None explorer_link = None if bridge_request.execution_data.tx_hashes: diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 71b6bf1a2..8e48f8325 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -266,11 +266,11 @@ def _get_transactions( ("Bridge transaction", bridge_tx), ] - def update_execution_status(self, bridge_request: BridgeRequest) -> None: + def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" self._validate(bridge_request) - if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING): + if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING, BridgeRequestStatus.EXECUTION_UNKNOWN): return if not bridge_request.execution_data: @@ -289,30 +289,29 @@ def update_execution_status(self, bridge_request: BridgeRequest) -> None: params = { "txHash": tx_hash, } + try: self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") response = requests.get(url=url, headers=headers, params=params, timeout=30) - response.raise_for_status() response_json = response.json() + execution.bridge_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) + execution.message = response_json.get("substatusMessage", response_json.get("message")) + response.raise_for_status() except Exception as e: self.logger.error( f"[LI.FI BRIDGE] Failed to update bridge status for {tx_hash}: {e}" ) - bridge_request.status = ( - BridgeRequestStatus.EXECUTION_UNKNOWN - ) # TODO should be EXECUTION_FAILED ? - return - - lifi_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) - execution.message = response_json.get("substatusMessage") - if execution.bridge_status != lifi_status: - execution.bridge_status = lifi_status - if lifi_status == LiFiTransactionStatus.DONE: - bridge_request.status = BridgeRequestStatus.EXECUTION_DONE - elif lifi_status == LiFiTransactionStatus.FAILED: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + if execution.bridge_status == LiFiTransactionStatus.DONE: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + elif execution.bridge_status == LiFiTransactionStatus.FAILED: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + elif execution.bridge_status == LiFiTransactionStatus.PENDING: + bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING + else: + bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN def _get_explorer_link(self, tx_hash: str) -> str: """Get the explorer link for a transaction.""" return f"https://scan.li.fi/tx/{tx_hash}" + \ No newline at end of file diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 36abc2b23..791a6966e 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -27,13 +27,8 @@ import pytest from deepdiff import DeepDiff -from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, - BridgeRequest, - LiFiBridgeProvider, -) from operate.bridge.providers.bridge_provider import ( BridgeRequestStatus, - MESSAGE_EXECUTION_SKIPPED, MESSAGE_QUOTE_ZERO, ) from operate.cli import OperateApp @@ -50,373 +45,8 @@ ) -class TestLiFiBridge: - """Tests for bridge.bridge.BridgeWorkflow class.""" - - def test_bridge_zero( - self, - tmp_path: Path, - password: str, - ) -> None: - """test_bridge_zero""" - operate = OperateApp( - home=tmp_path / OPERATE, - ) - operate.setup() - operate.create_user_account(password=password) - operate.password = password - operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - - wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address - params = { - "from": { - "chain": "gnosis", - "address": wallet_address, - "token": OLAS[Chain.GNOSIS], - }, - "to": { - "chain": "base", - "address": wallet_address, - "token": OLAS[Chain.BASE], - "amount": 0, - }, - } - - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id()) - - assert not bridge_request.quote_data, "Unexpected quote data." - - with pytest.raises(RuntimeError): - bridge.execute(bridge_request) - - status1 = bridge_request.status - bridge.update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.CREATED, "Wrong status." - assert status2 == BridgeRequestStatus.CREATED, "Wrong status." - - for _ in range(2): - timestamp = int(time.time()) - bridge.quote(bridge_request=bridge_request) - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts == 0, "Wrong quote data." - assert qd.elapsed_time == 0, "Wrong quote data." - assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." - assert qd.response is None, "Wrong quote data." - assert timestamp <= qd.timestamp, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_DONE - ), "Wrong status." - - sj = bridge.get_status_json(bridge_request) - expected_sj = { - "message": MESSAGE_QUOTE_ZERO, - "status": BridgeRequestStatus.QUOTE_DONE.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) - - assert not diff, "Wrong status." - assert bridge_request.quote_data is not None, "Missing quote data." - - br = bridge.bridge_requirements(bridge_request) - expected_br = { - "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} - } - diff = DeepDiff(br, expected_br) - if diff: - print(diff) - - assert not diff, "Wrong bridge requirements." - - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts == 0, "Wrong quote data." - assert qd.elapsed_time == 0, "Wrong quote data." - assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." - assert qd.response is None, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - - status1 = bridge_request.status - bridge.update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - - timestamp = int(time.time()) - bridge.execute(bridge_request=bridge_request) - ed = bridge_request.execution_data - assert ed is not None, "Missing execution data." - assert ed.bridge_status is None, "Wrong execution data." - assert ed.elapsed_time == 0, "Wrong execution data." - assert ed.message is not None, "Wrong execution data." - assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." - assert timestamp <= ed.timestamp, "Wrong quote data." - assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hashes is None, "Wrong execution data." - assert ed.tx_status is None, "Wrong execution data." - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong status." - - bridge.update_execution_status(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong status." - - sj = bridge.get_status_json(bridge_request) - assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." - expected_sj = { - "explorer_link": sj["explorer_link"], - "tx_hash": None, # type: ignore - "message": sj["message"], - "status": BridgeRequestStatus.EXECUTION_DONE.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) - - assert not diff, "Wrong status." - - def test_bridge_error( - self, - tmp_path: Path, - password: str, - ) -> None: - """test_bridge_error""" - operate = OperateApp( - home=tmp_path / OPERATE, - ) - operate.setup() - operate.create_user_account(password=password) - operate.password = password - operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - - wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address - params = { - "from": { - "chain": "gnosis", - "address": wallet_address, - "token": OLAS[Chain.GNOSIS], - }, - "to": { - "chain": "base", - "address": wallet_address, - "token": OLAS[Chain.BASE], - "amount": 1, # This will cause a quote error - }, - } - - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id()) - - assert not bridge_request.quote_data, "Unexpected quote data." - - with pytest.raises(RuntimeError): - bridge.execute(bridge_request) - - status1 = bridge_request.status - bridge.update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.CREATED, "Wrong status." - assert status2 == BridgeRequestStatus.CREATED, "Wrong status." - - for _ in range(2): - timestamp = int(time.time()) - bridge.quote(bridge_request=bridge_request) - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is not None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert timestamp <= qd.timestamp, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_FAILED - ), "Wrong status." - - assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge.get_status_json(bridge_request) - expected_sj = { - "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_FAILED.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) - - assert not diff, "Wrong status." - - br = bridge.bridge_requirements(bridge_request) - expected_br = { - "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} - } - diff = DeepDiff(br, expected_br) - if diff: - print(diff) - - assert not diff, "Wrong bridge requirements." - - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is not None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_FAILED - ), "Wrong status." - - status1 = bridge_request.status - bridge.update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." - assert status2 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." - - timestamp = int(time.time()) - bridge.execute(bridge_request=bridge_request) - ed = bridge_request.execution_data - assert ed is not None, "Missing execution data." - assert ed.bridge_status is None, "Wrong execution data." - assert ed.elapsed_time == 0, "Wrong execution data." - assert ed.message is not None, "Wrong execution data." - assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." - assert timestamp <= ed.timestamp, "Wrong quote data." - assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hashes is None, "Wrong execution data." - assert ed.tx_status is None, "Wrong execution data." - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED - ), "Wrong status." - - bridge.update_execution_status(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED - ), "Wrong status." - - sj = bridge.get_status_json(bridge_request) - assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." - expected_sj = { - "explorer_link": sj["explorer_link"], - "tx_hash": None, - "message": sj["message"], - "status": BridgeRequestStatus.EXECUTION_FAILED.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) - - assert not diff, "Wrong status." - - @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") - def test_bridge_quote( - self, - tmp_path: Path, - password: str, - ) -> None: - """test_bridge_quote""" - operate = OperateApp( - home=tmp_path / OPERATE, - ) - operate.setup() - operate.create_user_account(password=password) - operate.password = password - operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - - wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address - params = { - "from": { - "chain": "gnosis", - "address": wallet_address, - "token": OLAS[Chain.GNOSIS], - }, - "to": { - "chain": "base", - "address": wallet_address, - "token": OLAS[Chain.BASE], - "amount": 1_000_000_000_000_000_000, - }, - } - - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id()) - - assert not bridge_request.quote_data, "Unexpected quote data." - - with pytest.raises(RuntimeError): - bridge.execute(bridge_request) - - status1 = bridge_request.status - bridge.update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.CREATED, "Wrong status." - assert status2 == BridgeRequestStatus.CREATED, "Wrong status." - - for _ in range(2): - timestamp = int(time.time()) - bridge.quote(bridge_request=bridge_request) - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert timestamp <= qd.timestamp, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_DONE - ), "Wrong status." - - assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge.get_status_json(bridge_request) - expected_sj = { - "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_DONE.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) - - assert not diff, "Wrong status." - assert bridge_request.quote_data.response is not None, "Missing quote data." - - quote = bridge_request.quote_data.response - br = bridge.bridge_requirements(bridge_request) - expected_br = { - "gnosis": { - wallet_address: { - ZERO_ADDRESS: br["gnosis"][wallet_address][ZERO_ADDRESS], - OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore - } - } - } - diff = DeepDiff(br, expected_br) - if diff: - print(diff) - - assert not diff, "Wrong bridge requirements." - - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - - status1 = bridge_request.status - bridge.update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." +class TestBridgeManager: + """Tests for bridge.bridge.BridgeManager class.""" def test_bundle_zero( self, From dbf495f95e9faff294d1341bc20a5d5fe4b32b97 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 5 May 2025 17:05:14 +0200 Subject: [PATCH 078/173] chore: add l1 standard bridge contract --- .../contracts/l1_standard_bridge/__init__.py | 20 + .../build/L1StandardBridge.json | 831 ++++++++++++++++++ .../contracts/l1_standard_bridge/contract.py | 50 ++ .../l1_standard_bridge/contract.yaml | 23 + 4 files changed, 924 insertions(+) create mode 100644 operate/data/contracts/l1_standard_bridge/__init__.py create mode 100644 operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json create mode 100644 operate/data/contracts/l1_standard_bridge/contract.py create mode 100644 operate/data/contracts/l1_standard_bridge/contract.yaml diff --git a/operate/data/contracts/l1_standard_bridge/__init__.py b/operate/data/contracts/l1_standard_bridge/__init__.py new file mode 100644 index 000000000..a87c4a4fc --- /dev/null +++ b/operate/data/contracts/l1_standard_bridge/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the `L1StandardBridge` contract.""" diff --git a/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json b/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json new file mode 100644 index 000000000..3366c1f21 --- /dev/null +++ b/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json @@ -0,0 +1,831 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "L1StandardBridge", + "sourceName": "L1StandardBridge.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20DepositInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20WithdrawalFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHDepositInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHWithdrawalFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "inputs": [], + "name": "MESSENGER", + "outputs": [ + { + "internalType": "contract ICrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OTHER_BRIDGE", + "outputs": [ + { + "internalType": "contract StandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeERC20To", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeETHTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "depositERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "depositERC20To", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "depositETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "depositETHTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeBridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeBridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeERC20Withdrawal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeETHWithdrawal", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ICrossDomainMessenger", + "name": "_messenger", + "type": "address" + }, + { + "internalType": "contract ISuperchainConfig", + "name": "_superchainConfig", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l2TokenBridge", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messenger", + "outputs": [ + { + "internalType": "contract ICrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "otherBridge", + "outputs": [ + { + "internalType": "contract StandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "", + "deployedBytecode": "0x6080604052600436106101795760003560e01c80637f46ddb2116100cb578063927ede2d1161007f578063b1a1a88211610059578063b1a1a88214610503578063c89701a214610516578063e11013dd1461054357600080fd5b8063927ede2d146104a55780639a2ac6d5146104d0578063a9f9e675146104e357600080fd5b806387087623116100b0578063870876231461043f5780638f601f661461045f57806391c49bf8146103f457600080fd5b80637f46ddb2146103f4578063838b25201461041f57600080fd5b80633cb747bf1161012d57806354fd4d501161010757806354fd4d501461035957806358a997f6146103af5780635c975abb146103cf57600080fd5b80633cb747bf146102ec578063485cc95514610319578063540abf731461033957600080fd5b80631532ec341161015e5780631532ec341461026f5780631635f5fd1461028257806335e80ab31461029557600080fd5b80630166a07a1461023c57806309fc88431461025c57600080fd5b3661023757610186610556565b610217576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f4100000000000000000060648201526084015b60405180910390fd5b610235333362030d4060405180602001604052806000815250610593565b005b600080fd5b34801561024857600080fd5b50610235610257366004612777565b6105a6565b61023561026a366004612828565b6109c0565b61023561027d36600461287b565b610a9c565b61023561029036600461287b565b610ab0565b3480156102a157600080fd5b506032546102c29073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b3480156102f857600080fd5b506003546102c29073ffffffffffffffffffffffffffffffffffffffff1681565b34801561032557600080fd5b506102356103343660046128ee565b610f79565b34801561034557600080fd5b50610235610354366004612927565b611162565b34801561036557600080fd5b506103a26040518060400160405280600581526020017f322e332e3000000000000000000000000000000000000000000000000000000081525081565b6040516102e39190612a14565b3480156103bb57600080fd5b506102356103ca366004612a27565b6111a7565b3480156103db57600080fd5b506103e4611280565b60405190151581526020016102e3565b34801561040057600080fd5b5060045473ffffffffffffffffffffffffffffffffffffffff166102c2565b34801561042b57600080fd5b5061023561043a366004612927565b611319565b34801561044b57600080fd5b5061023561045a366004612a27565b61135e565b34801561046b57600080fd5b5061049761047a3660046128ee565b600260209081526000928352604080842090915290825290205481565b6040519081526020016102e3565b3480156104b157600080fd5b5060035473ffffffffffffffffffffffffffffffffffffffff166102c2565b6102356104de366004612aaa565b611437565b3480156104ef57600080fd5b506102356104fe366004612777565b611479565b610235610511366004612828565b611488565b34801561052257600080fd5b506004546102c29073ffffffffffffffffffffffffffffffffffffffff1681565b610235610551366004612aaa565b61155e565b60003233036105655750600190565b333b60170361058d57604051602081016040526020600082333c5160e81c62ef010014905090565b50600090565b6105a084843485856115a1565b50505050565b60035473ffffffffffffffffffffffffffffffffffffffff1633148015610679575060048054600354604080517f6e296e45000000000000000000000000000000000000000000000000000000008152905173ffffffffffffffffffffffffffffffffffffffff938416949390921692636e296e459282820192602092908290030181865afa15801561063d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106619190612b0d565b73ffffffffffffffffffffffffffffffffffffffff16145b61072b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20746865206f7468657220627269646760648201527f6500000000000000000000000000000000000000000000000000000000000000608482015260a40161020e565b610733611280565b1561079a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f5374616e646172644272696467653a2070617573656400000000000000000000604482015260640161020e565b6107a38761176b565b156108f1576107b287876117cd565b610864576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f5374616e646172644272696467653a2077726f6e672072656d6f746520746f6b60448201527f656e20666f72204f7074696d69736d204d696e7461626c65204552433230206c60648201527f6f63616c20746f6b656e00000000000000000000000000000000000000000000608482015260a40161020e565b6040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8581166004830152602482018590528816906340c10f1990604401600060405180830381600087803b1580156108d457600080fd5b505af11580156108e8573d6000803e3d6000fd5b50505050610973565b73ffffffffffffffffffffffffffffffffffffffff8088166000908152600260209081526040808320938a168352929052205461092f908490612b59565b73ffffffffffffffffffffffffffffffffffffffff8089166000818152600260209081526040808320948c16835293905291909120919091556109739085856118ed565b6109b7878787878787878080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506119c192505050565b50505050505050565b6109c8610556565b610a54576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161020e565b610a973333348686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506115a192505050565b505050565b610aa98585858585610ab0565b5050505050565b60035473ffffffffffffffffffffffffffffffffffffffff1633148015610b83575060048054600354604080517f6e296e45000000000000000000000000000000000000000000000000000000008152905173ffffffffffffffffffffffffffffffffffffffff938416949390921692636e296e459282820192602092908290030181865afa158015610b47573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b6b9190612b0d565b73ffffffffffffffffffffffffffffffffffffffff16145b610c35576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20746865206f7468657220627269646760648201527f6500000000000000000000000000000000000000000000000000000000000000608482015260a40161020e565b610c3d611280565b15610ca4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f5374616e646172644272696467653a2070617573656400000000000000000000604482015260640161020e565b823414610d33576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603a60248201527f5374616e646172644272696467653a20616d6f756e742073656e7420646f657360448201527f206e6f74206d6174636820616d6f756e74207265717569726564000000000000606482015260840161020e565b3073ffffffffffffffffffffffffffffffffffffffff851603610dd8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f5374616e646172644272696467653a2063616e6e6f742073656e6420746f207360448201527f656c660000000000000000000000000000000000000000000000000000000000606482015260840161020e565b60035473ffffffffffffffffffffffffffffffffffffffff90811690851603610e83576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f5374616e646172644272696467653a2063616e6e6f742073656e6420746f206d60448201527f657373656e676572000000000000000000000000000000000000000000000000606482015260840161020e565b610ec585858585858080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250611a4f92505050565b6000610ee2855a8660405180602001604052806000815250611ac2565b905080610f71576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f5374616e646172644272696467653a20455448207472616e736665722066616960448201527f6c65640000000000000000000000000000000000000000000000000000000000606482015260840161020e565b505050505050565b600054610100900460ff1615808015610f995750600054600160ff909116105b80610fb35750303b158015610fb3575060005460ff166001145b61103f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a6564000000000000000000000000000000000000606482015260840161020e565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055801561109d57600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b603280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff84161790556110fb83734200000000000000000000000000000000000010611ada565b8015610a9757600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a1505050565b6109b787873388888888888080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250611bc492505050565b6111af610556565b61123b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161020e565b610f7186863333888888888080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250611f7d92505050565b603254604080517f5c975abb000000000000000000000000000000000000000000000000000000008152905160009273ffffffffffffffffffffffffffffffffffffffff1691635c975abb9160048083019260209291908290030181865afa1580156112f0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906113149190612b70565b905090565b6109b787873388888888888080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250611f7d92505050565b611366610556565b6113f2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161020e565b610f7186863333888888888080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250611bc492505050565b6105a033858585858080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061059392505050565b6109b7878787878787876105a6565b611490610556565b61151c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161020e565b610a9733338585858080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061059392505050565b6105a03385348686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506115a192505050565b823414611630576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603e60248201527f5374616e646172644272696467653a206272696467696e6720455448206d757360448201527f7420696e636c7564652073756666696369656e74204554482076616c75650000606482015260840161020e565b61163c85858584611f8c565b60035460045460405173ffffffffffffffffffffffffffffffffffffffff92831692633dbb202b9287929116907f1635f5fd000000000000000000000000000000000000000000000000000000009061169f908b908b9086908a90602401612b92565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009485161790525160e086901b909216825261173292918890600401612bdb565b6000604051808303818588803b15801561174b57600080fd5b505af115801561175f573d6000803e3d6000fd5b50505050505050505050565b6000611797827f1d1d8b6300000000000000000000000000000000000000000000000000000000611fff565b806117c757506117c7827fec4fc8e300000000000000000000000000000000000000000000000000000000611fff565b92915050565b60006117f9837f1d1d8b6300000000000000000000000000000000000000000000000000000000611fff565b156118a2578273ffffffffffffffffffffffffffffffffffffffff1663c01e1bd66040518163ffffffff1660e01b8152600401602060405180830381865afa158015611849573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061186d9190612b0d565b73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161490506117c7565b8273ffffffffffffffffffffffffffffffffffffffff1663d6c0b2c46040518163ffffffff1660e01b8152600401602060405180830381865afa158015611849573d6000803e3d6000fd5b60405173ffffffffffffffffffffffffffffffffffffffff8316602482015260448101829052610a979084907fa9059cbb00000000000000000000000000000000000000000000000000000000906064015b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff0000000000000000000000000000000000000000000000000000000090931692909217909152612022565b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167f3ceee06c1e37648fcbb6ed52e17b3e1f275a1f8c7b22a84b2b84732431e046b3868686604051611a3993929190612c20565b60405180910390a4610f7186868686868661212e565b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f2ac69ee804d9a7a0984249f508dfab7cb2534b465b6ce1580f99a38ba9c5e6318484604051611aae929190612c5e565b60405180910390a36105a0848484846121b6565b6000806000835160208501868989f195945050505050565b600054610100900460ff16611b71576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e67000000000000000000000000000000000000000000606482015260840161020e565b6003805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff00000000000000000000000000000000000000009182161790915560048054929093169116179055565b3415611c52576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f5374616e646172644272696467653a2063616e6e6f742073656e642076616c7560448201527f6500000000000000000000000000000000000000000000000000000000000000606482015260840161020e565b611c5b8761176b565b15611da957611c6a87876117cd565b611d1c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f5374616e646172644272696467653a2077726f6e672072656d6f746520746f6b60448201527f656e20666f72204f7074696d69736d204d696e7461626c65204552433230206c60648201527f6f63616c20746f6b656e00000000000000000000000000000000000000000000608482015260a40161020e565b6040517f9dc29fac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff868116600483015260248201859052881690639dc29fac90604401600060405180830381600087803b158015611d8c57600080fd5b505af1158015611da0573d6000803e3d6000fd5b50505050611e3d565b611dcb73ffffffffffffffffffffffffffffffffffffffff8816863086612223565b73ffffffffffffffffffffffffffffffffffffffff8088166000908152600260209081526040808320938a1683529290522054611e09908490612c77565b73ffffffffffffffffffffffffffffffffffffffff8089166000908152600260209081526040808320938b16835292905220555b611e4b878787878786612281565b60035460045460405173ffffffffffffffffffffffffffffffffffffffff92831692633dbb202b9216907f0166a07a0000000000000000000000000000000000000000000000000000000090611eaf908b908d908c908c908c908b90602401612c8f565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009485161790525160e085901b9092168252611f4292918790600401612bdb565b600060405180830381600087803b158015611f5c57600080fd5b505af1158015611f70573d6000803e3d6000fd5b5050505050505050505050565b6109b787878787878787611bc4565b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f35d79ab81f2b2017e19afb5c5571778877782d7a8786f5907f93b0f4702f4f238484604051611feb929190612c5e565b60405180910390a36105a08484848461230f565b600061200a8361236e565b801561201b575061201b83836123d2565b9392505050565b6000612084826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166124a19092919063ffffffff16565b805190915015610a9757808060200190518101906120a29190612b70565b610a97576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f74207375636365656400000000000000000000000000000000000000000000606482015260840161020e565b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167fd59c65b35445225835c83f50b6ede06a7be047d22e357073e250d9af537518cd8686866040516121a693929190612c20565b60405180910390a4505050505050565b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f31b2166ff604fc5672ea5df08a78081d2bc6d746cadce880747f3643d819e83d8484604051612215929190612c5e565b60405180910390a350505050565b60405173ffffffffffffffffffffffffffffffffffffffff808516602483015283166044820152606481018290526105a09085907f23b872dd000000000000000000000000000000000000000000000000000000009060840161193f565b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167f718594027abd4eaed59f95162563e0cc6d0e8d5b86b1c7be8b1b0ac3343d03968686866040516122f993929190612c20565b60405180910390a4610f718686868686866124b8565b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f2849b43074093a05396b6f2a937dee8565b15a48a7b3d4bffb732a5017380af58484604051612215929190612c5e565b600061239a827f01ffc9a7000000000000000000000000000000000000000000000000000000006123d2565b80156117c757506123cb827fffffffff000000000000000000000000000000000000000000000000000000006123d2565b1592915050565b604080517fffffffff000000000000000000000000000000000000000000000000000000008316602480830191909152825180830390910181526044909101909152602080820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f01ffc9a700000000000000000000000000000000000000000000000000000000178152825160009392849283928392918391908a617530fa92503d9150600051905082801561248a575060208210155b80156124965750600081115b979650505050505050565b60606124b08484600085612530565b949350505050565b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167f7ff126db8024424bbfd9826e8ab82ff59136289ea440b04b39a0df1b03b9cabf8686866040516121a693929190612c20565b6060824710156125c2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f60448201527f722063616c6c0000000000000000000000000000000000000000000000000000606482015260840161020e565b73ffffffffffffffffffffffffffffffffffffffff85163b612640576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161020e565b6000808673ffffffffffffffffffffffffffffffffffffffff1685876040516126699190612cea565b60006040518083038185875af1925050503d80600081146126a6576040519150601f19603f3d011682016040523d82523d6000602084013e6126ab565b606091505b5091509150612496828286606083156126c557508161201b565b8251156126d55782518084602001fd5b816040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161020e9190612a14565b73ffffffffffffffffffffffffffffffffffffffff8116811461272b57600080fd5b50565b60008083601f84011261274057600080fd5b50813567ffffffffffffffff81111561275857600080fd5b60208301915083602082850101111561277057600080fd5b9250929050565b600080600080600080600060c0888a03121561279257600080fd5b873561279d81612709565b965060208801356127ad81612709565b955060408801356127bd81612709565b945060608801356127cd81612709565b93506080880135925060a088013567ffffffffffffffff8111156127f057600080fd5b6127fc8a828b0161272e565b989b979a50959850939692959293505050565b803563ffffffff8116811461282357600080fd5b919050565b60008060006040848603121561283d57600080fd5b6128468461280f565b9250602084013567ffffffffffffffff81111561286257600080fd5b61286e8682870161272e565b9497909650939450505050565b60008060008060006080868803121561289357600080fd5b853561289e81612709565b945060208601356128ae81612709565b935060408601359250606086013567ffffffffffffffff8111156128d157600080fd5b6128dd8882890161272e565b969995985093965092949392505050565b6000806040838503121561290157600080fd5b823561290c81612709565b9150602083013561291c81612709565b809150509250929050565b600080600080600080600060c0888a03121561294257600080fd5b873561294d81612709565b9650602088013561295d81612709565b9550604088013561296d81612709565b9450606088013593506129826080890161280f565b925060a088013567ffffffffffffffff8111156127f057600080fd5b60005b838110156129b95781810151838201526020016129a1565b838111156105a05750506000910152565b600081518084526129e281602086016020860161299e565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b60208152600061201b60208301846129ca565b60008060008060008060a08789031215612a4057600080fd5b8635612a4b81612709565b95506020870135612a5b81612709565b945060408701359350612a706060880161280f565b9250608087013567ffffffffffffffff811115612a8c57600080fd5b612a9889828a0161272e565b979a9699509497509295939492505050565b60008060008060608587031215612ac057600080fd5b8435612acb81612709565b9350612ad96020860161280f565b9250604085013567ffffffffffffffff811115612af557600080fd5b612b018782880161272e565b95989497509550505050565b600060208284031215612b1f57600080fd5b815161201b81612709565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600082821015612b6b57612b6b612b2a565b500390565b600060208284031215612b8257600080fd5b8151801515811461201b57600080fd5b600073ffffffffffffffffffffffffffffffffffffffff808716835280861660208401525083604083015260806060830152612bd160808301846129ca565b9695505050505050565b73ffffffffffffffffffffffffffffffffffffffff84168152606060208201526000612c0a60608301856129ca565b905063ffffffff83166040830152949350505050565b73ffffffffffffffffffffffffffffffffffffffff84168152826020820152606060408201526000612c5560608301846129ca565b95945050505050565b8281526040602082015260006124b060408301846129ca565b60008219821115612c8a57612c8a612b2a565b500190565b600073ffffffffffffffffffffffffffffffffffffffff80891683528088166020840152808716604084015280861660608401525083608083015260c060a0830152612cde60c08301846129ca565b98975050505050505050565b60008251612cfc81846020870161299e565b919091019291505056fea164736f6c634300080f000a", + "linkReferences": {}, + "deployedLinkReferences": {} +} \ No newline at end of file diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py new file mode 100644 index 000000000..a02b482fd --- /dev/null +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the class to connect to the `L1StandardBridge` contract.""" + +from enum import Enum + +from aea.common import JSONLike +from aea.configurations.base import PublicId +from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi + + +class L1StandardBridge(Contract): + """The Service Staking contract.""" + + contract_id = PublicId.from_str("valory/l1_standard_bridge:0.1.0") + + @classmethod + def build_deposit_erc20_to_tx( + cls, + ledger_api: LedgerApi, + contract_address: str, + l1_token: str, + l2_token: str, + to: str, + amount: int, + min_gas_limit: int, + extra_data: bytes, + ) -> JSONLike: + """Build depositERC20To tx.""" + contract_instance = cls.get_instance(ledger_api, contract_address) + data = contract_instance.encodeABI("depositERC20To", args=[l1_token, l2_token, to, amount, min_gas_limit, extra_data]) + return dict(data=bytes.fromhex(data[2:])) \ No newline at end of file diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml new file mode 100644 index 000000000..ea22476e9 --- /dev/null +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -0,0 +1,23 @@ +name: l1_standard_bridge +author: valory +version: 0.1.0 +type: contract +description: Optimism L1 Standard Bridge +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeid4hg5rx75ltdjmekmxdddwqzbvblygmvlhws5samjxfirnkp666i + build/L1StandardBridge.json: bafybeiagrufcoljrlo2zklc7kxwh7eyrf67usos2bqnf7hss47hgm6low4 + contract.py: bafybeihgiwyy5gj5tpyz6wnjlfsywssanrbuzskqctt557f3galofewqse +fingerprint_ignore_patterns: [] +contracts: [] +class_name: L1StandardBridge +contract_interface_paths: + ethereum: build/L1StandardBridge.json +dependencies: + open-aea-ledger-ethereum: + version: ==1.60.0 + open-aea-test-autonomy: + version: ==0.18.3 + web3: + version: <7,>=6.0.0 From 86fe1db9438f251e000047d53d1c7a88def70d8c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 6 May 2025 19:58:34 +0200 Subject: [PATCH 079/173] feat: native bridge --- operate/bridge/bridge.py | 13 +- operate/bridge/providers/bridge_provider.py | 2 +- .../bridge/providers/lifi_bridge_provider.py | 30 +- .../providers/native_bridge_provider.py | 409 ++++++++++++ .../contracts/l1_standard_bridge/contract.py | 52 +- .../l1_standard_bridge/contract.yaml | 6 +- .../contracts/l2_standard_bridge/__init__.py | 20 + .../build/L2StandardBridge.json | 626 ++++++++++++++++++ .../contracts/l2_standard_bridge/contract.py | 29 + .../l2_standard_bridge/contract.yaml | 23 + 10 files changed, 1194 insertions(+), 16 deletions(-) create mode 100644 operate/bridge/providers/native_bridge_provider.py create mode 100644 operate/data/contracts/l2_standard_bridge/__init__.py create mode 100644 operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json create mode 100644 operate/data/contracts/l2_standard_bridge/contract.py create mode 100644 operate/data/contracts/l2_standard_bridge/contract.yaml diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index ef37d744f..343bce06c 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -34,6 +34,10 @@ from operate.bridge.providers.bridge_provider import BridgeProvider, BridgeRequest from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider +from operate.bridge.providers.native_bridge_provider import ( + NATIVE_BRIDGE_ENDPOINTS, + NativeBridgeProvider, +) from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain from operate.resource import LocalResource @@ -139,6 +143,7 @@ def __init__( ) self._bridge_providers = { LiFiBridgeProvider.id(): LiFiBridgeProvider(wallet_manager, logger), + NativeBridgeProvider.id(): NativeBridgeProvider(wallet_manager, logger), } def _store_data(self) -> None: @@ -174,7 +179,13 @@ def _get_updated_bundle( bridge_requests = [] for params in requests_params: - bridge = self._bridge_providers[LiFiBridgeProvider.id()] + from_chain = params["from"]["chain"] + to_chain = params["to"]["chain"] + + if (from_chain, to_chain) in NATIVE_BRIDGE_ENDPOINTS: + bridge = self._bridge_providers[NativeBridgeProvider.id()] + else: + bridge = self._bridge_providers[LiFiBridgeProvider.id()] bridge_requests.append(bridge.create_request(params=params)) bundle = BridgeRequestBundle( diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 2c72c7054..e5f2850f7 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -104,7 +104,7 @@ class BridgeRequest(LocalResource): class BridgeProvider(ABC): """(Abstract) BridgeProvider. - + Derived classes must iplement the following methods: - description - quote diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 8e48f8325..b8175a40f 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -175,8 +175,12 @@ def quote(self, bridge_request: BridgeRequest) -> None: @staticmethod def _get_bridge_tx( - quote_data: QuoteData, ledger_api: LedgerApi + bridge_request: BridgeRequest, ledger_api: LedgerApi ) -> t.Optional[t.Dict]: + quote_data = bridge_request.quote_data + if not quote_data: + return None + quote = quote_data.response if not quote: return None @@ -205,8 +209,12 @@ def _get_bridge_tx( @staticmethod def _get_approve_tx( - quote_data: QuoteData, ledger_api: LedgerApi + bridge_request: BridgeRequest, ledger_api: LedgerApi ) -> t.Optional[t.Dict]: + quote_data = bridge_request.quote_data + if not quote_data: + return None + quote = quote_data.response if not quote: return None @@ -248,12 +256,12 @@ def _get_transactions( wallet = self.wallet_manager.load(chain.ledger_type) ledger_api = wallet.ledger_api(chain) - bridge_tx = self._get_bridge_tx(bridge_request.quote_data, ledger_api) + bridge_tx = self._get_bridge_tx(bridge_request, ledger_api) if not bridge_tx: return [] - approve_tx = self._get_approve_tx(bridge_request.quote_data, ledger_api) + approve_tx = self._get_approve_tx(bridge_request, ledger_api) if approve_tx: bridge_tx["nonce"] = approve_tx["nonce"] + 1 @@ -270,7 +278,10 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" self._validate(bridge_request) - if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING, BridgeRequestStatus.EXECUTION_UNKNOWN): + if bridge_request.status not in ( + BridgeRequestStatus.EXECUTION_PENDING, + BridgeRequestStatus.EXECUTION_UNKNOWN, + ): return if not bridge_request.execution_data: @@ -294,8 +305,12 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") response = requests.get(url=url, headers=headers, params=params, timeout=30) response_json = response.json() - execution.bridge_status = response_json.get("status", str(LiFiTransactionStatus.UNKNOWN)) - execution.message = response_json.get("substatusMessage", response_json.get("message")) + execution.bridge_status = response_json.get( + "status", str(LiFiTransactionStatus.UNKNOWN) + ) + execution.message = response_json.get( + "substatusMessage", response_json.get("message") + ) response.raise_for_status() except Exception as e: self.logger.error( @@ -314,4 +329,3 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: def _get_explorer_link(self, tx_hash: str) -> str: """Get the explorer link for a transaction.""" return f"https://scan.li.fi/tx/{tx_hash}" - \ No newline at end of file diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py new file mode 100644 index 000000000..6bfdd5303 --- /dev/null +++ b/operate/bridge/providers/native_bridge_provider.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ +"""Native bridge provider.""" + + +import enum +import time +import typing as t +from http import HTTPStatus +from urllib.parse import urlencode + +import eth_abi +import requests +from aea.crypto.base import LedgerApi +from autonomy.chain.base import registry_contracts +from web3 import Web3 + +from operate.bridge.providers.bridge_provider import ( + BridgeProvider, + BridgeRequest, + BridgeRequestStatus, + MESSAGE_QUOTE_ZERO, + QuoteData, +) +from operate.constants import ZERO_ADDRESS +from operate.data import DATA_DIR +from operate.data.contracts import l2_standard_bridge +from operate.data.contracts.l1_standard_bridge.contract import L1StandardBridge +from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge +from operate.operate_types import Chain + + +BRIDGE_MIN_GAS_LIMIT = 300000 +BLOCK_CHUNK_SIZE = 5000 + +NATIVE_BRIDGE_ENDPOINTS = { + (Chain.ETHEREUM.value, Chain.BASE.value): { + "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + "to_bridge": "0x4200000000000000000000000000000000000010", + "duration": 5 * 60, + } +} + +L1_STANDARD_BRIDGE_CONTRACT = t.cast( + L1StandardBridge, + L1StandardBridge.from_dir( + directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"), + ), +) +L2_STANDARD_BRIDGE_CONTRACT = t.cast( + L2StandardBridge, + L2StandardBridge.from_dir( + directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"), + ), +) + +ERC20_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( + text="ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" +).hex() +ETH_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( + text="ETHBridgeFinalized(address,address,uint256,bytes)" +).hex() + + +class NativeBridgeProvider(BridgeProvider): + """Native bridge provider.""" + + def create_request(self, params: t.Dict) -> BridgeRequest: + """Create a bridge request.""" + from_chain = params["from"]["chain"] + to_chain = params["to"]["chain"] + + if (from_chain, to_chain) not in NATIVE_BRIDGE_ENDPOINTS: + raise ValueError(f"Unsupported bridge from {from_chain} to {to_chain}.") + + return super().create_request(params) + + def description(self) -> str: + """Get a human-readable description of the bridge provider.""" + return "Native bridge provider." + + def quote(self, bridge_request: BridgeRequest) -> None: + """Update the request with the quote.""" + self._validate(bridge_request) + + if bridge_request.status not in ( + BridgeRequestStatus.CREATED, + BridgeRequestStatus.QUOTE_DONE, + BridgeRequestStatus.QUOTE_FAILED, + ): + raise RuntimeError( + f"Cannot quote bridge request {bridge_request.id} with status {bridge_request.status}." + ) + + if bridge_request.execution_data: + raise RuntimeError( + f"Cannot quote bridge request {bridge_request.id}: execution already present." + ) + + to_amount = bridge_request.params["to"]["amount"] + + message = None + if to_amount == 0: + self.logger.info(f"[NATIVE BRIDGE] {MESSAGE_QUOTE_ZERO}") + message = MESSAGE_QUOTE_ZERO + + quote_data = QuoteData( + attempts=0, + elapsed_time=0, + message=message, + response=None, + response_status=0, + timestamp=int(time.time()), + ) + bridge_request.quote_data = quote_data + bridge_request.status = BridgeRequestStatus.QUOTE_DONE + + @staticmethod + def _get_bridge_tx( + bridge_request: BridgeRequest, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote_data = bridge_request.quote_data + if not quote_data: + return None + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] + from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] + extra_data = Web3.keccak(text=bridge_request.id) + + if from_token == ZERO_ADDRESS: + bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_bridge_eth_to_tx( + ledger_api=ledger_api, + contract_address=from_bridge, + sender=from_address, + to=to_address, + amount=int(to_amount), + min_gas_limit=BRIDGE_MIN_GAS_LIMIT, + extra_data=extra_data, + ) + else: + bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_deposit_erc20_to_tx( + ledger_api=ledger_api, + contract_address=from_bridge, + sender=from_address, + l1_token=from_token, + l2_token=to_token, + to=to_address, + amount=int(to_amount), + min_gas_limit=BRIDGE_MIN_GAS_LIMIT, + extra_data=extra_data, + ) + + # TODO: fix this, gas estimation fails. + bridge_tx["gas"] = 1200000 # TODO remove + ledger_api.update_with_gas_estimate(bridge_tx) + + # w3 = Web3(Web3.HTTPProvider("https://rpc-gate.autonolas.tech/ethereum-rpc/")) + # estimated_gas = w3.eth.estimate_gas(bridge_tx) + # print(f"Estimated gas: {estimated_gas}") + # from icecream import ic + # ic(bridge_tx) + + return NativeBridgeProvider._update_with_gas_pricing(bridge_tx, ledger_api) + + @staticmethod + def _get_approve_tx( + bridge_request: BridgeRequest, ledger_api: LedgerApi + ) -> t.Optional[t.Dict]: + quote_data = bridge_request.quote_data + if not quote_data: + return None + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_amount = bridge_request.params["to"]["amount"] + from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] + + if from_token == ZERO_ADDRESS: + return None + + approve_tx = registry_contracts.erc20.get_approve_tx( + ledger_api=ledger_api, + contract_address=from_token, + spender=from_bridge, + sender=from_address, + amount=to_amount, + ) + + ledger_api.update_with_gas_estimate(approve_tx) + return NativeBridgeProvider._update_with_gas_pricing(approve_tx, ledger_api) + + def _get_transactions( + self, bridge_request: BridgeRequest + ) -> t.List[t.Tuple[str, t.Dict]]: + """Get the sorted list of transactions to execute the bridge request.""" + self._validate(bridge_request) + + if not bridge_request.quote_data: + return [] + + from_chain = bridge_request.params["from"]["chain"] + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + + bridge_tx = self._get_bridge_tx(bridge_request, ledger_api) + + if not bridge_tx: + return [] + + approve_tx = self._get_approve_tx(bridge_request, ledger_api) + + if approve_tx: + bridge_tx["nonce"] = approve_tx["nonce"] + 1 + return [ + ("ERC20 Approve transaction", approve_tx), + ("Bridge transaction", bridge_tx), + ] + + return [ + ("Bridge transaction", bridge_tx), + ] + + def _update_execution_status(self, bridge_request: BridgeRequest) -> None: + """Update the execution status. Returns `True` if the status changed.""" + self._validate(bridge_request) + + self.logger.info( + f"[NATIVE BRIDGE] Updating execution status for {bridge_request.id}..." + ) + if bridge_request.status not in ( + BridgeRequestStatus.EXECUTION_PENDING, + # BridgeRequestStatus.EXECUTION_UNKNOWN, + ): + return + + execution_data = bridge_request.execution_data + if not execution_data: + raise RuntimeError( + f"Cannot update bridge request {bridge_request.id}: execution data not present." + ) + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_token = bridge_request.params["to"]["token"] + to_address = bridge_request.params["to"]["address"] + + to_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["to_bridge"] + duration = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["duration"]) + + try: + chain = Chain(to_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + w3 = ledger_api.api + + if from_token == ZERO_ADDRESS: + topics = [ + ETH_BRIDGE_FINALIZED_TOPIC0, + "0x" + from_address.lower()[2:].rjust(64, "0"), # from + "0x" + to_address.lower()[2:].rjust(64, "0"), # from + ] + non_indexed_types = ["uint256", "bytes"] + else: + topics = [ + ERC20_BRIDGE_FINALIZED_TOPIC0, + "0x" + to_token.lower()[2:].rjust(64, "0"), # localToken + "0x" + from_token.lower()[2:].rjust(64, "0"), # remoteToken + "0x" + from_address.lower()[2:].rjust(64, "0"), # from + ] + non_indexed_types = ["address", "uint256", "bytes"] + + target_extra_data = Web3.keccak(text=bridge_request.id).hex() + + starting_block = self._find_starting_block(bridge_request) + starting_block_ts = w3.eth.get_block(starting_block).timestamp + latest_block = w3.eth.block_number + + for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): + to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) + event_found = self._find_event_in_range( + w3, + to_bridge, + from_block, + to_block, + topics, + non_indexed_types, + target_extra_data, + ) + if event_found: + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE + return + + last_block_ts = w3.eth.get_block(to_block).timestamp + if last_block_ts > starting_block_ts + duration * 2: + bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN + return + + except Exception as e: + self.logger.error(f"Error updating execution status: {e}") + bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN + + def _find_event_in_range( + self, + w3, + contract_address: str, + from_block: int, + to_block: int, + topics: list[str], + non_indexed_types: list[str], + target_extra_data: str, + ) -> bool: + """Check for a finalized bridge event in the given block range.""" + logs = w3.eth.get_logs( + { + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + } + ) + + from icecream import ic + + ic(logs) + + for log in logs: + decoded = eth_abi.decode(non_indexed_types, log["data"]) + extra_data = "0x" + decoded[-1].hex() + if extra_data.lower() == target_extra_data.lower(): + return True + + return False + + def _find_starting_block(self, bridge_request: BridgeRequest) -> int: + """Find the starting block for the event log search on the destination chain. + + The starting block to search for the event log is the largest block on the + destination chain so that its timestamp is less than the timestamp of the + bridge transaction on the source chain. + """ + self._validate(bridge_request) + + from_chain = bridge_request.params["from"]["chain"] + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + w3_source = ledger_api.api + + to_chain = bridge_request.params["to"]["chain"] + chain = Chain(to_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + w3_dest = ledger_api.api + + # 1. Get timestamp of the transaction on the source chain + tx = w3_source.eth.get_transaction_receipt( + bridge_request.execution_data.tx_hashes[-1] + ) + block = w3_source.eth.get_block(tx.blockNumber) + tx_timestamp = block.timestamp + + # 2. Binary search the destination chain for block just before this timestamp + def find_block_before_timestamp(w3, timestamp) -> int: + latest = w3.eth.block_number + low, high = 0, latest + best = 0 + while low <= high: + mid = (low + high) // 2 + block = w3.eth.get_block(mid) + if block.timestamp < timestamp: + best = mid + low = mid + 1 + else: + high = mid - 1 + return best + + return find_block_before_timestamp(w3_dest, tx_timestamp) - 1 + + def _get_explorer_link(self, tx_hash: str) -> str: + """Get the explorer link for a transaction.""" + return f"https://etherscan.io/tx/{tx_hash}" diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index a02b482fd..e2a0f06a3 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -32,19 +32,65 @@ class L1StandardBridge(Contract): contract_id = PublicId.from_str("valory/l1_standard_bridge:0.1.0") + @classmethod + def build_bridge_eth_to_tx( + cls, + ledger_api: LedgerApi, + contract_address: str, + sender: str, + to: str, + amount: int, + min_gas_limit: int, + extra_data: bytes, + raise_on_try: bool = False, + ) -> JSONLike: + """Build bridgeETHTo tx.""" + contract_instance = cls.get_instance( + ledger_api=ledger_api, + contract_address=contract_address + ) + tx = contract_instance.functions.bridgeETHTo(to, min_gas_limit, extra_data).build_transaction( + { + "from": sender, + "value": amount, + "gas": 1, + "gasPrice": ledger_api.api.eth.gas_price, + "nonce": ledger_api.api.eth.get_transaction_count(sender), + } + ) + return ledger_api.update_with_gas_estimate( + transaction=tx, + raise_on_try=raise_on_try, + ) + @classmethod def build_deposit_erc20_to_tx( cls, ledger_api: LedgerApi, contract_address: str, + sender: str, l1_token: str, l2_token: str, to: str, amount: int, min_gas_limit: int, extra_data: bytes, + raise_on_try: bool = False, ) -> JSONLike: """Build depositERC20To tx.""" - contract_instance = cls.get_instance(ledger_api, contract_address) - data = contract_instance.encodeABI("depositERC20To", args=[l1_token, l2_token, to, amount, min_gas_limit, extra_data]) - return dict(data=bytes.fromhex(data[2:])) \ No newline at end of file + contract_instance = cls.get_instance( + ledger_api=ledger_api, + contract_address=contract_address + ) + tx = contract_instance.functions.depositERC20To(l1_token, l2_token, to, amount, min_gas_limit, extra_data).build_transaction( + { + "from": sender, + "gas": 1, + "gasPrice": ledger_api.api.eth.gas_price, + "nonce": ledger_api.api.eth.get_transaction_count(sender), + } + ) + return ledger_api.update_with_gas_estimate( + transaction=tx, + raise_on_try=raise_on_try, + ) diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index ea22476e9..27561a18f 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -6,9 +6,9 @@ description: Optimism L1 Standard Bridge license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: bafybeid4hg5rx75ltdjmekmxdddwqzbvblygmvlhws5samjxfirnkp666i - build/L1StandardBridge.json: bafybeiagrufcoljrlo2zklc7kxwh7eyrf67usos2bqnf7hss47hgm6low4 - contract.py: bafybeihgiwyy5gj5tpyz6wnjlfsywssanrbuzskqctt557f3galofewqse + __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye + build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m + contract.py: bafybeiaekcbs6yjaft2jysvdxvrd7tknpkcnf6npmr3zgutlnss7gwgijq fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge diff --git a/operate/data/contracts/l2_standard_bridge/__init__.py b/operate/data/contracts/l2_standard_bridge/__init__.py new file mode 100644 index 000000000..970e82633 --- /dev/null +++ b/operate/data/contracts/l2_standard_bridge/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2025 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the `L2StandardBridge` contract.""" diff --git a/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json b/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json new file mode 100644 index 000000000..13755b45a --- /dev/null +++ b/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json @@ -0,0 +1,626 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "L2StandardBridge", + "sourceName": "L2StandardBridge.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "addresspayable", + "name": "_otherBridge", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "DepositFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "localToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "remoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ERC20BridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "ETHBridgeInitiated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "l1Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "extraData", + "type": "bytes" + } + ], + "name": "WithdrawalInitiated", + "type": "event" + }, + { + "inputs": [], + "name": "MESSENGER", + "outputs": [ + { + "internalType": "contractCrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OTHER_BRIDGE", + "outputs": [ + { + "internalType": "contractStandardBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeERC20To", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "bridgeETHTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_localToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeBridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeBridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "finalizeDeposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "l1TokenBridge", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messenger", + "outputs": [ + { + "internalType": "contractCrossDomainMessenger", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l2Token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_minGasLimit", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "_extraData", + "type": "bytes" + } + ], + "name": "withdrawTo", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bytecode": "", + "deployedBytecode": "0x6080604052600436106100ec5760003560e01c806354fd4d501161008a5780638f601f66116100595780638f601f661461034e578063927ede2d14610394578063a3a79548146103c8578063e11013dd146103db57600080fd5b806354fd4d50146102c5578063662a633a146102e75780637f46ddb2146102fa578063870876231461032e57600080fd5b806332b7006d116100c657806332b7006d1461020657806336c717c1146102195780633cb747bf14610272578063540abf73146102a557600080fd5b80630166a07a146101c057806309fc8843146101e05780631635f5fd146101f357600080fd5b366101bb57333b15610185576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f4100000000000000000060648201526084015b60405180910390fd5b6101b973deaddeaddeaddeaddeaddeaddeaddeaddead000033333462030d40604051806020016040528060008152506103ee565b005b600080fd5b3480156101cc57600080fd5b506101b96101db366004612372565b6104c9565b6101b96101ee366004612423565b6108b6565b6101b9610201366004612476565b61098d565b6101b96102143660046124e9565b610e5a565b34801561022557600080fd5b507f0000000000000000000000003154cf16ccdb4c6d922629664174b904d80f2c355b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561027e57600080fd5b507f0000000000000000000000004200000000000000000000000000000000000007610248565b3480156102b157600080fd5b506101b96102c036600461253d565b610f34565b3480156102d157600080fd5b506102da610f79565b604051610269919061262a565b6101b96102f5366004612372565b61101c565b34801561030657600080fd5b506102487f0000000000000000000000003154cf16ccdb4c6d922629664174b904d80f2c3581565b34801561033a57600080fd5b506101b961034936600461263d565b61108f565b34801561035a57600080fd5b506103866103693660046126c0565b600260209081526000928352604080842090915290825290205481565b604051908152602001610269565b3480156103a057600080fd5b506102487f000000000000000000000000420000000000000000000000000000000000000781565b6101b96103d636600461263d565b611163565b6101b96103e93660046126f9565b6111a7565b7fffffffffffffffffffffffff215221522152215221522152215221522153000073ffffffffffffffffffffffffffffffffffffffff87160161043d5761043885858585856111f0565b6104c1565b60008673ffffffffffffffffffffffffffffffffffffffff1663c01e1bd66040518163ffffffff1660e01b8152600401602060405180830381865afa15801561048a573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104ae919061275c565b90506104bf878288888888886113d4565b505b505050505050565b3373ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000004200000000000000000000000000000000000007161480156105e757507f0000000000000000000000003154cf16ccdb4c6d922629664174b904d80f2c3573ffffffffffffffffffffffffffffffffffffffff167f000000000000000000000000420000000000000000000000000000000000000773ffffffffffffffffffffffffffffffffffffffff16636e296e456040518163ffffffff1660e01b8152600401602060405180830381865afa1580156105ab573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105cf919061275c565b73ffffffffffffffffffffffffffffffffffffffff16145b610699576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20746865206f7468657220627269646760648201527f6500000000000000000000000000000000000000000000000000000000000000608482015260a40161017c565b6106a28761171b565b156107f0576106b1878761177d565b610763576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f5374616e646172644272696467653a2077726f6e672072656d6f746520746f6b60448201527f656e20666f72204f7074696d69736d204d696e7461626c65204552433230206c60648201527f6f63616c20746f6b656e00000000000000000000000000000000000000000000608482015260a40161017c565b6040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8581166004830152602482018590528816906340c10f1990604401600060405180830381600087803b1580156107d357600080fd5b505af11580156107e7573d6000803e3d6000fd5b50505050610872565b73ffffffffffffffffffffffffffffffffffffffff8088166000908152600260209081526040808320938a168352929052205461082e9084906127a8565b73ffffffffffffffffffffffffffffffffffffffff8089166000818152600260209081526040808320948c168352939052919091209190915561087290858561189d565b6104bf878787878787878080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525061197192505050565b333b15610945576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161017c565b6109883333348686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506111f092505050565b505050565b3373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000420000000000000000000000000000000000000716148015610aab57507f0000000000000000000000003154cf16ccdb4c6d922629664174b904d80f2c3573ffffffffffffffffffffffffffffffffffffffff167f000000000000000000000000420000000000000000000000000000000000000773ffffffffffffffffffffffffffffffffffffffff16636e296e456040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a6f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a93919061275c565b73ffffffffffffffffffffffffffffffffffffffff16145b610b5d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20746865206f7468657220627269646760648201527f6500000000000000000000000000000000000000000000000000000000000000608482015260a40161017c565b823414610bec576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603a60248201527f5374616e646172644272696467653a20616d6f756e742073656e7420646f657360448201527f206e6f74206d6174636820616d6f756e74207265717569726564000000000000606482015260840161017c565b3073ffffffffffffffffffffffffffffffffffffffff851603610c91576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f5374616e646172644272696467653a2063616e6e6f742073656e6420746f207360448201527f656c660000000000000000000000000000000000000000000000000000000000606482015260840161017c565b7f000000000000000000000000420000000000000000000000000000000000000773ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603610d6c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f5374616e646172644272696467653a2063616e6e6f742073656e6420746f206d60448201527f657373656e676572000000000000000000000000000000000000000000000000606482015260840161017c565b610dae85858585858080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506119ff92505050565b6000610dcb855a8660405180602001604052806000815250611aa0565b9050806104c1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f5374616e646172644272696467653a20455448207472616e736665722066616960448201527f6c65640000000000000000000000000000000000000000000000000000000000606482015260840161017c565b333b15610ee9576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161017c565b610f2d853333878787878080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506103ee92505050565b5050505050565b6104bf87873388888888888080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506113d492505050565b6060610fa47f0000000000000000000000000000000000000000000000000000000000000001611aba565b610fcd7f0000000000000000000000000000000000000000000000000000000000000001611aba565b610ff67f0000000000000000000000000000000000000000000000000000000000000000611aba565b604051602001611008939291906127bf565b604051602081830303815290604052905090565b73ffffffffffffffffffffffffffffffffffffffff8716158015611069575073ffffffffffffffffffffffffffffffffffffffff861673deaddeaddeaddeaddeaddeaddeaddeaddead0000145b156110805761107b858585858561098d565b6104bf565b6104bf868887878787876104c9565b333b1561111e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603760248201527f5374616e646172644272696467653a2066756e6374696f6e2063616e206f6e6c60448201527f792062652063616c6c65642066726f6d20616e20454f41000000000000000000606482015260840161017c565b6104c186863333888888888080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506113d492505050565b6104c1863387878787878080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506103ee92505050565b6111ea3385348686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506111f092505050565b50505050565b82341461127f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603e60248201527f5374616e646172644272696467653a206272696467696e6720455448206d757360448201527f7420696e636c7564652073756666696369656e74204554482076616c75650000606482015260840161017c565b61128b85858584611bf7565b7f000000000000000000000000420000000000000000000000000000000000000773ffffffffffffffffffffffffffffffffffffffff16633dbb202b847f0000000000000000000000003154cf16ccdb4c6d922629664174b904d80f2c35631635f5fd60e01b898989886040516024016113089493929190612835565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009485161790525160e086901b909216825261139b9291889060040161287e565b6000604051808303818588803b1580156113b457600080fd5b505af11580156113c8573d6000803e3d6000fd5b50505050505050505050565b6113dd8761171b565b1561152b576113ec878761177d565b61149e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f5374616e646172644272696467653a2077726f6e672072656d6f746520746f6b60448201527f656e20666f72204f7074696d69736d204d696e7461626c65204552433230206c60648201527f6f63616c20746f6b656e00000000000000000000000000000000000000000000608482015260a40161017c565b6040517f9dc29fac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff868116600483015260248201859052881690639dc29fac90604401600060405180830381600087803b15801561150e57600080fd5b505af1158015611522573d6000803e3d6000fd5b505050506115bf565b61154d73ffffffffffffffffffffffffffffffffffffffff8816863086611c98565b73ffffffffffffffffffffffffffffffffffffffff8088166000908152600260209081526040808320938a168352929052205461158b9084906128c3565b73ffffffffffffffffffffffffffffffffffffffff8089166000908152600260209081526040808320938b16835292905220555b6115cd878787878786611cf6565b7f000000000000000000000000420000000000000000000000000000000000000773ffffffffffffffffffffffffffffffffffffffff16633dbb202b7f0000000000000000000000003154cf16ccdb4c6d922629664174b904d80f2c35630166a07a60e01b898b8a8a8a8960405160240161164d969594939291906128db565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009485161790525160e085901b90921682526116e09291879060040161287e565b600060405180830381600087803b1580156116fa57600080fd5b505af115801561170e573d6000803e3d6000fd5b5050505050505050505050565b6000611747827f1d1d8b6300000000000000000000000000000000000000000000000000000000611d84565b806117775750611777827fec4fc8e300000000000000000000000000000000000000000000000000000000611d84565b92915050565b60006117a9837f1d1d8b6300000000000000000000000000000000000000000000000000000000611d84565b15611852578273ffffffffffffffffffffffffffffffffffffffff1663c01e1bd66040518163ffffffff1660e01b8152600401602060405180830381865afa1580156117f9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061181d919061275c565b73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16149050611777565b8273ffffffffffffffffffffffffffffffffffffffff1663d6c0b2c46040518163ffffffff1660e01b8152600401602060405180830381865afa1580156117f9573d6000803e3d6000fd5b60405173ffffffffffffffffffffffffffffffffffffffff83166024820152604481018290526109889084907fa9059cbb00000000000000000000000000000000000000000000000000000000906064015b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff0000000000000000000000000000000000000000000000000000000090931692909217909152611da7565b8373ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff167fb0444523268717a02698be47d0803aa7468c00acbed2f8bd93a0459cde61dd898686866040516119e993929190612936565b60405180910390a46104c1868686868686611eb3565b8373ffffffffffffffffffffffffffffffffffffffff1673deaddeaddeaddeaddeaddeaddeaddeaddead000073ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fb0444523268717a02698be47d0803aa7468c00acbed2f8bd93a0459cde61dd89868686604051611a8c93929190612936565b60405180910390a46111ea84848484611f3b565b600080600080845160208601878a8af19695505050505050565b606081600003611afd57505060408051808201909152600181527f3000000000000000000000000000000000000000000000000000000000000000602082015290565b8160005b8115611b275780611b1181612974565b9150611b209050600a836129db565b9150611b01565b60008167ffffffffffffffff811115611b4257611b426129ef565b6040519080825280601f01601f191660200182016040528015611b6c576020820181803683370190505b5090505b8415611bef57611b816001836127a8565b9150611b8e600a86612a1e565b611b999060306128c3565b60f81b818381518110611bae57611bae612a32565b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350611be8600a866129db565b9450611b70565b949350505050565b8373ffffffffffffffffffffffffffffffffffffffff1673deaddeaddeaddeaddeaddeaddeaddeaddead000073ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167f73d170910aba9e6d50b102db522b1dbcd796216f5128b445aa2135272886497e868686604051611c8493929190612936565b60405180910390a46111ea84848484611fa8565b60405173ffffffffffffffffffffffffffffffffffffffff808516602483015283166044820152606481018290526111ea9085907f23b872dd00000000000000000000000000000000000000000000000000000000906084016118ef565b8373ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff167f73d170910aba9e6d50b102db522b1dbcd796216f5128b445aa2135272886497e868686604051611d6e93929190612936565b60405180910390a46104c1868686868686612007565b6000611d8f8361207f565b8015611da05750611da083836120e3565b9392505050565b6000611e09826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166121b29092919063ffffffff16565b8051909150156109885780806020019051810190611e279190612a61565b610988576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f74207375636365656400000000000000000000000000000000000000000000606482015260840161017c565b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167fd59c65b35445225835c83f50b6ede06a7be047d22e357073e250d9af537518cd868686604051611f2b93929190612936565b60405180910390a4505050505050565b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f31b2166ff604fc5672ea5df08a78081d2bc6d746cadce880747f3643d819e83d8484604051611f9a929190612a83565b60405180910390a350505050565b8273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f2849b43074093a05396b6f2a937dee8565b15a48a7b3d4bffb732a5017380af58484604051611f9a929190612a83565b8373ffffffffffffffffffffffffffffffffffffffff168573ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff167f7ff126db8024424bbfd9826e8ab82ff59136289ea440b04b39a0df1b03b9cabf868686604051611f2b93929190612936565b60006120ab827f01ffc9a7000000000000000000000000000000000000000000000000000000006120e3565b801561177757506120dc827fffffffff000000000000000000000000000000000000000000000000000000006120e3565b1592915050565b604080517fffffffff000000000000000000000000000000000000000000000000000000008316602480830191909152825180830390910181526044909101909152602080820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f01ffc9a700000000000000000000000000000000000000000000000000000000178152825160009392849283928392918391908a617530fa92503d9150600051905082801561219b575060208210155b80156121a75750600081115b979650505050505050565b6060611bef84846000858573ffffffffffffffffffffffffffffffffffffffff85163b61223b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161017c565b6000808673ffffffffffffffffffffffffffffffffffffffff1685876040516122649190612a9c565b60006040518083038185875af1925050503d80600081146122a1576040519150601f19603f3d011682016040523d82523d6000602084013e6122a6565b606091505b50915091506121a7828286606083156122c0575081611da0565b8251156122d05782518084602001fd5b816040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161017c919061262a565b73ffffffffffffffffffffffffffffffffffffffff8116811461232657600080fd5b50565b60008083601f84011261233b57600080fd5b50813567ffffffffffffffff81111561235357600080fd5b60208301915083602082850101111561236b57600080fd5b9250929050565b600080600080600080600060c0888a03121561238d57600080fd5b873561239881612304565b965060208801356123a881612304565b955060408801356123b881612304565b945060608801356123c881612304565b93506080880135925060a088013567ffffffffffffffff8111156123eb57600080fd5b6123f78a828b01612329565b989b979a50959850939692959293505050565b803563ffffffff8116811461241e57600080fd5b919050565b60008060006040848603121561243857600080fd5b6124418461240a565b9250602084013567ffffffffffffffff81111561245d57600080fd5b61246986828701612329565b9497909650939450505050565b60008060008060006080868803121561248e57600080fd5b853561249981612304565b945060208601356124a981612304565b935060408601359250606086013567ffffffffffffffff8111156124cc57600080fd5b6124d888828901612329565b969995985093965092949392505050565b60008060008060006080868803121561250157600080fd5b853561250c81612304565b9450602086013593506125216040870161240a565b9250606086013567ffffffffffffffff8111156124cc57600080fd5b600080600080600080600060c0888a03121561255857600080fd5b873561256381612304565b9650602088013561257381612304565b9550604088013561258381612304565b9450606088013593506125986080890161240a565b925060a088013567ffffffffffffffff8111156123eb57600080fd5b60005b838110156125cf5781810151838201526020016125b7565b838111156111ea5750506000910152565b600081518084526125f88160208601602086016125b4565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b602081526000611da060208301846125e0565b60008060008060008060a0878903121561265657600080fd5b863561266181612304565b9550602087013561267181612304565b9450604087013593506126866060880161240a565b9250608087013567ffffffffffffffff8111156126a257600080fd5b6126ae89828a01612329565b979a9699509497509295939492505050565b600080604083850312156126d357600080fd5b82356126de81612304565b915060208301356126ee81612304565b809150509250929050565b6000806000806060858703121561270f57600080fd5b843561271a81612304565b93506127286020860161240a565b9250604085013567ffffffffffffffff81111561274457600080fd5b61275087828801612329565b95989497509550505050565b60006020828403121561276e57600080fd5b8151611da081612304565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000828210156127ba576127ba612779565b500390565b600084516127d18184602089016125b4565b80830190507f2e00000000000000000000000000000000000000000000000000000000000000808252855161280d816001850160208a016125b4565b600192019182015283516128288160028401602088016125b4565b0160020195945050505050565b600073ffffffffffffffffffffffffffffffffffffffff80871683528086166020840152508360408301526080606083015261287460808301846125e0565b9695505050505050565b73ffffffffffffffffffffffffffffffffffffffff841681526060602082015260006128ad60608301856125e0565b905063ffffffff83166040830152949350505050565b600082198211156128d6576128d6612779565b500190565b600073ffffffffffffffffffffffffffffffffffffffff80891683528088166020840152808716604084015280861660608401525083608083015260c060a083015261292a60c08301846125e0565b98975050505050505050565b73ffffffffffffffffffffffffffffffffffffffff8416815282602082015260606040820152600061296b60608301846125e0565b95945050505050565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036129a5576129a5612779565b5060010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000826129ea576129ea6129ac565b500490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600082612a2d57612a2d6129ac565b500690565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600060208284031215612a7357600080fd5b81518015158114611da057600080fd5b828152604060208201526000611bef60408301846125e0565b60008251612aae8184602087016125b4565b919091019291505056fea164736f6c634300080f000a", + "linkReferences": {}, + "deployedLinkReferences": {} +} \ No newline at end of file diff --git a/operate/data/contracts/l2_standard_bridge/contract.py b/operate/data/contracts/l2_standard_bridge/contract.py new file mode 100644 index 000000000..8a7408ef0 --- /dev/null +++ b/operate/data/contracts/l2_standard_bridge/contract.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the class to connect to the `L2StandardBridge` contract.""" + +from aea.configurations.base import PublicId +from aea.contracts.base import Contract + + +class L2StandardBridge(Contract): + """The Service Staking contract.""" + + contract_id = PublicId.from_str("valory/l2_standard_bridge:0.1.0") diff --git a/operate/data/contracts/l2_standard_bridge/contract.yaml b/operate/data/contracts/l2_standard_bridge/contract.yaml new file mode 100644 index 000000000..39e357ce2 --- /dev/null +++ b/operate/data/contracts/l2_standard_bridge/contract.yaml @@ -0,0 +1,23 @@ +name: l2_standard_bridge +author: valory +version: 0.1.0 +type: contract +description: Optimism L2 Standard Bridge +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeihp3ucqaxnuozotqcmvm6bjdds4rfbtr56sj7jeb6sbxlzikvtxyq + build/L2StandardBridge.json: bafybeid2zqptx4awocztkhmifqsdnqrqcib73hbsp53snmfd24u7k3q6me + contract.py: bafybeief7twob74tkqaw7oaxp6nhywfehr7hqurfb3woyr3xxfnx4ulkta +fingerprint_ignore_patterns: [] +contracts: [] +class_name: L2StandardBridge +contract_interface_paths: + ethereum: build/L2StandardBridge.json +dependencies: + open-aea-ledger-ethereum: + version: ==1.60.0 + open-aea-test-autonomy: + version: ==0.18.3 + web3: + version: <7,>=6.0.0 From df353fccf3746eae55db673bc250eebf0793fa11 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 6 May 2025 19:59:41 +0200 Subject: [PATCH 080/173] fix: linters --- operate/bridge/providers/native_bridge_provider.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 6bfdd5303..f331b63cc 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -20,14 +20,10 @@ """Native bridge provider.""" -import enum import time import typing as t -from http import HTTPStatus -from urllib.parse import urlencode import eth_abi -import requests from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts from web3 import Web3 @@ -41,7 +37,6 @@ ) from operate.constants import ZERO_ADDRESS from operate.data import DATA_DIR -from operate.data.contracts import l2_standard_bridge from operate.data.contracts.l1_standard_bridge.contract import L1StandardBridge from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge from operate.operate_types import Chain From ac68ab8d5afc3658f66754debaf8270f7848d60e Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 6 May 2025 20:01:40 +0200 Subject: [PATCH 081/173] chore: mypy --- operate/bridge/providers/native_bridge_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index f331b63cc..60f8a9a1a 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -383,7 +383,7 @@ def _find_starting_block(self, bridge_request: BridgeRequest) -> int: tx_timestamp = block.timestamp # 2. Binary search the destination chain for block just before this timestamp - def find_block_before_timestamp(w3, timestamp) -> int: + def find_block_before_timestamp(w3, timestamp: int) -> int: latest = w3.eth.block_number low, high = 0, latest best = 0 From a446510bf8fe3e1a4d3644220458e9d59fae4a3c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 6 May 2025 22:02:07 +0200 Subject: [PATCH 082/173] doc: readme --- operate/data/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/operate/data/README.md b/operate/data/README.md index 2161d3fca..a7c757859 100644 --- a/operate/data/README.md +++ b/operate/data/README.md @@ -1,11 +1,16 @@ # Contracts Data -This directory contains packages that are used in the middleware. +Reused packages from other repositories: - `contracts/staking_token` is copied from [valory/trader](https://github.com/valory-xyz/trader/tree/main/packages/valory/contracts/staking_token) - `contracts/mech_activity` is copied from [valory/trader](https://github.com/valory-xyz/trader/tree/main/packages/valory/contracts/mech_activity) - `contracts/uniswap_v2_erc20` is copied from [valory/optimus-quickstart](https://github.com/valory-xyz/optimus-quickstart/tree/main/operate/data/contracts/uniswap_v2_erc20) -- `contracts/requester_activity_checker` is new from this repository. -- `contracts/dual_staking_token` is new from this repository. + +New packages from this repository: + +- `contracts/dual_staking_token` +- `contracts/l1_standard_bridge` +- `contracts/l2_standard_bridge` +- `contracts/requester_activity_checker` TODO: Have a better way to import and reuse packages in the middleware. From 4db743b3a1eb3c8c4e91268d0e7be805378a932b Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 6 May 2025 22:47:39 +0200 Subject: [PATCH 083/173] fix: str to int --- operate/bridge/providers/bridge_provider.py | 1 + .../providers/native_bridge_provider.py | 23 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index e5f2850f7..a9d78484a 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -169,6 +169,7 @@ def create_request(self, params: t.Dict) -> BridgeRequest: "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) + params["to"]["amount"] = int(params["to"]["amount"]) return BridgeRequest( params=params, bridge_provider_id=self.id(), diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 60f8a9a1a..250fe474f 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -213,6 +213,10 @@ def _get_transactions( self, bridge_request: BridgeRequest ) -> t.List[t.Tuple[str, t.Dict]]: """Get the sorted list of transactions to execute the bridge request.""" + self.logger.info( + f"[NATIVE BRIDGE] Get transactions for bridge request {bridge_request.id}." + ) + self._validate(bridge_request) if not bridge_request.quote_data: @@ -243,11 +247,12 @@ def _get_transactions( def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" - self._validate(bridge_request) - self.logger.info( - f"[NATIVE BRIDGE] Updating execution status for {bridge_request.id}..." + f"[NATIVE BRIDGE] Updating execution status for bridge request {bridge_request.id}." ) + + self._validate(bridge_request) + if bridge_request.status not in ( BridgeRequestStatus.EXECUTION_PENDING, # BridgeRequestStatus.EXECUTION_UNKNOWN, @@ -294,13 +299,13 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: target_extra_data = Web3.keccak(text=bridge_request.id).hex() - starting_block = self._find_starting_block(bridge_request) + starting_block = self.__find_starting_block(bridge_request) starting_block_ts = w3.eth.get_block(starting_block).timestamp latest_block = w3.eth.block_number for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) - event_found = self._find_event_in_range( + event_found = self.__find_event_in_range( w3, to_bridge, from_block, @@ -322,7 +327,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.error(f"Error updating execution status: {e}") bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN - def _find_event_in_range( + def __find_event_in_range( self, w3, contract_address: str, @@ -342,10 +347,6 @@ def _find_event_in_range( } ) - from icecream import ic - - ic(logs) - for log in logs: decoded = eth_abi.decode(non_indexed_types, log["data"]) extra_data = "0x" + decoded[-1].hex() @@ -354,7 +355,7 @@ def _find_event_in_range( return False - def _find_starting_block(self, bridge_request: BridgeRequest) -> int: + def __find_starting_block(self, bridge_request: BridgeRequest) -> int: """Find the starting block for the event log search on the destination chain. The starting block to search for the event log is the largest block on the From 168d7190ed9dd8fed6e92f266e0cb11acd68700f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 6 May 2025 23:01:13 +0200 Subject: [PATCH 084/173] fix: int --- operate/bridge/providers/bridge_provider.py | 1 - operate/bridge/providers/lifi_bridge_provider.py | 2 +- operate/bridge/providers/native_bridge_provider.py | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index a9d78484a..e5f2850f7 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -169,7 +169,6 @@ def create_request(self, params: t.Dict) -> BridgeRequest: "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) - params["to"]["amount"] = int(params["to"]["amount"]) return BridgeRequest( params=params, bridge_provider_id=self.id(), diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index b8175a40f..9e1345f60 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -88,7 +88,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] - to_amount = bridge_request.params["to"]["amount"] + to_amount = int(bridge_request.params["to"]["amount"]) if to_amount == 0: self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 250fe474f..d1ef9af15 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -109,7 +109,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: f"Cannot quote bridge request {bridge_request.id}: execution already present." ) - to_amount = bridge_request.params["to"]["amount"] + to_amount = int(bridge_request.params["to"]["amount"]) message = None if to_amount == 0: @@ -141,7 +141,7 @@ def _get_bridge_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] - to_amount = bridge_request.params["to"]["amount"] + to_amount = int(bridge_request.params["to"]["amount"]) from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] extra_data = Web3.keccak(text=bridge_request.id) @@ -192,7 +192,7 @@ def _get_approve_tx( from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - to_amount = bridge_request.params["to"]["amount"] + to_amount = int(bridge_request.params["to"]["amount"]) from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] if from_token == ZERO_ADDRESS: From 419e480533a91b51801913ad2a7049a38ea13e19 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 00:17:37 +0200 Subject: [PATCH 085/173] chore: fix uuid --- operate/bridge/providers/bridge_provider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index e5f2850f7..5cdc8f29d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -96,7 +96,7 @@ class BridgeRequest(LocalResource): params: t.Dict bridge_provider_id: str - id: str = f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}" + id: str status: BridgeRequestStatus = BridgeRequestStatus.CREATED quote_data: t.Optional[QuoteData] = None execution_data: t.Optional[ExecutionData] = None @@ -172,6 +172,7 @@ def create_request(self, params: t.Dict) -> BridgeRequest: return BridgeRequest( params=params, bridge_provider_id=self.id(), + id=f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", ) @abstractmethod From f5f3eef0414b67a67faf7c4aaddc09c6863bf2b8 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 00:20:47 +0200 Subject: [PATCH 086/173] chore: poetry --- poetry.lock | 1736 ++++++++++++++++++++++++++------------------------- 1 file changed, 877 insertions(+), 859 deletions(-) diff --git a/poetry.lock b/poetry.lock index 38579eaf5..8e7f862a9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -134,13 +134,13 @@ files = [ [[package]] name = "anyio" -version = "4.8.0" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] @@ -150,8 +150,8 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -235,20 +235,20 @@ files = [ [[package]] name = "attrs" -version = "25.1.0" +version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" files = [ - {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, - {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -343,13 +343,13 @@ typecheck = ["mypy"] [[package]] name = "beautifulsoup4" -version = "4.13.3" +version = "4.13.4" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" files = [ - {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, - {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, ] [package.dependencies] @@ -547,13 +547,13 @@ virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] @@ -637,103 +637,103 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] @@ -854,74 +854,74 @@ requests = "*" [[package]] name = "coverage" -version = "7.6.12" +version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, - {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, - {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, - {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, - {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, - {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, - {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, - {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, - {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, - {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, - {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, - {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, - {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, - {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, - {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, - {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, - {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.extras] @@ -1097,13 +1097,13 @@ cython = ["cython"] [[package]] name = "deepdiff" -version = "8.2.0" +version = "8.4.2" description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." optional = false python-versions = ">=3.8" files = [ - {file = "deepdiff-8.2.0-py3-none-any.whl", hash = "sha256:5091f2cdfd372b1b9f6bfd8065ba323ae31118dc4e42594371b38c8bea3fd0a4"}, - {file = "deepdiff-8.2.0.tar.gz", hash = "sha256:6ec78f65031485735545ffbe7a61e716c3c2d12ca6416886d5e9291fc76c46c3"}, + {file = "deepdiff-8.4.2-py3-none-any.whl", hash = "sha256:7e39e5b26f3747c54f9d0e8b9b29daab670c3100166b77cc0185d5793121b099"}, + {file = "deepdiff-8.4.2.tar.gz", hash = "sha256:5c741c0867ebc7fcb83950ad5ed958369c17f424e14dee32a11c56073f4ee92a"}, ] [package.dependencies] @@ -1412,13 +1412,13 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)" [[package]] name = "filelock" -version = "3.17.0" +version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" files = [ - {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, - {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] @@ -1547,20 +1547,20 @@ files = [ [[package]] name = "googleapis-common-protos" -version = "1.68.0" +version = "1.70.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac"}, - {file = "googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c"}, + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "gql" @@ -1606,80 +1606,76 @@ typing-extensions = {version = ">=4,<5", markers = "python_version < \"3.10\""} [[package]] name = "grpcio" -version = "1.70.0" +version = "1.71.0" description = "HTTP/2-based RPC framework" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851"}, - {file = "grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf"}, - {file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:374d014f29f9dfdb40510b041792e0e2828a1389281eb590df066e1cc2b404e5"}, - {file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2af68a6f5c8f78d56c145161544ad0febbd7479524a59c16b3e25053f39c87f"}, - {file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7df14b2dcd1102a2ec32f621cc9fab6695effef516efbc6b063ad749867295"}, - {file = "grpcio-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c78b339869f4dbf89881e0b6fbf376313e4f845a42840a7bdf42ee6caed4b11f"}, - {file = "grpcio-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58ad9ba575b39edef71f4798fdb5c7b6d02ad36d47949cd381d4392a5c9cbcd3"}, - {file = "grpcio-1.70.0-cp310-cp310-win32.whl", hash = "sha256:2b0d02e4b25a5c1f9b6c7745d4fa06efc9fd6a611af0fb38d3ba956786b95199"}, - {file = "grpcio-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:0de706c0a5bb9d841e353f6343a9defc9fc35ec61d6eb6111802f3aa9fef29e1"}, - {file = "grpcio-1.70.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:17325b0be0c068f35770f944124e8839ea3185d6d54862800fc28cc2ffad205a"}, - {file = "grpcio-1.70.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:dbe41ad140df911e796d4463168e33ef80a24f5d21ef4d1e310553fcd2c4a386"}, - {file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5ea67c72101d687d44d9c56068328da39c9ccba634cabb336075fae2eab0d04b"}, - {file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb5277db254ab7586769e490b7b22f4ddab3876c490da0a1a9d7c695ccf0bf77"}, - {file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7831a0fc1beeeb7759f737f5acd9fdcda520e955049512d68fda03d91186eea"}, - {file = "grpcio-1.70.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:27cc75e22c5dba1fbaf5a66c778e36ca9b8ce850bf58a9db887754593080d839"}, - {file = "grpcio-1.70.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d63764963412e22f0491d0d32833d71087288f4e24cbcddbae82476bfa1d81fd"}, - {file = "grpcio-1.70.0-cp311-cp311-win32.whl", hash = "sha256:bb491125103c800ec209d84c9b51f1c60ea456038e4734688004f377cfacc113"}, - {file = "grpcio-1.70.0-cp311-cp311-win_amd64.whl", hash = "sha256:d24035d49e026353eb042bf7b058fb831db3e06d52bee75c5f2f3ab453e71aca"}, - {file = "grpcio-1.70.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:ef4c14508299b1406c32bdbb9fb7b47612ab979b04cf2b27686ea31882387cff"}, - {file = "grpcio-1.70.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:aa47688a65643afd8b166928a1da6247d3f46a2784d301e48ca1cc394d2ffb40"}, - {file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:880bfb43b1bb8905701b926274eafce5c70a105bc6b99e25f62e98ad59cb278e"}, - {file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e654c4b17d07eab259d392e12b149c3a134ec52b11ecdc6a515b39aceeec898"}, - {file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2394e3381071045a706ee2eeb6e08962dd87e8999b90ac15c55f56fa5a8c9597"}, - {file = "grpcio-1.70.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b3c76701428d2df01964bc6479422f20e62fcbc0a37d82ebd58050b86926ef8c"}, - {file = "grpcio-1.70.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac073fe1c4cd856ebcf49e9ed6240f4f84d7a4e6ee95baa5d66ea05d3dd0df7f"}, - {file = "grpcio-1.70.0-cp312-cp312-win32.whl", hash = "sha256:cd24d2d9d380fbbee7a5ac86afe9787813f285e684b0271599f95a51bce33528"}, - {file = "grpcio-1.70.0-cp312-cp312-win_amd64.whl", hash = "sha256:0495c86a55a04a874c7627fd33e5beaee771917d92c0e6d9d797628ac40e7655"}, - {file = "grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a"}, - {file = "grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429"}, - {file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9"}, - {file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c"}, - {file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f"}, - {file = "grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0"}, - {file = "grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40"}, - {file = "grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce"}, - {file = "grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68"}, - {file = "grpcio-1.70.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:8058667a755f97407fca257c844018b80004ae8035565ebc2812cc550110718d"}, - {file = "grpcio-1.70.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:879a61bf52ff8ccacbedf534665bb5478ec8e86ad483e76fe4f729aaef867cab"}, - {file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:0ba0a173f4feacf90ee618fbc1a27956bfd21260cd31ced9bc707ef551ff7dc7"}, - {file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558c386ecb0148f4f99b1a65160f9d4b790ed3163e8610d11db47838d452512d"}, - {file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:412faabcc787bbc826f51be261ae5fa996b21263de5368a55dc2cf824dc5090e"}, - {file = "grpcio-1.70.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3b0f01f6ed9994d7a0b27eeddea43ceac1b7e6f3f9d86aeec0f0064b8cf50fdb"}, - {file = "grpcio-1.70.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7385b1cb064734005204bc8994eed7dcb801ed6c2eda283f613ad8c6c75cf873"}, - {file = "grpcio-1.70.0-cp38-cp38-win32.whl", hash = "sha256:07269ff4940f6fb6710951116a04cd70284da86d0a4368fd5a3b552744511f5a"}, - {file = "grpcio-1.70.0-cp38-cp38-win_amd64.whl", hash = "sha256:aba19419aef9b254e15011b230a180e26e0f6864c90406fdbc255f01d83bc83c"}, - {file = "grpcio-1.70.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:4f1937f47c77392ccd555728f564a49128b6a197a05a5cd527b796d36f3387d0"}, - {file = "grpcio-1.70.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:0cd430b9215a15c10b0e7d78f51e8a39d6cf2ea819fd635a7214fae600b1da27"}, - {file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:e27585831aa6b57b9250abaf147003e126cd3a6c6ca0c531a01996f31709bed1"}, - {file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1af8e15b0f0fe0eac75195992a63df17579553b0c4af9f8362cc7cc99ccddf4"}, - {file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbce24409beaee911c574a3d75d12ffb8c3e3dd1b813321b1d7a96bbcac46bf4"}, - {file = "grpcio-1.70.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ff4a8112a79464919bb21c18e956c54add43ec9a4850e3949da54f61c241a4a6"}, - {file = "grpcio-1.70.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5413549fdf0b14046c545e19cfc4eb1e37e9e1ebba0ca390a8d4e9963cab44d2"}, - {file = "grpcio-1.70.0-cp39-cp39-win32.whl", hash = "sha256:b745d2c41b27650095e81dea7091668c040457483c9bdb5d0d9de8f8eb25e59f"}, - {file = "grpcio-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:a31d7e3b529c94e930a117b2175b2efd179d96eb3c7a21ccb0289a8ab05b645c"}, - {file = "grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56"}, + {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, + {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, + {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea"}, + {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69"}, + {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73"}, + {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804"}, + {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6"}, + {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5"}, + {file = "grpcio-1.71.0-cp310-cp310-win32.whl", hash = "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509"}, + {file = "grpcio-1.71.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a"}, + {file = "grpcio-1.71.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef"}, + {file = "grpcio-1.71.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7"}, + {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7"}, + {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7"}, + {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e"}, + {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b"}, + {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7"}, + {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3"}, + {file = "grpcio-1.71.0-cp311-cp311-win32.whl", hash = "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444"}, + {file = "grpcio-1.71.0-cp311-cp311-win_amd64.whl", hash = "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b"}, + {file = "grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537"}, + {file = "grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7"}, + {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec"}, + {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594"}, + {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c"}, + {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67"}, + {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db"}, + {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79"}, + {file = "grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a"}, + {file = "grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8"}, + {file = "grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379"}, + {file = "grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3"}, + {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db"}, + {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29"}, + {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4"}, + {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3"}, + {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b"}, + {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637"}, + {file = "grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb"}, + {file = "grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366"}, + {file = "grpcio-1.71.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d"}, + {file = "grpcio-1.71.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e"}, + {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033"}, + {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97"}, + {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d"}, + {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41"}, + {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3"}, + {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32"}, + {file = "grpcio-1.71.0-cp39-cp39-win32.whl", hash = "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455"}, + {file = "grpcio-1.71.0-cp39-cp39-win_amd64.whl", hash = "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a"}, + {file = "grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.70.0)"] +protobuf = ["grpcio-tools (>=1.71.0)"] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] @@ -1722,18 +1718,18 @@ test = ["eth-utils (>=1.0.1,<3)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>= [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] @@ -1782,13 +1778,13 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.6.1" +version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] @@ -1805,13 +1801,13 @@ type = ["pytest-mypy"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1842,13 +1838,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -1985,149 +1981,140 @@ test = ["pytest"] [[package]] name = "lxml" -version = "5.3.1" +version = "5.4.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"}, - {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"}, - {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"}, - {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"}, - {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"}, - {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"}, - {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"}, - {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"}, - {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"}, - {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"}, - {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"}, - {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"}, - {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, - {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, - {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, - {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, - {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, - {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, - {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"}, - {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"}, - {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"}, - {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"}, - {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"}, - {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"}, - {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"}, - {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"}, - {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"}, - {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"}, - {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"}, - {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"}, - {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"}, - {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"}, - {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"}, - {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"}, - {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"}, - {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"}, - {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"}, - {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"}, - {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"}, - {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"}, - {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"}, - {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"}, - {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"}, - {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"}, - {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"}, - {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"}, - {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"}, - {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"}, - {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"}, - {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"}, - {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"}, - {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, + {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, + {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, + {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, + {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, + {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, + {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, + {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, + {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, + {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, + {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, + {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, + {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, + {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, + {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, + {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, + {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, + {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, + {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, + {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, + {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, + {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, ] [package.extras] @@ -2520,13 +2507,13 @@ cli = ["click (>=8.1.0,<9)", "coverage (>=6.4.4,<8.0.0)", "open-aea-cli-ipfs (== [[package]] name = "orderly-set" -version = "5.3.0" +version = "5.4.0" description = "Orderly set" optional = false python-versions = ">=3.8" files = [ - {file = "orderly_set-5.3.0-py3-none-any.whl", hash = "sha256:c2c0bfe604f5d3d9b24e8262a06feb612594f37aa3845650548befd7772945d1"}, - {file = "orderly_set-5.3.0.tar.gz", hash = "sha256:80b3d8fdd3d39004d9aad389eaa0eab02c71f0a0511ba3a6d54a935a6c6a0acc"}, + {file = "orderly_set-5.4.0-py3-none-any.whl", hash = "sha256:f0192a7f9ae3385b587b71688353fae491d1ca45878496eb71ea118be1623639"}, + {file = "orderly_set-5.4.0.tar.gz", hash = "sha256:c8ff5ba824abe4eebcbbdd3f646ff3648ad0dd52239319d90056d8d30b6cccdd"}, ] [[package]] @@ -2588,19 +2575,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -2619,109 +2606,109 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "propcache" -version = "0.3.0" +version = "0.3.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" files = [ - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e7420211f5a65a54675fd860ea04173cde60a7cc20ccfbafcccd155225f8bc"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3302c5287e504d23bb0e64d2a921d1eb4a03fb93a0a0aa3b53de059f5a5d737d"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2e068a83552ddf7a39a99488bcba05ac13454fb205c847674da0352602082f"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d913d36bdaf368637b4f88d554fb9cb9d53d6920b9c5563846555938d5450bf"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee1983728964d6070ab443399c476de93d5d741f71e8f6e7880a065f878e0b9"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36ca5e9a21822cc1746023e88f5c0af6fce3af3b85d4520efb1ce4221bed75cc"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9ecde3671e62eeb99e977f5221abcf40c208f69b5eb986b061ccec317c82ebd0"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d383bf5e045d7f9d239b38e6acadd7b7fdf6c0087259a84ae3475d18e9a2ae8b"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8cb625bcb5add899cb8ba7bf716ec1d3e8f7cdea9b0713fa99eadf73b6d4986f"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5fa159dcee5dba00c1def3231c249cf261185189205073bde13797e57dd7540a"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7080b0159ce05f179cfac592cda1a82898ca9cd097dacf8ea20ae33474fbb25"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed7161bccab7696a473fe7ddb619c1d75963732b37da4618ba12e60899fefe4f"}, - {file = "propcache-0.3.0-cp310-cp310-win32.whl", hash = "sha256:bf0d9a171908f32d54f651648c7290397b8792f4303821c42a74e7805bfb813c"}, - {file = "propcache-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:42924dc0c9d73e49908e35bbdec87adedd651ea24c53c29cac103ede0ea1d340"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9ddd49258610499aab83b4f5b61b32e11fce873586282a0e972e5ab3bcadee51"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2578541776769b500bada3f8a4eeaf944530516b6e90c089aa368266ed70c49e"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8074c5dd61c8a3e915fa8fc04754fa55cfa5978200d2daa1e2d4294c1f136aa"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b58229a844931bca61b3a20efd2be2a2acb4ad1622fc026504309a6883686fbf"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e45377d5d6fefe1677da2a2c07b024a6dac782088e37c0b1efea4cfe2b1be19b"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5060592d83454e8063e487696ac3783cc48c9a329498bafae0d972bc7816c9"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15010f29fbed80e711db272909a074dc79858c6d28e2915704cfc487a8ac89c6"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a254537b9b696ede293bfdbc0a65200e8e4507bc9f37831e2a0318a9b333c85c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2b975528998de037dfbc10144b8aed9b8dd5a99ec547f14d1cb7c5665a43f075"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:19d36bb351ad5554ff20f2ae75f88ce205b0748c38b146c75628577020351e3c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6032231d4a5abd67c7f71168fd64a47b6b451fbcb91c8397c2f7610e67683810"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6985a593417cdbc94c7f9c3403747335e450c1599da1647a5af76539672464d3"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a1948df1bb1d56b5e7b0553c0fa04fd0e320997ae99689488201f19fa90d2e7"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8319293e85feadbbfe2150a5659dbc2ebc4afdeaf7d98936fb9a2f2ba0d4c35c"}, - {file = "propcache-0.3.0-cp311-cp311-win32.whl", hash = "sha256:63f26258a163c34542c24808f03d734b338da66ba91f410a703e505c8485791d"}, - {file = "propcache-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cacea77ef7a2195f04f9279297684955e3d1ae4241092ff0cfcef532bb7a1c32"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c"}, - {file = "propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d"}, - {file = "propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626"}, - {file = "propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374"}, - {file = "propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf"}, - {file = "propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863"}, - {file = "propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:03c091bb752349402f23ee43bb2bff6bd80ccab7c9df6b88ad4322258d6960fc"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46ed02532cb66612d42ae5c3929b5e98ae330ea0f3900bc66ec5f4862069519b"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11ae6a8a01b8a4dc79093b5d3ca2c8a4436f5ee251a9840d7790dccbd96cb649"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df03cd88f95b1b99052b52b1bb92173229d7a674df0ab06d2b25765ee8404bce"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03acd9ff19021bd0567582ac88f821b66883e158274183b9e5586f678984f8fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd54895e4ae7d32f1e3dd91261df46ee7483a735017dc6f987904f194aa5fd14"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a67e5c04e3119594d8cfae517f4b9330c395df07ea65eab16f3d559b7068fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee25f1ac091def37c4b59d192bbe3a206298feeb89132a470325bf76ad122a1e"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58e6d2a5a7cb3e5f166fd58e71e9a4ff504be9dc61b88167e75f835da5764d07"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:be90c94570840939fecedf99fa72839aed70b0ced449b415c85e01ae67422c90"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49ea05212a529c2caffe411e25a59308b07d6e10bf2505d77da72891f9a05641"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:119e244ab40f70a98c91906d4c1f4c5f2e68bd0b14e7ab0a06922038fae8a20f"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:507c5357a8d8b4593b97fb669c50598f4e6cccbbf77e22fa9598aba78292b4d7"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8526b0941ec5a40220fc4dfde76aed58808e2b309c03e9fa8e2260083ef7157f"}, - {file = "propcache-0.3.0-cp39-cp39-win32.whl", hash = "sha256:7cedd25e5f678f7738da38037435b340694ab34d424938041aa630d8bac42663"}, - {file = "propcache-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf4298f366ca7e1ad1d21bbb58300a6985015909964077afd37559084590c929"}, - {file = "propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043"}, - {file = "propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, + {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, + {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, + {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, + {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, + {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, + {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, + {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, + {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, + {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, + {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, + {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, + {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, + {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, + {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, ] [[package]] @@ -2830,60 +2817,58 @@ files = [ [[package]] name = "pycryptodome" -version = "3.21.0" +version = "3.22.0" description = "Cryptographic library for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, - {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, - {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, - {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "pycryptodome-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:96e73527c9185a3d9b4c6d1cfb4494f6ced418573150be170f6580cb975a7f5a"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9e1bb165ea1dc83a11e5dbbe00ef2c378d148f3a2d3834fb5ba4e0f6fd0afe4b"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:d4d1174677855c266eed5c4b4e25daa4225ad0c9ffe7584bb1816767892545d0"}, + {file = "pycryptodome-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:9dbb749cef71c28271484cbef684f9b5b19962153487735411e1020ca3f59cb1"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f1ae7beb64d4fc4903a6a6cca80f1f448e7a8a95b77d106f8a29f2eb44d17547"}, + {file = "pycryptodome-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a26bcfee1293b7257c83b0bd13235a4ee58165352be4f8c45db851ba46996dc6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6"}, + {file = "pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d"}, + {file = "pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8"}, + {file = "pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb"}, + {file = "pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:98fd9da809d5675f3a65dcd9ed384b9dc67edab6a4cda150c5870a8122ec961d"}, + {file = "pycryptodome-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:37ddcd18284e6b36b0a71ea495a4c4dca35bb09ccc9bfd5b91bfaf2321f131c1"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad"}, + {file = "pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56c6f9342fcb6c74e205fbd2fee568ec4cdbdaa6165c8fde55dbc4ba5f584464"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87a88dc543b62b5c669895caf6c5a958ac7abc8863919e94b7a6cafd2f64064f"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a683bc9fa585c0dfec7fa4801c96a48d30b30b096e3297f9374f40c2fedafc"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f4f6f47a7f411f2c157e77bbbda289e0c9f9e1e9944caa73c1c2e33f3f92d6e"}, + {file = "pycryptodome-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6cf9553b29624961cab0785a3177a333e09e37ba62ad22314ebdbb01ca79840"}, + {file = "pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723"}, ] [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.4" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, + {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" +pydantic-core = "2.33.2" typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2891,111 +2876,110 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] @@ -3003,32 +2987,32 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyinstaller" -version = "6.12.0" +version = "6.13.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false python-versions = "<3.14,>=3.8" files = [ - {file = "pyinstaller-6.12.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:68f1e4cecf88a6272063977fa2a2c69ad37cf568e5901769d7206d0314c74f47"}, - {file = "pyinstaller-6.12.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:fea76fc9b55ffa730fcf90beb897cce4399938460b0b6f40507fbebfc752c753"}, - {file = "pyinstaller-6.12.0-py3-none-manylinux2014_i686.whl", hash = "sha256:dac8a27988dbc33cdc34f2046803258bc3f6829de24de52745a5daa22bdba0f1"}, - {file = "pyinstaller-6.12.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:83c7f3bde9871b4a6aa71c66a96e8ba5c21668ce711ed97f510b9382d10aac6c"}, - {file = "pyinstaller-6.12.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:a69818815c6e0711c727edc30680cb1f81c691b59de35db81a2d9e0ae26a9ef1"}, - {file = "pyinstaller-6.12.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a2abf5fde31a8b38b6df7939bcef8ac1d0c51e97e25317ce3555cd675259750f"}, - {file = "pyinstaller-6.12.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:8e92e9873a616547bbabbb5a3a9843d5f2ab40c3d8b26810acdf0fe257bee4cf"}, - {file = "pyinstaller-6.12.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:aefe502d55c9cf6aeaed7feba80b5f8491ce43f8f2b5fe2d9aadca3ee5a05bc4"}, - {file = "pyinstaller-6.12.0-py3-none-win32.whl", hash = "sha256:138856a5a503bb69c066377e0a22671b0db063e9cc14d5cf5c798a53561200d3"}, - {file = "pyinstaller-6.12.0-py3-none-win_amd64.whl", hash = "sha256:0e62d3906309248409f215b386f33afec845214e69cc0f296b93222b26a88f43"}, - {file = "pyinstaller-6.12.0-py3-none-win_arm64.whl", hash = "sha256:0c271896a3a168f4f91827145702543db9c5427f4c7372a6df8c75925a3ac18a"}, - {file = "pyinstaller-6.12.0.tar.gz", hash = "sha256:1834797be48ce1b26015af68bdeb3c61a6c7500136f04e0fc65e468115dec777"}, + {file = "pyinstaller-6.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:aa404f0b02cd57948098055e76ee190b8e65ccf7a2a3f048e5000f668317069f"}, + {file = "pyinstaller-6.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:92efcf2f09e78f07b568c5cb7ed48c9940f5dad627af4b49bede6320fab2a06e"}, + {file = "pyinstaller-6.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:9f82f113c463f012faa0e323d952ca30a6f922685d9636e754bd3a256c7ed200"}, + {file = "pyinstaller-6.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:db0e7945ebe276f604eb7c36e536479556ab32853412095e19172a5ec8fca1c5"}, + {file = "pyinstaller-6.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:92fe7337c5aa08d42b38d7a79614492cb571489f2cb0a8f91dc9ef9ccbe01ed3"}, + {file = "pyinstaller-6.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bc09795f5954135dd4486c1535650958c8218acb954f43860e4b05fb515a21c0"}, + {file = "pyinstaller-6.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:589937548d34978c568cfdc39f31cf386f45202bc27fdb8facb989c79dfb4c02"}, + {file = "pyinstaller-6.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b7260832f7501ba1d2ce1834d4cddc0f2b94315282bc89c59333433715015447"}, + {file = "pyinstaller-6.13.0-py3-none-win32.whl", hash = "sha256:80c568848529635aa7ca46d8d525f68486d53e03f68b7bb5eba2c88d742e302c"}, + {file = "pyinstaller-6.13.0-py3-none-win_amd64.whl", hash = "sha256:8d4296236b85aae570379488c2da833b28828b17c57c2cc21fccd7e3811fe372"}, + {file = "pyinstaller-6.13.0-py3-none-win_arm64.whl", hash = "sha256:d9f21d56ca2443aa6a1e255e7ad285c76453893a454105abe1b4d45e92bb9a20"}, + {file = "pyinstaller-6.13.0.tar.gz", hash = "sha256:38911feec2c5e215e5159a7e66fdb12400168bd116143b54a8a7a37f08733456"}, ] [package.dependencies] altgraph = "*" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +importlib_metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""} -pyinstaller-hooks-contrib = ">=2025.1" +pyinstaller-hooks-contrib = ">=2025.2" pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" @@ -3038,13 +3022,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2025.1" +version = "2025.4" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.8" files = [ - {file = "pyinstaller_hooks_contrib-2025.1-py3-none-any.whl", hash = "sha256:d3c799470cbc0bda60dcc8e6b4ab976777532b77621337f2037f558905e3a8e9"}, - {file = "pyinstaller_hooks_contrib-2025.1.tar.gz", hash = "sha256:130818f9e9a0a7f2261f1fd66054966a3a50c99d000981c5d1db11d3ad0c6ab2"}, + {file = "pyinstaller_hooks_contrib-2025.4-py3-none-any.whl", hash = "sha256:6c2d73269b4c484eb40051fc1acee0beb113c2cfb3b37437b8394faae6f0d072"}, + {file = "pyinstaller_hooks_contrib-2025.4.tar.gz", hash = "sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446"}, ] [package.dependencies] @@ -3222,29 +3206,27 @@ cli = ["click (>=5.0)"] [[package]] name = "pywin32" -version = "308" +version = "310" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, - {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, - {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, - {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, - {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, - {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, - {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, - {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, - {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, - {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, - {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, - {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, - {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, - {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, - {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, - {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, - {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, - {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, + {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, + {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, + {file = "pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213"}, + {file = "pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd"}, + {file = "pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c"}, + {file = "pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582"}, + {file = "pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d"}, + {file = "pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060"}, + {file = "pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966"}, + {file = "pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab"}, + {file = "pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e"}, + {file = "pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33"}, + {file = "pywin32-310-cp38-cp38-win32.whl", hash = "sha256:0867beb8addefa2e3979d4084352e4ac6e991ca45373390775f7084cc0209b9c"}, + {file = "pywin32-310-cp38-cp38-win_amd64.whl", hash = "sha256:30f0a9b3138fb5e07eb4973b7077e1883f558e40c578c6925acc7a94c34eaa36"}, + {file = "pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a"}, + {file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"}, ] [[package]] @@ -3490,18 +3472,18 @@ files = [ [[package]] name = "setuptools" -version = "75.8.2" +version = "80.3.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f"}, - {file = "setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2"}, + {file = "setuptools-80.3.1-py3-none-any.whl", hash = "sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537"}, + {file = "setuptools-80.3.1.tar.gz", hash = "sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] @@ -3543,13 +3525,13 @@ files = [ [[package]] name = "soupsieve" -version = "2.6" +version = "2.7" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, + {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, + {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, ] [[package]] @@ -3583,13 +3565,13 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "termcolor" -version = "2.5.0" +version = "3.1.0" description = "ANSI color formatting for output in terminal" optional = false python-versions = ">=3.9" files = [ - {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, - {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, + {file = "termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa"}, + {file = "termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970"}, ] [package.extras] @@ -3735,24 +3717,38 @@ pyotp = "*" [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, ] +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] @@ -3820,13 +3816,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.29.2" +version = "20.31.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, - {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, + {file = "virtualenv-20.31.1-py3-none-any.whl", hash = "sha256:f448cd2f1604c831afb9ea238021060be2c0edbcad8eb0a4e8b4e14ff11a5482"}, + {file = "virtualenv-20.31.1.tar.gz", hash = "sha256:65442939608aeebb9284cd30baca5865fcd9f12b58bb740a24b220030df46d26"}, ] [package.dependencies] @@ -3929,80 +3925,80 @@ six = "*" [[package]] name = "websockets" -version = "15.0" +version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" files = [ - {file = "websockets-15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5e6ee18a53dd5743e6155b8ff7e8e477c25b29b440f87f65be8165275c87fef0"}, - {file = "websockets-15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee06405ea2e67366a661ed313e14cf2a86e84142a3462852eb96348f7219cee3"}, - {file = "websockets-15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8711682a629bbcaf492f5e0af72d378e976ea1d127a2d47584fa1c2c080b436b"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94c4a9b01eede952442c088d415861b0cf2053cbd696b863f6d5022d4e4e2453"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45535fead66e873f411c1d3cf0d3e175e66f4dd83c4f59d707d5b3e4c56541c4"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e389efe46ccb25a1f93d08c7a74e8123a2517f7b7458f043bd7529d1a63ffeb"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:67a04754d121ea5ca39ddedc3f77071651fb5b0bc6b973c71c515415b44ed9c5"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bd66b4865c8b853b8cca7379afb692fc7f52cf898786537dfb5e5e2d64f0a47f"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4cc73a6ae0a6751b76e69cece9d0311f054da9b22df6a12f2c53111735657c8"}, - {file = "websockets-15.0-cp310-cp310-win32.whl", hash = "sha256:89da58e4005e153b03fe8b8794330e3f6a9774ee9e1c3bd5bc52eb098c3b0c4f"}, - {file = "websockets-15.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ff380aabd7a74a42a760ee76c68826a8f417ceb6ea415bd574a035a111fd133"}, - {file = "websockets-15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd24c4d256558429aeeb8d6c24ebad4e982ac52c50bc3670ae8646c181263965"}, - {file = "websockets-15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f83eca8cbfd168e424dfa3b3b5c955d6c281e8fc09feb9d870886ff8d03683c7"}, - {file = "websockets-15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4095a1f2093002c2208becf6f9a178b336b7572512ee0a1179731acb7788e8ad"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb915101dfbf318486364ce85662bb7b020840f68138014972c08331458d41f3"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45d464622314973d78f364689d5dbb9144e559f93dca11b11af3f2480b5034e1"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace960769d60037ca9625b4c578a6f28a14301bd2a1ff13bb00e824ac9f73e55"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd4b1015d2f60dfe539ee6c95bc968d5d5fad92ab01bb5501a77393da4f596"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f7290295794b5dec470867c7baa4a14182b9732603fd0caf2a5bf1dc3ccabf3"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3abd670ca7ce230d5a624fd3d55e055215d8d9b723adee0a348352f5d8d12ff4"}, - {file = "websockets-15.0-cp311-cp311-win32.whl", hash = "sha256:110a847085246ab8d4d119632145224d6b49e406c64f1bbeed45c6f05097b680"}, - {file = "websockets-15.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7bbbe2cd6ed80aceef2a14e9f1c1b61683194c216472ed5ff33b700e784e37"}, - {file = "websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f"}, - {file = "websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d"}, - {file = "websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc"}, - {file = "websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904"}, - {file = "websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa"}, - {file = "websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1"}, - {file = "websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7"}, - {file = "websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72"}, - {file = "websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c"}, - {file = "websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8"}, - {file = "websockets-15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c348abc5924caa02a62896300e32ea80a81521f91d6db2e853e6b1994017c9f6"}, - {file = "websockets-15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5294fcb410ed0a45d5d1cdedc4e51a60aab5b2b3193999028ea94afc2f554b05"}, - {file = "websockets-15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c24ba103ecf45861e2e1f933d40b2d93f5d52d8228870c3e7bf1299cd1cb8ff1"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc8821a03bcfb36e4e4705316f6b66af28450357af8a575dc8f4b09bf02a3dee"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc5ae23ada6515f31604f700009e2df90b091b67d463a8401c1d8a37f76c1d7"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac67b542505186b3bbdaffbc303292e1ee9c8729e5d5df243c1f20f4bb9057e"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c86dc2068f1c5ca2065aca34f257bbf4f78caf566eb230f692ad347da191f0a1"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:30cff3ef329682b6182c01c568f551481774c476722020b8f7d0daacbed07a17"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98dcf978d4c6048965d1762abd534c9d53bae981a035bfe486690ba11f49bbbb"}, - {file = "websockets-15.0-cp39-cp39-win32.whl", hash = "sha256:37d66646f929ae7c22c79bc73ec4074d6db45e6384500ee3e0d476daf55482a9"}, - {file = "websockets-15.0-cp39-cp39-win_amd64.whl", hash = "sha256:24d5333a9b2343330f0f4eb88546e2c32a7f5c280f8dd7d3cc079beb0901781b"}, - {file = "websockets-15.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b499caef4bca9cbd0bd23cd3386f5113ee7378094a3cb613a2fa543260fe9506"}, - {file = "websockets-15.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:17f2854c6bd9ee008c4b270f7010fe2da6c16eac5724a175e75010aacd905b31"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89f72524033abbfde880ad338fd3c2c16e31ae232323ebdfbc745cbb1b3dcc03"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1657a9eecb29d7838e3b415458cc494e6d1b194f7ac73a34aa55c6fb6c72d1f3"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e413352a921f5ad5d66f9e2869b977e88d5103fc528b6deb8423028a2befd842"}, - {file = "websockets-15.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8561c48b0090993e3b2a54db480cab1d23eb2c5735067213bb90f402806339f5"}, - {file = "websockets-15.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:190bc6ef8690cd88232a038d1b15714c258f79653abad62f7048249b09438af3"}, - {file = "websockets-15.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:327adab7671f3726b0ba69be9e865bba23b37a605b585e65895c428f6e47e766"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd8ef197c87afe0a9009f7a28b5dc613bfc585d329f80b7af404e766aa9e8c7"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789c43bf4a10cd067c24c321238e800b8b2716c863ddb2294d2fed886fa5a689"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7394c0b7d460569c9285fa089a429f58465db930012566c03046f9e3ab0ed181"}, - {file = "websockets-15.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ea4f210422b912ebe58ef0ad33088bc8e5c5ff9655a8822500690abc3b1232d"}, - {file = "websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3"}, - {file = "websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] [[package]] @@ -4021,99 +4017,121 @@ watchdog = ["watchdog"] [[package]] name = "yarl" -version = "1.18.3" +version = "1.20.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" files = [ - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, - {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, - {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, - {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, - {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, - {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, - {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, - {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, - {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, - {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, - {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, - {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, - {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, + {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, + {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, + {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, + {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, + {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, + {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, + {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, + {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, + {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, + {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, + {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, + {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, + {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, + {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" -propcache = ">=0.2.0" +propcache = ">=0.2.1" [[package]] name = "zipp" From 7ca1e16962167f0789b670dd074b858ee6159b4c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 15:59:37 +0200 Subject: [PATCH 087/173] fix: contracts --- .../contracts/dual_staking_token/contract.py | 2 -- .../dual_staking_token/contract.yaml | 2 +- .../contracts/l1_standard_bridge/contract.py | 26 +++++++++---------- .../l1_standard_bridge/contract.yaml | 2 +- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/operate/data/contracts/dual_staking_token/contract.py b/operate/data/contracts/dual_staking_token/contract.py index 85d0cd355..1ecc71b9e 100644 --- a/operate/data/contracts/dual_staking_token/contract.py +++ b/operate/data/contracts/dual_staking_token/contract.py @@ -19,8 +19,6 @@ """This module contains the class to connect to the `DualStakingToken` contract.""" -from enum import Enum - from aea.common import JSONLike from aea.configurations.base import PublicId from aea.contracts.base import Contract diff --git a/operate/data/contracts/dual_staking_token/contract.yaml b/operate/data/contracts/dual_staking_token/contract.yaml index a0813b239..1c2e70071 100644 --- a/operate/data/contracts/dual_staking_token/contract.yaml +++ b/operate/data/contracts/dual_staking_token/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeickx6ot3syhywbaewl3mmnowa75dd5rogqy7pmmc5ctk4yd74pvoa build/DualStakingToken.json: bafybeibvh3lcvo242jqtyrfrbezgziedlymjlr7bmttvezhgjjxlgklcxa - contract.py: bafybeidb5b57xzx2sbfc25hnq4f3pabg66rnygivnihjigcllylavk344y + contract.py: bafybeiftspqmbq23cjifektsgwtxfcf7uaeywwdbsdvp67a7yi3wqc3pnm fingerprint_ignore_patterns: [] contracts: [] class_name: DualStakingTokenContract diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index e2a0f06a3..d54ed116f 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -19,8 +19,6 @@ """This module contains the class to connect to the `L1StandardBridge` contract.""" -from enum import Enum - from aea.common import JSONLike from aea.configurations.base import PublicId from aea.contracts.base import Contract @@ -46,10 +44,11 @@ def build_bridge_eth_to_tx( ) -> JSONLike: """Build bridgeETHTo tx.""" contract_instance = cls.get_instance( - ledger_api=ledger_api, - contract_address=contract_address + ledger_api=ledger_api, contract_address=contract_address ) - tx = contract_instance.functions.bridgeETHTo(to, min_gas_limit, extra_data).build_transaction( + tx = contract_instance.functions.bridgeETHTo( + to, min_gas_limit, extra_data + ).build_transaction( { "from": sender, "value": amount, @@ -64,28 +63,29 @@ def build_bridge_eth_to_tx( ) @classmethod - def build_deposit_erc20_to_tx( + def build_bridge_erc20_to_tx( cls, ledger_api: LedgerApi, contract_address: str, sender: str, - l1_token: str, - l2_token: str, + local_token: str, + remote_token: str, to: str, amount: int, min_gas_limit: int, extra_data: bytes, raise_on_try: bool = False, ) -> JSONLike: - """Build depositERC20To tx.""" + """Build bridgeERC20To tx.""" contract_instance = cls.get_instance( - ledger_api=ledger_api, - contract_address=contract_address + ledger_api=ledger_api, contract_address=contract_address ) - tx = contract_instance.functions.depositERC20To(l1_token, l2_token, to, amount, min_gas_limit, extra_data).build_transaction( + tx = contract_instance.functions.bridgeERC20To( + local_token, remote_token, to, amount, min_gas_limit, extra_data + ).build_transaction( { "from": sender, - "gas": 1, + "gas": 1_000_000, "gasPrice": ledger_api.api.eth.gas_price, "nonce": ledger_api.api.eth.get_transaction_count(sender), } diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index 27561a18f..712b8aaac 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeiaekcbs6yjaft2jysvdxvrd7tknpkcnf6npmr3zgutlnss7gwgijq + contract.py: bafybeifirlhd3as7x7ot5gdgjlpjeo53z3qmhzuhkcixe2qu3sykfgxuta fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge From 00d36902901f24e1ba7716787ea66fe383631cfd Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 17:12:48 +0200 Subject: [PATCH 088/173] chore: fixed gas --- .../providers/native_bridge_provider.py | 77 +++++++++++-------- .../contracts/l1_standard_bridge/contract.py | 2 +- .../l1_standard_bridge/contract.yaml | 2 +- tests/test_bridge_providers.py | 6 +- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index d1ef9af15..fa0f73bf6 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -127,10 +127,13 @@ def quote(self, bridge_request: BridgeRequest) -> None: bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE - @staticmethod def _get_bridge_tx( - bridge_request: BridgeRequest, ledger_api: LedgerApi + self, bridge_request: BridgeRequest, ledger_api: LedgerApi ) -> t.Optional[t.Dict]: + self.logger.info( + f"[NATIVE BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." + ) + quote_data = bridge_request.quote_data if not quote_data: return None @@ -156,34 +159,29 @@ def _get_bridge_tx( extra_data=extra_data, ) else: - bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_deposit_erc20_to_tx( + bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_bridge_erc20_to_tx( ledger_api=ledger_api, contract_address=from_bridge, sender=from_address, - l1_token=from_token, - l2_token=to_token, + local_token=from_token, + remote_token=to_token, to=to_address, amount=int(to_amount), min_gas_limit=BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) - - # TODO: fix this, gas estimation fails. - bridge_tx["gas"] = 1200000 # TODO remove + self.logger.info(f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}.") ledger_api.update_with_gas_estimate(bridge_tx) - - # w3 = Web3(Web3.HTTPProvider("https://rpc-gate.autonolas.tech/ethereum-rpc/")) - # estimated_gas = w3.eth.estimate_gas(bridge_tx) - # print(f"Estimated gas: {estimated_gas}") - # from icecream import ic - # ic(bridge_tx) - + self.logger.info(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") return NativeBridgeProvider._update_with_gas_pricing(bridge_tx, ledger_api) - @staticmethod def _get_approve_tx( - bridge_request: BridgeRequest, ledger_api: LedgerApi + self, bridge_request: BridgeRequest, ledger_api: LedgerApi ) -> t.Optional[t.Dict]: + self.logger.info( + f"[NATIVE BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." + ) + quote_data = bridge_request.quote_data if not quote_data: return None @@ -205,8 +203,12 @@ def _get_approve_tx( sender=from_address, amount=to_amount, ) - + approve_tx["gas"] = 200_000 # TODO backport to ERC20 contract as default + self.logger.info( + f"[NATIVE BRIDGE] Gas before updating {approve_tx.get('gas')}." + ) ledger_api.update_with_gas_estimate(approve_tx) + self.logger.info(f"[NATIVE BRIDGE] Gas after updating {approve_tx.get('gas')}.") return NativeBridgeProvider._update_with_gas_pricing(approve_tx, ledger_api) def _get_transactions( @@ -265,6 +267,12 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"Cannot update bridge request {bridge_request.id}: execution data not present." ) + if execution_data.tx_status and any( + status == 0 for status in execution_data.tx_status + ): + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return + from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -299,7 +307,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: target_extra_data = Web3.keccak(text=bridge_request.id).hex() - starting_block = self.__find_starting_block(bridge_request) + starting_block = self.__find_starting_block_number(bridge_request) starting_block_ts = w3.eth.get_block(starting_block).timestamp latest_block = w3.eth.block_number @@ -320,12 +328,16 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: last_block_ts = w3.eth.get_block(to_block).timestamp if last_block_ts > starting_block_ts + duration * 2: - bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN + bridge_request.status = ( + BridgeRequestStatus.EXECUTION_FAILED + ) # TODO EXECUTION_UNKNOWN ? return except Exception as e: self.logger.error(f"Error updating execution status: {e}") - bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN + bridge_request.status = ( + BridgeRequestStatus.EXECUTION_FAILED + ) # TODO EXECUTION_UNKNOWN ? def __find_event_in_range( self, @@ -355,28 +367,21 @@ def __find_event_in_range( return False - def __find_starting_block(self, bridge_request: BridgeRequest) -> int: + def __find_starting_block_number(self, bridge_request: BridgeRequest) -> int: """Find the starting block for the event log search on the destination chain. - The starting block to search for the event log is the largest block on the - destination chain so that its timestamp is less than the timestamp of the - bridge transaction on the source chain. + The starting block to search for the event log is the block with the largest + block number on the destination chain so that its timestamp is less than the + timestamp of the bridge transaction on the source chain. """ self._validate(bridge_request) + # 1. Get timestamp of the transaction on the source chain from_chain = bridge_request.params["from"]["chain"] chain = Chain(from_chain) wallet = self.wallet_manager.load(chain.ledger_type) ledger_api = wallet.ledger_api(chain) w3_source = ledger_api.api - - to_chain = bridge_request.params["to"]["chain"] - chain = Chain(to_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - w3_dest = ledger_api.api - - # 1. Get timestamp of the transaction on the source chain tx = w3_source.eth.get_transaction_receipt( bridge_request.execution_data.tx_hashes[-1] ) @@ -384,6 +389,12 @@ def __find_starting_block(self, bridge_request: BridgeRequest) -> int: tx_timestamp = block.timestamp # 2. Binary search the destination chain for block just before this timestamp + to_chain = bridge_request.params["to"]["chain"] + chain = Chain(to_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + w3_dest = ledger_api.api + def find_block_before_timestamp(w3, timestamp: int) -> int: latest = w3.eth.block_number low, high = 0, latest diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index d54ed116f..b6641c07e 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -52,7 +52,7 @@ def build_bridge_eth_to_tx( { "from": sender, "value": amount, - "gas": 1, + "gas": 1_000_000, "gasPrice": ledger_api.api.eth.gas_price, "nonce": ledger_api.api.eth.get_transaction_count(sender), } diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index 712b8aaac..d532d239d 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeifirlhd3as7x7ot5gdgjlpjeo53z3qmhzuhkcixe2qu3sykfgxuta + contract.py: bafybeidnmjvtkozlw2hsf2bjwwykfvsu3mrcs4aelu7hw7pjco74nkhqbi fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 5143f89bf..36133961a 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -83,7 +83,7 @@ def test_bridge_zero( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id()) + bridge_request = BridgeRequest(params, bridge.id(), "test-id") assert not bridge_request.quote_data, "Unexpected quote data." @@ -213,7 +213,7 @@ def test_bridge_error( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id()) + bridge_request = BridgeRequest(params, bridge.id(), "test-id") assert not bridge_request.quote_data, "Unexpected quote data." @@ -346,7 +346,7 @@ def test_bridge_quote( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id()) + bridge_request = BridgeRequest(params, bridge.id(), "test-id") assert not bridge_request.quote_data, "Unexpected quote data." From 38eddcaabad5d0f26cd43b93047afd7d823400da Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 17:44:33 +0200 Subject: [PATCH 089/173] fix: update refill requirements --- operate/services/manage.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/operate/services/manage.py b/operate/services/manage.py index bdbb12ede..b28e2739a 100644 --- a/operate/services/manage.py +++ b/operate/services/manage.py @@ -2290,6 +2290,26 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, raise_on_invalid_address=False, ) + # TODO this is a patch for the case when balance is in MasterEOA (bridging) + if master_safe == "master_safe": + eoa_funding_values = self._get_master_eoa_native_funding_values( + master_safe_exists=master_safe_exists, + chain=Chain(chain), + balance=balances[chain][master_eoa][ZERO_ADDRESS], + ) + + for asset in balances[chain][master_safe]: + if asset == ZERO_ADDRESS: + balances[chain][master_safe][asset] = max( + balances[chain][master_eoa][asset] + - eoa_funding_values["topup"], + 0, + ) + else: + balances[chain][master_safe][asset] = balances[chain][ + master_eoa + ][asset] + # TODO this is a balances patch to count wrapped native asset as # native assets for the service safe if Chain(chain) in WRAPPED_NATIVE_ASSET: From 298e1976e9f226236e767414498a90f387ca5972 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 18:40:19 +0200 Subject: [PATCH 090/173] fix: linters --- .../providers/native_bridge_provider.py | 27 ++++++++++++++----- .../contracts/l1_standard_bridge/contract.py | 3 +++ .../l1_standard_bridge/contract.yaml | 2 +- tox.ini | 3 +++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index fa0f73bf6..fafe11939 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -37,15 +37,17 @@ ) from operate.constants import ZERO_ADDRESS from operate.data import DATA_DIR -from operate.data.contracts.l1_standard_bridge.contract import L1StandardBridge +from operate.data.contracts.l1_standard_bridge.contract import ( + DEFAULT_BRIDGE_MIN_GAS_LIMIT, + L1StandardBridge, +) from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge from operate.operate_types import Chain -BRIDGE_MIN_GAS_LIMIT = 300000 BLOCK_CHUNK_SIZE = 5000 -NATIVE_BRIDGE_ENDPOINTS = { +NATIVE_BRIDGE_ENDPOINTS: t.Dict[t.Any, t.Dict[str, t.Any]] = { (Chain.ETHEREUM.value, Chain.BASE.value): { "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "to_bridge": "0x4200000000000000000000000000000000000010", @@ -155,7 +157,7 @@ def _get_bridge_tx( sender=from_address, to=to_address, amount=int(to_amount), - min_gas_limit=BRIDGE_MIN_GAS_LIMIT, + min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) else: @@ -167,7 +169,7 @@ def _get_bridge_tx( remote_token=to_token, to=to_address, amount=int(to_amount), - min_gas_limit=BRIDGE_MIN_GAS_LIMIT, + min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) self.logger.info(f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}.") @@ -341,7 +343,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: def __find_event_in_range( self, - w3, + w3: Web3, contract_address: str, from_block: int, to_block: int, @@ -382,6 +384,17 @@ def __find_starting_block_number(self, bridge_request: BridgeRequest) -> int: wallet = self.wallet_manager.load(chain.ledger_type) ledger_api = wallet.ledger_api(chain) w3_source = ledger_api.api + + if not bridge_request.execution_data: + raise RuntimeError( + f"Error on bridge request {bridge_request.id}: execution data not present." + ) + + if not bridge_request.execution_data.tx_hashes: + raise RuntimeError( + f"Error on bridge request {bridge_request.id}: tx_hashes not present." + ) + tx = w3_source.eth.get_transaction_receipt( bridge_request.execution_data.tx_hashes[-1] ) @@ -395,7 +408,7 @@ def __find_starting_block_number(self, bridge_request: BridgeRequest) -> int: ledger_api = wallet.ledger_api(chain) w3_dest = ledger_api.api - def find_block_before_timestamp(w3, timestamp: int) -> int: + def find_block_before_timestamp(w3: Web3, timestamp: int) -> int: latest = w3.eth.block_number low, high = 0, latest best = 0 diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index b6641c07e..a6928e7b1 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -25,6 +25,9 @@ from aea.crypto.base import LedgerApi +DEFAULT_BRIDGE_MIN_GAS_LIMIT = 300000 + + class L1StandardBridge(Contract): """The Service Staking contract.""" diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index d532d239d..9301077bf 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeidnmjvtkozlw2hsf2bjwwykfvsu3mrcs4aelu7hw7pjco74nkhqbi + contract.py: bafybeifwcvdi2r7c6qdbpxqcvdb37sg5eawukoh4iqv7yamkkwhmhu4njq fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge diff --git a/tox.ini b/tox.ini index 7c18c03e8..c975e8e23 100644 --- a/tox.ini +++ b/tox.ini @@ -240,6 +240,9 @@ ignore_missing_imports = True [mypy-argon2.*] ignore_missing_imports = True +[mypy-eth_abi.*] +ignore_missing_imports = True + [testenv:unit-tests] deps = pytest==7.2.1 From 5b42ade16588137757ff6eb95bd084c7aa021919 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 7 May 2025 20:25:52 +0200 Subject: [PATCH 091/173] chore: add eta --- operate/bridge/providers/bridge_provider.py | 1 + operate/bridge/providers/lifi_bridge_provider.py | 4 ++++ operate/bridge/providers/native_bridge_provider.py | 10 +++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 5cdc8f29d..9c52f93ed 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -55,6 +55,7 @@ class QuoteData(LocalResource): """QuoteData""" attempts: int + bridge_eta: t.Optional[int] elapsed_time: float message: t.Optional[str] response: t.Optional[t.Dict] diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 9e1345f60..57c087739 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -94,6 +94,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") quote_data = QuoteData( attempts=0, + bridge_eta=None, elapsed_time=0, message=MESSAGE_QUOTE_ZERO, response=None, @@ -127,6 +128,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: response_json = response.json() quote_data = QuoteData( attempts=attempt, + bridge_eta=None, elapsed_time=time.time() - start, message=None, response=response_json, @@ -142,6 +144,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: ) quote_data = QuoteData( attempts=attempt, + bridge_eta=None, elapsed_time=time.time() - start, message=str(e), response=None, @@ -155,6 +158,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: response_json = response.json() quote_data = QuoteData( attempts=attempt, + bridge_eta=None, elapsed_time=time.time() - start, message=response_json.get("message") or str(e), response=response_json, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index fafe11939..cb40458d9 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -51,7 +51,7 @@ (Chain.ETHEREUM.value, Chain.BASE.value): { "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "to_bridge": "0x4200000000000000000000000000000000000010", - "duration": 5 * 60, + "bridge_eta": 5 * 60, } } @@ -111,7 +111,10 @@ def quote(self, bridge_request: BridgeRequest) -> None: f"Cannot quote bridge request {bridge_request.id}: execution already present." ) + from_chain = bridge_request.params["from"]["chain"] + to_chain = bridge_request.params["to"]["chain"] to_amount = int(bridge_request.params["to"]["amount"]) + bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"] message = None if to_amount == 0: @@ -120,6 +123,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: quote_data = QuoteData( attempts=0, + bridge_eta=bridge_eta, elapsed_time=0, message=message, response=None, @@ -283,7 +287,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: to_address = bridge_request.params["to"]["address"] to_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["to_bridge"] - duration = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["duration"]) + bridge_eta = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"]) try: chain = Chain(to_chain) @@ -329,7 +333,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: return last_block_ts = w3.eth.get_block(to_block).timestamp - if last_block_ts > starting_block_ts + duration * 2: + if last_block_ts > starting_block_ts + bridge_eta * 2: bridge_request.status = ( BridgeRequestStatus.EXECUTION_FAILED ) # TODO EXECUTION_UNKNOWN ? From e84449d7a3423a305f5127e54dd915b953fba112 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 8 May 2025 13:43:48 +0200 Subject: [PATCH 092/173] chore: add total_requirements --- .../providers/native_bridge_provider.py | 11 +++++---- operate/cli.py | 3 +++ operate/services/manage.py | 23 ++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index cb40458d9..1bd3fec16 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -255,10 +255,6 @@ def _get_transactions( def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" - self.logger.info( - f"[NATIVE BRIDGE] Updating execution status for bridge request {bridge_request.id}." - ) - self._validate(bridge_request) if bridge_request.status not in ( @@ -267,6 +263,10 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ): return + self.logger.info( + f"[NATIVE BRIDGE] Updating execution status for bridge request {bridge_request.id}." + ) + execution_data = bridge_request.execution_data if not execution_data: raise RuntimeError( @@ -329,6 +329,9 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: target_extra_data, ) if event_found: + self.logger.info( + f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." + ) bridge_request.status = BridgeRequestStatus.EXECUTION_DONE return diff --git a/operate/cli.py b/operate/cli.py index 56db8f660..8c0766d65 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -611,6 +611,9 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: ) data = await request.json() + + logger.info(f"POST /api/wallet/safe {data=}") + chain = Chain(data["chain"]) ledger_type = chain.ledger_type manager = operate.wallet_manager diff --git a/operate/services/manage.py b/operate/services/manage.py index b28e2739a..e6d28f190 100644 --- a/operate/services/manage.py +++ b/operate/services/manage.py @@ -2239,6 +2239,7 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, bonded_assets: t.Dict = {} protocol_asset_requirements: t.Dict = {} refill_requirements: t.Dict = {} + total_requirements: t.Dict = {} allow_start_agent = True is_refill_required = False @@ -2290,7 +2291,8 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, raise_on_invalid_address=False, ) - # TODO this is a patch for the case when balance is in MasterEOA (bridging) + # TODO this is a patch for the case when excess balance is in MasterEOA + # and MasterSafe is not created (typically for onboarding bridging) if master_safe == "master_safe": eoa_funding_values = self._get_master_eoa_native_funding_values( master_safe_exists=master_safe_exists, @@ -2305,10 +2307,12 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, - eoa_funding_values["topup"], 0, ) + # This line would keep the sum of balances constant, but then it will not be able to transfer the correct amount to MasterSafe: balances[chain][master_eoa][asset] = min(balances[chain][master_eoa][asset], eoa_funding_values["topup"]) else: balances[chain][master_safe][asset] = balances[chain][ master_eoa ][asset] + # This line would keep the sum of balances constant, but then it will not be able to transfer the correct amount to MasterSafe: balances[chain][master_eoa][asset] = 0 # TODO this is a balances patch to count wrapped native asset as # native assets for the service safe @@ -2323,6 +2327,7 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, # Refill requirements refill_requirements[chain] = {} + total_requirements[chain] = {} # Refill requirements for Master Safe for asset_address in ( @@ -2366,6 +2371,17 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, asset_address ] = recommended_refill + total_requirements[chain].setdefault(master_safe, {})[ + asset_address + ] = sum( + agent_asset_funding_values[address]["topup"] + for address in agent_asset_funding_values + ) + protocol_asset_requirements[ + chain + ].get( + asset_address, 0 + ) + if asset_address == ZERO_ADDRESS and any( balances[chain][master_safe][asset_address] == 0 and balances[chain][address][asset_address] == 0 @@ -2392,6 +2408,10 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, ZERO_ADDRESS ] = eoa_recommended_refill + total_requirements[chain].setdefault(master_eoa, {})[ + ZERO_ADDRESS + ] = eoa_funding_values["topup"] + is_refill_required = any( amount > 0 for chain in refill_requirements.values() @@ -2402,6 +2422,7 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, return { "balances": balances, "bonded_assets": bonded_assets, + "total_requirements": total_requirements, "refill_requirements": refill_requirements, "protocol_asset_requirements": protocol_asset_requirements, "is_refill_required": is_refill_required, From 814bcd33ec8a6414e2397c324c5e5c00dc59bfed Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 9 May 2025 10:34:10 +0200 Subject: [PATCH 093/173] chore: validate amount as nonnegative integer in bridge providers --- operate/bridge/bridge.py | 1 + operate/bridge/providers/bridge_provider.py | 15 +- .../bridge/providers/lifi_bridge_provider.py | 2 +- .../providers/native_bridge_provider.py | 19 +- tests/test_bridge_providers.py | 370 ++++++++++++++++++ 5 files changed, 402 insertions(+), 5 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 343bce06c..7f4e22663 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -207,6 +207,7 @@ def _raise_if_invalid(self, requests_params: t.List) -> None: for params in requests_params: from_chain = params["from"]["chain"] from_address = params["from"]["address"] + params["to"]["amount"] = int(params["to"]["amount"]) wallet = self.wallet_manager.load(Chain(from_chain).ledger_type) wallet_address = wallet.address diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 9c52f93ed..b2acaf935 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -170,6 +170,15 @@ def create_request(self, params: t.Dict) -> BridgeRequest: "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) + amount = to["amount"] + if ( + not isinstance(amount, int) + or amount < 0 + ): + raise ValueError( + "Invalid input: 'amount' must be a nonnegative integer." + ) + return BridgeRequest( params=params, bridge_provider_id=self.id(), @@ -214,13 +223,17 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: total_native = 0 total_token = 0 - for _, tx in transactions: + for tx_label, tx in transactions: tx = self._update_with_gas_pricing(tx, ledger_api) gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" gas_fees = tx.get(gas_key, 0) * tx["gas"] tx_value = int(tx.get("value", 0)) total_native += tx_value + gas_fees + self.logger.info(f"[BRIDGE PROVIDER] Transaction {tx_label}: {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}") + self.logger.info(f"[BRIDGE PROVIDER] {ledger_api.api.eth.gas_price=}") + self.logger.info(f"[BRIDGE PROVIDER] {ledger_api.api.eth.get_block('latest').baseFeePerGas=}") + if tx.get("to", "").lower() == from_token.lower() and tx.get( "data", "" ).startswith( diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 57c087739..f2a9f3edc 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -88,7 +88,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] - to_amount = int(bridge_request.params["to"]["amount"]) + to_amount = bridge_request.params["to"]["amount"] if to_amount == 0: self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 1bd3fec16..ad49bd8f6 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -79,6 +79,16 @@ class NativeBridgeProvider(BridgeProvider): """Native bridge provider.""" + def _validate(self, bridge_request: BridgeRequest) -> None: + """Validate the bridge request.""" + from_chain = bridge_request.params["from"]["chain"] + to_chain = bridge_request.params["to"]["chain"] + + if (from_chain, to_chain) not in NATIVE_BRIDGE_ENDPOINTS: + raise ValueError(f"Unsupported bridge from {from_chain} to {to_chain}.") + + super()._validate(bridge_request) + def create_request(self, params: t.Dict) -> BridgeRequest: """Create a bridge request.""" from_chain = params["from"]["chain"] @@ -113,7 +123,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] - to_amount = int(bridge_request.params["to"]["amount"]) + to_amount = bridge_request.params["to"]["amount"] bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"] message = None @@ -150,7 +160,7 @@ def _get_bridge_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] - to_amount = int(bridge_request.params["to"]["amount"]) + to_amount = bridge_request.params["to"]["amount"] from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] extra_data = Web3.keccak(text=bridge_request.id) @@ -196,7 +206,7 @@ def _get_approve_tx( from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - to_amount = int(bridge_request.params["to"]["amount"]) + to_amount = bridge_request.params["to"]["amount"] from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] if from_token == ZERO_ADDRESS: @@ -230,6 +240,9 @@ def _get_transactions( if not bridge_request.quote_data: return [] + if bridge_request.params["to"]["amount"] == 0: + return [] + from_chain = bridge_request.params["from"]["chain"] chain = Chain(from_chain) wallet = self.wallet_manager.load(chain.ledger_type) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 36133961a..4097769b3 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -24,6 +24,7 @@ import time from pathlib import Path +from operate.bridge.providers.native_bridge_provider import NativeBridgeProvider import pytest from deepdiff import DeepDiff @@ -417,3 +418,372 @@ def test_bridge_quote( status2 = bridge_request.status assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + +class TestNativeBridge: + """Tests for bridge.providers.NativeBridgeProvider class.""" + + def test_bridge_zero( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_zero""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": Chain.ETHEREUM.value, + "address": wallet_address, + "token": OLAS[Chain.ETHEREUM], + }, + "to": { + "chain": Chain.BASE.value, + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 0, + }, + } + + bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = bridge.create_request(params) + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts == 0, "Wrong quote data." + assert qd.elapsed_time == 0, "Wrong quote data." + assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." + assert qd.response is None, "Wrong quote data." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_DONE + ), "Wrong status." + + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + assert bridge_request.quote_data is not None, "Missing quote data." + + br = bridge.bridge_requirements(bridge_request) + expected_br = { + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts == 0, "Wrong quote data." + assert qd.elapsed_time == 0, "Wrong quote data." + assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." + assert qd.response is None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + timestamp = int(time.time()) + bridge.execute(bridge_request=bridge_request) + ed = bridge_request.execution_data + assert ed is not None, "Missing execution data." + assert ed.bridge_status is None, "Wrong execution data." + assert ed.elapsed_time == 0, "Wrong execution data." + assert ed.message is not None, "Wrong execution data." + assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." + assert timestamp <= ed.timestamp, "Wrong quote data." + assert ed.timestamp <= int(time.time()), "Wrong quote data." + assert ed.tx_hashes is None, "Wrong execution data." + assert ed.tx_status is None, "Wrong execution data." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong status." + + bridge._update_execution_status(bridge_request) + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong status." + + sj = bridge.get_status_json(bridge_request) + assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." + expected_sj = { + "explorer_link": sj["explorer_link"], + "tx_hash": None, # type: ignore + "message": sj["message"], + "status": BridgeRequestStatus.EXECUTION_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + def test_bridge_error( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_error""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1, # This will cause a quote error + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params, bridge.id(), "test-id") + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is not None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_FAILED + ), "Wrong status." + + assert bridge_request.quote_data is not None, "Wrong quote data." + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": bridge_request.quote_data.message, + "status": BridgeRequestStatus.QUOTE_FAILED.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + br = bridge.bridge_requirements(bridge_request) + expected_br = { + "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is not None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_FAILED + ), "Wrong status." + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + + timestamp = int(time.time()) + bridge.execute(bridge_request=bridge_request) + ed = bridge_request.execution_data + assert ed is not None, "Missing execution data." + assert ed.bridge_status is None, "Wrong execution data." + assert ed.elapsed_time == 0, "Wrong execution data." + assert ed.message is not None, "Wrong execution data." + assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." + assert timestamp <= ed.timestamp, "Wrong quote data." + assert ed.timestamp <= int(time.time()), "Wrong quote data." + assert ed.tx_hashes is None, "Wrong execution data." + assert ed.tx_status is None, "Wrong execution data." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong status." + + bridge._update_execution_status(bridge_request) + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong status." + + sj = bridge.get_status_json(bridge_request) + assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." + expected_sj = { + "explorer_link": sj["explorer_link"], + "tx_hash": None, + "message": sj["message"], + "status": BridgeRequestStatus.EXECUTION_FAILED.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + + @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") + def test_bridge_quote( + self, + tmp_path: Path, + password: str, + ) -> None: + """test_bridge_quote""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = { + "from": { + "chain": "gnosis", + "address": wallet_address, + "token": OLAS[Chain.GNOSIS], + }, + "to": { + "chain": "base", + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1_000_000_000_000_000_000, + }, + } + + bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge_request = BridgeRequest(params, bridge.id(), "test-id") + + assert not bridge_request.quote_data, "Unexpected quote data." + + with pytest.raises(RuntimeError): + bridge.execute(bridge_request) + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.CREATED, "Wrong status." + assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + for _ in range(2): + timestamp = int(time.time()) + bridge.quote(bridge_request=bridge_request) + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert timestamp <= qd.timestamp, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert ( + bridge_request.status == BridgeRequestStatus.QUOTE_DONE + ), "Wrong status." + + assert bridge_request.quote_data is not None, "Wrong quote data." + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": bridge_request.quote_data.message, + "status": BridgeRequestStatus.QUOTE_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) + + assert not diff, "Wrong status." + assert bridge_request.quote_data.response is not None, "Missing quote data." + + quote = bridge_request.quote_data.response + br = bridge.bridge_requirements(bridge_request) + expected_br = { + "gnosis": { + wallet_address: { + ZERO_ADDRESS: br["gnosis"][wallet_address][ZERO_ADDRESS], + OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore + } + } + } + diff = DeepDiff(br, expected_br) + if diff: + print(diff) + + assert not diff, "Wrong bridge requirements." + + qd = bridge_request.quote_data + assert qd is not None, "Missing quote data." + assert qd.attempts > 0, "Wrong quote data." + assert qd.elapsed_time > 0, "Wrong quote data." + assert qd.message is None, "Wrong quote data." + assert qd.response is not None, "Wrong quote data." + assert qd.timestamp <= int(time.time()), "Wrong quote data." + assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + status1 = bridge_request.status + bridge._update_execution_status(bridge_request) + status2 = bridge_request.status + assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." From d2bece2337ba0f993d521716c4842085a7417154 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 9 May 2025 13:03:40 +0200 Subject: [PATCH 094/173] feat: add flag transfer_excess_assets --- operate/cli.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index 8c0766d65..ed679a5b2 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -44,8 +44,8 @@ from operate import services from operate.account.user import UserAccount from operate.bridge.bridge import BridgeManager -from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES -from operate.ledger.profiles import DEFAULT_NEW_SAFE_FUNDS_AMOUNT +from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES, ZERO_ADDRESS +from operate.ledger.profiles import DEFAULT_NEW_SAFE_FUNDS_AMOUNT, OLAS, USDC from operate.migration import MigrationManager from operate.operate_types import Chain, DeploymentStatus, LedgerType from operate.quickstart.analyse_logs import analyse_logs @@ -56,6 +56,8 @@ from operate.quickstart.stop_service import stop_service from operate.quickstart.terminate_on_chain_service import terminate_service from operate.services.health_checker import HealthChecker +from operate.services.manage import ServiceManager +from operate.utils.gnosis import get_assets_balances from operate.wallet.master import MasterWalletManager @@ -642,8 +644,34 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: ) safe_address = t.cast(str, safes.get(chain)) + + transfer_excess_assets = ( + str(data.get("transfer_excess_assets", "false")).lower() == "true" + ) initial_funds = data.get("initial_funds", DEFAULT_NEW_SAFE_FUNDS_AMOUNT[chain]) + if transfer_excess_assets: + balances = get_assets_balances( + ledger_api=ledger_api, + addresses=[wallet.address], + asset_addresses=[ZERO_ADDRESS, OLAS[Chain(chain)], USDC[Chain(chain)]], + raise_on_invalid_address=False, + )[wallet.address] + + initial_funds = {} + keep_funds = { + ZERO_ADDRESS: ServiceManager._get_master_eoa_native_funding_values( + master_safe_exists=False, + chain=Chain(chain), + balance=balances.get(ZERO_ADDRESS, 0), + )["topup"] + } + for asset, balance in balances.items(): + if balance > 0: + initial_funds[asset] = max(balance - keep_funds.get(asset, 0), 0) + + logger.info(f"POST /api/wallet/safe Computed {initial_funds=}") + transfer_txs = {} for asset, amount in initial_funds.items(): tx_hash = wallet.transfer_asset( From 416b3c6fa691895843f0ddf206510bcba74a9446 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 10 May 2025 11:51:02 +0200 Subject: [PATCH 095/173] chore: update --- operate/bridge/bridge.py | 1 - operate/bridge/providers/bridge_provider.py | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 7f4e22663..343bce06c 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -207,7 +207,6 @@ def _raise_if_invalid(self, requests_params: t.List) -> None: for params in requests_params: from_chain = params["from"]["chain"] from_address = params["from"]["address"] - params["to"]["amount"] = int(params["to"]["amount"]) wallet = self.wallet_manager.load(Chain(from_chain).ledger_type) wallet_address = wallet.address diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index b2acaf935..ba08ddd0d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -170,14 +170,7 @@ def create_request(self, params: t.Dict) -> BridgeRequest: "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) - amount = to["amount"] - if ( - not isinstance(amount, int) - or amount < 0 - ): - raise ValueError( - "Invalid input: 'amount' must be a nonnegative integer." - ) + to["amount"] = int(to["amount"]) return BridgeRequest( params=params, From 4e7dfa7a8c391cc9e494db8ee45e3ea752679f6f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 10 May 2025 19:01:07 +0200 Subject: [PATCH 096/173] chore: ledger_api field --- operate/bridge/providers/bridge_provider.py | 41 ++++++++++++------- .../providers/native_bridge_provider.py | 29 ++++--------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index ba08ddd0d..658a2e2ba 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -20,6 +20,7 @@ """Bridge provider.""" +import copy import enum import logging import time @@ -170,7 +171,8 @@ def create_request(self, params: t.Dict) -> BridgeRequest: "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) - to["amount"] = int(to["amount"]) + params = copy.deepcopy(params) + params["to"]["amount"] = int(params["to"]["amount"]) return BridgeRequest( params=params, @@ -178,6 +180,22 @@ def create_request(self, params: t.Dict) -> BridgeRequest: id=f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", ) + def _from_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: + """Get the from ledger api.""" + from_chain = bridge_request.params["from"]["chain"] + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + return ledger_api + + def _to_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: + """Get the from ledger api.""" + from_chain = bridge_request.params["from"]["chain"] + chain = Chain(from_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + ledger_api = wallet.ledger_api(chain) + return ledger_api + @abstractmethod def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" @@ -196,11 +214,8 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] - from_token = bridge_request.params["from"]["token"] - - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) + from_token = .params["from"]["token"] + from_ledger_api = self._from_ledger_api(bridge_request) transactions = self._get_transactions(bridge_request) if not transactions: @@ -217,15 +232,15 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: total_token = 0 for tx_label, tx in transactions: - tx = self._update_with_gas_pricing(tx, ledger_api) + tx = self._update_with_gas_pricing(tx, from_ledger_api) gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" gas_fees = tx.get(gas_key, 0) * tx["gas"] tx_value = int(tx.get("value", 0)) total_native += tx_value + gas_fees self.logger.info(f"[BRIDGE PROVIDER] Transaction {tx_label}: {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}") - self.logger.info(f"[BRIDGE PROVIDER] {ledger_api.api.eth.gas_price=}") - self.logger.info(f"[BRIDGE PROVIDER] {ledger_api.api.eth.get_block('latest').baseFeePerGas=}") + self.logger.info(f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.gas_price=}") + self.logger.info(f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.get_block('latest').baseFeePerGas=}") if tx.get("to", "").lower() == from_token.lower() and tx.get( "data", "" @@ -296,14 +311,12 @@ def execute(self, bridge_request: BridgeRequest) -> None: try: self.logger.info(f"[BRIDGE] Executing bridge request {bridge_request.id}.") - - chain = Chain(bridge_request.params["from"]["chain"]) wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) + from_ledger_api = self._from_ledger_api(bridge_request) tx_settler = TxSettler( - ledger_api=ledger_api, + ledger_api=from_ledger_api, crypto=wallet.crypto, - chain_type=chain, + chain_type=Chain(bridge_request.params["from"]["chain"]), timeout=ON_CHAIN_INTERACT_TIMEOUT, retries=ON_CHAIN_INTERACT_RETRIES, sleep=ON_CHAIN_INTERACT_SLEEP, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index ad49bd8f6..8e675cbac 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -243,17 +243,14 @@ def _get_transactions( if bridge_request.params["to"]["amount"] == 0: return [] - from_chain = bridge_request.params["from"]["chain"] - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) + from_ledger_api = self._from_ledger_api(bridge_request) - bridge_tx = self._get_bridge_tx(bridge_request, ledger_api) + bridge_tx = self._get_bridge_tx(bridge_request, from_ledger_api) if not bridge_tx: return [] - approve_tx = self._get_approve_tx(bridge_request, ledger_api) + approve_tx = self._get_approve_tx(bridge_request, from_ledger_api) if approve_tx: bridge_tx["nonce"] = approve_tx["nonce"] + 1 @@ -303,10 +300,8 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_eta = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"]) try: - chain = Chain(to_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - w3 = ledger_api.api + to_ledger_api = self._to_ledger_api(bridge_request) + w3 = to_ledger_api.api if from_token == ZERO_ADDRESS: topics = [ @@ -399,11 +394,8 @@ def __find_starting_block_number(self, bridge_request: BridgeRequest) -> int: self._validate(bridge_request) # 1. Get timestamp of the transaction on the source chain - from_chain = bridge_request.params["from"]["chain"] - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - w3_source = ledger_api.api + to_ledger_api = self._from_ledger_api(bridge_request) + w3_source = to_ledger_api.api if not bridge_request.execution_data: raise RuntimeError( @@ -422,11 +414,8 @@ def __find_starting_block_number(self, bridge_request: BridgeRequest) -> int: tx_timestamp = block.timestamp # 2. Binary search the destination chain for block just before this timestamp - to_chain = bridge_request.params["to"]["chain"] - chain = Chain(to_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) - w3_dest = ledger_api.api + to_ledger_api = self._to_ledger_api(bridge_request) + w3_dest = to_ledger_api.api def find_block_before_timestamp(w3: Web3, timestamp: int) -> int: latest = w3.eth.block_number From e1055015c701cb45b0cbc2b643b28cdaef9d13e0 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 11 May 2025 12:31:45 +0200 Subject: [PATCH 097/173] chore: update --- operate/bridge/providers/bridge_provider.py | 13 +- .../providers/native_bridge_provider.py | 118 +++++----- operate/cli.py | 2 +- operate/services/manage.py | 7 +- tests/test_bridge_providers.py | 213 +++++++++++++++++- tests/test_operate_cli.py | 4 +- 6 files changed, 270 insertions(+), 87 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 658a2e2ba..bcfe29ed1 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -190,7 +190,7 @@ def _from_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: def _to_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: """Get the from ledger api.""" - from_chain = bridge_request.params["from"]["chain"] + from_chain = bridge_request.params["to"]["chain"] chain = Chain(from_chain) wallet = self.wallet_manager.load(chain.ledger_type) ledger_api = wallet.ledger_api(chain) @@ -214,7 +214,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] - from_token = .params["from"]["token"] + from_token = bridge_request.params["from"]["token"] from_ledger_api = self._from_ledger_api(bridge_request) transactions = self._get_transactions(bridge_request) @@ -238,9 +238,13 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: tx_value = int(tx.get("value", 0)) total_native += tx_value + gas_fees - self.logger.info(f"[BRIDGE PROVIDER] Transaction {tx_label}: {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}") + self.logger.info( + f"[BRIDGE PROVIDER] Transaction {tx_label}: {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}" + ) self.logger.info(f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.gas_price=}") - self.logger.info(f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.get_block('latest').baseFeePerGas=}") + self.logger.info( + f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.get_block('latest').baseFeePerGas=}" + ) if tx.get("to", "").lower() == from_token.lower() and tx.get( "data", "" @@ -311,6 +315,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: try: self.logger.info(f"[BRIDGE] Executing bridge request {bridge_request.id}.") + chain = Chain(bridge_request.params["from"]["chain"]) wallet = self.wallet_manager.load(chain.ledger_type) from_ledger_api = self._from_ledger_api(bridge_request) tx_settler = TxSettler( diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 8e675cbac..59fbfd194 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -293,48 +293,67 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - to_token = bridge_request.params["to"]["token"] to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] to_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["to_bridge"] bridge_eta = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"]) try: - to_ledger_api = self._to_ledger_api(bridge_request) - w3 = to_ledger_api.api + # Get the timestamp of the bridge_tx on the 'from' chain + from_ledger_api = self._from_ledger_api(bridge_request) + bridge_tx_receipt = from_ledger_api.api.eth.get_transaction_receipt( + bridge_request.execution_data.tx_hashes[-1] + ) + bridge_tx_block = from_ledger_api.api.eth.get_block( + bridge_tx_receipt.blockNumber + ) + bridge_tx_ts = bridge_tx_block.timestamp + # Prepare the event data if from_token == ZERO_ADDRESS: topics = [ - ETH_BRIDGE_FINALIZED_TOPIC0, + ETH_BRIDGE_FINALIZED_TOPIC0, # ETHBridgeFinalized "0x" + from_address.lower()[2:].rjust(64, "0"), # from "0x" + to_address.lower()[2:].rjust(64, "0"), # from ] non_indexed_types = ["uint256", "bytes"] + non_indexed_values = [ + to_amount, # amount + Web3.keccak(text=bridge_request.id), # extraData + ] else: topics = [ - ERC20_BRIDGE_FINALIZED_TOPIC0, + ERC20_BRIDGE_FINALIZED_TOPIC0, # ERC20BridgeFinalized "0x" + to_token.lower()[2:].rjust(64, "0"), # localToken "0x" + from_token.lower()[2:].rjust(64, "0"), # remoteToken "0x" + from_address.lower()[2:].rjust(64, "0"), # from ] non_indexed_types = ["address", "uint256", "bytes"] + non_indexed_values = [ + to_address.lower(), # to + to_amount, # amount + Web3.keccak(text=bridge_request.id), # extraData + ] - target_extra_data = Web3.keccak(text=bridge_request.id).hex() - - starting_block = self.__find_starting_block_number(bridge_request) + # Find the event on the 'to' chain + to_ledger_api = self._to_ledger_api(bridge_request) + w3 = to_ledger_api.api + starting_block = self._find_block_before_timestamp(w3, bridge_tx_ts) starting_block_ts = w3.eth.get_block(starting_block).timestamp latest_block = w3.eth.block_number for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) - event_found = self.__find_event_in_range( + event_found = self._is_event_in_range( w3, to_bridge, from_block, to_block, topics, non_indexed_types, - target_extra_data, + non_indexed_values, ) if event_found: self.logger.info( @@ -345,9 +364,10 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: last_block_ts = w3.eth.get_block(to_block).timestamp if last_block_ts > starting_block_ts + bridge_eta * 2: - bridge_request.status = ( - BridgeRequestStatus.EXECUTION_FAILED - ) # TODO EXECUTION_UNKNOWN ? + self.logger.info( + f"[NATIVE BRIDGE] Execution failed for {bridge_request.id}: bridge exceeds 2*ETA." + ) + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return except Exception as e: @@ -356,15 +376,15 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: BridgeRequestStatus.EXECUTION_FAILED ) # TODO EXECUTION_UNKNOWN ? - def __find_event_in_range( - self, + @staticmethod + def _is_event_in_range( w3: Web3, contract_address: str, from_block: int, to_block: int, topics: list[str], non_indexed_types: list[str], - target_extra_data: str, + non_indexed_values: list[t.Any], ) -> bool: """Check for a finalized bridge event in the given block range.""" logs = w3.eth.get_logs( @@ -378,60 +398,26 @@ def __find_event_in_range( for log in logs: decoded = eth_abi.decode(non_indexed_types, log["data"]) - extra_data = "0x" + decoded[-1].hex() - if extra_data.lower() == target_extra_data.lower(): + if all(a == b for a, b in zip(decoded, non_indexed_values)): return True return False - def __find_starting_block_number(self, bridge_request: BridgeRequest) -> int: - """Find the starting block for the event log search on the destination chain. - - The starting block to search for the event log is the block with the largest - block number on the destination chain so that its timestamp is less than the - timestamp of the bridge transaction on the source chain. - """ - self._validate(bridge_request) - - # 1. Get timestamp of the transaction on the source chain - to_ledger_api = self._from_ledger_api(bridge_request) - w3_source = to_ledger_api.api - - if not bridge_request.execution_data: - raise RuntimeError( - f"Error on bridge request {bridge_request.id}: execution data not present." - ) - - if not bridge_request.execution_data.tx_hashes: - raise RuntimeError( - f"Error on bridge request {bridge_request.id}: tx_hashes not present." - ) - - tx = w3_source.eth.get_transaction_receipt( - bridge_request.execution_data.tx_hashes[-1] - ) - block = w3_source.eth.get_block(tx.blockNumber) - tx_timestamp = block.timestamp - - # 2. Binary search the destination chain for block just before this timestamp - to_ledger_api = self._to_ledger_api(bridge_request) - w3_dest = to_ledger_api.api - - def find_block_before_timestamp(w3: Web3, timestamp: int) -> int: - latest = w3.eth.block_number - low, high = 0, latest - best = 0 - while low <= high: - mid = (low + high) // 2 - block = w3.eth.get_block(mid) - if block.timestamp < timestamp: - best = mid - low = mid + 1 - else: - high = mid - 1 - return best - - return find_block_before_timestamp(w3_dest, tx_timestamp) - 1 + @staticmethod + def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: + """Returns the largest block number of the block before `timestamp`.""" + latest = w3.eth.block_number + low, high = 0, latest + best = 0 + while low <= high: + mid = (low + high) // 2 + block = w3.eth.get_block(mid) + if block.timestamp < timestamp: + best = mid + low = mid + 1 + else: + high = mid - 1 + return best def _get_explorer_link(self, tx_hash: str) -> str: """Get the explorer link for a transaction.""" diff --git a/operate/cli.py b/operate/cli.py index ed679a5b2..55ae0735a 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -660,7 +660,7 @@ async def _create_safe(request: Request) -> t.List[t.Dict]: initial_funds = {} keep_funds = { - ZERO_ADDRESS: ServiceManager._get_master_eoa_native_funding_values( + ZERO_ADDRESS: ServiceManager.get_master_eoa_native_funding_values( master_safe_exists=False, chain=Chain(chain), balance=balances.get(ZERO_ADDRESS, 0), diff --git a/operate/services/manage.py b/operate/services/manage.py index e6d28f190..403690e03 100644 --- a/operate/services/manage.py +++ b/operate/services/manage.py @@ -2294,7 +2294,7 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, # TODO this is a patch for the case when excess balance is in MasterEOA # and MasterSafe is not created (typically for onboarding bridging) if master_safe == "master_safe": - eoa_funding_values = self._get_master_eoa_native_funding_values( + eoa_funding_values = self.get_master_eoa_native_funding_values( master_safe_exists=master_safe_exists, chain=Chain(chain), balance=balances[chain][master_eoa][ZERO_ADDRESS], @@ -2391,7 +2391,7 @@ def refill_requirements( # pylint: disable=too-many-locals,too-many-statements, allow_start_agent = False # Refill requirements for Master EOA - eoa_funding_values = self._get_master_eoa_native_funding_values( + eoa_funding_values = self.get_master_eoa_native_funding_values( master_safe_exists=master_safe_exists, chain=Chain(chain), balance=balances[chain][master_eoa][ZERO_ADDRESS], @@ -2664,9 +2664,10 @@ def _compute_refill_requirement( } @staticmethod - def _get_master_eoa_native_funding_values( + def get_master_eoa_native_funding_values( master_safe_exists: bool, chain: Chain, balance: int ) -> t.Dict: + """Get Master EOA native funding values.""" funding_values = { True: { Chain.ETHEREUM: { diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 4097769b3..e069c17de 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -24,9 +24,9 @@ import time from pathlib import Path -from operate.bridge.providers.native_bridge_provider import NativeBridgeProvider import pytest from deepdiff import DeepDiff +from web3 import Web3 from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, @@ -34,11 +34,18 @@ ) from operate.bridge.providers.bridge_provider import ( BridgeRequestStatus, + ExecutionData, MESSAGE_EXECUTION_SKIPPED, MESSAGE_QUOTE_ZERO, + QuoteData, +) +from operate.bridge.providers.native_bridge_provider import ( + NATIVE_BRIDGE_ENDPOINTS, + NativeBridgeProvider, ) from operate.cli import OperateApp from operate.constants import ZERO_ADDRESS +from operate.ledger import DEFAULT_RPCS from operate.ledger.profiles import OLAS from operate.operate_types import Chain, LedgerType @@ -103,6 +110,7 @@ def test_bridge_zero( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts == 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.response is None, "Wrong quote data." @@ -137,6 +145,7 @@ def test_bridge_zero( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts == 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.response is None, "Wrong quote data." @@ -233,6 +242,7 @@ def test_bridge_error( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -267,6 +277,7 @@ def test_bridge_error( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -366,6 +377,7 @@ def test_bridge_quote( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -407,6 +419,7 @@ def test_bridge_quote( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -423,6 +436,8 @@ def test_bridge_quote( class TestNativeBridge: """Tests for bridge.providers.NativeBridgeProvider class.""" + # TODO: test existing executions: failed and done + def test_bridge_zero( self, tmp_path: Path, @@ -448,23 +463,43 @@ def test_bridge_zero( "chain": Chain.BASE.value, "address": wallet_address, "token": OLAS[Chain.BASE], - "amount": 0, + "amount": "0", }, } bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) bridge_request = bridge.create_request(params) + expected_request = BridgeRequest( + params={ + "from": { + "chain": Chain.ETHEREUM.value, + "address": wallet_address, + "token": OLAS[Chain.ETHEREUM], + }, + "to": { + "chain": Chain.BASE.value, + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 0, + }, + }, + bridge_provider_id=NativeBridgeProvider.id(), + id=bridge_request.id, + status=BridgeRequestStatus.CREATED, + quote_data=None, + execution_data=None, + ) - assert not bridge_request.quote_data, "Unexpected quote data." + assert bridge_request == expected_request, "Wrong bridge request." with pytest.raises(RuntimeError): bridge.execute(bridge_request) - status1 = bridge_request.status + assert bridge_request == expected_request, "Wrong bridge request." + bridge._update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.CREATED, "Wrong status." - assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + assert bridge_request == expected_request, "Wrong bridge request." for _ in range(2): timestamp = int(time.time()) @@ -472,6 +507,10 @@ def test_bridge_zero( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts == 0, "Wrong quote data." + assert ( + qd.bridge_eta + == NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"] + ), "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.response is None, "Wrong quote data." @@ -495,7 +534,7 @@ def test_bridge_zero( br = bridge.bridge_requirements(bridge_request) expected_br = { - "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + "ethereum": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.ETHEREUM]: 0}} } diff = DeepDiff(br, expected_br) if diff: @@ -506,6 +545,9 @@ def test_bridge_zero( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts == 0, "Wrong quote data." + assert ( + qd.bridge_eta == NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"] + ), "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.response is None, "Wrong quote data." @@ -570,12 +612,12 @@ def test_bridge_error( wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address params = { "from": { - "chain": "gnosis", + "chain": Chain.ETHEREUM.value, "address": wallet_address, - "token": OLAS[Chain.GNOSIS], + "token": OLAS[Chain.ETHEREUM], }, "to": { - "chain": "base", + "chain": Chain.BASE.value, "address": wallet_address, "token": OLAS[Chain.BASE], "amount": 1, # This will cause a quote error @@ -602,6 +644,7 @@ def test_bridge_error( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -636,6 +679,7 @@ def test_bridge_error( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -735,6 +779,7 @@ def test_bridge_quote( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -776,6 +821,7 @@ def test_bridge_quote( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." assert qd.attempts > 0, "Wrong quote data." + assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.response is not None, "Wrong quote data." @@ -787,3 +833,148 @@ def test_bridge_quote( status2 = bridge_request.status assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + + @pytest.mark.parametrize("rpc", ["https://rpc-gate.autonolas.tech/base-rpc/"]) + @pytest.mark.parametrize( + ("timestamp", "expected_block"), + [ + (1706789346, 9999999), + (1706789347, 9999999), # timestamp block 10000000 + (1706789348, 10000000), + (1706789349, 10000000), # timestamp block 10000001 + (1706789350, 10000001), + (0, 0), + (1686789346, 0), + (1686789347, 0), # timestamp block 0 + (1686789348, 0), + (1686789349, 0), # timestamp block 1 + (1686789350, 1), + ], + ) + def test_find_block_before_timestamp( + self, + rpc: str, + timestamp: int, + expected_block: int, + ) -> None: + """test_find_block_before_timestamp""" + w3 = Web3(Web3.HTTPProvider(rpc)) + block = NativeBridgeProvider._find_block_before_timestamp(w3, timestamp) + assert block == expected_block, f"Expected block {expected_block}, got {block}." + + def test_update_execution_status(self, tmp_path: Path, password: str) -> None: + """test_update_execution_status""" + + DEFAULT_RPCS[Chain.ETHEREUM] = "https://rpc-gate.autonolas.tech/ethereum-rpc/" + DEFAULT_RPCS[Chain.BASE] = "https://rpc-gate.autonolas.tech/base-rpc/" + + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + + bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + + params = { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0000000000000000000000000000000000000000", + "amount": 300000000000000, + }, + } + + quote_data = QuoteData( + attempts=0, + bridge_eta=0, + elapsed_time=0, + message=None, + response=None, + response_status=0, + timestamp=0, + ) + + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=0, + message=None, + timestamp=0, + tx_hashes=[ + "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd" + ], + tx_status=[1], + ) + + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=NativeBridgeProvider.id(), + id="b-76a298b9-b243-4cfb-b48a-f59183ae0e85", + status=BridgeRequestStatus.EXECUTION_PENDING, + quote_data=quote_data, + execution_data=execution_data, + ) + + bridge._update_execution_status(bridge_request) + + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong execution status." + + params = { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "base", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x54330d28ca3357F294334BDC454a032e7f353416", + "amount": 100000000000000000, + }, + } + + quote_data = QuoteData( + attempts=0, + bridge_eta=0, + elapsed_time=0, + message=None, + response=None, + response_status=0, + timestamp=0, + ) + + execution_data = ExecutionData( + bridge_status=None, + elapsed_time=0, + message=None, + timestamp=0, + tx_hashes=[ + "0x0b269344009722d1a8f7ee10c03117dc5e7f833d6ba403b140b580c1016645ff", + "0xa1139bb4ba963d7979417f49fed03b365c1f1bfc31d0100257caed888a491c4c", + ], + tx_status=[1, 1], + ) + + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=NativeBridgeProvider.id(), + id="b-7221ece2-e15e-4aec-bac2-7fd4c4d4851a", + status=BridgeRequestStatus.EXECUTION_PENDING, + quote_data=quote_data, + execution_data=execution_data, + ) + + bridge._update_execution_status(bridge_request) + + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong execution status." diff --git a/tests/test_operate_cli.py b/tests/test_operate_cli.py index 9d82f0767..3083ff401 100644 --- a/tests/test_operate_cli.py +++ b/tests/test_operate_cli.py @@ -28,7 +28,7 @@ from web3 import Web3 from operate.cli import OperateApp -from operate.constants import OPERATE_HOME +from operate.constants import OPERATE from operate.operate_types import LedgerType from tests.conftest import random_string @@ -59,7 +59,7 @@ def test_update_password( """Test operate.update_password() and operate.update_password_with_mnemonic()""" operate = OperateApp( - home=tmp_path / OPERATE_HOME, + home=tmp_path / OPERATE, ) operate.setup() password1 = random_string() From f63dc92ce185d39491cfe030cf2f1933535ea901 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 11 May 2025 12:42:58 +0200 Subject: [PATCH 098/173] chore: fix test --- tests/test_operate_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_operate_cli.py b/tests/test_operate_cli.py index 3083ff401..a912e3f23 100644 --- a/tests/test_operate_cli.py +++ b/tests/test_operate_cli.py @@ -144,7 +144,7 @@ def test_migrate_account( ) -> None: """Test operate.user_account.is_valid(password) and MigrationManager.migrate_user_account()""" - operate_home_path = tmp_path / OPERATE_HOME + operate_home_path = tmp_path / OPERATE operate = OperateApp( home=operate_home_path, ) From 1601daa58911c28083aaa7c3d9493d32b535a4d1 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 11 May 2025 13:10:24 +0200 Subject: [PATCH 099/173] chore: remove bridge status --- operate/bridge/providers/bridge_provider.py | 4 ---- operate/bridge/providers/lifi_bridge_provider.py | 8 ++++---- tests/test_bridge_providers.py | 6 ------ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index bcfe29ed1..df5daf494 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -68,7 +68,6 @@ class QuoteData(LocalResource): class ExecutionData(LocalResource): """ExecutionData""" - bridge_status: t.Optional[enum.Enum] elapsed_time: float message: t.Optional[str] timestamp: int @@ -298,7 +297,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" ) execution_data = ExecutionData( - bridge_status=None, elapsed_time=0, message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", timestamp=int(timestamp), @@ -345,7 +343,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: tx_status.append(tx_receipt.get("status", 0)) execution_data = ExecutionData( - bridge_status=None, elapsed_time=time.time() - timestamp, message=None, timestamp=int(timestamp), @@ -361,7 +358,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: except Exception as e: # pylint: disable=broad-except self.logger.error(f"[BRIDGE] Error executing bridge request: {e}") execution_data = ExecutionData( - bridge_status=None, elapsed_time=time.time() - timestamp, message=f"Error executing quote: {str(e)}", timestamp=int(timestamp), diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index f2a9f3edc..bf8687172 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -309,7 +309,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.info(f"[LI.FI BRIDGE] GET {url}?{urlencode(params)}") response = requests.get(url=url, headers=headers, params=params, timeout=30) response_json = response.json() - execution.bridge_status = response_json.get( + lifi_status = response_json.get( "status", str(LiFiTransactionStatus.UNKNOWN) ) execution.message = response_json.get( @@ -321,11 +321,11 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"[LI.FI BRIDGE] Failed to update bridge status for {tx_hash}: {e}" ) - if execution.bridge_status == LiFiTransactionStatus.DONE: + if lifi_status == LiFiTransactionStatus.DONE: bridge_request.status = BridgeRequestStatus.EXECUTION_DONE - elif execution.bridge_status == LiFiTransactionStatus.FAILED: + elif lifi_status == LiFiTransactionStatus.FAILED: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - elif execution.bridge_status == LiFiTransactionStatus.PENDING: + elif lifi_status == LiFiTransactionStatus.PENDING: bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING else: bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index e069c17de..d2fb68582 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -162,7 +162,6 @@ def test_bridge_zero( bridge.execute(bridge_request=bridge_request) ed = bridge_request.execution_data assert ed is not None, "Missing execution data." - assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.message is not None, "Wrong execution data." assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." @@ -296,7 +295,6 @@ def test_bridge_error( bridge.execute(bridge_request=bridge_request) ed = bridge_request.execution_data assert ed is not None, "Missing execution data." - assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.message is not None, "Wrong execution data." assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." @@ -564,7 +562,6 @@ def test_bridge_zero( bridge.execute(bridge_request=bridge_request) ed = bridge_request.execution_data assert ed is not None, "Missing execution data." - assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.message is not None, "Wrong execution data." assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." @@ -698,7 +695,6 @@ def test_bridge_error( bridge.execute(bridge_request=bridge_request) ed = bridge_request.execution_data assert ed is not None, "Missing execution data." - assert ed.bridge_status is None, "Wrong execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.message is not None, "Wrong execution data." assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." @@ -903,7 +899,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: ) execution_data = ExecutionData( - bridge_status=None, elapsed_time=0, message=None, timestamp=0, @@ -953,7 +948,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: ) execution_data = ExecutionData( - bridge_status=None, elapsed_time=0, message=None, timestamp=0, From 81d3b38690591eba754d546aab2ce3b760c11de1 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 12 May 2025 16:10:16 +0200 Subject: [PATCH 100/173] refactor: merge sum --- operate/bridge/bridge.py | 29 ++++++----------------------- operate/cli.py | 4 ++-- operate/ledger/profiles.py | 1 - 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 343bce06c..63bda1038 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -32,6 +32,7 @@ from aea.helpers.logging import setup_logger from deepdiff import DeepDiff +from merge_dicts import merge_sum_dicts, subtract_dicts from operate.bridge.providers.bridge_provider import BridgeProvider, BridgeRequest from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider from operate.bridge.providers.native_bridge_provider import ( @@ -242,14 +243,7 @@ def bridge_refill_requirements( bridge_total_requirements = self.bridge_total_requirements(bundle) - bridge_refill_requirements: t.Dict = {} - for from_chain, from_addresses in bridge_total_requirements.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - balance = balances[from_chain][from_address][from_token] - bridge_refill_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - )[from_token] = max(from_amount - balance, 0) + bridge_refill_requirements = subtract_dicts(bridge_total_requirements, balances) is_refill_required = any( amount > 0 @@ -341,23 +335,12 @@ def get_status_json(self, bundle_id: str) -> t.Dict: def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: """Sum bridge requirements.""" - bridge_total_requirements: t.Dict = {} + requirements = [] for request in bundle.bridge_requests: - if not request.quote_data: - continue bridge = self._bridge_providers[request.bridge_provider_id] - bridge_requirements = bridge.bridge_requirements(request) - for from_chain, from_addresses in bridge_requirements.items(): - for from_address, from_tokens in from_addresses.items(): - for from_token, from_amount in from_tokens.items(): - bridge_total_requirements.setdefault(from_chain, {}).setdefault( - from_address, {} - ).setdefault(from_token, 0) - bridge_total_requirements[from_chain][from_address][ - from_token - ] += from_amount - - return bridge_total_requirements + requirements.append(bridge.bridge_requirements(request)) + + return merge_sum_dicts(requirements) def quote_bundle(self, bundle: BridgeRequestBundle) -> None: """Update the bundle with the quotes.""" diff --git a/operate/cli.py b/operate/cli.py index 0a7380aeb..8563b0bc6 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -44,12 +44,12 @@ from operate import services from operate.account.user import UserAccount from operate.bridge.bridge import BridgeManager -from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES, ZERO_ADDRESS, ZERO_ADDRESS +from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES, ZERO_ADDRESS from operate.ledger.profiles import ( DEFAULT_MASTER_EOA_FUNDS, DEFAULT_NEW_SAFE_FUNDS, ERC20_TOKENS, -), OLAS, USDC +) from operate.migration import MigrationManager from operate.operate_types import Chain, DeploymentStatus, LedgerType from operate.quickstart.analyse_logs import analyse_logs diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index 5b2fe01f1..a755880ff 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -122,7 +122,6 @@ "meme_base_beta": "0x6011E09e7c095e76980b22498d69dF18EB62BeD8", "meme_base_beta_2": "0xfb7669c3AdF673b3A545Fa5acd987dbfdA805e22", "meme_base_beta_3": "0xCA61633b03c54F64b6A7F1f9A9C0A6Feb231Cc4D", - "dual_staking_testnet": "0xd64Cf67500b7d15A41E02DDeb40F3A73CB533eB5", "marketplace_supply_alpha": "0xB14Cd66c6c601230EA79fa7Cc072E5E0C2F3A756", "marketplace_demand_alpha_1": "0x38Eb3838Dab06932E7E1E965c6F922aDfE494b88", "marketplace_demand_alpha_2": "0xBE6E12364B549622395999dB0dB53f163994D7AF", From 502cd37e780839687608bef7af7088d8b30256a2 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 12 May 2025 17:59:33 +0200 Subject: [PATCH 101/173] chore: update tests --- operate/bridge/providers/bridge_provider.py | 4 + .../providers/native_bridge_provider.py | 17 +-- tests/test_bridge_providers.py | 103 +++++++----------- 3 files changed, 55 insertions(+), 69 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index df5daf494..ed3ecfd0f 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -328,6 +328,10 @@ def execute(self, bridge_request: BridgeRequest) -> None: tx_status = [] for tx_label, tx in txs: + + # TODO backport to wallet execute + # Wallet should return hash + # Here we re-check the receipt. self.logger.info(f"[BRIDGE] Executing transaction {tx_label}.") setattr( # noqa: B010 tx_settler, "build", lambda *args, **kwargs: tx # noqa: B023 diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 59fbfd194..fad9b3742 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -283,6 +283,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"Cannot update bridge request {bridge_request.id}: execution data not present." ) + # TODO check from the RPC, not stored value. if execution_data.tx_status and any( status == 0 for status in execution_data.tx_status ): @@ -347,13 +348,13 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) event_found = self._is_event_in_range( - w3, - to_bridge, - from_block, - to_block, - topics, - non_indexed_types, - non_indexed_values, + w3=w3, + contract_address=to_bridge, + from_block=from_block, + to_block=to_block, + topics=topics, + non_indexed_types=non_indexed_types, + non_indexed_values=non_indexed_values, ) if event_found: self.logger.info( @@ -421,4 +422,4 @@ def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: def _get_explorer_link(self, tx_hash: str) -> str: """Get the explorer link for a transaction.""" - return f"https://etherscan.io/tx/{tx_hash}" + return f"https://etherscan.io/tx/{tx_hash}" # TODO this bridge should return None here - discuss with FE diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index d2fb68582..eb399f283 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -466,6 +466,8 @@ def test_bridge_zero( } bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + + # Create bridge_request = bridge.create_request(params) expected_request = BridgeRequest( params={ @@ -499,37 +501,37 @@ def test_bridge_zero( assert bridge_request == expected_request, "Wrong bridge request." + # Quote + expected_quote_data = QuoteData( + attempts=0, + bridge_eta=NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"], + elapsed_time=0, + message=MESSAGE_QUOTE_ZERO, + response=None, + response_status=0, + timestamp=int(time.time()), + ) + expected_request.quote_data = expected_quote_data + expected_request.status = BridgeRequestStatus.QUOTE_DONE + for _ in range(2): timestamp = int(time.time()) bridge.quote(bridge_request=bridge_request) - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts == 0, "Wrong quote data." - assert ( - qd.bridge_eta - == NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"] - ), "Wrong quote data." - assert qd.elapsed_time == 0, "Wrong quote data." - assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." - assert qd.response is None, "Wrong quote data." - assert timestamp <= qd.timestamp, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_DONE - ), "Wrong status." - - sj = bridge.get_status_json(bridge_request) - expected_sj = { - "message": MESSAGE_QUOTE_ZERO, - "status": BridgeRequestStatus.QUOTE_DONE.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) + expected_quote_data.timestamp = bridge_request.quote_data.timestamp + assert bridge_request == expected_request, "Wrong bridge request." + sj = bridge.get_status_json(bridge_request) + expected_sj = { + "message": MESSAGE_QUOTE_ZERO, + "status": BridgeRequestStatus.QUOTE_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) - assert not diff, "Wrong status." - assert bridge_request.quote_data is not None, "Missing quote data." + assert not diff, "Wrong status." + assert bridge_request == expected_request, "Wrong bridge request." + # Get requirements br = bridge.bridge_requirements(bridge_request) expected_br = { "ethereum": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.ETHEREUM]: 0}} @@ -539,45 +541,23 @@ def test_bridge_zero( print(diff) assert not diff, "Wrong bridge requirements." + assert bridge_request == expected_request, "Wrong bridge request." - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts == 0, "Wrong quote data." - assert ( - qd.bridge_eta == NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"] - ), "Wrong quote data." - assert qd.elapsed_time == 0, "Wrong quote data." - assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." - assert qd.response is None, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - - status1 = bridge_request.status - bridge._update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + # Execute + expected_execution_data = ExecutionData( + elapsed_time=0, + message=f"{MESSAGE_EXECUTION_SKIPPED} (bridge_request.status=)", + timestamp=int(timestamp), + tx_hashes=None, + tx_status=None, + ) + expected_request.execution_data = expected_execution_data + expected_request.status = BridgeRequestStatus.EXECUTION_DONE - timestamp = int(time.time()) bridge.execute(bridge_request=bridge_request) - ed = bridge_request.execution_data - assert ed is not None, "Missing execution data." - assert ed.elapsed_time == 0, "Wrong execution data." - assert ed.message is not None, "Wrong execution data." - assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." - assert timestamp <= ed.timestamp, "Wrong quote data." - assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hashes is None, "Wrong execution data." - assert ed.tx_status is None, "Wrong execution data." - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong status." - - bridge._update_execution_status(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong status." + expected_execution_data.timestamp = bridge_request.execution_data.timestamp + assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.get_status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { @@ -591,6 +571,7 @@ def test_bridge_zero( print(diff) assert not diff, "Wrong status." + assert bridge_request == expected_request, "Wrong bridge request." def test_bridge_error( self, From 84d2479392a338b453fec58eb15e559d65f25b55 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 12 May 2025 19:30:54 +0200 Subject: [PATCH 102/173] chore: tests --- tests/test_bridge_providers.py | 103 ++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index eb399f283..addc0a615 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -898,7 +898,7 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: execution_data=execution_data, ) - bridge._update_execution_status(bridge_request) + bridge.get_status_json(bridge_request) assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE @@ -948,7 +948,106 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: execution_data=execution_data, ) - bridge._update_execution_status(bridge_request) + bridge.get_status_json(bridge_request) + + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong execution status." + + params = { + "from": { + "chain": "ethereum", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0000000000000000000000000000000000000000", + "amount": 30750000000000000, + }, + } + + quote_data = QuoteData( + attempts=0, + bridge_eta=0, + elapsed_time=0, + message=None, + response=None, + response_status=0, + timestamp=0, + ) + + execution_data = ExecutionData( + elapsed_time=0, + message=None, + timestamp=0, + tx_hashes=[ + "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba" + ], + tx_status=[1], + ) + + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=NativeBridgeProvider.id(), + id="b-7ca71220-4336-414f-985e-bdfe11707c71", + status=BridgeRequestStatus.EXECUTION_PENDING, + quote_data=quote_data, + execution_data=execution_data, + ) + + bridge.get_status_json(bridge_request) + + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong execution status." + + params = { + "from": { + "chain": "ethereum", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "base", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x54330d28ca3357F294334BDC454a032e7f353416", + "amount": 100000000000000000000, + }, + } + + quote_data = QuoteData( + attempts=0, + bridge_eta=0, + elapsed_time=0, + message=None, + response=None, + response_status=0, + timestamp=0, + ) + + execution_data = ExecutionData( + elapsed_time=0, + message=None, + timestamp=0, + tx_hashes=[ + "0x0de7870695400316eb3f71b3ea7d0e44cd4890dc6aa33b9ffdb5d35b05852232", + "0x4a755c455f029a645f5bfe3fcd999c24acbde49991cb54f5b9b8fcf286ad2ac0", + ], + tx_status=[1, 1], + ) + + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=NativeBridgeProvider.id(), + id="b-fef67eea-d55c-45f0-8b5b-e7987c843ced", + status=BridgeRequestStatus.EXECUTION_PENDING, + quote_data=quote_data, + execution_data=execution_data, + ) + + bridge.get_status_json(bridge_request) assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE From 8321e29134e2efbf6db826d83619836e99c6e290 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 12 May 2025 23:23:29 +0200 Subject: [PATCH 103/173] remove tx_status --- operate/bridge/providers/bridge_provider.py | 6 --- .../providers/native_bridge_provider.py | 45 ++++++++++--------- tests/test_bridge_providers.py | 8 ---- 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index ed3ecfd0f..5d49b11b3 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -72,7 +72,6 @@ class ExecutionData(LocalResource): message: t.Optional[str] timestamp: int tx_hashes: t.Optional[t.List[str]] - tx_status: t.Optional[t.List[int]] class BridgeRequestStatus(str, enum.Enum): @@ -301,7 +300,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", timestamp=int(timestamp), tx_hashes=None, - tx_status=None, ) bridge_request.execution_data = execution_data @@ -325,7 +323,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: sleep=ON_CHAIN_INTERACT_SLEEP, ) tx_hashes = [] - tx_status = [] for tx_label, tx in txs: @@ -344,14 +341,12 @@ def execute(self, bridge_request: BridgeRequest) -> None: ) self.logger.info(f"[BRIDGE] Transaction {tx_label} settled.") tx_hashes.append(tx_receipt.get("transactionHash", "").hex()) - tx_status.append(tx_receipt.get("status", 0)) execution_data = ExecutionData( elapsed_time=time.time() - timestamp, message=None, timestamp=int(timestamp), tx_hashes=tx_hashes, - tx_status=tx_status, ) bridge_request.execution_data = execution_data if len(tx_hashes) == len(txs): @@ -366,7 +361,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: message=f"Error executing quote: {str(e)}", timestamp=int(timestamp), tx_hashes=None, - tx_status=None, ) bridge_request.execution_data = execution_data bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index fad9b3742..227fd532f 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -68,12 +68,14 @@ ), ) -ERC20_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( - text="ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" -).hex() ETH_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( text="ETHBridgeFinalized(address,address,uint256,bytes)" ).hex() +ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["uint256", "bytes"] +ERC20_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( + text="ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" +).hex() +ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["address", "uint256", "bytes"] class NativeBridgeProvider(BridgeProvider): @@ -283,13 +285,6 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"Cannot update bridge request {bridge_request.id}: execution data not present." ) - # TODO check from the RPC, not stored value. - if execution_data.tx_status and any( - status == 0 for status in execution_data.tx_status - ): - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - return - from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -302,12 +297,19 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_eta = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"]) try: + from_w3 = self._from_ledger_api(bridge_request).api + + for tx_hash in execution_data.tx_hashes: + receipt = from_w3.eth.get_transaction_receipt(tx_hash) + if receipt.status == 0: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return + # Get the timestamp of the bridge_tx on the 'from' chain - from_ledger_api = self._from_ledger_api(bridge_request) - bridge_tx_receipt = from_ledger_api.api.eth.get_transaction_receipt( + bridge_tx_receipt = from_w3.eth.get_transaction_receipt( bridge_request.execution_data.tx_hashes[-1] ) - bridge_tx_block = from_ledger_api.api.eth.get_block( + bridge_tx_block = from_w3.eth.get_block( bridge_tx_receipt.blockNumber ) bridge_tx_ts = bridge_tx_block.timestamp @@ -319,7 +321,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: "0x" + from_address.lower()[2:].rjust(64, "0"), # from "0x" + to_address.lower()[2:].rjust(64, "0"), # from ] - non_indexed_types = ["uint256", "bytes"] + non_indexed_types = ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES non_indexed_values = [ to_amount, # amount Web3.keccak(text=bridge_request.id), # extraData @@ -331,7 +333,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: "0x" + from_token.lower()[2:].rjust(64, "0"), # remoteToken "0x" + from_address.lower()[2:].rjust(64, "0"), # from ] - non_indexed_types = ["address", "uint256", "bytes"] + non_indexed_types = ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES non_indexed_values = [ to_address.lower(), # to to_amount, # amount @@ -339,16 +341,15 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ] # Find the event on the 'to' chain - to_ledger_api = self._to_ledger_api(bridge_request) - w3 = to_ledger_api.api - starting_block = self._find_block_before_timestamp(w3, bridge_tx_ts) - starting_block_ts = w3.eth.get_block(starting_block).timestamp - latest_block = w3.eth.block_number + to_w3 = self._to_ledger_api(bridge_request).api + starting_block = self._find_block_before_timestamp(to_w3, bridge_tx_ts) + starting_block_ts = to_w3.eth.get_block(starting_block).timestamp + latest_block = to_w3.eth.block_number for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) event_found = self._is_event_in_range( - w3=w3, + w3=to_w3, contract_address=to_bridge, from_block=from_block, to_block=to_block, @@ -363,7 +364,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_request.status = BridgeRequestStatus.EXECUTION_DONE return - last_block_ts = w3.eth.get_block(to_block).timestamp + last_block_ts = to_w3.eth.get_block(to_block).timestamp if last_block_ts > starting_block_ts + bridge_eta * 2: self.logger.info( f"[NATIVE BRIDGE] Execution failed for {bridge_request.id}: bridge exceeds 2*ETA." diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index addc0a615..44b7474b1 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -168,7 +168,6 @@ def test_bridge_zero( assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hashes is None, "Wrong execution data." - assert ed.tx_status is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE ), "Wrong status." @@ -301,7 +300,6 @@ def test_bridge_error( assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hashes is None, "Wrong execution data." - assert ed.tx_status is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED ), "Wrong status." @@ -549,7 +547,6 @@ def test_bridge_zero( message=f"{MESSAGE_EXECUTION_SKIPPED} (bridge_request.status=)", timestamp=int(timestamp), tx_hashes=None, - tx_status=None, ) expected_request.execution_data = expected_execution_data expected_request.status = BridgeRequestStatus.EXECUTION_DONE @@ -682,7 +679,6 @@ def test_bridge_error( assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.tx_hashes is None, "Wrong execution data." - assert ed.tx_status is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED ), "Wrong status." @@ -886,7 +882,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: tx_hashes=[ "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd" ], - tx_status=[1], ) bridge_request = BridgeRequest( @@ -936,7 +931,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: "0x0b269344009722d1a8f7ee10c03117dc5e7f833d6ba403b140b580c1016645ff", "0xa1139bb4ba963d7979417f49fed03b365c1f1bfc31d0100257caed888a491c4c", ], - tx_status=[1, 1], ) bridge_request = BridgeRequest( @@ -985,7 +979,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: tx_hashes=[ "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba" ], - tx_status=[1], ) bridge_request = BridgeRequest( @@ -1035,7 +1028,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: "0x0de7870695400316eb3f71b3ea7d0e44cd4890dc6aa33b9ffdb5d35b05852232", "0x4a755c455f029a645f5bfe3fcd999c24acbde49991cb54f5b9b8fcf286ad2ac0", ], - tx_status=[1, 1], ) bridge_request = BridgeRequest( From 0ca9d7b95f02d56c587315668744be2d82980902 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 12 May 2025 23:26:24 +0200 Subject: [PATCH 104/173] chore: bridgerequest --- operate/bridge/providers/bridge_provider.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 5d49b11b3..89c3aa298 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -97,9 +97,9 @@ class BridgeRequest(LocalResource): params: t.Dict bridge_provider_id: str id: str - status: BridgeRequestStatus = BridgeRequestStatus.CREATED - quote_data: t.Optional[QuoteData] = None - execution_data: t.Optional[ExecutionData] = None + status: BridgeRequestStatus + quote_data: t.Optional[QuoteData] + execution_data: t.Optional[ExecutionData] class BridgeProvider(ABC): @@ -176,6 +176,9 @@ def create_request(self, params: t.Dict) -> BridgeRequest: params=params, bridge_provider_id=self.id(), id=f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", + quote_data=None, + execution_data=None, + status=BridgeRequestStatus.CREATED ) def _from_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: From 9a40534bbf05c8c91283bd5744974aa8f8fe1e77 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 13 May 2025 10:27:11 +0200 Subject: [PATCH 105/173] chore: linters --- operate/bridge/providers/bridge_provider.py | 11 ++--- .../providers/native_bridge_provider.py | 8 ++-- operate/cli.py | 2 - operate/utils/__init__.py | 6 ++- tests/test_bridge_providers.py | 47 ++++++++++++++++--- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 89c3aa298..a5de0a181 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -48,7 +48,9 @@ BRIDGE_REQUEST_PREFIX = "b-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." MESSAGE_EXECUTION_SKIPPED = "Execution skipped." -ERC20_APPROVE_SELECTOR = "0x095ea7b3" +ERC20_APPROVE_SELECTOR = ( + "0x095ea7b3" # 4 first bytes of Keccak('approve(address,uint256)') +) @dataclass @@ -178,7 +180,7 @@ def create_request(self, params: t.Dict) -> BridgeRequest: id=f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", quote_data=None, execution_data=None, - status=BridgeRequestStatus.CREATED + status=BridgeRequestStatus.CREATED, ) def _from_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: @@ -249,9 +251,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: if tx.get("to", "").lower() == from_token.lower() and tx.get( "data", "" - ).startswith( - ERC20_APPROVE_SELECTOR - ): # approve(address,uint256) + ).startswith(ERC20_APPROVE_SELECTOR): try: amount = int(tx["data"][-64:], 16) total_token += amount @@ -328,7 +328,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: tx_hashes = [] for tx_label, tx in txs: - # TODO backport to wallet execute # Wallet should return hash # Here we re-check the receipt. diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 227fd532f..72830fe6c 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -285,6 +285,10 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"Cannot update bridge request {bridge_request.id}: execution data not present." ) + if not execution_data.tx_hashes: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return + from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -309,9 +313,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_tx_receipt = from_w3.eth.get_transaction_receipt( bridge_request.execution_data.tx_hashes[-1] ) - bridge_tx_block = from_w3.eth.get_block( - bridge_tx_receipt.blockNumber - ) + bridge_tx_block = from_w3.eth.get_block(bridge_tx_receipt.blockNumber) bridge_tx_ts = bridge_tx_block.timestamp # Prepare the event data diff --git a/operate/cli.py b/operate/cli.py index 8563b0bc6..aa0cb49a2 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -62,8 +62,6 @@ from operate.services.health_checker import HealthChecker from operate.utils import subtract_dicts from operate.utils.gnosis import get_assets_balances -from operate.services.manage import ServiceManager -from operate.utils.gnosis import get_assets_balances from operate.wallet.master import MasterWalletManager diff --git a/operate/utils/__init__.py b/operate/utils/__init__.py index d55c940ce..8a23aeed7 100644 --- a/operate/utils/__init__.py +++ b/operate/utils/__init__.py @@ -85,10 +85,12 @@ def subtract_dicts(a: NestedDict, b: NestedDict) -> NestedDict: result = {} for key in a.keys() | b.keys(): # type: ignore - va, vb = a.get(key), b.get(key) # type: ignore + va = a.get(key) # type: ignore + vb = b.get(key) # type: ignore if isinstance(va, dict) or isinstance(vb, dict): result[key] = subtract_dicts( - va if isinstance(va, dict) else {}, vb if isinstance(vb, dict) else {} + va if isinstance(va, dict) else {}, + vb if isinstance(vb, dict) else {} ) else: result[key] = max((va or 0) - (vb or 0), 0) # type: ignore diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 44b7474b1..aad31df07 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -91,7 +91,14 @@ def test_bridge_zero( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id(), "test-id") + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=bridge.id(), + id="test-id", + quote_data=None, + execution_data=None, + status=BridgeRequestStatus.CREATED, + ) assert not bridge_request.quote_data, "Unexpected quote data." @@ -221,7 +228,14 @@ def test_bridge_error( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id(), "test-id") + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=bridge.id(), + id="test-id", + quote_data=None, + execution_data=None, + status=BridgeRequestStatus.CREATED, + ) assert not bridge_request.quote_data, "Unexpected quote data." @@ -354,7 +368,14 @@ def test_bridge_quote( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id(), "test-id") + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=bridge.id(), + id="test-id", + quote_data=None, + execution_data=None, + status=BridgeRequestStatus.CREATED, + ) assert not bridge_request.quote_data, "Unexpected quote data." @@ -545,7 +566,7 @@ def test_bridge_zero( expected_execution_data = ExecutionData( elapsed_time=0, message=f"{MESSAGE_EXECUTION_SKIPPED} (bridge_request.status=)", - timestamp=int(timestamp), + timestamp=0, tx_hashes=None, ) expected_request.execution_data = expected_execution_data @@ -600,7 +621,14 @@ def test_bridge_error( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id(), "test-id") + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=bridge.id(), + id="test-id", + quote_data=None, + execution_data=None, + status=BridgeRequestStatus.CREATED, + ) assert not bridge_request.quote_data, "Unexpected quote data." @@ -733,7 +761,14 @@ def test_bridge_quote( } bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest(params, bridge.id(), "test-id") + bridge_request = BridgeRequest( + params=params, + bridge_provider_id=bridge.id(), + id="test-id", + quote_data=None, + execution_data=None, + status=BridgeRequestStatus.CREATED, + ) assert not bridge_request.quote_data, "Unexpected quote data." From ac4c85692d4a22df86cd9b3d3ee57c0923aa7b1f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 13 May 2025 18:42:11 +0200 Subject: [PATCH 106/173] chore: update --- operate/bridge/bridge.py | 2 +- operate/bridge/providers/bridge_provider.py | 46 +- .../bridge/providers/lifi_bridge_provider.py | 6 +- .../providers/native_bridge_provider.py | 32 +- tests/test_bridge_providers.py | 625 ++++++++---------- 5 files changed, 337 insertions(+), 374 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 63bda1038..11b422653 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -321,7 +321,7 @@ def get_status_json(self, bundle_id: str) -> t.Dict: bridge_request_status = [] for request in bundle.bridge_requests: bridge = self._bridge_providers[request.bridge_provider_id] - bridge_request_status.append(bridge.get_status_json(request)) + bridge_request_status.append(bridge.status_json(request)) updated_status = [request.status for request in bundle.bridge_requests] diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index a5de0a181..41121a952 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -73,7 +73,8 @@ class ExecutionData(LocalResource): elapsed_time: float message: t.Optional[str] timestamp: int - tx_hashes: t.Optional[t.List[str]] + from_tx_hash: t.Optional[str] + to_tx_hash: t.Optional[str] class BridgeRequestStatus(str, enum.Enum): @@ -107,11 +108,19 @@ class BridgeRequest(LocalResource): class BridgeProvider(ABC): """(Abstract) BridgeProvider. - Derived classes must iplement the following methods: + Expected usage: + 1. params = {...} + 2. request = bridge.create_request(params) + 3. bridge.quote(request) + 4. bridge.requirements(request) + 5. bridge.execute(request) + 6. bridge.status_json(request) + + Derived classes must implement the following methods: - description - quote + - _update_execution_status - _get_transactions - - update_execution_status - _get_explorer_link """ @@ -255,8 +264,8 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: try: amount = int(tx["data"][-64:], 16) total_token += amount - except Exception: - raise RuntimeError("Malformed ERC20 approve transaction.") + except Exception as e: + raise RuntimeError("Malformed ERC20 approve transaction.") from e result = { from_chain: { @@ -302,7 +311,8 @@ def execute(self, bridge_request: BridgeRequest) -> None: elapsed_time=0, message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", timestamp=int(timestamp), - tx_hashes=None, + from_tx_hash=None, + to_tx_hash=None, ) bridge_request.execution_data = execution_data @@ -328,9 +338,6 @@ def execute(self, bridge_request: BridgeRequest) -> None: tx_hashes = [] for tx_label, tx in txs: - # TODO backport to wallet execute - # Wallet should return hash - # Here we re-check the receipt. self.logger.info(f"[BRIDGE] Executing transaction {tx_label}.") setattr( # noqa: B010 tx_settler, "build", lambda *args, **kwargs: tx # noqa: B023 @@ -348,7 +355,8 @@ def execute(self, bridge_request: BridgeRequest) -> None: elapsed_time=time.time() - timestamp, message=None, timestamp=int(timestamp), - tx_hashes=tx_hashes, + from_tx_hash=tx_hashes[-1], + to_tx_hash=None, ) bridge_request.execution_data = execution_data if len(tx_hashes) == len(txs): @@ -362,7 +370,8 @@ def execute(self, bridge_request: BridgeRequest) -> None: elapsed_time=time.time() - timestamp, message=f"Error executing quote: {str(e)}", timestamp=int(timestamp), - tx_hashes=None, + from_tx_hash=None, + to_tx_hash=None, ) bridge_request.execution_data = execution_data bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @@ -377,14 +386,14 @@ def _get_explorer_link(self, tx_hash: str) -> str: """Get the explorer link for a transaction.""" raise NotImplementedError() - def get_status_json(self, bridge_request: BridgeRequest) -> t.Dict: + def status_json(self, bridge_request: BridgeRequest) -> t.Dict: """JSON representation of the status.""" if bridge_request.execution_data: self._update_execution_status(bridge_request) tx_hash = None explorer_link = None - if bridge_request.execution_data.tx_hashes: - tx_hash = bridge_request.execution_data.tx_hashes[-1] + if bridge_request.execution_data.from_tx_hash: + tx_hash = bridge_request.execution_data.from_tx_hash explorer_link = self._get_explorer_link(tx_hash) return { @@ -422,3 +431,12 @@ def _update_with_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: raise RuntimeError("Retrieved invalid gas pricing.") return output_tx + + # @staticmethod + # def _update_with_gas_estimate(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: + # original_from = tx["from"] + # original_gas = tx["gas"] + # tx["gas"] = 1 + # ledger_api.update_with_gas_estimate(tx) + # if tx["gas"] == 1: + # tx["from"] = diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index bf8687172..4acb9cc89 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -294,15 +294,13 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ) execution = bridge_request.execution_data - if not execution.tx_hashes: + if not execution.from_tx_hash: return - tx_hash = execution.tx_hashes[-1] - url = "https://li.quest/v1/status" headers = {"accept": "application/json"} params = { - "txHash": tx_hash, + "txHash": execution.from_tx_hash, } try: diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 72830fe6c..c8c79c7e9 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -279,13 +279,12 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"[NATIVE BRIDGE] Updating execution status for bridge request {bridge_request.id}." ) - execution_data = bridge_request.execution_data - if not execution_data: + if not bridge_request.execution_data: raise RuntimeError( f"Cannot update bridge request {bridge_request.id}: execution data not present." ) - if not execution_data.tx_hashes: + if not bridge_request.execution_data.from_tx_hash: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return @@ -303,15 +302,15 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: try: from_w3 = self._from_ledger_api(bridge_request).api - for tx_hash in execution_data.tx_hashes: - receipt = from_w3.eth.get_transaction_receipt(tx_hash) - if receipt.status == 0: - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - return + from_tx_hash = bridge_request.execution_data.from_tx_hash + receipt = from_w3.eth.get_transaction_receipt(from_tx_hash) + if receipt.status == 0: + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return # Get the timestamp of the bridge_tx on the 'from' chain bridge_tx_receipt = from_w3.eth.get_transaction_receipt( - bridge_request.execution_data.tx_hashes[-1] + from_tx_hash ) bridge_tx_block = from_w3.eth.get_block(bridge_tx_receipt.blockNumber) bridge_tx_ts = bridge_tx_block.timestamp @@ -350,7 +349,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) - event_found = self._is_event_in_range( + to_tx_hash = self._find_transaction_in_range( w3=to_w3, contract_address=to_bridge, from_block=from_block, @@ -359,10 +358,11 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: non_indexed_types=non_indexed_types, non_indexed_values=non_indexed_values, ) - if event_found: + if to_tx_hash: self.logger.info( f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." ) + bridge_request.execution_data.to_tx_hash = to_tx_hash bridge_request.status = BridgeRequestStatus.EXECUTION_DONE return @@ -381,7 +381,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ) # TODO EXECUTION_UNKNOWN ? @staticmethod - def _is_event_in_range( + def _find_transaction_in_range( w3: Web3, contract_address: str, from_block: int, @@ -389,8 +389,8 @@ def _is_event_in_range( topics: list[str], non_indexed_types: list[str], non_indexed_values: list[t.Any], - ) -> bool: - """Check for a finalized bridge event in the given block range.""" + ) -> t.Optional[str]: + """Return the transaction hash of a matching event in the given block range, if any.""" logs = w3.eth.get_logs( { "fromBlock": from_block, @@ -403,9 +403,9 @@ def _is_event_in_range( for log in logs: decoded = eth_abi.decode(non_indexed_types, log["data"]) if all(a == b for a, b in zip(decoded, non_indexed_values)): - return True + return log["transactionHash"].hex() - return False + return None @staticmethod def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index aad31df07..465e383ab 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -24,6 +24,7 @@ import time from pathlib import Path +from operate import wallet import pytest from deepdiff import DeepDiff from web3 import Web3 @@ -127,7 +128,7 @@ def test_bridge_zero( bridge_request.status == BridgeRequestStatus.QUOTE_DONE ), "Wrong status." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) expected_sj = { "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, @@ -174,7 +175,7 @@ def test_bridge_zero( assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hashes is None, "Wrong execution data." + assert ed.from_tx_hash is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE ), "Wrong status." @@ -184,7 +185,7 @@ def test_bridge_zero( bridge_request.status == BridgeRequestStatus.EXECUTION_DONE ), "Wrong status." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], @@ -265,7 +266,7 @@ def test_bridge_error( ), "Wrong status." assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) expected_sj = { "message": bridge_request.quote_data.message, "status": BridgeRequestStatus.QUOTE_FAILED.value, @@ -313,7 +314,7 @@ def test_bridge_error( assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hashes is None, "Wrong execution data." + assert ed.from_tx_hash is None, "Wrong execution data." assert ( bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED ), "Wrong status." @@ -323,7 +324,7 @@ def test_bridge_error( bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED ), "Wrong status." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], @@ -405,7 +406,7 @@ def test_bridge_quote( ), "Wrong status." assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) expected_sj = { "message": bridge_request.quote_data.message, "status": BridgeRequestStatus.QUOTE_DONE.value, @@ -534,11 +535,10 @@ def test_bridge_zero( expected_request.status = BridgeRequestStatus.QUOTE_DONE for _ in range(2): - timestamp = int(time.time()) bridge.quote(bridge_request=bridge_request) expected_quote_data.timestamp = bridge_request.quote_data.timestamp assert bridge_request == expected_request, "Wrong bridge request." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) expected_sj = { "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, @@ -567,7 +567,8 @@ def test_bridge_zero( elapsed_time=0, message=f"{MESSAGE_EXECUTION_SKIPPED} (bridge_request.status=)", timestamp=0, - tx_hashes=None, + from_tx_hash=None, + to_tx_hash=None, ) expected_request.execution_data = expected_execution_data expected_request.status = BridgeRequestStatus.EXECUTION_DONE @@ -576,7 +577,7 @@ def test_bridge_zero( expected_execution_data.timestamp = bridge_request.execution_data.timestamp assert bridge_request == expected_request, "Wrong bridge request." - sj = bridge.get_status_json(bridge_request) + sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], @@ -591,12 +592,16 @@ def test_bridge_zero( assert not diff, "Wrong status." assert bridge_request == expected_request, "Wrong bridge request." - def test_bridge_error( + def test_bridge_execute_error( self, tmp_path: Path, password: str, ) -> None: - """test_bridge_error""" + """test_bridge_execute_error""" + + DEFAULT_RPCS[Chain.ETHEREUM] = "https://rpc-gate.autonolas.tech/ethereum-rpc/" + DEFAULT_RPCS[Chain.BASE] = "https://rpc-gate.autonolas.tech/base-rpc/" + operate = OperateApp( home=tmp_path / OPERATE, ) @@ -616,111 +621,110 @@ def test_bridge_error( "chain": Chain.BASE.value, "address": wallet_address, "token": OLAS[Chain.BASE], - "amount": 1, # This will cause a quote error + "amount": "1000000000000000000", }, } - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest( - params=params, - bridge_provider_id=bridge.id(), - id="test-id", + bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + + # Create + bridge_request = bridge.create_request(params) + expected_request = BridgeRequest( + params={ + "from": { + "chain": Chain.ETHEREUM.value, + "address": wallet_address, + "token": OLAS[Chain.ETHEREUM], + }, + "to": { + "chain": Chain.BASE.value, + "address": wallet_address, + "token": OLAS[Chain.BASE], + "amount": 1000000000000000000, + }, + }, + bridge_provider_id=NativeBridgeProvider.id(), + id=bridge_request.id, + status=BridgeRequestStatus.CREATED, quote_data=None, execution_data=None, - status=BridgeRequestStatus.CREATED, ) - assert not bridge_request.quote_data, "Unexpected quote data." + assert bridge_request == expected_request, "Wrong bridge request." with pytest.raises(RuntimeError): bridge.execute(bridge_request) - status1 = bridge_request.status + assert bridge_request == expected_request, "Wrong bridge request." + bridge._update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.CREATED, "Wrong status." - assert status2 == BridgeRequestStatus.CREATED, "Wrong status." + + assert bridge_request == expected_request, "Wrong bridge request." + + # Quote + expected_quote_data = QuoteData( + attempts=0, + bridge_eta=NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"], + elapsed_time=0, + message=None, + response=None, + response_status=0, + timestamp=int(time.time()), + ) + expected_request.quote_data = expected_quote_data + expected_request.status = BridgeRequestStatus.QUOTE_DONE for _ in range(2): - timestamp = int(time.time()) bridge.quote(bridge_request=bridge_request) - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.bridge_eta is None, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is not None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert timestamp <= qd.timestamp, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_FAILED - ), "Wrong status." - - assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge.get_status_json(bridge_request) - expected_sj = { - "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_FAILED.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) + expected_quote_data.timestamp = bridge_request.quote_data.timestamp + assert bridge_request == expected_request, "Wrong bridge request." + sj = bridge.status_json(bridge_request) + expected_sj = { + "message": None, + "status": BridgeRequestStatus.QUOTE_DONE.value, + } + diff = DeepDiff(sj, expected_sj) + if diff: + print(diff) - assert not diff, "Wrong status." + assert not diff, "Wrong status." + assert bridge_request == expected_request, "Wrong bridge request." + # Get requirements br = bridge.bridge_requirements(bridge_request) + assert br["ethereum"][wallet_address][ZERO_ADDRESS] > 0, "Wrong bridge requirements." expected_br = { - "gnosis": {wallet_address: {ZERO_ADDRESS: 0, OLAS[Chain.GNOSIS]: 0}} + "ethereum": {wallet_address: {ZERO_ADDRESS: br["ethereum"][wallet_address][ZERO_ADDRESS], OLAS[Chain.ETHEREUM]: 1000000000000000000}} } diff = DeepDiff(br, expected_br) if diff: print(diff) assert not diff, "Wrong bridge requirements." + assert bridge_request == expected_request, "Wrong bridge request." - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.bridge_eta is None, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is not None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_FAILED - ), "Wrong status." - - status1 = bridge_request.status - bridge._update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." - assert status2 == BridgeRequestStatus.QUOTE_FAILED, "Wrong status." + # Execute + expected_execution_data = ExecutionData( + elapsed_time=0, + message=None, + timestamp=0, + from_tx_hash=None, + to_tx_hash=None, + ) + expected_request.execution_data = expected_execution_data + expected_request.status = BridgeRequestStatus.EXECUTION_FAILED - timestamp = int(time.time()) bridge.execute(bridge_request=bridge_request) - ed = bridge_request.execution_data - assert ed is not None, "Missing execution data." - assert ed.elapsed_time == 0, "Wrong execution data." - assert ed.message is not None, "Wrong execution data." - assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." - assert timestamp <= ed.timestamp, "Wrong quote data." - assert ed.timestamp <= int(time.time()), "Wrong quote data." - assert ed.tx_hashes is None, "Wrong execution data." - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED - ), "Wrong status." - - bridge._update_execution_status(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED - ), "Wrong status." + expected_execution_data.message = bridge_request.execution_data.message + expected_execution_data.elapsed_time = bridge_request.execution_data.elapsed_time + expected_execution_data.timestamp = bridge_request.execution_data.timestamp - sj = bridge.get_status_json(bridge_request) - assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." + assert bridge_request == expected_request, "Wrong bridge request." + sj = bridge.status_json(bridge_request) + assert "Error executing quote" in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], - "tx_hash": None, + "tx_hash": None, # type: ignore "message": sj["message"], "status": BridgeRequestStatus.EXECUTION_FAILED.value, } @@ -729,118 +733,7 @@ def test_bridge_error( print(diff) assert not diff, "Wrong status." - - @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") - def test_bridge_quote( - self, - tmp_path: Path, - password: str, - ) -> None: - """test_bridge_quote""" - operate = OperateApp( - home=tmp_path / OPERATE, - ) - operate.setup() - operate.create_user_account(password=password) - operate.password = password - operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - - wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address - params = { - "from": { - "chain": "gnosis", - "address": wallet_address, - "token": OLAS[Chain.GNOSIS], - }, - "to": { - "chain": "base", - "address": wallet_address, - "token": OLAS[Chain.BASE], - "amount": 1_000_000_000_000_000_000, - }, - } - - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) - bridge_request = BridgeRequest( - params=params, - bridge_provider_id=bridge.id(), - id="test-id", - quote_data=None, - execution_data=None, - status=BridgeRequestStatus.CREATED, - ) - - assert not bridge_request.quote_data, "Unexpected quote data." - - with pytest.raises(RuntimeError): - bridge.execute(bridge_request) - - status1 = bridge_request.status - bridge._update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.CREATED, "Wrong status." - assert status2 == BridgeRequestStatus.CREATED, "Wrong status." - - for _ in range(2): - timestamp = int(time.time()) - bridge.quote(bridge_request=bridge_request) - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.bridge_eta is None, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert timestamp <= qd.timestamp, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert ( - bridge_request.status == BridgeRequestStatus.QUOTE_DONE - ), "Wrong status." - - assert bridge_request.quote_data is not None, "Wrong quote data." - sj = bridge.get_status_json(bridge_request) - expected_sj = { - "message": bridge_request.quote_data.message, - "status": BridgeRequestStatus.QUOTE_DONE.value, - } - diff = DeepDiff(sj, expected_sj) - if diff: - print(diff) - - assert not diff, "Wrong status." - assert bridge_request.quote_data.response is not None, "Missing quote data." - - quote = bridge_request.quote_data.response - br = bridge.bridge_requirements(bridge_request) - expected_br = { - "gnosis": { - wallet_address: { - ZERO_ADDRESS: br["gnosis"][wallet_address][ZERO_ADDRESS], - OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore - } - } - } - diff = DeepDiff(br, expected_br) - if diff: - print(diff) - - assert not diff, "Wrong bridge requirements." - - qd = bridge_request.quote_data - assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." - assert qd.bridge_eta is None, "Wrong quote data." - assert qd.elapsed_time > 0, "Wrong quote data." - assert qd.message is None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." - assert qd.timestamp <= int(time.time()), "Wrong quote data." - assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - - status1 = bridge_request.status - bridge._update_execution_status(bridge_request) - status2 = bridge_request.status - assert status1 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." - assert status2 == BridgeRequestStatus.QUOTE_DONE, "Wrong status." + assert bridge_request == expected_request, "Wrong bridge request." @pytest.mark.parametrize("rpc", ["https://rpc-gate.autonolas.tech/base-rpc/"]) @pytest.mark.parametrize( @@ -870,15 +763,98 @@ def test_find_block_before_timestamp( block = NativeBridgeProvider._find_block_before_timestamp(w3, timestamp) assert block == expected_block, f"Expected block {expected_block}, got {block}." - def test_update_execution_status(self, tmp_path: Path, password: str) -> None: - """test_update_execution_status""" + @pytest.mark.parametrize( + ("params, request_id, from_tx_hash, expected_to_tx_hash"), + [ + ( + { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0000000000000000000000000000000000000000", + "amount": 300000000000000, + }, + }, + "b-76a298b9-b243-4cfb-b48a-f59183ae0e85", + "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd", + "0xc97722c1310b94043fb37219285cb4f80ce4189f158033b84c935ec54166eb19", + ), + ( + { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "base", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x54330d28ca3357F294334BDC454a032e7f353416", + "amount": 100000000000000000, + }, + }, + "b-7221ece2-e15e-4aec-bac2-7fd4c4d4851a", + "0xa1139bb4ba963d7979417f49fed03b365c1f1bfc31d0100257caed888a491c4c", + "0x9b8f8998b1cd8f256914751606f772bee9ebbf459b3a1c8ca177838597464739", + ), + ( + { + "from": { + "chain": "ethereum", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0000000000000000000000000000000000000000", + "amount": 30750000000000000, + }, + }, + "b-7ca71220-4336-414f-985e-bdfe11707c71", + "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba", + "0x5718e6f0da2e0b1a02bcb53db239cef49a731f9f52cccf193f7d0abe62e971d4", + ), + ( + { + "from": { + "chain": "ethereum", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "base", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x54330d28ca3357F294334BDC454a032e7f353416", + "amount": 100000000000000000000, + }, + }, + "b-fef67eea-d55c-45f0-8b5b-e7987c843ced", + "0x4a755c455f029a645f5bfe3fcd999c24acbde49991cb54f5b9b8fcf286ad2ac0", + "0xf4ccb5f6547c188e638ac3d84f80158e3d7462211e15bc3657f8585b0bbffb68", + ), + ], + ) + def test_update_execution_status( + self, + tmp_path: Path, + password: str, + request_id: str, + params: dict, + from_tx_hash: str, + expected_to_tx_hash: str, + ) -> None: + """Parametrized test for update_execution_status.""" DEFAULT_RPCS[Chain.ETHEREUM] = "https://rpc-gate.autonolas.tech/ethereum-rpc/" DEFAULT_RPCS[Chain.BASE] = "https://rpc-gate.autonolas.tech/base-rpc/" - operate = OperateApp( - home=tmp_path / OPERATE, - ) + operate = OperateApp(home=tmp_path / OPERATE) operate.setup() operate.create_user_account(password=password) operate.password = password @@ -886,20 +862,6 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) - params = { - "from": { - "chain": "ethereum", - "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", - "token": "0x0000000000000000000000000000000000000000", - }, - "to": { - "chain": "base", - "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", - "token": "0x0000000000000000000000000000000000000000", - "amount": 300000000000000, - }, - } - quote_data = QuoteData( attempts=0, bridge_eta=0, @@ -914,136 +876,124 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: elapsed_time=0, message=None, timestamp=0, - tx_hashes=[ - "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd" - ], + from_tx_hash=from_tx_hash, + to_tx_hash=None, ) bridge_request = BridgeRequest( params=params, bridge_provider_id=NativeBridgeProvider.id(), - id="b-76a298b9-b243-4cfb-b48a-f59183ae0e85", + id=request_id, status=BridgeRequestStatus.EXECUTION_PENDING, quote_data=quote_data, execution_data=execution_data, ) - bridge.get_status_json(bridge_request) + bridge.status_json(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong execution status." + assert bridge_request.status == BridgeRequestStatus.EXECUTION_DONE, "Wrong execution status." + assert execution_data.to_tx_hash == expected_to_tx_hash, "Wrong to_tx_hash." - params = { - "from": { - "chain": "ethereum", - "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", - "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", - }, - "to": { - "chain": "base", - "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", - "token": "0x54330d28ca3357F294334BDC454a032e7f353416", - "amount": 100000000000000000, - }, - } - quote_data = QuoteData( - attempts=0, - bridge_eta=0, - elapsed_time=0, - message=None, - response=None, - response_status=0, - timestamp=0, - ) - execution_data = ExecutionData( - elapsed_time=0, - message=None, - timestamp=0, - tx_hashes=[ - "0x0b269344009722d1a8f7ee10c03117dc5e7f833d6ba403b140b580c1016645ff", + @pytest.mark.parametrize( + ("params, request_id, from_tx_hash, expected_to_tx_hash"), + [ + ( + { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0000000000000000000000000000000000000000", + "amount": 42, # Wrong amount + }, + }, + "b-76a298b9-b243-4cfb-b48a-f59183ae0e85", + "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd", + "0xc97722c1310b94043fb37219285cb4f80ce4189f158033b84c935ec54166eb19", + ), + ( + { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "base", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x54330d28ca3357F294334BDC454a032e7f353416", + "amount": 100000000000000000, + }, + }, + "b-42", # Wrong id "0xa1139bb4ba963d7979417f49fed03b365c1f1bfc31d0100257caed888a491c4c", - ], - ) - - bridge_request = BridgeRequest( - params=params, - bridge_provider_id=NativeBridgeProvider.id(), - id="b-7221ece2-e15e-4aec-bac2-7fd4c4d4851a", - status=BridgeRequestStatus.EXECUTION_PENDING, - quote_data=quote_data, - execution_data=execution_data, - ) - - bridge.get_status_json(bridge_request) - - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong execution status." - - params = { - "from": { - "chain": "ethereum", - "address": "0xC0a12402089ce761E6496892AF4754350639bf94", - "token": "0x0000000000000000000000000000000000000000", - }, - "to": { - "chain": "base", - "address": "0xC0a12402089ce761E6496892AF4754350639bf94", - "token": "0x0000000000000000000000000000000000000000", - "amount": 30750000000000000, - }, - } - - quote_data = QuoteData( - attempts=0, - bridge_eta=0, - elapsed_time=0, - message=None, - response=None, - response_status=0, - timestamp=0, - ) - - execution_data = ExecutionData( - elapsed_time=0, - message=None, - timestamp=0, - tx_hashes=[ - "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba" - ], - ) - - bridge_request = BridgeRequest( - params=params, - bridge_provider_id=NativeBridgeProvider.id(), - id="b-7ca71220-4336-414f-985e-bdfe11707c71", - status=BridgeRequestStatus.EXECUTION_PENDING, - quote_data=quote_data, - execution_data=execution_data, - ) + "0x9b8f8998b1cd8f256914751606f772bee9ebbf459b3a1c8ca177838597464739", + ), + ( + { + "from": { + "chain": "ethereum", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0x54330d28ca3357F294334BDC454a032e7f353416", # Wrong address + "token": "0x0000000000000000000000000000000000000000", + "amount": 30750000000000000, + }, + }, + "b-7ca71220-4336-414f-985e-bdfe11707c71", + "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba", + "0x5718e6f0da2e0b1a02bcb53db239cef49a731f9f52cccf193f7d0abe62e971d4", + ), + ( + { + "from": { + "chain": "ethereum", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "base", + "address": "0xC0a12402089ce761E6496892AF4754350639bf94", + "token": "0x54330d28ca3357F294334BDC454a032e7f353416", + "amount": 100000000000000000000, + }, + }, + "b-fef67eea-d55c-45f0-8b5b-e7987c843ced", + "0x7cefa52970f4e1b12a07b9795b8f03de2bbc2ee7c42cba5913d923316e96b3c5", # Wrong from_tx_hash + "0xf4ccb5f6547c188e638ac3d84f80158e3d7462211e15bc3657f8585b0bbffb68", + ), + ], + ) + def test_update_execution_status_fail( + self, + tmp_path: Path, + password: str, + request_id: str, + params: dict, + from_tx_hash: str, + expected_to_tx_hash: str, + ) -> None: + """Parametrized test for update_execution_status.""" - bridge.get_status_json(bridge_request) + DEFAULT_RPCS[Chain.ETHEREUM] = "https://rpc-gate.autonolas.tech/ethereum-rpc/" + DEFAULT_RPCS[Chain.BASE] = "https://rpc-gate.autonolas.tech/base-rpc/" - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong execution status." + operate = OperateApp(home=tmp_path / OPERATE) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - params = { - "from": { - "chain": "ethereum", - "address": "0xC0a12402089ce761E6496892AF4754350639bf94", - "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", - }, - "to": { - "chain": "base", - "address": "0xC0a12402089ce761E6496892AF4754350639bf94", - "token": "0x54330d28ca3357F294334BDC454a032e7f353416", - "amount": 100000000000000000000, - }, - } + bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) quote_data = QuoteData( attempts=0, @@ -1059,23 +1009,20 @@ def test_update_execution_status(self, tmp_path: Path, password: str) -> None: elapsed_time=0, message=None, timestamp=0, - tx_hashes=[ - "0x0de7870695400316eb3f71b3ea7d0e44cd4890dc6aa33b9ffdb5d35b05852232", - "0x4a755c455f029a645f5bfe3fcd999c24acbde49991cb54f5b9b8fcf286ad2ac0", - ], + from_tx_hash=from_tx_hash, + to_tx_hash=None, ) bridge_request = BridgeRequest( params=params, bridge_provider_id=NativeBridgeProvider.id(), - id="b-fef67eea-d55c-45f0-8b5b-e7987c843ced", + id=request_id, status=BridgeRequestStatus.EXECUTION_PENDING, quote_data=quote_data, execution_data=execution_data, ) - bridge.get_status_json(bridge_request) + bridge.status_json(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong execution status." + assert bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED, "Wrong execution status." + assert execution_data.to_tx_hash is None, "Wrong to_tx_hash." \ No newline at end of file From a93817d81a7d56915608b97c86070f5df74e25b3 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 13 May 2025 21:33:13 +0200 Subject: [PATCH 107/173] chore: update --- operate/bridge/providers/bridge_provider.py | 58 +++++++++++++------ .../bridge/providers/lifi_bridge_provider.py | 16 ++--- .../providers/native_bridge_provider.py | 10 ++-- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 41121a952..82e8c2d8d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -244,7 +244,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: total_token = 0 for tx_label, tx in transactions: - tx = self._update_with_gas_pricing(tx, from_ledger_api) + self._update_with_gas_pricing(tx, from_ledger_api) gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" gas_fees = tx.get(gas_key, 0) * tx["gas"] tx_value = int(tx.get("value", 0)) @@ -410,33 +410,55 @@ def status_json(self, bridge_request: BridgeRequest) -> t.Dict: return {"message": None, "status": bridge_request.status.value} + # TODO backport to open aea/autonomy # TODO This gas pricing management should possibly be done at a lower level in the library @staticmethod - def _update_with_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: - output_tx = tx.copy() - output_tx.pop("maxFeePerGas", None) - output_tx.pop("gasPrice", None) - output_tx.pop("maxPriorityFeePerGas", None) + def _update_with_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> None: + tx.pop("maxFeePerGas", None) + tx.pop("gasPrice", None) + tx.pop("maxPriorityFeePerGas", None) gas_pricing = ledger_api.try_get_gas_pricing() if gas_pricing is None: raise RuntimeError("Unable to retrieve gas pricing.") if "maxFeePerGas" in gas_pricing and "maxPriorityFeePerGas" in gas_pricing: - output_tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] - output_tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] + tx["maxFeePerGas"] = gas_pricing["maxFeePerGas"] + tx["maxPriorityFeePerGas"] = gas_pricing["maxPriorityFeePerGas"] elif "gasPrice" in gas_pricing: - output_tx["gasPrice"] = gas_pricing["gasPrice"] + tx["gasPrice"] = gas_pricing["gasPrice"] else: raise RuntimeError("Retrieved invalid gas pricing.") - return output_tx + # TODO backport to open aea/autonomy + @staticmethod + def _update_with_gas_estimate(tx: t.Dict, ledger_api: LedgerApi) -> None: + original_gas = tx.get("gas", 1) + tx["gas"] = 1 + ledger_api.update_with_gas_estimate(tx) + + if tx["gas"] > 1: + return + + print("FAILED GAS ESTIMATION 1") + + + + original_from = tx["from"] + tx["from"] = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + ledger_api.update_with_gas_estimate(tx) + tx["from"] = original_from + + from icecream import ic + ic(tx) + if tx["gas"] > 1: + print("WORKED !!!!") + return + print("---------------- FAILED GAS ESTIMATION 2") + + + + tx["gas"] = original_gas - # @staticmethod - # def _update_with_gas_estimate(tx: t.Dict, ledger_api: LedgerApi) -> t.Dict: - # original_from = tx["from"] - # original_gas = tx["gas"] - # tx["gas"] = 1 - # ledger_api.update_with_gas_estimate(tx) - # if tx["gas"] == 1: - # tx["from"] = + from icecream import ic + ic(tx) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 4acb9cc89..402842a13 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -177,9 +177,8 @@ def quote(self, bridge_request: BridgeRequest) -> None: time.sleep(2) - @staticmethod def _get_bridge_tx( - bridge_request: BridgeRequest, ledger_api: LedgerApi + self, bridge_request: BridgeRequest, ledger_api: LedgerApi ) -> t.Optional[t.Dict]: quote_data = bridge_request.quote_data if not quote_data: @@ -208,12 +207,12 @@ def _get_bridge_tx( transaction_request["from"] ), } - ledger_api.update_with_gas_estimate(bridge_tx) - return LiFiBridgeProvider._update_with_gas_pricing(bridge_tx, ledger_api) + self._update_with_gas_estimate(bridge_tx, ledger_api) + self._update_with_gas_pricing(bridge_tx, ledger_api) + return bridge_tx - @staticmethod def _get_approve_tx( - bridge_request: BridgeRequest, ledger_api: LedgerApi + self, bridge_request: BridgeRequest, ledger_api: LedgerApi ) -> t.Optional[t.Dict]: quote_data = bridge_request.quote_data if not quote_data: @@ -243,8 +242,9 @@ def _get_approve_tx( sender=transaction_request["from"], amount=from_amount, ) - ledger_api.update_with_gas_estimate(approve_tx) - return LiFiBridgeProvider._update_with_gas_pricing(approve_tx, ledger_api) + self._update_with_gas_estimate(approve_tx, ledger_api) + self._update_with_gas_pricing(approve_tx, ledger_api) + return approve_tx def _get_transactions( self, bridge_request: BridgeRequest diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index c8c79c7e9..54754f4bd 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -189,9 +189,10 @@ def _get_bridge_tx( extra_data=extra_data, ) self.logger.info(f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}.") - ledger_api.update_with_gas_estimate(bridge_tx) + self._update_with_gas_estimate(bridge_tx, ledger_api) self.logger.info(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") - return NativeBridgeProvider._update_with_gas_pricing(bridge_tx, ledger_api) + self._update_with_gas_pricing(bridge_tx, ledger_api) + return bridge_tx def _get_approve_tx( self, bridge_request: BridgeRequest, ledger_api: LedgerApi @@ -225,9 +226,10 @@ def _get_approve_tx( self.logger.info( f"[NATIVE BRIDGE] Gas before updating {approve_tx.get('gas')}." ) - ledger_api.update_with_gas_estimate(approve_tx) + self._update_with_gas_estimate(approve_tx, ledger_api) self.logger.info(f"[NATIVE BRIDGE] Gas after updating {approve_tx.get('gas')}.") - return NativeBridgeProvider._update_with_gas_pricing(approve_tx, ledger_api) + self._update_with_gas_pricing(approve_tx, ledger_api) + return approve_tx def _get_transactions( self, bridge_request: BridgeRequest From 1d7d25fd8b6b5afae41a963eb1550b45e7508df1 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 14 May 2025 12:02:25 +0200 Subject: [PATCH 108/173] chore: update --- operate/bridge/providers/bridge_provider.py | 5 +- .../providers/native_bridge_provider.py | 49 ++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 82e8c2d8d..cbcbcd8b7 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -440,10 +440,7 @@ def _update_with_gas_estimate(tx: t.Dict, ledger_api: LedgerApi) -> None: if tx["gas"] > 1: return - print("FAILED GAS ESTIMATION 1") - - - + # Try to estimate gas with a funded address original_from = tx["from"] tx["from"] = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ledger_api.update_with_gas_estimate(tx) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 54754f4bd..34e1833de 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -26,6 +26,8 @@ import eth_abi from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts +from operate.ledger.profiles import CONTRACTS +from operate.utils.gnosis import MultiSendOperation from web3 import Web3 from operate.bridge.providers.bridge_provider import ( @@ -42,7 +44,8 @@ L1StandardBridge, ) from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge -from operate.operate_types import Chain +from operate.operate_types import Chain, ChainType +from autonomy.chain.config import ContractConfigs BLOCK_CHUNK_SIZE = 5000 @@ -188,6 +191,7 @@ def _get_bridge_tx( min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) + bridge_tx["gas"] = 160000 self.logger.info(f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}.") self._update_with_gas_estimate(bridge_tx, ledger_api) self.logger.info(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") @@ -256,6 +260,49 @@ def _get_transactions( approve_tx = self._get_approve_tx(bridge_request, from_ledger_api) + + # **************************************** + bridge_tx["operation"] = MultiSendOperation.CALL + bridge_tx.pop("gas", None) + bridge_tx.pop("gasPrice", None) + bridge_tx.pop("maxFeePerGas", None) + bridge_tx.pop("maxPriorityFeePerGas", None) + + from icecream import ic + if approve_tx: + approve_tx["operation"] = MultiSendOperation.CALL + txs = [approve_tx, bridge_tx] + else: + txs = [bridge_tx] + + ic(txs) + ic(ContractConfigs.multisend.contracts[ChainType.ETHEREUM]) + from_chain = bridge_request.params["from"]["chain"] + multisend_data = bytes.fromhex( + registry_contracts.multisend.get_tx_data( + ledger_api=self._from_ledger_api(bridge_request), + contract_address=CONTRACTS[Chain(from_chain)]["multisend"], + multi_send_txs=txs, + ).get("data")[2:] + ) + + hash_payload_to_hex( + safe_tx_hash=tx_hash, + ether_value=ETHER_VALUE, + safe_tx_gas=SAFE_TX_GAS, + operation=SafeOperation.DELEGATE_CALL.value, + to_address=self.params.multisend_address, + data=tx_data, + ) + print("*"*40) + ic(multisend_data) + print(len(multisend_data)) + print("="*40) + + # **************************************** + + + if approve_tx: bridge_tx["nonce"] = approve_tx["nonce"] + 1 return [ From 07d465ebffda9ed9f0b6ad2050e63479d3f6be47 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 14 May 2025 13:22:50 +0200 Subject: [PATCH 109/173] chore: debug --- operate/bridge/providers/bridge_provider.py | 20 ++++++--- .../providers/native_bridge_provider.py | 44 ------------------- 2 files changed, 13 insertions(+), 51 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index cbcbcd8b7..3b285c7dc 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -433,29 +433,35 @@ def _update_with_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> None: # TODO backport to open aea/autonomy @staticmethod def _update_with_gas_estimate(tx: t.Dict, ledger_api: LedgerApi) -> None: + + from icecream import ic + ic(tx) original_gas = tx.get("gas", 1) tx["gas"] = 1 ledger_api.update_with_gas_estimate(tx) if tx["gas"] > 1: + print("!!!!!!!!!!!! WORKED 1 !!!!!!!!!!!!!") + ic(tx) return + + # ********************** # Try to estimate gas with a funded address original_from = tx["from"] tx["from"] = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ledger_api.update_with_gas_estimate(tx) tx["from"] = original_from - from icecream import ic - ic(tx) + if tx["gas"] > 1: - print("WORKED !!!!") + print("!!!!!!!!!!!! WORKED 2 !!!!!!!!!!!!!") + ic(tx) return - print("---------------- FAILED GAS ESTIMATION 2") - + print("---------------- FAILED GAS ESTIMATION 2--------------------") + # ********************** tx["gas"] = original_gas - - from icecream import ic + print("NOT WORKED") ic(tx) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 34e1833de..7d6916236 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -191,7 +191,6 @@ def _get_bridge_tx( min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) - bridge_tx["gas"] = 160000 self.logger.info(f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}.") self._update_with_gas_estimate(bridge_tx, ledger_api) self.logger.info(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") @@ -260,49 +259,6 @@ def _get_transactions( approve_tx = self._get_approve_tx(bridge_request, from_ledger_api) - - # **************************************** - bridge_tx["operation"] = MultiSendOperation.CALL - bridge_tx.pop("gas", None) - bridge_tx.pop("gasPrice", None) - bridge_tx.pop("maxFeePerGas", None) - bridge_tx.pop("maxPriorityFeePerGas", None) - - from icecream import ic - if approve_tx: - approve_tx["operation"] = MultiSendOperation.CALL - txs = [approve_tx, bridge_tx] - else: - txs = [bridge_tx] - - ic(txs) - ic(ContractConfigs.multisend.contracts[ChainType.ETHEREUM]) - from_chain = bridge_request.params["from"]["chain"] - multisend_data = bytes.fromhex( - registry_contracts.multisend.get_tx_data( - ledger_api=self._from_ledger_api(bridge_request), - contract_address=CONTRACTS[Chain(from_chain)]["multisend"], - multi_send_txs=txs, - ).get("data")[2:] - ) - - hash_payload_to_hex( - safe_tx_hash=tx_hash, - ether_value=ETHER_VALUE, - safe_tx_gas=SAFE_TX_GAS, - operation=SafeOperation.DELEGATE_CALL.value, - to_address=self.params.multisend_address, - data=tx_data, - ) - print("*"*40) - ic(multisend_data) - print(len(multisend_data)) - print("="*40) - - # **************************************** - - - if approve_tx: bridge_tx["nonce"] = approve_tx["nonce"] + 1 return [ From 3495ddaa4d48628a9dae40643ed76b017dff03da Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 14 May 2025 19:35:00 +0200 Subject: [PATCH 110/173] chore: fix gas --- operate/bridge/bridge.py | 4 +- operate/bridge/providers/bridge_provider.py | 34 +++++--------- .../bridge/providers/lifi_bridge_provider.py | 2 +- .../providers/native_bridge_provider.py | 29 +++++++----- .../contracts/l1_standard_bridge/contract.py | 47 +++++++++++++++++-- .../l1_standard_bridge/contract.yaml | 2 +- operate/utils/__init__.py | 17 +++---- tests/test_bridge_providers.py | 33 +++++++++---- tests/test_utils.py | 9 ---- 9 files changed, 105 insertions(+), 72 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 11b422653..c41f7c496 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -32,7 +32,6 @@ from aea.helpers.logging import setup_logger from deepdiff import DeepDiff -from merge_dicts import merge_sum_dicts, subtract_dicts from operate.bridge.providers.bridge_provider import BridgeProvider, BridgeRequest from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider from operate.bridge.providers.native_bridge_provider import ( @@ -43,6 +42,7 @@ from operate.operate_types import Chain from operate.resource import LocalResource from operate.services.manage import get_assets_balances +from operate.utils import merge_sum_dicts, subtract_dicts from operate.wallet.master import MasterWalletManager @@ -243,7 +243,7 @@ def bridge_refill_requirements( bridge_total_requirements = self.bridge_total_requirements(bundle) - bridge_refill_requirements = subtract_dicts(bridge_total_requirements, balances) + bridge_refill_requirements: t.Dict[str, t.Dict[str, t.Dict[str, int]]] = subtract_dicts(bridge_total_requirements, balances) is_refill_required = any( amount > 0 diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 3b285c7dc..20ed7b418 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -28,6 +28,7 @@ import uuid from abc import ABC, abstractmethod from dataclasses import dataclass +from math import ceil from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger @@ -44,6 +45,8 @@ from operate.wallet.master import MasterWalletManager +PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + DEFAULT_MAX_QUOTE_RETRIES = 3 BRIDGE_REQUEST_PREFIX = "b-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." @@ -109,12 +112,13 @@ class BridgeProvider(ABC): """(Abstract) BridgeProvider. Expected usage: - 1. params = {...} - 2. request = bridge.create_request(params) - 3. bridge.quote(request) - 4. bridge.requirements(request) - 5. bridge.execute(request) - 6. bridge.status_json(request) + params = {...} + + 1. request = bridge.create_request(params) + 2. bridge.quote(request) + 3. bridge.requirements(request) + 4. bridge.execute(request) + 5. bridge.status_json(request) Derived classes must implement the following methods: - description @@ -433,35 +437,19 @@ def _update_with_gas_pricing(tx: t.Dict, ledger_api: LedgerApi) -> None: # TODO backport to open aea/autonomy @staticmethod def _update_with_gas_estimate(tx: t.Dict, ledger_api: LedgerApi) -> None: - - from icecream import ic - ic(tx) original_gas = tx.get("gas", 1) tx["gas"] = 1 ledger_api.update_with_gas_estimate(tx) if tx["gas"] > 1: - print("!!!!!!!!!!!! WORKED 1 !!!!!!!!!!!!!") - ic(tx) return - - # ********************** - # Try to estimate gas with a funded address original_from = tx["from"] - tx["from"] = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + tx["from"] = PLACEHOLDER_NATIVE_TOKEN_ADDRESS ledger_api.update_with_gas_estimate(tx) tx["from"] = original_from - if tx["gas"] > 1: - print("!!!!!!!!!!!! WORKED 2 !!!!!!!!!!!!!") - ic(tx) return - print("---------------- FAILED GAS ESTIMATION 2--------------------") - # ********************** - tx["gas"] = original_gas - print("NOT WORKED") - ic(tx) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 402842a13..b9f704d19 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -316,7 +316,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: response.raise_for_status() except Exception as e: self.logger.error( - f"[LI.FI BRIDGE] Failed to update bridge status for {tx_hash}: {e}" + f"[LI.FI BRIDGE] Failed to update bridge status for {bridge_request.id}: {e}" ) if lifi_status == LiFiTransactionStatus.DONE: diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 7d6916236..9beaeb197 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -22,12 +22,11 @@ import time import typing as t +from math import ceil import eth_abi from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts -from operate.ledger.profiles import CONTRACTS -from operate.utils.gnosis import MultiSendOperation from web3 import Web3 from operate.bridge.providers.bridge_provider import ( @@ -44,11 +43,11 @@ L1StandardBridge, ) from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge -from operate.operate_types import Chain, ChainType -from autonomy.chain.config import ContractConfigs +from operate.operate_types import Chain BLOCK_CHUNK_SIZE = 5000 +GAS_ESTIMATE_BUFFER = 1.10 NATIVE_BRIDGE_ENDPOINTS: t.Dict[t.Any, t.Dict[str, t.Any]] = { (Chain.ETHEREUM.value, Chain.BASE.value): { @@ -191,10 +190,13 @@ def _get_bridge_tx( min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) - self.logger.info(f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}.") - self._update_with_gas_estimate(bridge_tx, ledger_api) - self.logger.info(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") self._update_with_gas_pricing(bridge_tx, ledger_api) + self.logger.debug( + f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}." + ) + self._update_with_gas_estimate(bridge_tx, ledger_api) + self.logger.debug(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") + bridge_tx["gas"] = ceil(bridge_tx["gas"] * GAS_ESTIMATE_BUFFER) return bridge_tx def _get_approve_tx( @@ -226,12 +228,15 @@ def _get_approve_tx( amount=to_amount, ) approve_tx["gas"] = 200_000 # TODO backport to ERC20 contract as default - self.logger.info( + self._update_with_gas_pricing(approve_tx, ledger_api) + self.logger.debug( f"[NATIVE BRIDGE] Gas before updating {approve_tx.get('gas')}." ) self._update_with_gas_estimate(approve_tx, ledger_api) - self.logger.info(f"[NATIVE BRIDGE] Gas after updating {approve_tx.get('gas')}.") - self._update_with_gas_pricing(approve_tx, ledger_api) + self.logger.debug( + f"[NATIVE BRIDGE] Gas after updating {approve_tx.get('gas')}." + ) + approve_tx["gas"] = ceil(approve_tx["gas"] * GAS_ESTIMATE_BUFFER) return approve_tx def _get_transactions( @@ -314,9 +319,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: return # Get the timestamp of the bridge_tx on the 'from' chain - bridge_tx_receipt = from_w3.eth.get_transaction_receipt( - from_tx_hash - ) + bridge_tx_receipt = from_w3.eth.get_transaction_receipt(from_tx_hash) bridge_tx_block = from_w3.eth.get_block(bridge_tx_receipt.blockNumber) bridge_tx_ts = bridge_tx_block.timestamp diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index a6928e7b1..57d84d19c 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -19,13 +19,25 @@ """This module contains the class to connect to the `L1StandardBridge` contract.""" +from math import ceil + from aea.common import JSONLike from aea.configurations.base import PublicId from aea.contracts.base import Contract from aea.crypto.base import LedgerApi -DEFAULT_BRIDGE_MIN_GAS_LIMIT = 300000 +PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + +# Minimum gas required for L2 execution when bridging from L1. +# Prevents underfunded messages that could fail on L2. +DEFAULT_BRIDGE_MIN_GAS_LIMIT = 300_000 + +DEFAULT_GAS_BRIDGE_ETH_TO = 800_000 +DEFAULT_GAS_BRIDGE_ERC20_TO = 800_000 + +# By simulations, nonzero-ERC20-bridge gas ~ 1.05 zero-ERC20-bridge gas +NONZERO_ERC20_GAS_FACTOR = 1.15 class L1StandardBridge(Contract): @@ -55,7 +67,7 @@ def build_bridge_eth_to_tx( { "from": sender, "value": amount, - "gas": 1_000_000, + "gas": DEFAULT_GAS_BRIDGE_ETH_TO, "gasPrice": ledger_api.api.eth.gas_price, "nonce": ledger_api.api.eth.get_transaction_count(sender), } @@ -88,12 +100,39 @@ def build_bridge_erc20_to_tx( ).build_transaction( { "from": sender, - "gas": 1_000_000, + "gas": 1, "gasPrice": ledger_api.api.eth.gas_price, "nonce": ledger_api.api.eth.get_transaction_count(sender), } ) - return ledger_api.update_with_gas_estimate( + + ledger_api.update_with_gas_estimate( transaction=tx, raise_on_try=raise_on_try, ) + + if tx["gas"] > 1: + return tx + + tx_zero = contract_instance.functions.bridgeERC20To( + local_token, remote_token, to, 0, min_gas_limit, extra_data + ).build_transaction( + { + "from": PLACEHOLDER_NATIVE_TOKEN_ADDRESS, + "gas": 1, + "gasPrice": ledger_api.api.eth.gas_price, + "nonce": ledger_api.api.eth.get_transaction_count(sender), + } + ) + + ledger_api.update_with_gas_estimate( + transaction=tx_zero, + raise_on_try=raise_on_try, + ) + + if tx_zero["gas"] > 1: + tx["gas"] = ceil(tx_zero["gas"] * NONZERO_ERC20_GAS_FACTOR) + return tx + + tx["gas"] = DEFAULT_GAS_BRIDGE_ERC20_TO + return tx diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index 9301077bf..5282c6184 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeifwcvdi2r7c6qdbpxqcvdb37sg5eawukoh4iqv7yamkkwhmhu4njq + contract.py: bafybeihze4ykbmmip33zbds2fxj3y3gnv4rhn2hlsatf2nrcmcnlhlnvje fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge diff --git a/operate/utils/__init__.py b/operate/utils/__init__.py index 8a23aeed7..eb45bccab 100644 --- a/operate/utils/__init__.py +++ b/operate/utils/__init__.py @@ -51,7 +51,7 @@ def create_backup(path: Path) -> Path: NestedDict = t.Union[int, t.Dict[str, "NestedDict"]] -def merge_sum_dicts(*dicts: NestedDict) -> NestedDict: +def merge_sum_dicts(*dicts: t.List[t.Dict[str, NestedDict]]) -> t.Dict[str, NestedDict]: """ Merge a list of nested dicts by summing all innermost `int` values. @@ -59,20 +59,20 @@ def merge_sum_dicts(*dicts: NestedDict) -> NestedDict: Missing values are treated as 0. All `dicts` must follow the same nesting structure. """ - if all(isinstance(o, int) for o in dicts): - return sum(dicts) # type: ignore result: t.Dict[str, NestedDict] = {} for d in dicts: for k, v in d.items(): # type: ignore if isinstance(v, dict): - result[k] = merge_sum_dicts(result.get(k, {}), v) + result[k] = merge_sum_dicts(result.get(k, {}), v) # type: ignore elif isinstance(v, int): result[k] = result.get(k, 0) + v # type: ignore return result -def subtract_dicts(a: NestedDict, b: NestedDict) -> NestedDict: +def subtract_dicts( + a: t.Dict[str, NestedDict], b: t.Dict[str, NestedDict] +) -> t.Dict[str, NestedDict]: """ Recursively subtract values in `b` from `a`. Negative results are upper bounded at 0. @@ -80,17 +80,14 @@ def subtract_dicts(a: NestedDict, b: NestedDict) -> NestedDict: Missing values are treated as 0. All `dicts` must follow the same nesting structure. """ - if isinstance(a, int) and isinstance(b, int): - return max(a - b, 0) # type: ignore - result = {} + result: t.Dict[str, NestedDict] = {} for key in a.keys() | b.keys(): # type: ignore va = a.get(key) # type: ignore vb = b.get(key) # type: ignore if isinstance(va, dict) or isinstance(vb, dict): result[key] = subtract_dicts( - va if isinstance(va, dict) else {}, - vb if isinstance(vb, dict) else {} + va if isinstance(va, dict) else {}, vb if isinstance(vb, dict) else {} ) else: result[key] = max((va or 0) - (vb or 0), 0) # type: ignore diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 465e383ab..1cd76639e 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -24,11 +24,11 @@ import time from pathlib import Path -from operate import wallet import pytest from deepdiff import DeepDiff from web3 import Web3 +from operate import wallet from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, LiFiBridgeProvider, @@ -536,6 +536,7 @@ def test_bridge_zero( for _ in range(2): bridge.quote(bridge_request=bridge_request) + assert bridge_request.quote_data is not None, "Wrong bridge request." expected_quote_data.timestamp = bridge_request.quote_data.timestamp assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.status_json(bridge_request) @@ -574,6 +575,7 @@ def test_bridge_zero( expected_request.status = BridgeRequestStatus.EXECUTION_DONE bridge.execute(bridge_request=bridge_request) + assert bridge_request.execution_data is not None, "Wrong bridge request." expected_execution_data.timestamp = bridge_request.execution_data.timestamp assert bridge_request == expected_request, "Wrong bridge request." @@ -676,6 +678,7 @@ def test_bridge_execute_error( for _ in range(2): bridge.quote(bridge_request=bridge_request) + assert bridge_request.quote_data is not None, "Wrong bridge request." expected_quote_data.timestamp = bridge_request.quote_data.timestamp assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.status_json(bridge_request) @@ -692,9 +695,16 @@ def test_bridge_execute_error( # Get requirements br = bridge.bridge_requirements(bridge_request) - assert br["ethereum"][wallet_address][ZERO_ADDRESS] > 0, "Wrong bridge requirements." + assert ( + br["ethereum"][wallet_address][ZERO_ADDRESS] > 0 + ), "Wrong bridge requirements." expected_br = { - "ethereum": {wallet_address: {ZERO_ADDRESS: br["ethereum"][wallet_address][ZERO_ADDRESS], OLAS[Chain.ETHEREUM]: 1000000000000000000}} + "ethereum": { + wallet_address: { + ZERO_ADDRESS: br["ethereum"][wallet_address][ZERO_ADDRESS], + OLAS[Chain.ETHEREUM]: 1000000000000000000, + } + } } diff = DeepDiff(br, expected_br) if diff: @@ -715,8 +725,11 @@ def test_bridge_execute_error( expected_request.status = BridgeRequestStatus.EXECUTION_FAILED bridge.execute(bridge_request=bridge_request) + assert bridge_request.execution_data is not None, "Wrong bridge request." expected_execution_data.message = bridge_request.execution_data.message - expected_execution_data.elapsed_time = bridge_request.execution_data.elapsed_time + expected_execution_data.elapsed_time = ( + bridge_request.execution_data.elapsed_time + ) expected_execution_data.timestamp = bridge_request.execution_data.timestamp assert bridge_request == expected_request, "Wrong bridge request." @@ -891,11 +904,11 @@ def test_update_execution_status( bridge.status_json(bridge_request) - assert bridge_request.status == BridgeRequestStatus.EXECUTION_DONE, "Wrong execution status." + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_DONE + ), "Wrong execution status." assert execution_data.to_tx_hash == expected_to_tx_hash, "Wrong to_tx_hash." - - @pytest.mark.parametrize( ("params, request_id, from_tx_hash, expected_to_tx_hash"), [ @@ -1024,5 +1037,7 @@ def test_update_execution_status_fail( bridge.status_json(bridge_request) - assert bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED, "Wrong execution status." - assert execution_data.to_tx_hash is None, "Wrong to_tx_hash." \ No newline at end of file + assert ( + bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED + ), "Wrong execution status." + assert execution_data.to_tx_hash is None, "Wrong to_tx_hash." diff --git a/tests/test_utils.py b/tests/test_utils.py index 6b3efec0d..600c98a98 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -76,13 +76,6 @@ class TestUtils: {}, {"a1": {"b1": {"c1": 1, "c2": 2}, "b2": 5}}, ), - ( - 1, - 2, - 3, - 4, - 10, - ), ], ) def test_merge_sum_dicts( @@ -99,8 +92,6 @@ def test_merge_sum_dicts( ("a", "b", "expected_result"), [ ({}, {}, {}), - (10, 2, 8), - (10, 11, 0), ( {"a1": {"b1": {"c1": 10, "c2": 20}}}, {"a1": {"b1": {"c1": 1, "c2": 2}}}, From 65e53cb3c9782bae45a120cc3cf179dc94231a26 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 14 May 2025 19:43:51 +0200 Subject: [PATCH 111/173] fix: tests --- operate/bridge/bridge.py | 6 ++++-- operate/utils/__init__.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index c41f7c496..7bd123374 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -243,7 +243,9 @@ def bridge_refill_requirements( bridge_total_requirements = self.bridge_total_requirements(bundle) - bridge_refill_requirements: t.Dict[str, t.Dict[str, t.Dict[str, int]]] = subtract_dicts(bridge_total_requirements, balances) + bridge_refill_requirements: t.Dict[ + str, t.Dict[str, t.Dict[str, int]] + ] = subtract_dicts(bridge_total_requirements, balances) is_refill_required = any( amount > 0 @@ -340,7 +342,7 @@ def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: bridge = self._bridge_providers[request.bridge_provider_id] requirements.append(bridge.bridge_requirements(request)) - return merge_sum_dicts(requirements) + return merge_sum_dicts(*requirements) def quote_bundle(self, bundle: BridgeRequestBundle) -> None: """Update the bundle with the quotes.""" diff --git a/operate/utils/__init__.py b/operate/utils/__init__.py index eb45bccab..98a52f4d1 100644 --- a/operate/utils/__init__.py +++ b/operate/utils/__init__.py @@ -51,7 +51,7 @@ def create_backup(path: Path) -> Path: NestedDict = t.Union[int, t.Dict[str, "NestedDict"]] -def merge_sum_dicts(*dicts: t.List[t.Dict[str, NestedDict]]) -> t.Dict[str, NestedDict]: +def merge_sum_dicts(*dicts: t.Dict[str, NestedDict]) -> t.Dict[str, NestedDict]: """ Merge a list of nested dicts by summing all innermost `int` values. From 7e9177c87d6cb2d2453f2c877555e0c295f8c037 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 14 May 2025 20:19:45 +0200 Subject: [PATCH 112/173] fix: linters --- operate/bridge/bridge.py | 7 ++++--- operate/bridge/providers/bridge_provider.py | 12 +++++++----- tests/test_bridge_providers.py | 5 ++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 7bd123374..f0d631f62 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -243,9 +243,10 @@ def bridge_refill_requirements( bridge_total_requirements = self.bridge_total_requirements(bundle) - bridge_refill_requirements: t.Dict[ - str, t.Dict[str, t.Dict[str, int]] - ] = subtract_dicts(bridge_total_requirements, balances) + bridge_refill_requirements = cast( + t.Dict[str, t.Dict[str, t.Dict[str, int]]], + subtract_dicts(bridge_total_requirements, balances), + ) is_refill_required = any( amount > 0 diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 20ed7b418..d46832272 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -28,7 +28,6 @@ import uuid from abc import ABC, abstractmethod from dataclasses import dataclass -from math import ceil from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger @@ -248,17 +247,20 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: total_token = 0 for tx_label, tx in transactions: + self.logger.debug( + f"[BRIDGE PROVIDER] Processing transaction {tx_label} for bridge request {bridge_request.id}." + ) self._update_with_gas_pricing(tx, from_ledger_api) gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" gas_fees = tx.get(gas_key, 0) * tx["gas"] tx_value = int(tx.get("value", 0)) total_native += tx_value + gas_fees - self.logger.info( - f"[BRIDGE PROVIDER] Transaction {tx_label}: {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}" + self.logger.debug( + f"[BRIDGE PROVIDER] Transaction {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}" ) - self.logger.info(f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.gas_price=}") - self.logger.info( + self.logger.debug(f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.gas_price=}") + self.logger.debug( f"[BRIDGE PROVIDER] {from_ledger_api.api.eth.get_block('latest').baseFeePerGas=}" ) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 1cd76639e..1ab4b391c 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -28,7 +28,6 @@ from deepdiff import DeepDiff from web3 import Web3 -from operate import wallet from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, BridgeRequest, LiFiBridgeProvider, @@ -777,7 +776,7 @@ def test_find_block_before_timestamp( assert block == expected_block, f"Expected block {expected_block}, got {block}." @pytest.mark.parametrize( - ("params, request_id, from_tx_hash, expected_to_tx_hash"), + ("params", "request_id", "from_tx_hash", "expected_to_tx_hash"), [ ( { @@ -910,7 +909,7 @@ def test_update_execution_status( assert execution_data.to_tx_hash == expected_to_tx_hash, "Wrong to_tx_hash." @pytest.mark.parametrize( - ("params, request_id, from_tx_hash, expected_to_tx_hash"), + ("params", "request_id", "from_tx_hash", "expected_to_tx_hash"), [ ( { From 01a2cd78f7ccf22cba50cb6f471543c8cadaf87f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 14 May 2025 20:25:02 +0200 Subject: [PATCH 113/173] fix: linters --- operate/bridge/providers/bridge_provider.py | 2 +- operate/data/contracts/l1_standard_bridge/contract.py | 2 +- operate/data/contracts/l1_standard_bridge/contract.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index d46832272..1d7c81447 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -44,7 +44,7 @@ from operate.wallet.master import MasterWalletManager -PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" +PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" # nosec DEFAULT_MAX_QUOTE_RETRIES = 3 BRIDGE_REQUEST_PREFIX = "b-" diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index 57d84d19c..89d6f87e6 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -27,7 +27,7 @@ from aea.crypto.base import LedgerApi -PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" +PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" # nosec # Minimum gas required for L2 execution when bridging from L1. # Prevents underfunded messages that could fail on L2. diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index 5282c6184..bf89a8ddd 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeihze4ykbmmip33zbds2fxj3y3gnv4rhn2hlsatf2nrcmcnlhlnvje + contract.py: bafybeidyutctyfurwyx44lz7ynj3ohmbqyyrwtfj3bt2tinkgym2hwnsvu fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge From 0a63933a6c13ab7a02ecd38a8c6d68ba6426fd8c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 15 May 2025 11:50:28 +0200 Subject: [PATCH 114/173] chore: refactor bridge data --- operate/bridge/providers/bridge_provider.py | 22 ++++-- .../providers/native_bridge_provider.py | 68 ++++++++++--------- tests/test_bridge_providers.py | 19 ++---- 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 1d7c81447..d186dbb6b 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -50,6 +50,16 @@ BRIDGE_REQUEST_PREFIX = "b-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." MESSAGE_EXECUTION_SKIPPED = "Execution skipped." +MESSAGE_EXECUTION_FAILED = "Execution failed:" +MESSAGE_EXECUTION_FAILED_ETA = f"{MESSAGE_EXECUTION_FAILED} ETA exceeded." +MESSAGE_EXECUTION_FAILED_QUOTE_FAILED = f"{MESSAGE_EXECUTION_FAILED} quote failed." +MESSAGE_EXECUTION_FAILED_REVERTED = ( + f"{MESSAGE_EXECUTION_FAILED} bridge transaction reverted." +) +MESSAGE_EXECUTION_FAILED_SETTLEMENT = ( + f"{MESSAGE_EXECUTION_FAILED} transaction settlement failed." +) + ERC20_APPROVE_SELECTOR = ( "0x095ea7b3" # 4 first bytes of Keccak('approve(address,uint256)') ) @@ -59,13 +69,11 @@ class QuoteData(LocalResource): """QuoteData""" - attempts: int bridge_eta: t.Optional[int] elapsed_time: float message: t.Optional[str] - response: t.Optional[t.Dict] - response_status: int timestamp: int + provider_data: t.Optional[t.Dict] # Provider-specific data @dataclass @@ -325,6 +333,9 @@ def execute(self, bridge_request: BridgeRequest) -> None: if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: bridge_request.status = BridgeRequestStatus.EXECUTION_DONE else: + bridge_request.execution_data.message = ( + MESSAGE_EXECUTION_FAILED_QUOTE_FAILED + ) bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return @@ -368,13 +379,16 @@ def execute(self, bridge_request: BridgeRequest) -> None: if len(tx_hashes) == len(txs): bridge_request.status = BridgeRequestStatus.EXECUTION_PENDING else: + bridge_request.execution_data.message = ( + MESSAGE_EXECUTION_FAILED_SETTLEMENT + ) bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED except Exception as e: # pylint: disable=broad-except self.logger.error(f"[BRIDGE] Error executing bridge request: {e}") execution_data = ExecutionData( elapsed_time=time.time() - timestamp, - message=f"Error executing quote: {str(e)}", + message=f"{MESSAGE_EXECUTION_FAILED} {str(e)}", timestamp=int(timestamp), from_tx_hash=None, to_tx_hash=None, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 9beaeb197..14abd7375 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -25,7 +25,6 @@ from math import ceil import eth_abi -from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts from web3 import Web3 @@ -33,6 +32,9 @@ BridgeProvider, BridgeRequest, BridgeRequestStatus, + MESSAGE_EXECUTION_FAILED, + MESSAGE_EXECUTION_FAILED_ETA, + MESSAGE_EXECUTION_FAILED_REVERTED, MESSAGE_QUOTE_ZERO, QuoteData, ) @@ -136,20 +138,16 @@ def quote(self, bridge_request: BridgeRequest) -> None: message = MESSAGE_QUOTE_ZERO quote_data = QuoteData( - attempts=0, bridge_eta=bridge_eta, elapsed_time=0, message=message, - response=None, - response_status=0, + provider_data=None, timestamp=int(time.time()), ) bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE - def _get_bridge_tx( - self, bridge_request: BridgeRequest, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: + def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: self.logger.info( f"[NATIVE BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." ) @@ -166,11 +164,13 @@ def _get_bridge_tx( to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] + from_ledger_api = self._from_ledger_api(bridge_request) + extra_data = Web3.keccak(text=bridge_request.id) if from_token == ZERO_ADDRESS: bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_bridge_eth_to_tx( - ledger_api=ledger_api, + ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, to=to_address, @@ -180,7 +180,7 @@ def _get_bridge_tx( ) else: bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_bridge_erc20_to_tx( - ledger_api=ledger_api, + ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, local_token=from_token, @@ -190,18 +190,16 @@ def _get_bridge_tx( min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) - self._update_with_gas_pricing(bridge_tx, ledger_api) + self._update_with_gas_pricing(bridge_tx, from_ledger_api) self.logger.debug( f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}." ) - self._update_with_gas_estimate(bridge_tx, ledger_api) + self._update_with_gas_estimate(bridge_tx, from_ledger_api) self.logger.debug(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") bridge_tx["gas"] = ceil(bridge_tx["gas"] * GAS_ESTIMATE_BUFFER) return bridge_tx - def _get_approve_tx( - self, bridge_request: BridgeRequest, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: + def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: self.logger.info( f"[NATIVE BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." ) @@ -216,23 +214,24 @@ def _get_approve_tx( to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] + from_ledger_api = self._from_ledger_api(bridge_request) if from_token == ZERO_ADDRESS: return None approve_tx = registry_contracts.erc20.get_approve_tx( - ledger_api=ledger_api, + ledger_api=from_ledger_api, contract_address=from_token, spender=from_bridge, sender=from_address, amount=to_amount, ) approve_tx["gas"] = 200_000 # TODO backport to ERC20 contract as default - self._update_with_gas_pricing(approve_tx, ledger_api) + self._update_with_gas_pricing(approve_tx, from_ledger_api) self.logger.debug( f"[NATIVE BRIDGE] Gas before updating {approve_tx.get('gas')}." ) - self._update_with_gas_estimate(approve_tx, ledger_api) + self._update_with_gas_estimate(approve_tx, from_ledger_api) self.logger.debug( f"[NATIVE BRIDGE] Gas after updating {approve_tx.get('gas')}." ) @@ -255,34 +254,29 @@ def _get_transactions( if bridge_request.params["to"]["amount"] == 0: return [] - from_ledger_api = self._from_ledger_api(bridge_request) - - bridge_tx = self._get_bridge_tx(bridge_request, from_ledger_api) + bridge_tx = self._get_bridge_tx(bridge_request) if not bridge_tx: return [] - approve_tx = self._get_approve_tx(bridge_request, from_ledger_api) + approve_tx = self._get_approve_tx(bridge_request) - if approve_tx: - bridge_tx["nonce"] = approve_tx["nonce"] + 1 + if not approve_tx: return [ - ("ERC20 Approve transaction", approve_tx), - ("Bridge transaction", bridge_tx), + ("bridge_tx", bridge_tx), ] + bridge_tx["nonce"] = approve_tx["nonce"] + 1 return [ - ("Bridge transaction", bridge_tx), + ("approve_tx", approve_tx), + ("bridge_tx", bridge_tx), ] def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" self._validate(bridge_request) - if bridge_request.status not in ( - BridgeRequestStatus.EXECUTION_PENDING, - # BridgeRequestStatus.EXECUTION_UNKNOWN, - ): + if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING,): return self.logger.info( @@ -295,6 +289,9 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ) if not bridge_request.execution_data.from_tx_hash: + bridge_request.execution_data.message = ( + f"{MESSAGE_EXECUTION_FAILED} missing transaction hash." + ) bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return @@ -315,6 +312,9 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_tx_hash = bridge_request.execution_data.from_tx_hash receipt = from_w3.eth.get_transaction_receipt(from_tx_hash) if receipt.status == 0: + bridge_request.execution_data.message = ( + MESSAGE_EXECUTION_FAILED_REVERTED + ) bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return @@ -379,14 +379,16 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.info( f"[NATIVE BRIDGE] Execution failed for {bridge_request.id}: bridge exceeds 2*ETA." ) + bridge_request.execution_data.message = MESSAGE_EXECUTION_FAILED_ETA bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return except Exception as e: self.logger.error(f"Error updating execution status: {e}") - bridge_request.status = ( - BridgeRequestStatus.EXECUTION_FAILED - ) # TODO EXECUTION_UNKNOWN ? + bridge_request.execution_data.message = ( + f"{MESSAGE_EXECUTION_FAILED} {str(e)}" + ) + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @staticmethod def _find_transaction_in_range( diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 1ab4b391c..bde8f7b4c 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -35,6 +35,7 @@ from operate.bridge.providers.bridge_provider import ( BridgeRequestStatus, ExecutionData, + MESSAGE_EXECUTION_FAILED, MESSAGE_EXECUTION_SKIPPED, MESSAGE_QUOTE_ZERO, QuoteData, @@ -522,12 +523,10 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - attempts=0, bridge_eta=NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"], elapsed_time=0, message=MESSAGE_QUOTE_ZERO, - response=None, - response_status=0, + provider_data=None, timestamp=int(time.time()), ) expected_request.quote_data = expected_quote_data @@ -664,12 +663,10 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - attempts=0, bridge_eta=NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"], elapsed_time=0, message=None, - response=None, - response_status=0, + provider_data=None, timestamp=int(time.time()), ) expected_request.quote_data = expected_quote_data @@ -733,7 +730,7 @@ def test_bridge_execute_error( assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.status_json(bridge_request) - assert "Error executing quote" in sj["message"], "Wrong execution data." + assert MESSAGE_EXECUTION_FAILED in sj["message"], "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], "tx_hash": None, # type: ignore @@ -875,12 +872,10 @@ def test_update_execution_status( bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) quote_data = QuoteData( - attempts=0, bridge_eta=0, elapsed_time=0, message=None, - response=None, - response_status=0, + provider_data=None, timestamp=0, ) @@ -1008,12 +1003,10 @@ def test_update_execution_status_fail( bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) quote_data = QuoteData( - attempts=0, bridge_eta=0, elapsed_time=0, message=None, - response=None, - response_status=0, + provider_data=None, timestamp=0, ) From a02e0d399fbc38a6d93de0e295dfbc576ccc80c5 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 15 May 2025 13:49:31 +0200 Subject: [PATCH 115/173] chore: refactor --- operate/bridge/providers/bridge_provider.py | 2 + .../bridge/providers/lifi_bridge_provider.py | 98 +++++++++++-------- .../providers/native_bridge_provider.py | 20 +--- tests/test_bridge_providers.py | 30 +++--- 4 files changed, 81 insertions(+), 69 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index d186dbb6b..c910459b3 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -64,6 +64,8 @@ "0x095ea7b3" # 4 first bytes of Keccak('approve(address,uint256)') ) +GAS_ESTIMATE_BUFFER = 1.10 + @dataclass class QuoteData(LocalResource): diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index b9f704d19..b55a714ea 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -27,7 +27,6 @@ from urllib.parse import urlencode import requests -from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts from operate.bridge.providers.bridge_provider import ( @@ -93,12 +92,10 @@ def quote(self, bridge_request: BridgeRequest) -> None: if to_amount == 0: self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") quote_data = QuoteData( - attempts=0, bridge_eta=None, elapsed_time=0, message=MESSAGE_QUOTE_ZERO, - response=None, - response_status=0, + provider_data=None, timestamp=int(time.time()), ) bridge_request.quote_data = quote_data @@ -127,12 +124,14 @@ def quote(self, bridge_request: BridgeRequest) -> None: response.raise_for_status() response_json = response.json() quote_data = QuoteData( - attempts=attempt, bridge_eta=None, elapsed_time=time.time() - start, message=None, - response=response_json, - response_status=response.status_code, + provider_data={ + "attempts": attempt, + "response": response_json, + "response_status": response.status_code, + }, timestamp=int(time.time()), ) bridge_request.quote_data = quote_data @@ -143,12 +142,14 @@ def quote(self, bridge_request: BridgeRequest) -> None: f"[LI.FI BRIDGE] Timeout request on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." ) quote_data = QuoteData( - attempts=attempt, bridge_eta=None, elapsed_time=time.time() - start, message=str(e), - response=None, - response_status=HTTPStatus.GATEWAY_TIMEOUT, + provider_data={ + "attempts": attempt, + "response": None, + "response_status": HTTPStatus.GATEWAY_TIMEOUT, + }, timestamp=int(time.time()), ) except requests.RequestException as e: @@ -157,14 +158,16 @@ def quote(self, bridge_request: BridgeRequest) -> None: ) response_json = response.json() quote_data = QuoteData( - attempts=attempt, bridge_eta=None, elapsed_time=time.time() - start, message=response_json.get("message") or str(e), - response=response_json, - response_status=getattr( - response, "status_code", HTTPStatus.BAD_GATEWAY - ), + provider_data={ + "attempts": attempt, + "response": response_json, + "response_status": getattr( + response, "status_code", HTTPStatus.BAD_GATEWAY + ), + }, timestamp=int(time.time()), ) if attempt >= DEFAULT_MAX_QUOTE_RETRIES: @@ -177,14 +180,19 @@ def quote(self, bridge_request: BridgeRequest) -> None: time.sleep(2) - def _get_bridge_tx( - self, bridge_request: BridgeRequest, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: + def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + self.logger.info( + f"[LI.FI BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." + ) + quote_data = bridge_request.quote_data if not quote_data: return None - quote = quote_data.response + if not quote_data.provider_data: + return None + + quote = quote_data.provider_data.get("response") if not quote: return None @@ -195,6 +203,8 @@ def _get_bridge_tx( if not transaction_request: return None + from_ledger_api = self._from_ledger_api(bridge_request) + bridge_tx = { "value": int(transaction_request["value"], 16), "to": transaction_request["to"], @@ -203,22 +213,27 @@ def _get_bridge_tx( "chainId": transaction_request["chainId"], "gasPrice": int(transaction_request["gasPrice"], 16), "gas": int(transaction_request["gasLimit"], 16), - "nonce": ledger_api.api.eth.get_transaction_count( + "nonce": from_ledger_api.api.eth.get_transaction_count( transaction_request["from"] ), } - self._update_with_gas_estimate(bridge_tx, ledger_api) - self._update_with_gas_pricing(bridge_tx, ledger_api) + BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) + BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) return bridge_tx - def _get_approve_tx( - self, bridge_request: BridgeRequest, ledger_api: LedgerApi - ) -> t.Optional[t.Dict]: + def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + self.logger.info( + f"[LI.FI BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." + ) + quote_data = bridge_request.quote_data if not quote_data: return None - quote = quote_data.response + if not quote_data.provider_data: + return None + + quote = quote_data.provider_data.get("response") if not quote: return None @@ -234,48 +249,51 @@ def _get_approve_tx( return None from_amount = int(quote["action"]["fromAmount"]) + from_ledger_api = self._from_ledger_api(bridge_request) approve_tx = registry_contracts.erc20.get_approve_tx( - ledger_api=ledger_api, + ledger_api=from_ledger_api, contract_address=from_token, spender=transaction_request["to"], sender=transaction_request["from"], amount=from_amount, ) - self._update_with_gas_estimate(approve_tx, ledger_api) - self._update_with_gas_pricing(approve_tx, ledger_api) + BridgeProvider._update_with_gas_pricing(approve_tx, from_ledger_api) + BridgeProvider._update_with_gas_estimate(approve_tx, from_ledger_api) return approve_tx def _get_transactions( self, bridge_request: BridgeRequest ) -> t.List[t.Tuple[str, t.Dict]]: """Get the sorted list of transactions to execute the bridge request.""" + self.logger.info( + f"[LI.FI BRIDGE] Get transactions for bridge request {bridge_request.id}." + ) + self._validate(bridge_request) if not bridge_request.quote_data: return [] - from_chain = bridge_request.params["from"]["chain"] - chain = Chain(from_chain) - wallet = self.wallet_manager.load(chain.ledger_type) - ledger_api = wallet.ledger_api(chain) + if bridge_request.params["to"]["amount"] == 0: + return [] - bridge_tx = self._get_bridge_tx(bridge_request, ledger_api) + bridge_tx = self._get_bridge_tx(bridge_request) if not bridge_tx: return [] - approve_tx = self._get_approve_tx(bridge_request, ledger_api) + approve_tx = self._get_approve_tx(bridge_request) - if approve_tx: - bridge_tx["nonce"] = approve_tx["nonce"] + 1 + if not approve_tx: return [ - ("ERC20 Approve transaction", approve_tx), - ("Bridge transaction", bridge_tx), + ("bridge_tx", bridge_tx), ] + bridge_tx["nonce"] = approve_tx["nonce"] + 1 return [ - ("Bridge transaction", bridge_tx), + ("approve_tx", approve_tx), + ("bridge_tx", bridge_tx), ] def _update_execution_status(self, bridge_request: BridgeRequest) -> None: diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 14abd7375..c1336d35b 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -32,6 +32,7 @@ BridgeProvider, BridgeRequest, BridgeRequestStatus, + GAS_ESTIMATE_BUFFER, MESSAGE_EXECUTION_FAILED, MESSAGE_EXECUTION_FAILED_ETA, MESSAGE_EXECUTION_FAILED_REVERTED, @@ -49,7 +50,6 @@ BLOCK_CHUNK_SIZE = 5000 -GAS_ESTIMATE_BUFFER = 1.10 NATIVE_BRIDGE_ENDPOINTS: t.Dict[t.Any, t.Dict[str, t.Any]] = { (Chain.ETHEREUM.value, Chain.BASE.value): { @@ -190,12 +190,8 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, extra_data=extra_data, ) - self._update_with_gas_pricing(bridge_tx, from_ledger_api) - self.logger.debug( - f"[NATIVE BRIDGE] Gas before updating {bridge_tx.get('gas')}." - ) - self._update_with_gas_estimate(bridge_tx, from_ledger_api) - self.logger.debug(f"[NATIVE BRIDGE] Gas after updating {bridge_tx.get('gas')}.") + BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) + BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) bridge_tx["gas"] = ceil(bridge_tx["gas"] * GAS_ESTIMATE_BUFFER) return bridge_tx @@ -227,14 +223,8 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: amount=to_amount, ) approve_tx["gas"] = 200_000 # TODO backport to ERC20 contract as default - self._update_with_gas_pricing(approve_tx, from_ledger_api) - self.logger.debug( - f"[NATIVE BRIDGE] Gas before updating {approve_tx.get('gas')}." - ) - self._update_with_gas_estimate(approve_tx, from_ledger_api) - self.logger.debug( - f"[NATIVE BRIDGE] Gas after updating {approve_tx.get('gas')}." - ) + BridgeProvider._update_with_gas_pricing(approve_tx, from_ledger_api) + BridgeProvider._update_with_gas_estimate(approve_tx, from_ledger_api) approve_tx["gas"] = ceil(approve_tx["gas"] * GAS_ESTIMATE_BUFFER) return approve_tx diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index bde8f7b4c..02af032b4 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -117,11 +117,10 @@ def test_bridge_zero( bridge.quote(bridge_request=bridge_request) qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.attempts == 0, "Wrong quote data." assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." - assert qd.response is None, "Wrong quote data." + assert qd.provider_data is None, "Wrong quote data." assert timestamp <= qd.timestamp, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." assert ( @@ -152,11 +151,10 @@ def test_bridge_zero( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.attempts == 0, "Wrong quote data." assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." - assert qd.response is None, "Wrong quote data." + assert qd.provider_data is None, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." @@ -254,11 +252,12 @@ def test_bridge_error( bridge.quote(bridge_request=bridge_request) qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." + assert qd.provider_data is not None, "Wrong quote data." + assert qd.provider_data.get("response") is not None, "Wrong quote data." + assert qd.provider_data.get("attempts", 0) > 0, "Wrong quote data." assert timestamp <= qd.timestamp, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." assert ( @@ -289,11 +288,12 @@ def test_bridge_error( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is not None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." + assert qd.provider_data is not None, "Wrong quote data." + assert qd.provider_data.get("response") is not None, "Wrong quote data." + assert qd.provider_data.get("attempts", 0) > 0, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." assert ( bridge_request.status == BridgeRequestStatus.QUOTE_FAILED @@ -394,11 +394,12 @@ def test_bridge_quote( bridge.quote(bridge_request=bridge_request) qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." + assert qd.provider_data is not None, "Wrong quote data." + assert qd.provider_data.get("response") is not None, "Wrong quote data." + assert qd.provider_data.get("attempts", 0) > 0, "Wrong quote data." assert timestamp <= qd.timestamp, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." assert ( @@ -416,9 +417,9 @@ def test_bridge_quote( print(diff) assert not diff, "Wrong status." - assert bridge_request.quote_data.response is not None, "Missing quote data." + assert bridge_request.quote_data.provider_data is not None, "Missing quote data." - quote = bridge_request.quote_data.response + quote = bridge_request.quote_data.provider_data.get("response") br = bridge.bridge_requirements(bridge_request) expected_br = { "gnosis": { @@ -436,11 +437,12 @@ def test_bridge_quote( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.attempts > 0, "Wrong quote data." assert qd.bridge_eta is None, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." - assert qd.response is not None, "Wrong quote data." + assert qd.provider_data is not None, "Wrong quote data." + assert qd.provider_data.get("response") is not None, "Wrong quote data." + assert qd.provider_data.get("attempts", 0) > 0, "Wrong quote data." assert qd.timestamp <= int(time.time()), "Wrong quote data." assert bridge_request.status == BridgeRequestStatus.QUOTE_DONE, "Wrong status." From 295839cf21436e3a779099b3946e5d87a0e302bb Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 15 May 2025 17:26:04 +0200 Subject: [PATCH 116/173] chore: consolidate bridge classes --- operate/bridge/providers/bridge_provider.py | 50 ++++-- .../bridge/providers/lifi_bridge_provider.py | 19 +- .../providers/native_bridge_provider.py | 28 +-- tests/test_bridge_providers.py | 164 ++++++++++-------- 4 files changed, 150 insertions(+), 111 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index c910459b3..2572c5e9d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -297,13 +297,30 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: return result def execute(self, bridge_request: BridgeRequest) -> None: - """Execute the quote.""" + """Execute the request.""" + + self.logger.info( + f"[BRIDGE PROVIDER] Executing bridge request {bridge_request.id}." + ) + self._validate(bridge_request) - if bridge_request.status not in ( - BridgeRequestStatus.QUOTE_DONE, - BridgeRequestStatus.QUOTE_FAILED, - ): + if bridge_request.status in (BridgeRequestStatus.QUOTE_FAILED): + self.logger.info( + f"[BRIDGE PROVIDER] {MESSAGE_EXECUTION_FAILED_QUOTE_FAILED}." + ) + execution_data = ExecutionData( + elapsed_time=0, + message=f"{MESSAGE_EXECUTION_FAILED_QUOTE_FAILED}", + timestamp=int(time.time()), + from_tx_hash=None, + to_tx_hash=None, + ) + bridge_request.execution_data = execution_data + bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + return + + if bridge_request.status not in (BridgeRequestStatus.QUOTE_DONE,): raise RuntimeError( f"Cannot execute bridge request {bridge_request.id} with status {bridge_request.status}." ) @@ -316,33 +333,26 @@ def execute(self, bridge_request: BridgeRequest) -> None: f"Cannot execute bridge request {bridge_request.id}: execution data already present." ) - timestamp = time.time() txs = self._get_transactions(bridge_request) if not txs: self.logger.info( - f"[LI.FI BRIDGE] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" + f"[BRIDGE PROVIDER] {MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})" ) execution_data = ExecutionData( elapsed_time=0, message=f"{MESSAGE_EXECUTION_SKIPPED} ({bridge_request.status=})", - timestamp=int(timestamp), + timestamp=int(time.time()), from_tx_hash=None, to_tx_hash=None, ) bridge_request.execution_data = execution_data - - if bridge_request.status == BridgeRequestStatus.QUOTE_DONE: - bridge_request.status = BridgeRequestStatus.EXECUTION_DONE - else: - bridge_request.execution_data.message = ( - MESSAGE_EXECUTION_FAILED_QUOTE_FAILED - ) - bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED + bridge_request.status = BridgeRequestStatus.EXECUTION_DONE return try: self.logger.info(f"[BRIDGE] Executing bridge request {bridge_request.id}.") + timestamp = time.time() chain = Chain(bridge_request.params["from"]["chain"]) wallet = self.wallet_manager.load(chain.ledger_type) from_ledger_api = self._from_ledger_api(bridge_request) @@ -391,7 +401,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: execution_data = ExecutionData( elapsed_time=time.time() - timestamp, message=f"{MESSAGE_EXECUTION_FAILED} {str(e)}", - timestamp=int(timestamp), + timestamp=int(time.time()), from_tx_hash=None, to_tx_hash=None, ) @@ -432,6 +442,12 @@ def status_json(self, bridge_request: BridgeRequest) -> t.Dict: return {"message": None, "status": bridge_request.status.value} + @staticmethod + def _tx_timestamp(tx_hash: str, ledger_api: LedgerApi) -> int: + receipt = ledger_api.api.eth.get_transaction_receipt(tx_hash) + block = ledger_api.api.eth.get_block(receipt.blockNumber) + return block.timestamp + # TODO backport to open aea/autonomy # TODO This gas pricing management should possibly be done at a lower level in the library @staticmethod diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index b55a714ea..9eafe6845 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -306,19 +306,19 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ): return - if not bridge_request.execution_data: + execution_data = bridge_request.execution_data + if not execution_data: raise RuntimeError( f"Cannot update bridge request {bridge_request.id}: execution data not present." ) - execution = bridge_request.execution_data - if not execution.from_tx_hash: + if not execution_data.from_tx_hash: return url = "https://li.quest/v1/status" headers = {"accept": "application/json"} params = { - "txHash": execution.from_tx_hash, + "txHash": execution_data.from_tx_hash, } try: @@ -328,7 +328,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: lifi_status = response_json.get( "status", str(LiFiTransactionStatus.UNKNOWN) ) - execution.message = response_json.get( + execution_data.message = response_json.get( "substatusMessage", response_json.get("message") ) response.raise_for_status() @@ -338,6 +338,15 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ) if lifi_status == LiFiTransactionStatus.DONE: + self.logger.info(f"[LI.FI BRIDGE] Execution done for {bridge_request.id}.") + from_ledger_api = self._from_ledger_api(bridge_request) + from_tx_hash = execution_data.from_tx_hash + to_ledger_api = self._to_ledger_api(bridge_request) + to_tx_hash = response_json.get("receiving", {}).get("txHash") + execution_data.to_tx_hash = to_tx_hash + execution_data.elapsed_time = BridgeProvider._tx_timestamp( + to_tx_hash, to_ledger_api + ) - BridgeProvider._tx_timestamp(from_tx_hash, from_ledger_api) bridge_request.status = BridgeRequestStatus.EXECUTION_DONE elif lifi_status == LiFiTransactionStatus.FAILED: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index c1336d35b..ee6d9c385 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -278,8 +278,9 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: f"Cannot update bridge request {bridge_request.id}: execution data not present." ) - if not bridge_request.execution_data.from_tx_hash: - bridge_request.execution_data.message = ( + execution_data = bridge_request.execution_data + if not execution_data.from_tx_hash: + execution_data.message = ( f"{MESSAGE_EXECUTION_FAILED} missing transaction hash." ) bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @@ -297,14 +298,13 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_eta = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"]) try: - from_w3 = self._from_ledger_api(bridge_request).api + from_ledger_api = self._from_ledger_api(bridge_request) + from_w3 = from_ledger_api.api - from_tx_hash = bridge_request.execution_data.from_tx_hash + from_tx_hash = execution_data.from_tx_hash receipt = from_w3.eth.get_transaction_receipt(from_tx_hash) if receipt.status == 0: - bridge_request.execution_data.message = ( - MESSAGE_EXECUTION_FAILED_REVERTED - ) + execution_data.message = MESSAGE_EXECUTION_FAILED_REVERTED bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return @@ -340,7 +340,8 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: ] # Find the event on the 'to' chain - to_w3 = self._to_ledger_api(bridge_request).api + to_ledger_api = self._to_ledger_api(bridge_request) + to_w3 = to_ledger_api.api starting_block = self._find_block_before_timestamp(to_w3, bridge_tx_ts) starting_block_ts = to_w3.eth.get_block(starting_block).timestamp latest_block = to_w3.eth.block_number @@ -360,7 +361,10 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.info( f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." ) - bridge_request.execution_data.to_tx_hash = to_tx_hash + execution_data.to_tx_hash = to_tx_hash + execution_data.elapsed_time = BridgeProvider._tx_timestamp( + to_tx_hash, to_ledger_api + ) - BridgeProvider._tx_timestamp(from_tx_hash, from_ledger_api) bridge_request.status = BridgeRequestStatus.EXECUTION_DONE return @@ -369,15 +373,13 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.info( f"[NATIVE BRIDGE] Execution failed for {bridge_request.id}: bridge exceeds 2*ETA." ) - bridge_request.execution_data.message = MESSAGE_EXECUTION_FAILED_ETA + execution_data.message = MESSAGE_EXECUTION_FAILED_ETA bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return except Exception as e: self.logger.error(f"Error updating execution status: {e}") - bridge_request.execution_data.message = ( - f"{MESSAGE_EXECUTION_FAILED} {str(e)}" - ) + execution_data.message = f"{MESSAGE_EXECUTION_FAILED} {str(e)}" bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @staticmethod diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 02af032b4..c06d1f437 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -22,6 +22,7 @@ import os import time +import typing as t from pathlib import Path import pytest @@ -33,9 +34,11 @@ LiFiBridgeProvider, ) from operate.bridge.providers.bridge_provider import ( + BridgeProvider, BridgeRequestStatus, ExecutionData, MESSAGE_EXECUTION_FAILED, + MESSAGE_EXECUTION_FAILED_QUOTE_FAILED, MESSAGE_EXECUTION_SKIPPED, MESSAGE_QUOTE_ZERO, QuoteData, @@ -311,7 +314,9 @@ def test_bridge_error( assert ed is not None, "Missing execution data." assert ed.elapsed_time == 0, "Wrong execution data." assert ed.message is not None, "Wrong execution data." - assert MESSAGE_EXECUTION_SKIPPED in ed.message, "Wrong execution data." + assert ( + MESSAGE_EXECUTION_FAILED_QUOTE_FAILED in ed.message + ), "Wrong execution data." assert timestamp <= ed.timestamp, "Wrong quote data." assert ed.timestamp <= int(time.time()), "Wrong quote data." assert ed.from_tx_hash is None, "Wrong execution data." @@ -325,7 +330,9 @@ def test_bridge_error( ), "Wrong status." sj = bridge.status_json(bridge_request) - assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." + assert ( + MESSAGE_EXECUTION_FAILED_QUOTE_FAILED in sj["message"] + ), "Wrong execution data." expected_sj = { "explorer_link": sj["explorer_link"], "tx_hash": None, @@ -417,7 +424,9 @@ def test_bridge_quote( print(diff) assert not diff, "Wrong status." - assert bridge_request.quote_data.provider_data is not None, "Missing quote data." + assert ( + bridge_request.quote_data.provider_data is not None + ), "Missing quote data." quote = bridge_request.quote_data.provider_data.get("response") br = bridge.bridge_requirements(bridge_request) @@ -774,10 +783,46 @@ def test_find_block_before_timestamp( block = NativeBridgeProvider._find_block_before_timestamp(w3, timestamp) assert block == expected_block, f"Expected block {expected_block}, got {block}." + +class TestBridgeProvider: + """Tests for bridge.providers.BridgeProvider class.""" + @pytest.mark.parametrize( - ("params", "request_id", "from_tx_hash", "expected_to_tx_hash"), + ( + "bridge_provider_class", + "params", + "request_id", + "from_tx_hash", + "expected_status", + "expected_to_tx_hash", + "expected_elapsed_time", + ), [ + # LiFiBridgeProvider - EXECUTION_DONE tests + ( + LiFiBridgeProvider, + { + "from": { + "chain": "gnosis", + "address": "0xE95866Fa91ce81109aA900550133654A4795C20e", + "token": "0x0000000000000000000000000000000000000000", + }, + "to": { + "chain": "base", + "address": "0xE95866Fa91ce81109aA900550133654A4795C20e", + "token": "0x0000000000000000000000000000000000000000", + "amount": 10000000000000, + }, + }, + "b-184035d4-18b4-42e1-8983-d30f7daff1b9", + "0x333f5a51163576c9d90599bffa6b038dbec45f4f6f761b87e29ab59235403861", + BridgeRequestStatus.EXECUTION_DONE, + "0x6cd9176f1da953e4464adb8bdc81fbe4133ebcd1bb6aeac49946a38ff025e623", + 374, + ), + # NativeBridgeProvider - EXECUTION_DONE tests ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -793,9 +838,12 @@ def test_find_block_before_timestamp( }, "b-76a298b9-b243-4cfb-b48a-f59183ae0e85", "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd", + BridgeRequestStatus.EXECUTION_DONE, "0xc97722c1310b94043fb37219285cb4f80ce4189f158033b84c935ec54166eb19", + 178, ), ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -811,9 +859,12 @@ def test_find_block_before_timestamp( }, "b-7221ece2-e15e-4aec-bac2-7fd4c4d4851a", "0xa1139bb4ba963d7979417f49fed03b365c1f1bfc31d0100257caed888a491c4c", + BridgeRequestStatus.EXECUTION_DONE, "0x9b8f8998b1cd8f256914751606f772bee9ebbf459b3a1c8ca177838597464739", + 184, ), ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -829,9 +880,12 @@ def test_find_block_before_timestamp( }, "b-7ca71220-4336-414f-985e-bdfe11707c71", "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba", + BridgeRequestStatus.EXECUTION_DONE, "0x5718e6f0da2e0b1a02bcb53db239cef49a731f9f52cccf193f7d0abe62e971d4", + 198, ), ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -847,68 +901,13 @@ def test_find_block_before_timestamp( }, "b-fef67eea-d55c-45f0-8b5b-e7987c843ced", "0x4a755c455f029a645f5bfe3fcd999c24acbde49991cb54f5b9b8fcf286ad2ac0", + BridgeRequestStatus.EXECUTION_DONE, "0xf4ccb5f6547c188e638ac3d84f80158e3d7462211e15bc3657f8585b0bbffb68", + 186, ), - ], - ) - def test_update_execution_status( - self, - tmp_path: Path, - password: str, - request_id: str, - params: dict, - from_tx_hash: str, - expected_to_tx_hash: str, - ) -> None: - """Parametrized test for update_execution_status.""" - - DEFAULT_RPCS[Chain.ETHEREUM] = "https://rpc-gate.autonolas.tech/ethereum-rpc/" - DEFAULT_RPCS[Chain.BASE] = "https://rpc-gate.autonolas.tech/base-rpc/" - - operate = OperateApp(home=tmp_path / OPERATE) - operate.setup() - operate.create_user_account(password=password) - operate.password = password - operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - - bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) - - quote_data = QuoteData( - bridge_eta=0, - elapsed_time=0, - message=None, - provider_data=None, - timestamp=0, - ) - - execution_data = ExecutionData( - elapsed_time=0, - message=None, - timestamp=0, - from_tx_hash=from_tx_hash, - to_tx_hash=None, - ) - - bridge_request = BridgeRequest( - params=params, - bridge_provider_id=NativeBridgeProvider.id(), - id=request_id, - status=BridgeRequestStatus.EXECUTION_PENDING, - quote_data=quote_data, - execution_data=execution_data, - ) - - bridge.status_json(bridge_request) - - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_DONE - ), "Wrong execution status." - assert execution_data.to_tx_hash == expected_to_tx_hash, "Wrong to_tx_hash." - - @pytest.mark.parametrize( - ("params", "request_id", "from_tx_hash", "expected_to_tx_hash"), - [ + # NativeBridgeProvider - EXECUTION_FAILED tests ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -924,9 +923,12 @@ def test_update_execution_status( }, "b-76a298b9-b243-4cfb-b48a-f59183ae0e85", "0xf649cdce0075a950ed031cc32775990facdcefc8d2bfff695a8023895dd47ebd", - "0xc97722c1310b94043fb37219285cb4f80ce4189f158033b84c935ec54166eb19", + BridgeRequestStatus.EXECUTION_FAILED, + None, + 0, ), ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -942,9 +944,12 @@ def test_update_execution_status( }, "b-42", # Wrong id "0xa1139bb4ba963d7979417f49fed03b365c1f1bfc31d0100257caed888a491c4c", - "0x9b8f8998b1cd8f256914751606f772bee9ebbf459b3a1c8ca177838597464739", + BridgeRequestStatus.EXECUTION_FAILED, + None, + 0, ), ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -960,9 +965,12 @@ def test_update_execution_status( }, "b-7ca71220-4336-414f-985e-bdfe11707c71", "0xcf2b263ab1149bc6691537d09f3ed97e1ac4a8411a49ca9d81219c32f98228ba", - "0x5718e6f0da2e0b1a02bcb53db239cef49a731f9f52cccf193f7d0abe62e971d4", + BridgeRequestStatus.EXECUTION_FAILED, + None, + 0, ), ( + NativeBridgeProvider, { "from": { "chain": "ethereum", @@ -978,20 +986,25 @@ def test_update_execution_status( }, "b-fef67eea-d55c-45f0-8b5b-e7987c843ced", "0x7cefa52970f4e1b12a07b9795b8f03de2bbc2ee7c42cba5913d923316e96b3c5", # Wrong from_tx_hash - "0xf4ccb5f6547c188e638ac3d84f80158e3d7462211e15bc3657f8585b0bbffb68", + BridgeRequestStatus.EXECUTION_FAILED, + None, + 0, ), ], ) - def test_update_execution_status_fail( + def test_update_execution_status( self, tmp_path: Path, password: str, - request_id: str, + bridge_provider_class: t.Type[BridgeProvider], params: dict, + request_id: str, from_tx_hash: str, + expected_status: BridgeRequestStatus, expected_to_tx_hash: str, + expected_elapsed_time: int, ) -> None: - """Parametrized test for update_execution_status.""" + """test_update_execution_status""" DEFAULT_RPCS[Chain.ETHEREUM] = "https://rpc-gate.autonolas.tech/ethereum-rpc/" DEFAULT_RPCS[Chain.BASE] = "https://rpc-gate.autonolas.tech/base-rpc/" @@ -1002,7 +1015,7 @@ def test_update_execution_status_fail( operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + bridge = bridge_provider_class(wallet_manager=operate.wallet_manager) quote_data = QuoteData( bridge_eta=0, @@ -1022,7 +1035,7 @@ def test_update_execution_status_fail( bridge_request = BridgeRequest( params=params, - bridge_provider_id=NativeBridgeProvider.id(), + bridge_provider_id=bridge_provider_class.id(), id=request_id, status=BridgeRequestStatus.EXECUTION_PENDING, quote_data=quote_data, @@ -1031,7 +1044,6 @@ def test_update_execution_status_fail( bridge.status_json(bridge_request) - assert ( - bridge_request.status == BridgeRequestStatus.EXECUTION_FAILED - ), "Wrong execution status." - assert execution_data.to_tx_hash is None, "Wrong to_tx_hash." + assert bridge_request.status == expected_status, "Wrong execution status." + assert execution_data.to_tx_hash == expected_to_tx_hash, "Wrong to_tx_hash." + assert execution_data.elapsed_time == expected_elapsed_time, "Wrong timestamp." From 90d35c93c39c510615f4bd84bfa19b0e5a788ed0 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 15 May 2025 19:13:12 +0200 Subject: [PATCH 117/173] chore: update --- operate/bridge/providers/bridge_provider.py | 2 +- .../bridge/providers/lifi_bridge_provider.py | 4 +++ .../providers/native_bridge_provider.py | 29 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 2572c5e9d..495aefe15 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -61,7 +61,7 @@ ) ERC20_APPROVE_SELECTOR = ( - "0x095ea7b3" # 4 first bytes of Keccak('approve(address,uint256)') + "0x095ea7b3" # First4 bytes of Keccak('approve(address,uint256)') ) GAS_ESTIMATE_BUFFER = 1.10 diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 9eafe6845..e5a2c7bac 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -24,6 +24,7 @@ import time import typing as t from http import HTTPStatus +from math import ceil from urllib.parse import urlencode import requests @@ -34,6 +35,7 @@ BridgeRequest, BridgeRequestStatus, DEFAULT_MAX_QUOTE_RETRIES, + GAS_ESTIMATE_BUFFER, MESSAGE_QUOTE_ZERO, QuoteData, ) @@ -258,8 +260,10 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: sender=transaction_request["from"], amount=from_amount, ) + approve_tx["gas"] = 200_000 # TODO backport to ERC20 contract as default BridgeProvider._update_with_gas_pricing(approve_tx, from_ledger_api) BridgeProvider._update_with_gas_estimate(approve_tx, from_ledger_api) + approve_tx["gas"] = ceil(approve_tx["gas"] * GAS_ESTIMATE_BUFFER) return approve_tx def _get_transactions( diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index ee6d9c385..5fc696b30 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -56,7 +56,12 @@ "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "to_bridge": "0x4200000000000000000000000000000000000010", "bridge_eta": 5 * 60, - } + }, + (Chain.ETHEREUM.value, Chain.MODE.value): { + "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 5 * 60, + }, } L1_STANDARD_BRIDGE_CONTRACT = t.cast( @@ -65,21 +70,15 @@ directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"), ), ) -L2_STANDARD_BRIDGE_CONTRACT = t.cast( - L2StandardBridge, - L2StandardBridge.from_dir( - directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"), - ), -) -ETH_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( +EVENT_ETH_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( text="ETHBridgeFinalized(address,address,uint256,bytes)" ).hex() -ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["uint256", "bytes"] -ERC20_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( +EVENT_ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["uint256", "bytes"] +EVENT_ERC20_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( text="ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" ).hex() -ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["address", "uint256", "bytes"] +EVENT_ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["address", "uint256", "bytes"] class NativeBridgeProvider(BridgeProvider): @@ -316,23 +315,23 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: # Prepare the event data if from_token == ZERO_ADDRESS: topics = [ - ETH_BRIDGE_FINALIZED_TOPIC0, # ETHBridgeFinalized + EVENT_ETH_BRIDGE_FINALIZED_TOPIC0, # ETHBridgeFinalized "0x" + from_address.lower()[2:].rjust(64, "0"), # from "0x" + to_address.lower()[2:].rjust(64, "0"), # from ] - non_indexed_types = ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES + non_indexed_types = EVENT_ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES non_indexed_values = [ to_amount, # amount Web3.keccak(text=bridge_request.id), # extraData ] else: topics = [ - ERC20_BRIDGE_FINALIZED_TOPIC0, # ERC20BridgeFinalized + EVENT_ERC20_BRIDGE_FINALIZED_TOPIC0, # ERC20BridgeFinalized "0x" + to_token.lower()[2:].rjust(64, "0"), # localToken "0x" + from_token.lower()[2:].rjust(64, "0"), # remoteToken "0x" + from_address.lower()[2:].rjust(64, "0"), # from ] - non_indexed_types = ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES + non_indexed_types = EVENT_ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES non_indexed_values = [ to_address.lower(), # to to_amount, # amount From 3bd378f86f18162e8a1cebedaefcf8b890080582 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 15 May 2025 19:20:39 +0200 Subject: [PATCH 118/173] chore: fix validation --- operate/bridge/providers/bridge_provider.py | 7 ++++++- operate/bridge/providers/lifi_bridge_provider.py | 3 --- operate/bridge/providers/native_bridge_provider.py | 4 ---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 495aefe15..a59a38a58 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -235,6 +235,10 @@ def _get_transactions( def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: """Gets the bridge requirements to execute the quote, with updated gas estimation.""" + self.logger.info( + f"[BRIDGE PROVIDER] Bridge requirements for request {bridge_request.id}." + ) + self._validate(bridge_request) from_chain = bridge_request.params["from"]["chain"] @@ -298,7 +302,6 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: def execute(self, bridge_request: BridgeRequest) -> None: """Execute the request.""" - self.logger.info( f"[BRIDGE PROVIDER] Executing bridge request {bridge_request.id}." ) @@ -420,6 +423,8 @@ def _get_explorer_link(self, tx_hash: str) -> str: def status_json(self, bridge_request: BridgeRequest) -> t.Dict: """JSON representation of the status.""" + self._validate(bridge_request) + if bridge_request.execution_data: self._update_execution_status(bridge_request) tx_hash = None diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index e5a2c7bac..68b8040a1 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -274,8 +274,6 @@ def _get_transactions( f"[LI.FI BRIDGE] Get transactions for bridge request {bridge_request.id}." ) - self._validate(bridge_request) - if not bridge_request.quote_data: return [] @@ -302,7 +300,6 @@ def _get_transactions( def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" - self._validate(bridge_request) if bridge_request.status not in ( BridgeRequestStatus.EXECUTION_PENDING, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 5fc696b30..549f03360 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -45,7 +45,6 @@ DEFAULT_BRIDGE_MIN_GAS_LIMIT, L1StandardBridge, ) -from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge from operate.operate_types import Chain @@ -235,8 +234,6 @@ def _get_transactions( f"[NATIVE BRIDGE] Get transactions for bridge request {bridge_request.id}." ) - self._validate(bridge_request) - if not bridge_request.quote_data: return [] @@ -263,7 +260,6 @@ def _get_transactions( def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" - self._validate(bridge_request) if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING,): return From cdb1a666fae51b67b5c502a70b18bf167a63bf37 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 17 May 2025 12:16:26 +0200 Subject: [PATCH 119/173] chore: can process --- operate/bridge/bridge.py | 25 ++++--- operate/bridge/providers/bridge_provider.py | 42 ++++++++--- .../providers/native_bridge_provider.py | 74 +++++++++++++------ 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index f0d631f62..0186bc844 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -34,10 +34,7 @@ from operate.bridge.providers.bridge_provider import BridgeProvider, BridgeRequest from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider -from operate.bridge.providers.native_bridge_provider import ( - NATIVE_BRIDGE_ENDPOINTS, - NativeBridgeProvider, -) +from operate.bridge.providers.native_bridge_provider import NativeBridgeProvider from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain from operate.resource import LocalResource @@ -178,16 +175,22 @@ def _get_updated_bundle( if not bundle or create_new_bundle: self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.") + bridge_providers = [ # Sorted in order of preference + self._bridge_providers[NativeBridgeProvider.id()], + self._bridge_providers[LiFiBridgeProvider.id()], + ] bridge_requests = [] for params in requests_params: - from_chain = params["from"]["chain"] - to_chain = params["to"]["chain"] - - if (from_chain, to_chain) in NATIVE_BRIDGE_ENDPOINTS: - bridge = self._bridge_providers[NativeBridgeProvider.id()] + for bridge_provider in bridge_providers: + if bridge_provider.can_handle_request(params): + bridge_requests.append( + bridge_provider.create_request(params=params) + ) + break else: - bridge = self._bridge_providers[LiFiBridgeProvider.id()] - bridge_requests.append(bridge.create_request(params=params)) + raise RuntimeError( + f"Cannot find an appropriate bridge provider for params {params}." + ) bundle = BridgeRequestBundle( id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}", diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index a59a38a58..b9205e93d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -156,18 +156,20 @@ def description(self) -> str: return self.__class__.__name__ def _validate(self, bridge_request: BridgeRequest) -> None: - """Validate the bridge request.""" + """Validate theat the bridge request was created by this bridge.""" if bridge_request.bridge_provider_id != self.id(): raise ValueError( f"Bridge request provider id {bridge_request.bridge_provider_id} does not match the bridge provider id {self.id()}" ) - def create_request(self, params: t.Dict) -> BridgeRequest: - """Create a bridge request.""" + def can_handle_request(self, params: t.Dict) -> bool: + """Returns 'true' if the bridge can handle a request for 'params'.""" + if "from" not in params or "to" not in params: - raise ValueError( - "Invalid input: All requests must contain exactly one 'from' and one 'to' sender." + self.logger.error( + "[BRIDGE PROVIDER] Invalid input: All requests must contain exactly one 'from' and one 'to' sender." ) + return False from_ = params["from"] to = params["to"] @@ -178,9 +180,10 @@ def create_request(self, params: t.Dict) -> BridgeRequest: or "address" not in from_ or "token" not in from_ ): - raise ValueError( - "Invalid input: 'from' must contain 'chain', 'address', and 'token'." + self.logger.error( + "[BRIDGE PROVIDER] Invalid input: 'from' must contain 'chain', 'address', and 'token'." ) + return False if ( not isinstance(to, t.Dict) @@ -189,9 +192,18 @@ def create_request(self, params: t.Dict) -> BridgeRequest: or "token" not in to or "amount" not in to ): - raise ValueError( - "Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." + self.logger.error( + "[BRIDGE PROVIDER] Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'." ) + return False + + return True + + def create_request(self, params: t.Dict) -> BridgeRequest: + """Create a bridge request.""" + + if not self.can_handle_request(params): + raise ValueError("Invalid input: Cannot process bridge request.") params = copy.deepcopy(params) params["to"]["amount"] = int(params["to"]["amount"]) @@ -258,6 +270,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: } total_native = 0 + total_gas_fees = 0 total_token = 0 for tx_label, tx in transactions: @@ -268,6 +281,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas" gas_fees = tx.get(gas_key, 0) * tx["gas"] tx_value = int(tx.get("value", 0)) + total_gas_fees += gas_fees total_native += tx_value + gas_fees self.logger.debug( @@ -287,6 +301,10 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: except Exception as e: raise RuntimeError("Malformed ERC20 approve transaction.") from e + self.logger.info( + f"[BRIDGE PROVIDER] Total gas fees for bridge request {bridge_request.id}: {total_gas_fees} native units." + ) + result = { from_chain: { from_address: { @@ -354,7 +372,9 @@ def execute(self, bridge_request: BridgeRequest) -> None: return try: - self.logger.info(f"[BRIDGE] Executing bridge request {bridge_request.id}.") + self.logger.info( + f"[BRIDGE PROVIDER] Executing bridge request {bridge_request.id}." + ) timestamp = time.time() chain = Chain(bridge_request.params["from"]["chain"]) wallet = self.wallet_manager.load(chain.ledger_type) @@ -400,7 +420,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED except Exception as e: # pylint: disable=broad-except - self.logger.error(f"[BRIDGE] Error executing bridge request: {e}") + self.logger.error(f"[BRIDGE PROVIDER] Error executing bridge request: {e}") execution_data = ExecutionData( elapsed_time=time.time() - timestamp, message=f"{MESSAGE_EXECUTION_FAILED} {str(e)}", diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 549f03360..297c84687 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -45,22 +45,28 @@ DEFAULT_BRIDGE_MIN_GAS_LIMIT, L1StandardBridge, ) +from operate.ledger.profiles import ERC20_TOKENS from operate.operate_types import Chain BLOCK_CHUNK_SIZE = 5000 NATIVE_BRIDGE_ENDPOINTS: t.Dict[t.Any, t.Dict[str, t.Any]] = { - (Chain.ETHEREUM.value, Chain.BASE.value): { + (Chain.ETHEREUM, Chain.BASE): { "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "to_bridge": "0x4200000000000000000000000000000000000010", "bridge_eta": 5 * 60, }, - (Chain.ETHEREUM.value, Chain.MODE.value): { + (Chain.ETHEREUM, Chain.MODE): { "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21", "to_bridge": "0x4200000000000000000000000000000000000010", "bridge_eta": 5 * 60, }, + (Chain.ETHEREUM, Chain.OPTIMISTIC): { + "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 5 * 60, + }, } L1_STANDARD_BRIDGE_CONTRACT = t.cast( @@ -83,25 +89,39 @@ class NativeBridgeProvider(BridgeProvider): """Native bridge provider.""" - def _validate(self, bridge_request: BridgeRequest) -> None: - """Validate the bridge request.""" - from_chain = bridge_request.params["from"]["chain"] - to_chain = bridge_request.params["to"]["chain"] - - if (from_chain, to_chain) not in NATIVE_BRIDGE_ENDPOINTS: - raise ValueError(f"Unsupported bridge from {from_chain} to {to_chain}.") + def can_handle_request(self, params: t.Dict) -> bool: + """Returns 'true' if the bridge can handle a request for 'params'.""" - super()._validate(bridge_request) + if not super().can_handle_request(params): + return False - def create_request(self, params: t.Dict) -> BridgeRequest: - """Create a bridge request.""" - from_chain = params["from"]["chain"] - to_chain = params["to"]["chain"] + from_chain = Chain(params["from"]["chain"]) + from_token = params["from"]["token"] + to_chain = Chain(params["to"]["chain"]) + to_token = params["to"]["token"] if (from_chain, to_chain) not in NATIVE_BRIDGE_ENDPOINTS: - raise ValueError(f"Unsupported bridge from {from_chain} to {to_chain}.") - - return super().create_request(params) + self.logger.warning( + f"[NATIVE BRIDGE] Unsupported bridge from {from_chain} to {to_chain}." + ) + return False + + if from_token == ZERO_ADDRESS and to_token == ZERO_ADDRESS: + return True + + for token_map in ERC20_TOKENS: + if ( + from_chain in token_map + and to_chain in token_map + and token_map[from_chain].lower() == from_token.lower() + and token_map[to_chain].lower() == to_token.lower() + ): + return True + + self.logger.warning( + f"[NATIVE BRIDGE] Unsupported token pair: {from_chain} {from_token} -> {to_chain} {to_token}" + ) + return False def description(self) -> str: """Get a human-readable description of the bridge provider.""" @@ -128,7 +148,9 @@ def quote(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"] + bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "bridge_eta" + ] message = None if to_amount == 0: @@ -161,7 +183,9 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] + from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] from_ledger_api = self._from_ledger_api(bridge_request) extra_data = Web3.keccak(text=bridge_request.id) @@ -207,7 +231,9 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["from_bridge"] + from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] from_ledger_api = self._from_ledger_api(bridge_request) if from_token == ZERO_ADDRESS: @@ -289,8 +315,12 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["to_bridge"] - bridge_eta = int(NATIVE_BRIDGE_ENDPOINTS[(from_chain, to_chain)]["bridge_eta"]) + to_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "to_bridge" + ] + bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "bridge_eta" + ] try: from_ledger_api = self._from_ledger_api(bridge_request) From 3efed9b3e7e259412a1fc6e87cb0f7a12dc06e51 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 17 May 2025 20:43:56 +0200 Subject: [PATCH 120/173] chore: replace event search from get_logs to create_filter --- .../providers/native_bridge_provider.py | 109 ++++++------------ .../contracts/l1_standard_bridge/contract.py | 2 +- .../l1_standard_bridge/contract.yaml | 2 +- .../contracts/l2_standard_bridge/contract.py | 73 +++++++++++- .../l2_standard_bridge/contract.yaml | 2 +- 5 files changed, 112 insertions(+), 76 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 297c84687..617e0ed6c 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -24,7 +24,6 @@ import typing as t from math import ceil -import eth_abi from autonomy.chain.base import registry_contracts from web3 import Web3 @@ -45,6 +44,7 @@ DEFAULT_BRIDGE_MIN_GAS_LIMIT, L1StandardBridge, ) +from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge from operate.ledger.profiles import ERC20_TOKENS from operate.operate_types import Chain @@ -75,15 +75,12 @@ directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"), ), ) - -EVENT_ETH_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( - text="ETHBridgeFinalized(address,address,uint256,bytes)" -).hex() -EVENT_ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["uint256", "bytes"] -EVENT_ERC20_BRIDGE_FINALIZED_TOPIC0 = Web3.keccak( - text="ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" -).hex() -EVENT_ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES = ["address", "uint256", "bytes"] +L2_STANDARD_BRIDGE_CONTRACT = t.cast( + L2StandardBridge, + L2StandardBridge.from_dir( + directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"), + ), +) class NativeBridgeProvider(BridgeProvider): @@ -338,32 +335,6 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_tx_block = from_w3.eth.get_block(bridge_tx_receipt.blockNumber) bridge_tx_ts = bridge_tx_block.timestamp - # Prepare the event data - if from_token == ZERO_ADDRESS: - topics = [ - EVENT_ETH_BRIDGE_FINALIZED_TOPIC0, # ETHBridgeFinalized - "0x" + from_address.lower()[2:].rjust(64, "0"), # from - "0x" + to_address.lower()[2:].rjust(64, "0"), # from - ] - non_indexed_types = EVENT_ETH_BRIDGE_FINALIZED_NON_INDEXED_TYPES - non_indexed_values = [ - to_amount, # amount - Web3.keccak(text=bridge_request.id), # extraData - ] - else: - topics = [ - EVENT_ERC20_BRIDGE_FINALIZED_TOPIC0, # ERC20BridgeFinalized - "0x" + to_token.lower()[2:].rjust(64, "0"), # localToken - "0x" + from_token.lower()[2:].rjust(64, "0"), # remoteToken - "0x" + from_address.lower()[2:].rjust(64, "0"), # from - ] - non_indexed_types = EVENT_ERC20_BRIDGE_FINALIZED_NON_INDEXED_TYPES - non_indexed_values = [ - to_address.lower(), # to - to_amount, # amount - Web3.keccak(text=bridge_request.id), # extraData - ] - # Find the event on the 'to' chain to_ledger_api = self._to_ledger_api(bridge_request) to_w3 = to_ledger_api.api @@ -373,15 +344,36 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) - to_tx_hash = self._find_transaction_in_range( - w3=to_w3, - contract_address=to_bridge, - from_block=from_block, - to_block=to_block, - topics=topics, - non_indexed_types=non_indexed_types, - non_indexed_values=non_indexed_values, - ) + + if from_token == ZERO_ADDRESS: + to_tx_hash = ( + L2_STANDARD_BRIDGE_CONTRACT.find_eth_bridge_finalized_tx( + ledger_api=to_ledger_api, + contract_address=to_bridge, + from_block=from_block, + to_block=to_block, + from_=from_address, + to=to_address, + amount=to_amount, + extra_data=Web3.keccak(text=bridge_request.id), + ) + ) + else: + to_tx_hash = ( + L2_STANDARD_BRIDGE_CONTRACT.find_erc20_bridge_finalized_tx( + ledger_api=to_ledger_api, + contract_address=to_bridge, + from_block=from_block, + to_block=to_block, + local_token=to_token, + remote_token=from_token, + from_=from_address, + to=to_address, + amount=to_amount, + extra_data=Web3.keccak(text=bridge_request.id), + ) + ) + if to_tx_hash: self.logger.info( f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." @@ -407,33 +399,6 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: execution_data.message = f"{MESSAGE_EXECUTION_FAILED} {str(e)}" bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED - @staticmethod - def _find_transaction_in_range( - w3: Web3, - contract_address: str, - from_block: int, - to_block: int, - topics: list[str], - non_indexed_types: list[str], - non_indexed_values: list[t.Any], - ) -> t.Optional[str]: - """Return the transaction hash of a matching event in the given block range, if any.""" - logs = w3.eth.get_logs( - { - "fromBlock": from_block, - "toBlock": to_block, - "address": contract_address, - "topics": topics, - } - ) - - for log in logs: - decoded = eth_abi.decode(non_indexed_types, log["data"]) - if all(a == b for a, b in zip(decoded, non_indexed_values)): - return log["transactionHash"].hex() - - return None - @staticmethod def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: """Returns the largest block number of the block before `timestamp`.""" diff --git a/operate/data/contracts/l1_standard_bridge/contract.py b/operate/data/contracts/l1_standard_bridge/contract.py index 89d6f87e6..9cdaff3ab 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.py +++ b/operate/data/contracts/l1_standard_bridge/contract.py @@ -41,7 +41,7 @@ class L1StandardBridge(Contract): - """The Service Staking contract.""" + """Optimism L1StandardBridge.""" contract_id = PublicId.from_str("valory/l1_standard_bridge:0.1.0") diff --git a/operate/data/contracts/l1_standard_bridge/contract.yaml b/operate/data/contracts/l1_standard_bridge/contract.yaml index bf89a8ddd..a617b83e1 100644 --- a/operate/data/contracts/l1_standard_bridge/contract.yaml +++ b/operate/data/contracts/l1_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeidyutctyfurwyx44lz7ynj3ohmbqyyrwtfj3bt2tinkgym2hwnsvu + contract.py: bafybeib2kiztts3436vccvraeura3jb23oihgzxgrt3biakbed24edcaai fingerprint_ignore_patterns: [] contracts: [] class_name: L1StandardBridge diff --git a/operate/data/contracts/l2_standard_bridge/contract.py b/operate/data/contracts/l2_standard_bridge/contract.py index 8a7408ef0..0b802f600 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.py +++ b/operate/data/contracts/l2_standard_bridge/contract.py @@ -19,11 +19,82 @@ """This module contains the class to connect to the `L2StandardBridge` contract.""" +from typing import Optional, cast + from aea.configurations.base import PublicId from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi +from aea_ledger_ethereum import EthereumApi class L2StandardBridge(Contract): - """The Service Staking contract.""" + """Optimism L2StandardBridge.""" contract_id = PublicId.from_str("valory/l2_standard_bridge:0.1.0") + + @classmethod + def find_eth_bridge_finalized_tx( + cls, + ledger_api: LedgerApi, + contract_address: str, + from_block: int, + to_block: int, + from_: str, + to: str, + amount: int, + extra_data: bytes, + ) -> Optional[str]: + """Return the transaction hash for the matching ETHBridgeFinalized event in the given block range.""" + ledger_api = cast(EthereumApi, ledger_api) + contract_instance = cls.get_instance(ledger_api, contract_address) + entries = contract_instance.events.ETHBridgeFinalized.create_filter( + fromBlock=from_block, + toBlock=to_block, + argument_filters={ + "from": from_, + "to": to, + }, + ).get_all_entries() + + for entry in entries: + args = entry["args"] + if args["amount"] == amount and args["extraData"] == extra_data: + return entry["transactionHash"].hex() + return None + + @classmethod + def find_erc20_bridge_finalized_tx( + cls, + ledger_api: LedgerApi, + contract_address: str, + from_block: int, + to_block: int, + local_token: str, + remote_token: str, + from_: str, + to: str, + amount: int, + extra_data: bytes, + ) -> Optional[str]: + """Return the transaction hash for the matching ERC20BridgeFinalized event in the given block range.""" + ledger_api = cast(EthereumApi, ledger_api) + contract_instance = cls.get_instance(ledger_api, contract_address) + entries = contract_instance.events.ERC20BridgeFinalized.create_filter( + fromBlock=from_block, + toBlock=to_block, + argument_filters={ + "localToken": local_token, + "remoteToken": remote_token, + "from": from_, + }, + ).get_all_entries() + + for entry in entries: + args = entry["args"] + if ( + args["to"].lower() == to.lower() + and args["amount"] == amount + and args["extraData"] == extra_data + ): + return entry["transactionHash"].hex() + return None diff --git a/operate/data/contracts/l2_standard_bridge/contract.yaml b/operate/data/contracts/l2_standard_bridge/contract.yaml index 39e357ce2..d94dfa29b 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.yaml +++ b/operate/data/contracts/l2_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihp3ucqaxnuozotqcmvm6bjdds4rfbtr56sj7jeb6sbxlzikvtxyq build/L2StandardBridge.json: bafybeid2zqptx4awocztkhmifqsdnqrqcib73hbsp53snmfd24u7k3q6me - contract.py: bafybeief7twob74tkqaw7oaxp6nhywfehr7hqurfb3woyr3xxfnx4ulkta + contract.py: bafybeifwkgtde5lm63r3tiwfhpwcbyoryxqj2rb7m4oxinym67cajwvl34 fingerprint_ignore_patterns: [] contracts: [] class_name: L2StandardBridge From ac9ef939be9e5c9f0acd140d32af3e94aea2517a Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sat, 17 May 2025 20:59:06 +0200 Subject: [PATCH 121/173] fix: tests --- tests/test_bridge_providers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index c06d1f437..888260159 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -534,7 +534,7 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - bridge_eta=NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"], + bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], elapsed_time=0, message=MESSAGE_QUOTE_ZERO, provider_data=None, @@ -674,7 +674,7 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=NATIVE_BRIDGE_ENDPOINTS["ethereum", "base"]["bridge_eta"], + bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], elapsed_time=0, message=None, provider_data=None, From bcfabd3339b96b93617782a4031717a5f2ee6e99 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 18 May 2025 14:15:06 +0200 Subject: [PATCH 122/173] chore: update --- operate/bridge/providers/native_bridge_provider.py | 8 ++++---- operate/data/contracts/l2_standard_bridge/contract.py | 5 +++-- operate/data/contracts/l2_standard_bridge/contract.yaml | 2 +- tests/test_bridge_providers.py | 8 ++++++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 617e0ed6c..6e9636c0e 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -350,12 +350,12 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: L2_STANDARD_BRIDGE_CONTRACT.find_eth_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, - from_block=from_block, - to_block=to_block, from_=from_address, to=to_address, amount=to_amount, extra_data=Web3.keccak(text=bridge_request.id), + from_block=from_block, + to_block=to_block, ) ) else: @@ -363,14 +363,14 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: L2_STANDARD_BRIDGE_CONTRACT.find_erc20_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, - from_block=from_block, - to_block=to_block, local_token=to_token, remote_token=from_token, from_=from_address, to=to_address, amount=to_amount, extra_data=Web3.keccak(text=bridge_request.id), + from_block=from_block, + to_block=to_block, ) ) diff --git a/operate/data/contracts/l2_standard_bridge/contract.py b/operate/data/contracts/l2_standard_bridge/contract.py index 0b802f600..cfbe06ff1 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.py +++ b/operate/data/contracts/l2_standard_bridge/contract.py @@ -25,6 +25,7 @@ from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi +from web3.types import BlockIdentifier class L2StandardBridge(Contract): @@ -37,12 +38,12 @@ def find_eth_bridge_finalized_tx( cls, ledger_api: LedgerApi, contract_address: str, - from_block: int, - to_block: int, from_: str, to: str, amount: int, extra_data: bytes, + from_block: BlockIdentifier = "earliest", + to_block: BlockIdentifier = "latest", ) -> Optional[str]: """Return the transaction hash for the matching ETHBridgeFinalized event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) diff --git a/operate/data/contracts/l2_standard_bridge/contract.yaml b/operate/data/contracts/l2_standard_bridge/contract.yaml index d94dfa29b..655a66fe2 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.yaml +++ b/operate/data/contracts/l2_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihp3ucqaxnuozotqcmvm6bjdds4rfbtr56sj7jeb6sbxlzikvtxyq build/L2StandardBridge.json: bafybeid2zqptx4awocztkhmifqsdnqrqcib73hbsp53snmfd24u7k3q6me - contract.py: bafybeifwkgtde5lm63r3tiwfhpwcbyoryxqj2rb7m4oxinym67cajwvl34 + contract.py: bafybeig37khtgkwz72lgq6xk72nsjrfglwlvip5lxblip7k6q5l2m4usju fingerprint_ignore_patterns: [] contracts: [] class_name: L2StandardBridge diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 888260159..f249647a2 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -534,7 +534,9 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], + bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE][ + "bridge_eta" + ], elapsed_time=0, message=MESSAGE_QUOTE_ZERO, provider_data=None, @@ -674,7 +676,9 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], + bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE][ + "bridge_eta" + ], elapsed_time=0, message=None, provider_data=None, From 1a646a2049772586ead6bdf4b96aa54cd42131b8 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 18 May 2025 18:51:48 +0200 Subject: [PATCH 123/173] chore: refactor to contract adapter --- operate/bridge/bridge.py | 9 +- .../providers/native_bridge_provider.py | 249 +++++++++++++----- .../contracts/l2_standard_bridge/contract.py | 8 +- .../l2_standard_bridge/contract.yaml | 2 +- tests/test_bridge_providers.py | 19 +- tox.ini | 3 + 6 files changed, 217 insertions(+), 73 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 0186bc844..c4d41cdd0 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -34,7 +34,10 @@ from operate.bridge.providers.bridge_provider import BridgeProvider, BridgeRequest from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider -from operate.bridge.providers.native_bridge_provider import NativeBridgeProvider +from operate.bridge.providers.native_bridge_provider import ( + NativeBridgeProvider, + OptimismContractAdaptor, +) from operate.constants import ZERO_ADDRESS from operate.operate_types import Chain from operate.resource import LocalResource @@ -141,7 +144,9 @@ def __init__( ) self._bridge_providers = { LiFiBridgeProvider.id(): LiFiBridgeProvider(wallet_manager, logger), - NativeBridgeProvider.id(): NativeBridgeProvider(wallet_manager, logger), + NativeBridgeProvider.id(): NativeBridgeProvider( + OptimismContractAdaptor(), wallet_manager, logger + ), } def _store_data(self) -> None: diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 6e9636c0e..2c9293118 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -20,11 +20,16 @@ """Native bridge provider.""" +import logging import time import typing as t +from abc import ABC, abstractmethod from math import ceil +from aea.common import JSONLike +from aea.crypto.base import LedgerApi from autonomy.chain.base import registry_contracts +from eth_typing import BlockIdentifier from web3 import Web3 from operate.bridge.providers.bridge_provider import ( @@ -47,6 +52,7 @@ from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge from operate.ledger.profiles import ERC20_TOKENS from operate.operate_types import Chain +from operate.wallet.master import MasterWalletManager BLOCK_CHUNK_SIZE = 5000 @@ -83,8 +89,172 @@ ) +class BridgeContractAdaptor(ABC): + """Adaptor class for bridge contract packages.""" + + @abstractmethod + def build_bridge_native_tx( + self, ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> JSONLike: + """Build bridge native asset transaction.""" + raise NotImplementedError() + + @abstractmethod + def build_bridge_erc20_tx( + self, ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> JSONLike: + """Build bridge ERC20 asset transaction.""" + raise NotImplementedError() + + @abstractmethod + def find_bridge_native_finalized_tx( + self, + ledger_api: LedgerApi, + bridge_request: BridgeRequest, + from_block: BlockIdentifier, + to_block: BlockIdentifier, + ) -> t.Optional[str]: + """Return the transaction hash of the event indicating native bridge completion.""" + raise NotImplementedError() + + @abstractmethod + def find_bridge_erc20_finalized_tx( + self, + ledger_api: LedgerApi, + bridge_request: BridgeRequest, + from_block: BlockIdentifier, + to_block: BlockIdentifier, + ) -> t.Optional[str]: + """Return the transaction hash of the event indicating ERC20 bridge completion.""" + raise NotImplementedError() + + +class OptimismContractAdaptor(BridgeContractAdaptor): + """Adaptor class for Optimism contract packages.""" + + def build_bridge_native_tx( + self, ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> JSONLike: + """Build bridge native asset transaction.""" + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_amount = bridge_request.params["to"]["amount"] + from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] + extra_data = Web3.keccak(text=bridge_request.id) + return L1_STANDARD_BRIDGE_CONTRACT.build_bridge_eth_to_tx( + ledger_api=ledger_api, + contract_address=from_bridge, + sender=from_address, + to=to_address, + amount=int(to_amount), + min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, + extra_data=extra_data, + ) + + def build_bridge_erc20_tx( + self, ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> JSONLike: + """Build bridge ERC20 asset transaction.""" + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] + from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] + extra_data = Web3.keccak(text=bridge_request.id) + return L1_STANDARD_BRIDGE_CONTRACT.build_bridge_erc20_to_tx( + ledger_api=ledger_api, + contract_address=from_bridge, + sender=from_address, + local_token=from_token, + remote_token=to_token, + to=to_address, + amount=int(to_amount), + min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, + extra_data=extra_data, + ) + + def find_bridge_native_finalized_tx( + self, + ledger_api: LedgerApi, + bridge_request: BridgeRequest, + from_block: BlockIdentifier, + to_block: BlockIdentifier, + ) -> t.Optional[str]: + """Return the transaction hash of the event indicating native bridge completion.""" + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_amount = bridge_request.params["to"]["amount"] + to_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "to_bridge" + ] + extra_data = Web3.keccak(text=bridge_request.id) + return L2_STANDARD_BRIDGE_CONTRACT.find_eth_bridge_finalized_tx( + ledger_api=ledger_api, + contract_address=to_bridge, + from_=from_address, + to=to_address, + amount=to_amount, + extra_data=extra_data, + from_block=from_block, + to_block=to_block, + ) + + def find_bridge_erc20_finalized_tx( + self, + ledger_api: LedgerApi, + bridge_request: BridgeRequest, + from_block: BlockIdentifier, + to_block: BlockIdentifier, + ) -> t.Optional[str]: + """Return the transaction hash of the event indicating ERC20 bridge completion.""" + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] + to_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + "to_bridge" + ] + extra_data = Web3.keccak(text=bridge_request.id) + return L2_STANDARD_BRIDGE_CONTRACT.find_erc20_bridge_finalized_tx( + ledger_api=ledger_api, + contract_address=to_bridge, + local_token=to_token, + remote_token=from_token, + from_=from_address, + to=to_address, + amount=to_amount, + extra_data=extra_data, + from_block=from_block, + to_block=to_block, + ) + + class NativeBridgeProvider(BridgeProvider): - """Native bridge provider.""" + """Native bridge provider""" + + def __init__( + self, + bridge_contract_adaptor: BridgeContractAdaptor, + wallet_manager: MasterWalletManager, + logger: t.Optional[logging.Logger] = None, + ) -> None: + """Initialize the bridge provider.""" + self.contract_adaptor = bridge_contract_adaptor + super().__init__(wallet_manager=wallet_manager, logger=logger) def can_handle_request(self, params: t.Dict) -> bool: """Returns 'true' if the bridge can handle a request for 'params'.""" @@ -173,42 +343,18 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: if not quote_data: return None - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] - to_address = bridge_request.params["to"]["address"] - to_token = bridge_request.params["to"]["token"] - to_amount = bridge_request.params["to"]["amount"] - from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] from_ledger_api = self._from_ledger_api(bridge_request) - extra_data = Web3.keccak(text=bridge_request.id) - if from_token == ZERO_ADDRESS: - bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_bridge_eth_to_tx( - ledger_api=from_ledger_api, - contract_address=from_bridge, - sender=from_address, - to=to_address, - amount=int(to_amount), - min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, - extra_data=extra_data, + bridge_tx = self.contract_adaptor.build_bridge_native_tx( + ledger_api=from_ledger_api, bridge_request=bridge_request ) else: - bridge_tx = L1_STANDARD_BRIDGE_CONTRACT.build_bridge_erc20_to_tx( - ledger_api=from_ledger_api, - contract_address=from_bridge, - sender=from_address, - local_token=from_token, - remote_token=to_token, - to=to_address, - amount=int(to_amount), - min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, - extra_data=extra_data, + bridge_tx = self.contract_adaptor.build_bridge_erc20_tx( + ledger_api=from_ledger_api, bridge_request=bridge_request ) + BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) bridge_tx["gas"] = ceil(bridge_tx["gas"] * GAS_ESTIMATE_BUFFER) @@ -305,16 +451,8 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: return from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - to_address = bridge_request.params["to"]["address"] - to_token = bridge_request.params["to"]["token"] - to_amount = bridge_request.params["to"]["amount"] - - to_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ - "to_bridge" - ] bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ "bridge_eta" ] @@ -346,34 +484,19 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) if from_token == ZERO_ADDRESS: - to_tx_hash = ( - L2_STANDARD_BRIDGE_CONTRACT.find_eth_bridge_finalized_tx( - ledger_api=to_ledger_api, - contract_address=to_bridge, - from_=from_address, - to=to_address, - amount=to_amount, - extra_data=Web3.keccak(text=bridge_request.id), - from_block=from_block, - to_block=to_block, - ) + to_tx_hash = self.contract_adaptor.find_bridge_native_finalized_tx( + ledger_api=to_ledger_api, + bridge_request=bridge_request, + from_block=from_block, + to_block=to_block, ) else: - to_tx_hash = ( - L2_STANDARD_BRIDGE_CONTRACT.find_erc20_bridge_finalized_tx( - ledger_api=to_ledger_api, - contract_address=to_bridge, - local_token=to_token, - remote_token=from_token, - from_=from_address, - to=to_address, - amount=to_amount, - extra_data=Web3.keccak(text=bridge_request.id), - from_block=from_block, - to_block=to_block, - ) + to_tx_hash = self.contract_adaptor.find_bridge_erc20_finalized_tx( + ledger_api=to_ledger_api, + bridge_request=bridge_request, + from_block=from_block, + to_block=to_block, ) - if to_tx_hash: self.logger.info( f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." diff --git a/operate/data/contracts/l2_standard_bridge/contract.py b/operate/data/contracts/l2_standard_bridge/contract.py index cfbe06ff1..89bd5b32b 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.py +++ b/operate/data/contracts/l2_standard_bridge/contract.py @@ -45,7 +45,7 @@ def find_eth_bridge_finalized_tx( from_block: BlockIdentifier = "earliest", to_block: BlockIdentifier = "latest", ) -> Optional[str]: - """Return the transaction hash for the matching ETHBridgeFinalized event in the given block range.""" + """Return the transaction hash of the matching ETHBridgeFinalized event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) contract_instance = cls.get_instance(ledger_api, contract_address) entries = contract_instance.events.ETHBridgeFinalized.create_filter( @@ -68,16 +68,16 @@ def find_erc20_bridge_finalized_tx( cls, ledger_api: LedgerApi, contract_address: str, - from_block: int, - to_block: int, local_token: str, remote_token: str, from_: str, to: str, amount: int, extra_data: bytes, + from_block: BlockIdentifier = "earliest", + to_block: BlockIdentifier = "latest", ) -> Optional[str]: - """Return the transaction hash for the matching ERC20BridgeFinalized event in the given block range.""" + """Return the transaction hash of the matching ERC20BridgeFinalized event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) contract_instance = cls.get_instance(ledger_api, contract_address) entries = contract_instance.events.ERC20BridgeFinalized.create_filter( diff --git a/operate/data/contracts/l2_standard_bridge/contract.yaml b/operate/data/contracts/l2_standard_bridge/contract.yaml index 655a66fe2..595e1c85b 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.yaml +++ b/operate/data/contracts/l2_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihp3ucqaxnuozotqcmvm6bjdds4rfbtr56sj7jeb6sbxlzikvtxyq build/L2StandardBridge.json: bafybeid2zqptx4awocztkhmifqsdnqrqcib73hbsp53snmfd24u7k3q6me - contract.py: bafybeig37khtgkwz72lgq6xk72nsjrfglwlvip5lxblip7k6q5l2m4usju + contract.py: bafybeievv75gwvgj6w42ng6pkrewkns6mwnyzeckufpbyhkq7npkbn35ve fingerprint_ignore_patterns: [] contracts: [] class_name: L2StandardBridge diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index f249647a2..67d81ed3d 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -46,6 +46,7 @@ from operate.bridge.providers.native_bridge_provider import ( NATIVE_BRIDGE_ENDPOINTS, NativeBridgeProvider, + OptimismContractAdaptor, ) from operate.cli import OperateApp from operate.constants import ZERO_ADDRESS @@ -496,7 +497,10 @@ def test_bridge_zero( }, } - bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + bridge = NativeBridgeProvider( + bridge_contract_adaptor=OptimismContractAdaptor(), + wallet_manager=operate.wallet_manager, + ) # Create bridge_request = bridge.create_request(params) @@ -638,7 +642,10 @@ def test_bridge_execute_error( }, } - bridge = NativeBridgeProvider(wallet_manager=operate.wallet_manager) + bridge = NativeBridgeProvider( + bridge_contract_adaptor=OptimismContractAdaptor(), + wallet_manager=operate.wallet_manager, + ) # Create bridge_request = bridge.create_request(params) @@ -1019,7 +1026,13 @@ def test_update_execution_status( operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - bridge = bridge_provider_class(wallet_manager=operate.wallet_manager) + if bridge_provider_class == NativeBridgeProvider: + bridge: BridgeProvider = NativeBridgeProvider( + bridge_contract_adaptor=OptimismContractAdaptor(), + wallet_manager=operate.wallet_manager, + ) + else: + bridge = bridge_provider_class(wallet_manager=operate.wallet_manager) quote_data = QuoteData( bridge_eta=0, diff --git a/tox.ini b/tox.ini index c975e8e23..5e429955b 100644 --- a/tox.ini +++ b/tox.ini @@ -243,6 +243,9 @@ ignore_missing_imports = True [mypy-eth_abi.*] ignore_missing_imports = True +[mypy-eth_typing.*] +ignore_missing_imports = true + [testenv:unit-tests] deps = pytest==7.2.1 From 80ee97a030fdbbdcb73ed4452466bab030653235 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 18 May 2025 20:15:21 +0200 Subject: [PATCH 124/173] chore: update --- .../providers/native_bridge_provider.py | 113 ++++++++++++------ tests/test_bridge_providers.py | 6 +- 2 files changed, 80 insertions(+), 39 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 2c9293118..9a7d86fb0 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -57,7 +57,7 @@ BLOCK_CHUNK_SIZE = 5000 -NATIVE_BRIDGE_ENDPOINTS: t.Dict[t.Any, t.Dict[str, t.Any]] = { +OPTIMISM_BRIDGE_DATA: t.Dict[t.Any, t.Dict[str, t.Any]] = { (Chain.ETHEREUM, Chain.BASE): { "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "to_bridge": "0x4200000000000000000000000000000000000010", @@ -75,19 +75,6 @@ }, } -L1_STANDARD_BRIDGE_CONTRACT = t.cast( - L1StandardBridge, - L1StandardBridge.from_dir( - directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"), - ), -) -L2_STANDARD_BRIDGE_CONTRACT = t.cast( - L2StandardBridge, - L2StandardBridge.from_dir( - directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"), - ), -) - class BridgeContractAdaptor(ABC): """Adaptor class for bridge contract packages.""" @@ -107,7 +94,7 @@ def build_bridge_erc20_tx( raise NotImplementedError() @abstractmethod - def find_bridge_native_finalized_tx( + def find_native_bridge_finalized_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -118,7 +105,7 @@ def find_bridge_native_finalized_tx( raise NotImplementedError() @abstractmethod - def find_bridge_erc20_finalized_tx( + def find_erc20_bridge_finalized_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -132,6 +119,19 @@ def find_bridge_erc20_finalized_tx( class OptimismContractAdaptor(BridgeContractAdaptor): """Adaptor class for Optimism contract packages.""" + l1_standard_bridge_contract = t.cast( + L1StandardBridge, + L1StandardBridge.from_dir( + directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"), + ), + ) + l2_standard_bridge_contract = t.cast( + L2StandardBridge, + L2StandardBridge.from_dir( + directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"), + ), + ) + def build_bridge_native_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: @@ -141,11 +141,11 @@ def build_bridge_native_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + from_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "from_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) - return L1_STANDARD_BRIDGE_CONTRACT.build_bridge_eth_to_tx( + return self.l1_standard_bridge_contract.build_bridge_eth_to_tx( ledger_api=ledger_api, contract_address=from_bridge, sender=from_address, @@ -166,11 +166,11 @@ def build_bridge_erc20_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + from_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "from_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) - return L1_STANDARD_BRIDGE_CONTRACT.build_bridge_erc20_to_tx( + return self.l1_standard_bridge_contract.build_bridge_erc20_to_tx( ledger_api=ledger_api, contract_address=from_bridge, sender=from_address, @@ -182,7 +182,7 @@ def build_bridge_erc20_tx( extra_data=extra_data, ) - def find_bridge_native_finalized_tx( + def find_native_bridge_finalized_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -195,11 +195,11 @@ def find_bridge_native_finalized_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + to_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "to_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) - return L2_STANDARD_BRIDGE_CONTRACT.find_eth_bridge_finalized_tx( + return self.l2_standard_bridge_contract.find_eth_bridge_finalized_tx( ledger_api=ledger_api, contract_address=to_bridge, from_=from_address, @@ -210,7 +210,7 @@ def find_bridge_native_finalized_tx( to_block=to_block, ) - def find_bridge_erc20_finalized_tx( + def find_erc20_bridge_finalized_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -225,11 +225,11 @@ def find_bridge_erc20_finalized_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + to_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "to_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) - return L2_STANDARD_BRIDGE_CONTRACT.find_erc20_bridge_finalized_tx( + return self.l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( ledger_api=ledger_api, contract_address=to_bridge, local_token=to_token, @@ -243,6 +243,47 @@ def find_bridge_erc20_finalized_tx( ) +class OmnibridgeContractAdaptor(ABC): + """Adaptor class for Omnibridge contract packages.""" + + @abstractmethod + def build_bridge_native_tx( + self, ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> JSONLike: + """Build bridge native asset transaction.""" + raise NotImplementedError() + + @abstractmethod + def build_bridge_erc20_tx( + self, ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> JSONLike: + """Build bridge ERC20 asset transaction.""" + raise NotImplementedError() + + @abstractmethod + def find_native_bridge_finalized_tx( + self, + ledger_api: LedgerApi, + bridge_request: BridgeRequest, + from_block: BlockIdentifier, + to_block: BlockIdentifier, + ) -> t.Optional[str]: + """Return the transaction hash of the event indicating native bridge completion.""" + raise NotImplementedError() + + @abstractmethod + def find_erc20_bridge_finalized_tx( + self, + ledger_api: LedgerApi, + bridge_request: BridgeRequest, + from_block: BlockIdentifier, + to_block: BlockIdentifier, + ) -> t.Optional[str]: + """Return the transaction hash of the event indicating ERC20 bridge completion.""" + raise NotImplementedError() + + + class NativeBridgeProvider(BridgeProvider): """Native bridge provider""" @@ -253,7 +294,7 @@ def __init__( logger: t.Optional[logging.Logger] = None, ) -> None: """Initialize the bridge provider.""" - self.contract_adaptor = bridge_contract_adaptor + self.bridge_contract_adaptor = bridge_contract_adaptor super().__init__(wallet_manager=wallet_manager, logger=logger) def can_handle_request(self, params: t.Dict) -> bool: @@ -267,7 +308,7 @@ def can_handle_request(self, params: t.Dict) -> bool: to_chain = Chain(params["to"]["chain"]) to_token = params["to"]["token"] - if (from_chain, to_chain) not in NATIVE_BRIDGE_ENDPOINTS: + if (from_chain, to_chain) not in OPTIMISM_BRIDGE_DATA: self.logger.warning( f"[NATIVE BRIDGE] Unsupported bridge from {from_chain} to {to_chain}." ) @@ -292,7 +333,7 @@ def can_handle_request(self, params: t.Dict) -> bool: def description(self) -> str: """Get a human-readable description of the bridge provider.""" - return "Native bridge provider." + return f"Native bridge provider ({self.bridge_contract_adaptor.__class__.__name__})." def quote(self, bridge_request: BridgeRequest) -> None: """Update the request with the quote.""" @@ -315,7 +356,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + bridge_eta = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "bridge_eta" ] @@ -347,11 +388,11 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: from_ledger_api = self._from_ledger_api(bridge_request) if from_token == ZERO_ADDRESS: - bridge_tx = self.contract_adaptor.build_bridge_native_tx( + bridge_tx = self.bridge_contract_adaptor.build_bridge_native_tx( ledger_api=from_ledger_api, bridge_request=bridge_request ) else: - bridge_tx = self.contract_adaptor.build_bridge_erc20_tx( + bridge_tx = self.bridge_contract_adaptor.build_bridge_erc20_tx( ledger_api=from_ledger_api, bridge_request=bridge_request ) @@ -374,7 +415,7 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + from_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "from_bridge" ] from_ledger_api = self._from_ledger_api(bridge_request) @@ -453,7 +494,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - bridge_eta = NATIVE_BRIDGE_ENDPOINTS[(Chain(from_chain), Chain(to_chain))][ + bridge_eta = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ "bridge_eta" ] @@ -484,14 +525,14 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) if from_token == ZERO_ADDRESS: - to_tx_hash = self.contract_adaptor.find_bridge_native_finalized_tx( + to_tx_hash = self.bridge_contract_adaptor.find_native_bridge_finalized_tx( ledger_api=to_ledger_api, bridge_request=bridge_request, from_block=from_block, to_block=to_block, ) else: - to_tx_hash = self.contract_adaptor.find_bridge_erc20_finalized_tx( + to_tx_hash = self.bridge_contract_adaptor.find_erc20_bridge_finalized_tx( ledger_api=to_ledger_api, bridge_request=bridge_request, from_block=from_block, diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 67d81ed3d..a8bf11a9a 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -44,7 +44,7 @@ QuoteData, ) from operate.bridge.providers.native_bridge_provider import ( - NATIVE_BRIDGE_ENDPOINTS, + OPTIMISM_BRIDGE_DATA, NativeBridgeProvider, OptimismContractAdaptor, ) @@ -538,7 +538,7 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE][ + bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE][ "bridge_eta" ], elapsed_time=0, @@ -683,7 +683,7 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=NATIVE_BRIDGE_ENDPOINTS[Chain.ETHEREUM, Chain.BASE][ + bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE][ "bridge_eta" ], elapsed_time=0, From d484b37e64bcf2aca4e9efc430619d598bf1d6cf Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Sun, 18 May 2025 20:17:12 +0200 Subject: [PATCH 125/173] chore: add omnibridge contracts --- .../contracts/foreign_omnibridge/__init__.py | 20 + .../build/ForeignOmnibridge.json | 1372 +++++++++++++++++ .../contracts/foreign_omnibridge/contract.py | 131 ++ .../foreign_omnibridge/contract.yaml | 23 + .../contracts/home_omnibridge/__init__.py | 20 + .../home_omnibridge/build/HomeOmnibridge.json | 10 + .../contracts/home_omnibridge/contract.py | 65 + .../contracts/home_omnibridge/contract.yaml | 23 + 8 files changed, 1664 insertions(+) create mode 100644 operate/data/contracts/foreign_omnibridge/__init__.py create mode 100644 operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json create mode 100644 operate/data/contracts/foreign_omnibridge/contract.py create mode 100644 operate/data/contracts/foreign_omnibridge/contract.yaml create mode 100644 operate/data/contracts/home_omnibridge/__init__.py create mode 100644 operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json create mode 100644 operate/data/contracts/home_omnibridge/contract.py create mode 100644 operate/data/contracts/home_omnibridge/contract.yaml diff --git a/operate/data/contracts/foreign_omnibridge/__init__.py b/operate/data/contracts/foreign_omnibridge/__init__.py new file mode 100644 index 000000000..786a23a1a --- /dev/null +++ b/operate/data/contracts/foreign_omnibridge/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the `ForeignOmnibridge` contract.""" diff --git a/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json b/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json new file mode 100644 index 000000000..6394e63d3 --- /dev/null +++ b/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json @@ -0,0 +1,1372 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ForeignOmnibridge", + "sourceName": "ForeignOmnibridge.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "_suffix", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLimit", + "type": "uint256" + } + ], + "name": "DailyLimitChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLimit", + "type": "uint256" + } + ], + "name": "ExecutionDailyLimitChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "FailedMessageFixed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "nativeToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "bridgedToken", + "type": "address" + } + ], + "name": "NewTokenRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + } + ], + "name": "TokensBridged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + } + ], + "name": "TokensBridgingInitiated", + "type": "event" + }, + { + "inputs": [], + "name": "bridgeContract", + "outputs": [ + { + "internalType": "contract IAMB", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_nativeToken", + "type": "address" + } + ], + "name": "bridgedTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + } + ], + "name": "claimTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + } + ], + "name": "claimTokensFromTokenContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "dailyLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "deployAndHandleBridgedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "deployAndHandleBridgedTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "disableInterest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "executionDailyLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "executionMaxPerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_messageId", + "type": "bytes32" + } + ], + "name": "fixFailedMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "fixMediatorBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getBridgeInterfacesVersion", + "outputs": [ + { + "internalType": "uint64", + "name": "major", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minor", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "patch", + "type": "uint64" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getBridgeMode", + "outputs": [ + { + "internalType": "bytes4", + "name": "_data", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentDay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "handleBridgedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "handleBridgedTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "handleNativeTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "handleNativeTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgeContract", + "type": "address" + }, + { + "internalType": "address", + "name": "_mediatorContract", + "type": "address" + }, + { + "internalType": "uint256[3]", + "name": "_dailyLimitMaxPerTxMinPerTxArray", + "type": "uint256[3]" + }, + { + "internalType": "uint256[2]", + "name": "_executionDailyLimitExecutionMaxPerTxArray", + "type": "uint256[2]" + }, + { + "internalType": "uint256", + "name": "_requestGasLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "address", + "name": "_tokenFactory", + "type": "address" + } + ], + "name": "initialize", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_impl", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minCashThreshold", + "type": "uint256" + } + ], + "name": "initializeInterest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "interestImplementation", + "outputs": [ + { + "internalType": "contract IInterestImplementation", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "invest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "isBridgedTokenDeployAcknowledged", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isInitialized", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "isRegisteredAsNativeToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "isTokenRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "maxAvailablePerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "maxPerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "mediatorBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mediatorContractOnOtherSide", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_messageId", + "type": "bytes32" + } + ], + "name": "messageFixed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenFactory", + "type": "address" + }, + { + "internalType": "address", + "name": "_interestImplementation", + "type": "address" + } + ], + "name": "migrateTo_3_3_0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "minCashThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "minPerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgedToken", + "type": "address" + } + ], + "name": "nativeTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "onTokenTransfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC677", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "relayTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC677", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "relayTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC677", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "relayTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_messageId", + "type": "bytes32" + } + ], + "name": "requestFailedMessageFix", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "requestGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgeContract", + "type": "address" + } + ], + "name": "setBridgeContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_nativeToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_bridgedToken", + "type": "address" + } + ], + "name": "setCustomTokenAddressPair", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_dailyLimit", + "type": "uint256" + } + ], + "name": "setDailyLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_dailyLimit", + "type": "uint256" + } + ], + "name": "setExecutionDailyLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxPerTx", + "type": "uint256" + } + ], + "name": "setExecutionMaxPerTx", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxPerTx", + "type": "uint256" + } + ], + "name": "setMaxPerTx", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_mediatorContract", + "type": "address" + } + ], + "name": "setMediatorContractOnOtherSide", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minCashThreshold", + "type": "uint256" + } + ], + "name": "setMinCashThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minPerTx", + "type": "uint256" + } + ], + "name": "setMinPerTx", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + } + ], + "name": "setRequestGasLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenFactory", + "type": "address" + } + ], + "name": "setTokenFactory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tokenFactory", + "outputs": [ + { + "internalType": "contract TokenFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_day", + "type": "uint256" + } + ], + "name": "totalExecutedPerDay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_day", + "type": "uint256" + } + ], + "name": "totalSpentPerDay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withinExecutionLimit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withinLimit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106103a45760003560e01c80637837cf91116101e9578063c2173d431161010f578063db6fff8c116100ad578063f2fde38b1161007c578063f2fde38b14611081578063f3b83791146110a7578063f3f51415146110c4578063f50dace6146110ea576103a4565b8063db6fff8c14610ff5578063e77772fe14611021578063ec47de2a14611029578063f2c54fe814611055576103a4565b8063d0342acd116100e9578063d0342acd14610d99578063d522cfd714610dc7578063d740548114610f03578063d814b1d714610fc7576103a4565b8063c2173d4314610ca7578063c534576114610ccd578063cd59658314610d91576103a4565b8063a4b1c24311610187578063ab4f5dc511610156578063ab4f5dc514610c0d578063ad58bdd114610c43578063ae813e9f14610c79578063be3b625b14610c9f576103a4565b8063a4b1c24314610ad6578063a4b4b23314610afc578063a4c0ed3614610b28578063ab3a25d914610be1576103a4565b8063871c0760116101c3578063871c076014610a755780638da5cb5b14610a7d5780639a4a439514610a855780639cb7595a14610aa2576103a4565b80637837cf911461095f57806385df73bd1461098b578063867f7a4d146109b1576103a4565b80632c3500a6116102ce578063437764df1161026c57806364696f971161023b57806364696f97146108ad57806369ffa08a146108e55780636e5d6bea146109135780637610722f14610939576103a4565b8063437764df1461081f5780635726ff3014610844578063593399821461086a57806361c04f8414610887576103a4565b8063392e53cd116102a8578063392e53cd146107bd5780633a50bc87146107c55780633e6968b6146107f157806340f8dd86146107f9576103a4565b80632c3500a6146107025780632d70061f146107555780632f73a9f814610797576103a4565b8063107752381161034657806326aa101f1161031557806326aa101f14610594578063272255bb146105ba5780632803212f146105f05780632ae87cdd1461061c576103a4565b806310775238146104d2578063125e4cfb1461051257806316ef191314610548578063194153d31461056e576103a4565b806303f9c7931161038257806303f9c7931461043b5780630950d515146104615780630b26cf661461047e5780630b71a4a7146104a4576103a4565b806301e4f53a146103a957806301fcc1d3146103d7578063032f693f14610403575b600080fd5b6103d5600480360360408110156103bf57600080fd5b506001600160a01b038135169060200135611110565b005b6103d5600480360360408110156103ed57600080fd5b506001600160a01b03813516906020013561114f565b6104296004803603602081101561041957600080fd5b50356001600160a01b03166111ee565b60408051918252519081900360200190f35b6103d56004803603602081101561045157600080fd5b50356001600160a01b0316611246565b6103d56004803603602081101561047757600080fd5b5035611388565b6103d56004803603602081101561049457600080fd5b50356001600160a01b0316611431565b6103d5600480360360408110156104ba57600080fd5b506001600160a01b0381358116916020013516611445565b6104fe600480360360408110156104e857600080fd5b506001600160a01b03813516906020013561151a565b604080519115158252519081900360200190f35b6103d56004803603606081101561052857600080fd5b506001600160a01b0381358116916020810135909116906040013561158d565b6104296004803603602081101561055e57600080fd5b50356001600160a01b03166115c7565b6104296004803603602081101561058457600080fd5b50356001600160a01b0316611624565b6104fe600480360360208110156105aa57600080fd5b50356001600160a01b031661167f565b6103d5600480360360608110156105d057600080fd5b506001600160a01b03813581169160208101359091169060400135611692565b6103d56004803603604081101561060657600080fd5b506001600160a01b0381351690602001356116b5565b6103d5600480360360c081101561063257600080fd5b6001600160a01b038235169190810190604081016020820135600160201b81111561065c57600080fd5b82018360208201111561066e57600080fd5b803590602001918460018302840111600160201b8311171561068f57600080fd5b919390929091602081019035600160201b8111156106ac57600080fd5b8201836020820111156106be57600080fd5b803590602001918460018302840111600160201b831117156106df57600080fd5b919350915060ff813516906001600160a01b036020820135169060400135611784565b6104fe600480360361014081101561071957600080fd5b506001600160a01b0381358116916020810135821691604082019160a081019160e08201359161010081013582169161012090910135166117ab565b61077b6004803603602081101561076b57600080fd5b50356001600160a01b031661197f565b604080516001600160a01b039092168252519081900360200190f35b6103d5600480360360208110156107ad57600080fd5b50356001600160a01b03166119e5565b6104fe6119f6565b6104fe600480360360408110156107db57600080fd5b506001600160a01b038135169060200135611a47565b610429611a9d565b6104296004803603602081101561080f57600080fd5b50356001600160a01b0316611aa6565b610827611b05565b604080516001600160e01b03199092168252519081900360200190f35b6104296004803603602081101561085a57600080fd5b50356001600160a01b0316611b10565b6104fe6004803603602081101561088057600080fd5b5035611b6c565b61077b6004803603602081101561089d57600080fd5b50356001600160a01b0316611bbf565b6103d5600480360360608110156108c357600080fd5b506001600160a01b038135811691602081013582169160409091013516611c28565b6103d5600480360360408110156108fb57600080fd5b506001600160a01b0381358116916020013516611cad565b6103d56004803603602081101561092957600080fd5b50356001600160a01b0316611ce4565b6104296004803603602081101561094f57600080fd5b50356001600160a01b0316611cf5565b6103d56004803603604081101561097557600080fd5b506001600160a01b038135169060200135611d4f565b61077b600480360360208110156109a157600080fd5b50356001600160a01b0316611e27565b6103d5600480360360808110156109c757600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610a0157600080fd5b820183602082011115610a1357600080fd5b803590602001918460018302840111600160201b83111715610a3457600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550611e86945050505050565b61077b611eb0565b61077b611f07565b6103d560048036036020811015610a9b57600080fd5b5035611f5e565b610aaa612153565b6040805167ffffffffffffffff9485168152928416602084015292168183015290519081900360600190f35b61042960048036036020811015610aec57600080fd5b50356001600160a01b031661215d565b6103d560048036036040811015610b1257600080fd5b506001600160a01b0381351690602001356121e2565b6104fe60048036036060811015610b3e57600080fd5b6001600160a01b0382351691602081013591810190606081016040820135600160201b811115610b6d57600080fd5b820183602082011115610b7f57600080fd5b803590602001918460018302840111600160201b83111715610ba057600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506121f4945050505050565b61042960048036036040811015610bf757600080fd5b506001600160a01b03813516906020013561225b565b6103d560048036036060811015610c2357600080fd5b506001600160a01b038135811691602081013590911690604001356122c1565b6103d560048036036060811015610c5957600080fd5b506001600160a01b038135811691602081013590911690604001356122fb565b6104fe60048036036020811015610c8f57600080fd5b50356001600160a01b0316612309565b610429612362565b6104fe60048036036020811015610cbd57600080fd5b50356001600160a01b03166123b0565b6103d560048036036080811015610ce357600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610d1d57600080fd5b820183602082011115610d2f57600080fd5b803590602001918460018302840111600160201b83111715610d5057600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506123dd945050505050565b61077b61241d565b6103d560048036036040811015610daf57600080fd5b506001600160a01b0381358116916020013516612474565b6103d5600480360360e0811015610ddd57600080fd5b6001600160a01b038235169190810190604081016020820135600160201b811115610e0757600080fd5b820183602082011115610e1957600080fd5b803590602001918460018302840111600160201b83111715610e3a57600080fd5b919390929091602081019035600160201b811115610e5757600080fd5b820183602082011115610e6957600080fd5b803590602001918460018302840111600160201b83111715610e8a57600080fd5b9193909260ff833516926001600160a01b03602082013516926040820135929091608081019060600135600160201b811115610ec557600080fd5b820183602082011115610ed757600080fd5b803590602001918460018302840111600160201b83111715610ef857600080fd5b509092509050612533565b6103d560048036036080811015610f1957600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610f5357600080fd5b820183602082011115610f6557600080fd5b803590602001918460018302840111600160201b83111715610f8657600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506125a9945050505050565b6103d560048036036040811015610fdd57600080fd5b506001600160a01b03813581169160200135166125b5565b6103d56004803603604081101561100b57600080fd5b506001600160a01b0381351690602001356127e6565b61077b612883565b6103d56004803603604081101561103f57600080fd5b506001600160a01b0381351690602001356128da565b6104296004803603604081101561106b57600080fd5b506001600160a01b03813516906020013561297b565b6103d56004803603602081101561109757600080fd5b50356001600160a01b03166129e4565b6103d5600480360360208110156110bd57600080fd5b50356129f5565b610429600480360360208110156110da57600080fd5b50356001600160a01b0316612a06565b6103d56004803603602081101561110057600080fd5b50356001600160a01b0316612a5c565b61114b8233836000805b506040519080825280601f01601f191660200182016040528015611145576020820181803683370190505b50612ae8565b5050565b611157612c49565b6111608261167f565b61116957600080fd5b8015806111885750600081118015611188575061118582611aa6565b81105b61119157600080fd5b60408051700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b60208083019190915260609490941b6001600160601b0319166031820152815180820360250181526045909101825280519084012060009081529283905290912055565b60408051670dac2f0a0cae4a8f60c31b6020808301919091526001600160601b0319606085901b1660288301528251601c818403018152603c909201835281519181019190912060009081529081905220545b919050565b600061125182611e27565b905060006112e0826001600160a01b031663cff77444856040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b1580156112a557600080fd5b505afa1580156112b9573d6000803e3d6000fd5b505050506040513d60208110156112cf57600080fd5b50516112da85611624565b90612c70565b905060006112ed84611b10565b90508082116112fb57600080fd5b8082036113126001600160a01b0386168583612cb9565b836001600160a01b031663b9b8c24686836040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050600060405180830381600087803b15801561136957600080fd5b505af115801561137d573d6000803e3d6000fd5b505050505050505050565b611390612d0b565b61139981611b6c565b156113a357600080fd5b60006113ae82612db5565b905060006113bb83612e0e565b905060006113c884612e6b565b90506113d384612eba565b6113de838383612f13565b604080516001600160a01b03808616825284166020820152808201839052905185917f07b5483b8e4bd8ea240a474d5117738350e7d431e3668c48a97910b0b397796a919081900360600190a250505050565b611439612c49565b61144281612f34565b50565b61144d612c49565b6114568161167f565b1561146057600080fd5b600061146b82611bbf565b6001600160a01b03161461147e57600080fd5b60006114898361197f565b6001600160a01b03161461149c57600080fd5b6114b16001600160a01b038216306001612fb0565b806001600160a01b03166342966c6860016040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156114f857600080fd5b505af115801561150c573d6000803e3d6000fd5b5050505061114b828261303c565b600080611538836115328661152d611a9d565b61225b565b90613137565b905060006115466000612a06565b11801561155b57508061155885612a06565b10155b801561156f575061156b846111ee565b8311155b8015611583575061157f8461215d565b8310155b9150505b92915050565b611595612d0b565b60006115a08461197f565b90506115ab8161167f565b6115b457600080fd5b6115c18160008585613191565b50505050565b60408051700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b60208083019190915260609390931b6001600160601b0319166031820152815180820360250181526045909101825280519083012060009081529182905290205490565b604080516e6d65646961746f7242616c616e636560881b60208083019190915260609390931b6001600160601b031916602f820152815180820360230181526043909101825280519083012060009081529182905290205490565b60008061168b8361215d565b1192915050565b61169a612d0b565b6116a38361322e565b6116b08360018484613191565b505050565b6116bd612c49565b6116c68261167f565b6116cf57600080fd5b6116d8826111ee565b8111806116e3575080155b6116ec57600080fd5b604080516919185a5b1e531a5b5a5d60b21b6020808301919091526001600160601b0319606086901b16602a8301528251601e818403018152603e83018085528151918301919091206000908152918290529083902084905583905290516001600160a01b038416917fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd25941599190819003605e0190a25050565b61178c612d0b565b600061179c8989898989896132ea565b905061137d8160008585613191565b60408051600481526024810182526020810180516001600160e01b03166337ef410160e11b1781529151815160009384936060933093919290918291908083835b6020831061180b5780518252601f1990920191602091820191016117ec565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461186b576040519150601f19603f3d011682016040523d82523d6000602084013e611870565b606091505b50915091508115806118ac5750805160201480156118ac575080806020019051602081101561189e57600080fd5b50516001600160a01b031633145b806118b657503330145b6118bf57600080fd5b6118c76119f6565b156118d157600080fd5b6118da8a612f34565b6118e389613638565b61191760008960038060200260405190810160405280929190826003602002808284376000920191909152506136a2915050565b60408051808201825261194691600091908a9060029083908390808284376000920191909152506137f6915050565b61194f866138e5565b61195885613946565b61196184613a0e565b611969613a8a565b6119716119f6565b9a9950505050505050505050565b604080516f686f6d65546f6b656e4164647265737360801b60208083019190915260609390931b6001600160601b03191660308201528151808203602401815260449091018252805190830120600090815260029092529020546001600160a01b031690565b6119ed612c49565b61144281613a0e565b7f0a6f646cd611241d8073675e00d1a1ff700fbf1b53fcf473de56d1e6e4b714ba60005260046020527f078d888f9b66f3f8bfa10909e31f1e16240db73449f0500afdbbe3a70da457cc5460ff1690565b600080611a5f8361153286611a5a611a9d565b61297b565b90506000611a6d6000611aa6565b118015611a82575080611a7f85611aa6565b10155b80156115835750611a92846115c7565b909211159392505050565b62015180420490565b6040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b60208083019190915260609390931b6001600160601b0319166033820152815180820360270181526047909101825280519083012060009081529182905290205490565b6358a8b61360e11b90565b604080516f1b5a5b90d85cda151a1c995cda1bdb1960821b60208083019190915260609390931b6001600160601b0319166030820152815180820360240181526044909101825280519083012060009081529182905290205490565b604080516b1b595cdcd859d9519a5e195960a21b602080830191909152602c80830185905283518084039091018152604c909201835281519181019190912060009081526004909152205460ff16919050565b6040805172666f726569676e546f6b656e4164647265737360681b60208083019190915260609390931b6001600160601b03191660338201528151808203602701815260479091018252805190830120600090815260029092529020546001600160a01b031690565b611c30613ae1565b826001600160a01b03166369ffa08a83836040518363ffffffff1660e01b815260040180836001600160a01b03168152602001826001600160a01b0316815260200192505050600060405180830381600087803b158015611c9057600080fd5b505af1158015611ca4573d6000803e3d6000fd5b50505050505050565b611cb5613ae1565b6001600160a01b0382161580611cd15750611ccf8261167f565b155b611cda57600080fd5b61114b8282613b5a565b611cec612c49565b61144281613638565b600080611d01836111ee565b90506000611d0e84612a06565b90506000611d1e8561152d611a9d565b90506000818311611d30576000611d34565b8183035b9050808410611d435780611d45565b835b9695505050505050565b611d57612c49565b611d608261167f565b611d6957600080fd5b611d72826115c7565b811180611d7d575080155b611d8657600080fd5b6040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b6020808301919091526001600160601b0319606086901b16603383015282516027818403018152604783018085528151918301919091206000908152918290529083902084905583905290516001600160a01b038416917f4c177b42dbe934b3abbc0208c11a42e46589983431616f1710ab19969c5ed62e919081900360670190a25050565b604080516b1a5b9d195c995cdd125b5c1b60a21b60208083019190915260609390931b6001600160601b031916602c8201528151808203840181529082018252805190830120600090815260029092529020546001600160a01b031690565b611e8e612d0b565b611e978461322e565b611ea48460018585613191565b6115c183858484613b94565b7f98aa806e31e94a687a31c65769cb99670064dd7f5a87526da075c5fb4eab988060005260026020527f0c1206883be66049a02d4937078367c00b3d71dd1a9465df969363c6ddeac96d546001600160a01b031690565b7f02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c060005260026020527fb7802e97e87ef2842a6cce7da7ffaeaedaa2f61a6a7870b23d9d01fc9b73712e546001600160a01b031690565b6000611f6861241d565b9050806001600160a01b031663cb08a10c836040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b158015611fae57600080fd5b505afa158015611fc2573d6000803e3d6000fd5b505050506040513d6020811015611fd857600080fd5b505115611fe457600080fd5b306001600160a01b0316816001600160a01b0316633f9a8e7e846040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b15801561203257600080fd5b505afa158015612046573d6000803e3d6000fd5b505050506040513d602081101561205c57600080fd5b50516001600160a01b03161461207157600080fd5b612079611eb0565b6001600160a01b0316816001600160a01b0316634a610b04846040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b1580156120c657600080fd5b505afa1580156120da573d6000803e3d6000fd5b505050506040513d60208110156120f057600080fd5b50516001600160a01b03161461210557600080fd5b6040805160248082018590528251808303909101815260449091019091526020810180516001600160e01b0316630950d51560e01b9081179091529061214c816001613d01565b5050505050565b6003806001909192565b60408051670dad2dca0cae4a8f60c31b6020808301919091526001600160601b0319606085901b1660288301528251601c818403018152603c9092018352815191810191909120600090815290819052908120546001600160a01b0383166121c6579050611241565b600081116121d55760006121d8565b60015b60ff169392505050565b6121ea612c49565b61114b8282613e0c565b60006121fe613e68565b61225157604080516000815260208101909152825185906014116122415761222584613e8d565b9050601484511115612241578351601319016014850190815291505b61224e3387838886613e94565b50505b5060019392505050565b604080516f746f74616c5370656e7450657244617960801b60208083019190915260609490941b6001600160601b031916603082015260448082019390935281518082039093018352606401815281519183019190912060009081529182905290205490565b6122c9612c49565b60006122d484611e27565b6001600160a01b0316146122e757600080fd5b6122f18383613f2d565b6116b08382613e0c565b6116b083838360008061111a565b604080516861636b4465706c6f7960b81b60208083019190915260609390931b6001600160601b03191660298201528151808203601d018152603d90910182528051908301206000908152600490925290205460ff1690565b7f2dfd6c9f781bb6bbb5369c114e949b69ebb440ef3d4dd6b2836225eb1dc3a2be60009081526020527f2de0d2cdc19d356cb53b5984f91bfd3b31fe0c678a0d190a6db39274bb34753f5490565b60006123bb8261167f565b8015611587575060006123cd83611bbf565b6001600160a01b03161492915050565b6123e5612d0b565b60006123f08561197f565b90506123fb8161167f565b61240457600080fd5b6124118160008686613191565b61214c84828585613b94565b7f811bbb11e8899da471f0e69a3ed55090fc90215227fc5fb1cb0d6e962ea7b74f60005260026020527fb4ed64697d3ef8518241966f7c6f28b0d72f20f51198717d198d2d55076c593d546001600160a01b031690565b61247c613ae1565b806001600160a01b03811661249057600080fd5b612499836123b0565b6124a257600080fd5b60006124ad84614033565b9050600081116124bc57600080fd5b60006124c785611cf5565b9050600081116124d657600080fd5b808211156124e2578091505b6124f4856124ee611a9d565b84614131565b60408051600080825260208201909252606091612516918890889087906141af565b90506000612525826001613d01565b9050611ca48188888761482a565b61253b612d0b565b600061254b8b8b8b8b8b8b6132ea565b905061255a8160008787613191565b61259c85828686868080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613b9492505050565b5050505050505050505050565b6115c184848484612ae8565b7fd814b1d787b8a2d93a1c320d66800a58a03ed3bf12b285ec5ec1e0e26d6550cc600081905260046020527f612ef5ed0e5e2bed1afa8019b1ae0542ab321409196a5627628d39f86fff869a5460ff161561260f57600080fd5b61261883613a0e565b612624621e84806138e5565b73a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48650246139ca80061264a8285613f2d565b6126548282613e0c565b600061265f83611624565b905081811161266d57600080fd5b8181036126846001600160a01b0385168783612cb9565b856001600160a01b031663b9b8c24685836040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050600060405180830381600087803b1580156126db57600080fd5b505af11580156126ef573d6000803e3d6000fd5b5050505073dac17f958d2ee523a2206206994597c13d831ec7935064ae9f7bcc00925061271c8487613f2d565b6127268484613e0c565b61272f84611624565b915082821161273d57600080fd5b508181036127556001600160a01b0385168783612cb9565b856001600160a01b031663b9b8c24685836040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050600060405180830381600087803b1580156127ac57600080fd5b505af11580156127c0573d6000803e3d6000fd5b50505060009586525050600460205250506040909120805460ff19166001179055505050565b6127ee612c49565b6127f78261167f565b61280057600080fd5b80158061282657506128118261215d565b81118015612826575061282382612a06565b81105b61282f57600080fd5b60408051670dac2f0a0cae4a8f60c31b60208083019190915260609490941b6001600160601b03191660288201528151808203601c018152603c909101825280519084012060009081529283905290912055565b7f269c5905f777ee6391c7a361d17039a7d62f52ba9fffeb98c5ade342705731a360005260026020527f15c764a0cd4bb3d72a49abedd3d6793c3b93c0d57f43174a348b443be86f79c1546001600160a01b031690565b6128e2612c49565b6128eb8261167f565b6128f457600080fd5b60008111801561290b575061290882612a06565b81105b801561291e575061291b826111ee565b81105b61292757600080fd5b60408051670dad2dca0cae4a8f60c31b60208083019190915260609490941b6001600160601b03191660288201528151808203601c018152603c909101825280519084012060009081529283905290912055565b6040805172746f74616c457865637574656450657244617960681b60208083019190915260609490941b6001600160601b031916603382015260478082019390935281518082039093018352606701815281519183019190912060009081529182905290205490565b6129ec612c49565b61144281613946565b6129fd612c49565b611442816138e5565b604080516919185a5b1e531a5b5a5d60b21b60208083019190915260609390931b6001600160601b031916602a8201528151808203601e018152603e909101825280519083012060009081529182905290205490565b612a64612c49565b612a6d81611e27565b6001600160a01b031663f3fef3a3826000196040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050600060405180830381600087803b158015612ac557600080fd5b505af1158015612ad9573d6000803e3d6000fd5b50505050611442816000613f2d565b612af0613e68565b15612afa57600080fd5b6000846001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b158015612b4957600080fd5b505afa158015612b5d573d6000803e3d6000fd5b505050506040513d6020811015612b7357600080fd5b50519050612b81600161489a565b612b966001600160a01b0386163330866148be565b612ba0600061489a565b6000612c2582876001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b158015612bf357600080fd5b505afa158015612c07573d6000803e3d6000fd5b505050506040513d6020811015612c1d57600080fd5b505190612c70565b905083811115612c3457600080fd5b612c418633878487613e94565b505050505050565b612c51611f07565b6001600160a01b0316336001600160a01b031614612c6e57600080fd5b565b6000612cb283836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250614918565b9392505050565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526116b09084906149af565b6000612d1561241d565b9050336001600160a01b03821614612d2c57600080fd5b612d34611eb0565b6001600160a01b0316816001600160a01b031663d67bdd256040518163ffffffff1660e01b815260040160206040518083038186803b158015612d7657600080fd5b505afa158015612d8a573d6000803e3d6000fd5b505050506040513d6020811015612da057600080fd5b50516001600160a01b03161461144257600080fd5b604080516b36b2b9b9b0b3b2aa37b5b2b760a11b602080830191909152602c80830185905283518084039091018152604c90920183528151918101919091206000908152600290915220546001600160a01b0316919050565b604080516f1b595cdcd859d9549958da5c1a595b9d60821b602080830191909152603080830185905283518084039091018152605090920183528151918101919091206000908152600290915220546001600160a01b0316919050565b604080516b6d65737361676556616c756560a01b602080830191909152602c80830185905283518084039091018152604c90920183528151918101919091206000908152908190522054919050565b604080516b1b595cdcd859d9519a5e195960a21b602080830191909152602c8083019490945282518083039094018452604c9091018252825192810192909220600090815260049092529020805460ff19166001179055565b6116b06000612f2185611bbf565b6001600160a01b03161484848485614a60565b612f3d81614c34565b612f4657600080fd5b7f811bbb11e8899da471f0e69a3ed55090fc90215227fc5fb1cb0d6e962ea7b74f60005260026020527fb4ed64697d3ef8518241966f7c6f28b0d72f20f51198717d198d2d55076c593d80546001600160a01b0319166001600160a01b0392909216919091179055565b826001600160a01b03166340c10f1983836040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050602060405180830381600087803b15801561300757600080fd5b505af115801561301b573d6000803e3d6000fd5b505050506040513d602081101561303157600080fd5b50516116b057600080fd5b604080516f686f6d65546f6b656e4164647265737360801b6020808301919091526001600160601b0319606086811b82166030850152845160248186030181526044850186528051908401206000908152600280855286822080546001600160a01b03808b166001600160a01b0319928316811790935572666f726569676e546f6b656e4164647265737360681b60648a0152948a901b90951660778801528751606b818903018152608b909701808952875197870197909720835294529485208054909216908716908117909155909290917f78d063210f4fb6b4cc932390bb8045fa2465e51349590182dab8b9e84c57a6ee9190a35050565b600082820183811015612cb2576040805162461bcd60e51b815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b613199613e68565b156131a357600080fd5b6131ad8482611a47565b6131b657600080fd5b6131c8846131c2611a9d565b83614c6d565b6131d58385848485614a60565b6131dd614cee565b826001600160a01b0316856001600160a01b03167f9afd47907e25028cdaca89d193518c302bbb128617d5a992c5abd45815526593846040518082815260200191505060405180910390a450505050565b604080516861636b4465706c6f7960b81b6020808301919091526001600160601b0319606085901b1660298301528251601d818403018152603d909201835281519181019190912060009081526004909152205460ff1661144257604080516861636b4465706c6f7960b81b6020808301919091526001600160601b0319606085901b1660298301528251601d818403018152603d90920183528151918101919091206000908152600490915220805460ff1916600117905550565b6000806132f68861197f565b90506001600160a01b03811661359d57606087878080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050604080516020601f8b018190048102820181019092528981529394506060939250899150889081908401838280828437600092019190915250508451929350505015158061338b575060008151115b61339457600080fd5b81516133a2578091506133ab565b80516133ab5750805b6133b482614d61565b91506133be612883565b6001600160a01b031663a39d6acf8383886133d761241d565b6001600160a01b0316631544298e6040518163ffffffff1660e01b815260040160206040518083038186803b15801561340f57600080fd5b505afa158015613423573d6000803e3d6000fd5b505050506040513d602081101561343957600080fd5b50516040516001600160e01b031960e087901b16815260ff831660448201526064810182905260806004820190815285516084830152855190918291602482019160a40190602089019080838360005b838110156134a1578181015183820152602001613489565b50505050905090810190601f1680156134ce5780820380516001836020036101000a031916815260200191505b50838103825286518152865160209182019188019080838360005b838110156135015781810151838201526020016134e9565b50505050905090810190601f16801561352e5780820380516001836020036101000a031916815260200191505b509650505050505050602060405180830381600087803b15801561355157600080fd5b505af1158015613565573d6000803e3d6000fd5b505050506040513d602081101561357b57600080fd5b505192506135898a8461303c565b613596838660ff16614e16565b505061362d565b6135a68161167f565b61362d578260ff16816001600160a01b031663313ce5676040518163ffffffff1660e01b815260040160206040518083038186803b1580156135e757600080fd5b505afa1580156135fb573d6000803e3d6000fd5b505050506040513d602081101561361157600080fd5b505160ff161461362057600080fd5b61362d818460ff16614e16565b979650505050505050565b7f98aa806e31e94a687a31c65769cb99670064dd7f5a87526da075c5fb4eab988060005260026020527f0c1206883be66049a02d4937078367c00b3d71dd1a9465df969363c6ddeac96d80546001600160a01b0319166001600160a01b0392909216919091179055565b6040810151158015906136bc575060408101516020820151115b80156136cc575060208101518151115b6136d557600080fd5b8051604080516919185a5b1e531a5b5a5d60b21b602082810191909152606086901b6001600160601b031916602a83018190528351808403601e018152603e8401855280519083012060009081528083528481209590955581860151670dac2f0a0cae4a8f60c31b605e850152606684018290528451605a818603018152607a8501865280519084012086528583528486205583860151670dad2dca0cae4a8f60c31b609a85015260a28401919091528351609681850301815260b690930184528251928201929092208452839052908220556001600160a01b038316907fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd25941599083905b60200201516040518082815260200191505060405180910390a25050565b805160208201511061380757600080fd5b80516040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b602082810191909152606086901b6001600160601b031916603383018190528351808403602701815260478401855280519083012060009081528083528481209590955581860151700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b606785015260788401919091528351606c818503018152608c90930184528251928201929092208452839052908220556001600160a01b038316907f4c177b42dbe934b3abbc0208c11a42e46589983431616f1710ab19969c5ed62e9083906137d8565b6138ed614f9a565b8111156138f957600080fd5b7f2dfd6c9f781bb6bbb5369c114e949b69ebb440ef3d4dd6b2836225eb1dc3a2be60009081526020527f2de0d2cdc19d356cb53b5984f91bfd3b31fe0c678a0d190a6db39274bb34753f55565b6001600160a01b03811661395957600080fd5b7f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0613982611f07565b604080516001600160a01b03928316815291841660208301528051918290030190a17f02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c060005260026020527fb7802e97e87ef2842a6cce7da7ffaeaedaa2f61a6a7870b23d9d01fc9b73712e80546001600160a01b0319166001600160a01b0392909216919091179055565b613a1781614c34565b613a2057600080fd5b7f269c5905f777ee6391c7a361d17039a7d62f52ba9fffeb98c5ade342705731a360005260026020527f15c764a0cd4bb3d72a49abedd3d6793c3b93c0d57f43174a348b443be86f79c180546001600160a01b0319166001600160a01b0392909216919091179055565b7f0a6f646cd611241d8073675e00d1a1ff700fbf1b53fcf473de56d1e6e4b714ba60005260046020527f078d888f9b66f3f8bfa10909e31f1e16240db73449f0500afdbbe3a70da457cc805460ff19166001179055565b306001600160a01b0316636fde82026040518163ffffffff1660e01b815260040160206040518083038186803b158015613b1a57600080fd5b505afa158015613b2e573d6000803e3d6000fd5b505050506040513d6020811015613b4457600080fd5b50516001600160a01b03163314612c6e57600080fd5b806001600160a01b038116613b6e57600080fd5b6001600160a01b038316613b8a57613b8582614fdc565b6116b0565b6116b08383614fe7565b613b9d84614c34565b156115c157836001600160a01b031663db7af85460e01b84848460405160240180846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015613c0c578181015183820152602001613bf4565b50505050905090810190601f168015613c395780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529181526020820180516001600160e01b03166001600160e01b031990991698909817885251815191979096508695509350915081905083835b60208310613c9f5780518252601f199092019160209182019101613c80565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114611ca4576040519150601f19603f3d011682016040523d82523d6000602084013e611ca4565b6000613d0b61241d565b6001600160a01b031663dc8601b3613d21611eb0565b85613d2a612362565b6040518463ffffffff1660e01b815260040180846001600160a01b0316815260200180602001838152602001828103825284818151815260200191508051906020019080838360005b83811015613d8b578181015183820152602001613d73565b50505050905090810190601f168015613db85780820380516001836020036101000a031916815260200191505b50945050505050602060405180830381600087803b158015613dd957600080fd5b505af1158015613ded573d6000803e3d6000fd5b505050506040513d6020811015613e0357600080fd5b50519392505050565b604080516f1b5a5b90d85cda151a1c995cda1bdb1960821b60208083019190915260609490941b6001600160601b0319166030820152815180820360240181526044909101825280519084012060009081529283905290912055565b7f6168652c307c1e813ca11cfb3a601f1cf3b22452021a5052d8b05f1f1f8a3e925490565b6014015190565b6001600160a01b03831615801590613ec55750613eaf611eb0565b6001600160a01b0316836001600160a01b031614155b613ece57600080fd5b613ed78561167f565b613ef7576000613ee686615074565b9050613ef5868260ff16614e16565b505b613f01858361151a565b613f0a57600080fd5b613f16856124ee611a9d565b6060612516613f2487611bbf565b878686866141af565b6001600160a01b0381161580613fb85750806001600160a01b031663bdd378a0836040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b158015613f8b57600080fd5b505afa158015613f9f573d6000803e3d6000fd5b505050506040513d6020811015613fb557600080fd5b50515b613fc157600080fd5b604080516b1a5b9d195c995cdd125b5c1b60a21b60208083019190915260609490941b6001600160601b031916602c82015281518082038501815290820182528051908401206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b60008061403f83611e27565b9050600061404c82614c34565b6140575760006140d1565b816001600160a01b031663cff77444856040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b1580156140a457600080fd5b505afa1580156140b8573d6000803e3d6000fd5b505050506040513d60208110156140ce57600080fd5b50515b90506141296140e3826112da87611624565b604080516370a0823160e01b815230600482015290516001600160a01b038816916370a08231916024808301926020929190829003018186803b158015612bf357600080fd5b949350505050565b61413f81611532858561225b565b600080858560405160200180806f746f74616c5370656e7450657244617960801b815250601001836001600160a01b031660601b81526014018281526020019250505060405160208183030381529060405280519060200120815260200190815260200160002081905550505050565b6060600080835111806141d457506000356001600160e01b03191663d740548160e01b145b90506001600160a01b03871661468f576141fa866141f5866115328a611624565b615242565b61420386612309565b15614340578061426157604080516001600160a01b0380891660248301528716604482015260648082018790528251808303909101815260849091019091526020810180516001600160e01b031663125e4cfb60e01b179052614338565b63c534576160e01b8686868660405160240180856001600160a01b03168152602001846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156142d15781810151838201526020016142b9565b50505050905090810190601f1680156142fe5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909916989098179097525050505050505b915050614821565b600061434b87615074565b905060606143588861529d565b9050606061436589615463565b9050600082511180614378575060008151115b61438157600080fd5b836144d457632ae87cdd60e01b898383868c8c60405160240180876001600160a01b0316815260200180602001806020018660ff168152602001856001600160a01b03168152602001848152602001838103835288818151815260200191508051906020019080838360005b838110156144055781810151838201526020016143ed565b50505050905090810190601f1680156144325780820380516001836020036101000a031916815260200191505b50838103825287518152875160209182019189019080838360005b8381101561446557818101518382015260200161444d565b50505050905090810190601f1680156144925780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909c169b909b17909a5250614684975050505050505050565b63d522cfd760e01b898383868c8c8c60405160240180886001600160a01b0316815260200180602001806020018760ff168152602001866001600160a01b031681526020018581526020018060200184810384528a818151815260200191508051906020019080838360005b83811015614558578181015183820152602001614540565b50505050905090810190601f1680156145855780820380516001836020036101000a031916815260200191505b5084810383528951815289516020918201918b019080838360005b838110156145b85781810151838201526020016145a0565b50505050905090810190601f1680156145e55780820380516001836020036101000a031916815260200191505b50848103825285518152855160209182019187019080838360005b83811015614618578181015183820152602001614600565b50505050905090810190601f1680156146455780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909e169d909d17909c5250505050505050505050505b945050505050614821565b856001600160a01b03166342966c68856040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156146d557600080fd5b505af11580156146e9573d6000803e3d6000fd5b505050508061474657604080516001600160a01b03808a1660248301528716604482015260648082018790528251808303909101815260849091019091526020810180516001600160e01b031663272255bb60e01b17905261481d565b63867f7a4d60e01b8786868660405160240180856001600160a01b03168152602001846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156147b657818101518382015260200161479e565b50505050905090810190601f1680156147e35780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909916989098179097525050505050505b9150505b95945050505050565b6148348484615596565b61483e8483615602565b6148488482615672565b83826001600160a01b0316846001600160a01b03167f59a9a8027b9c87b961e254899821c9a276b5efc35d1f7409ea4f291470f1629a846040518082815260200191505060405180910390a450505050565b7f6168652c307c1e813ca11cfb3a601f1cf3b22452021a5052d8b05f1f1f8a3e9255565b604080516001600160a01b0380861660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526115c19085906149af565b600081848411156149a75760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561496c578181015183820152602001614954565b50505050905090810190601f1680156149995780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b6060614a04826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166156c19092919063ffffffff16565b8051909150156116b057808060200190516020811015614a2357600080fd5b50516116b05760405162461bcd60e51b815260040180806020018281038252602a815260200180615b61602a913960400191505060405180910390fd5b8415614c16576000614a7185611624565b90506001600160a01b038516730ae055097c6d159879521c384f1d2123d1f195e6148015614a9e57508281105b15614abc57614ab96001600160a01b03861630838603612fb0565b50815b6000614ac786611e27565b90506001600160a01b03811615614bed576000614b5d826001600160a01b031663cff77444896040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b158015614b2a57600080fd5b505afa158015614b3e573d6000803e3d6000fd5b505050506040513d6020811015614b5457600080fd5b50518490612c70565b905080851115614beb57816001600160a01b031663f3fef3a388614b8c614b838b611b10565b858a0390613137565b6040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050600060405180830381600087803b158015614bd257600080fd5b505af1158015614be6573d6000803e3d6000fd5b505050505b505b614bfb866141f58486612c70565b614c0f6001600160a01b0387168686612cb9565b505061214c565b61214c8383614c24876156d0565b6001600160a01b03169190612fb0565b6000813f7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470818114801590614129575050151592915050565b614c7b81611532858561297b565b6000808585604051602001808072746f74616c457865637574656450657244617960681b815250601301836001600160a01b031660601b81526014018281526020019250505060405160208183030381529060405280519060200120815260200190815260200160002081905550505050565b6000614cf861241d565b6001600160a01b031663669f618b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614d3057600080fd5b505afa158015614d44573d6000803e3d6000fd5b505050506040513d6020811015614d5a57600080fd5b5051905090565b606080827f2066726f6d2078446169000000000000000000000000000000000000000000006040516020018083805190602001908083835b60208310614db85780518252601f199092019160209182019101614d99565b51815160209384036101000a60001901801990921691161790529201938452506040805180850381529390910190525093517f000000000000000000000000000000000000000000000000000000000000000a018452509192915050565b60006012821015614f0a5781601203600a0a90506000614e4082614e3a600061215d565b906156d3565b90506000614e5283614e3a60006111ee565b90506000614e6484614e3a6000612a06565b90506000614e7685614e3a60006115c7565b90506000614e8886614e3a6000611aa6565b905084614ebe5760019450848411614ebe5760649350606491508383111580614eb15750818111155b15614ebe57506127109150815b614ee2886040518060600160405280868152602001878152602001888152506136a2565b614f00886040518060400160405280848152602001858152506137f6565b50505050506116b0565b60128203600a0a9050614f63836040518060600160405280614f3685614f306000612a06565b90615715565b8152602001614f4985614f3060006111ee565b8152602001614f5c85614f30600061215d565b90526136a2565b6116b0836040518060400160405280614f8085614f306000611aa6565b8152602001614f9385614f3060006115c7565b90526137f6565b6000614fa461241d565b6001600160a01b031663e5789d036040518163ffffffff1660e01b815260040160206040518083038186803b158015614d3057600080fd5b4761114b828261576e565b604080516370a0823160e01b8152306004820152905183916000916001600160a01b038416916370a08231916024808301926020929190829003018186803b15801561503257600080fd5b505afa158015615046573d6000803e3d6000fd5b505050506040513d602081101561505c57600080fd5b505190506115c16001600160a01b0383168483612cb9565b60408051600481526024810182526020810180516001600160e01b031663313ce56760e01b1781529151815160009384936060936001600160a01b03881693919290918291908083835b602083106150dd5780518252601f1990920191602091820191016150be565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461513d576040519150601f19603f3d011682016040523d82523d6000602084013e615142565b606091505b5091509150816152235760408051600481526024810182526020810180516001600160e01b0316632e0f262560e01b178152915181516001600160a01b0388169382918083835b602083106151a85780518252601f199092019160209182019101615189565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615208576040519150601f19603f3d011682016040523d82523d6000602084013e61520d565b606091505b5090925090508161522357600092505050611241565b80806020019051602081101561523857600080fd5b5051949350505050565b604080516e6d65646961746f7242616c616e636560881b60208083019190915260609490941b6001600160601b031916602f820152815180820360230181526043909101825280519084012060009081529283905290912055565b60408051600481526024810182526020810180516001600160e01b03166306fdde0360e01b1781529151815160609360009385936001600160a01b03881693919290918291908083835b602083106153065780518252601f1990920191602091820191016152e7565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615366576040519150601f19603f3d011682016040523d82523d6000602084013e61536b565b606091505b50915091508161545a5760408051600481526024810182526020810180516001600160e01b03166351fa6fbf60e11b178152915181516001600160a01b0388169382918083835b602083106153d15780518252601f1990920191602091820191016153b2565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615431576040519150601f19603f3d011682016040523d82523d6000602084013e615436565b606091505b5090925090508161545a576040518060200160405280600081525092505050611241565b614129816157d3565b60408051600481526024810182526020810180516001600160e01b03166395d89b4160e01b1781529151815160609360009385936001600160a01b03881693919290918291908083835b602083106154cc5780518252601f1990920191602091820191016154ad565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461552c576040519150601f19603f3d011682016040523d82523d6000602084013e615531565b606091505b50915091508161545a5760408051600481526024810182526020810180516001600160e01b0316631eedf1af60e31b178152915181516001600160a01b038816938291808383602083106153d15780518252601f1990920191602091820191016153b2565b604080516b36b2b9b9b0b3b2aa37b5b2b760a11b602080830191909152602c8083019590955282518083039095018552604c90910182528351938101939093206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b604080516f1b595cdcd859d9549958da5c1a595b9d60821b60208083019190915260308083019590955282518083039095018552605090910182528351938101939093206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b604080516b6d65737361676556616c756560a01b602080830191909152602c8083019590955282518083039095018552604c909101825283519381019390932060009081529283905290912055565b6060614129848460008561592f565b90565b6000612cb283836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250615a9c565b60008261572457506000611587565b8282028284828161573157fe5b0414612cb25760405162461bcd60e51b8152600401808060200182810382526021815260200180615b406021913960400191505060405180910390fd5b6040516001600160a01b0383169082156108fc029083906000818181858888f1935050505061114b5780826040516157a590615b01565b6001600160a01b039091168152604051908190036020019082f09050801580156115c1573d6000803e3d6000fd5b60606020825111156158a9578180602001905160208110156157f457600080fd5b8101908080516040519392919084600160201b82111561581357600080fd5b90830190602082018581111561582857600080fd5b8251600160201b81118282018810171561584157600080fd5b82525081516020918201929091019080838360005b8381101561586e578181015183820152602001615856565b50505050905090810190601f16801561589b5780820380516001836020036101000a031916815260200191505b506040525050509050611241565b81516020141561591a5760008280602001905160208110156158ca57600080fd5b50516040805160208082528183019092529192506060919060208201818036833701905050905060008260208301525b821561590f5760089290921b916001016158fa565b815291506112419050565b50604080516020810190915260008152611241565b606061593a85614c34565b61598b576040805162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b60006060866001600160a01b031685876040518082805190602001908083835b602083106159ca5780518252601f1990920191602091820191016159ab565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d8060008114615a2c576040519150601f19603f3d011682016040523d82523d6000602084013e615a31565b606091505b50915091508115615a455791506141299050565b805115615a555780518082602001fd5b60405162461bcd60e51b815260206004820181815286516024840152865187939192839260440191908501908083836000831561496c578181015183820152602001614954565b60008183615aeb5760405162461bcd60e51b815260206004820181815283516024840152835190928392604490910191908501908083836000831561496c578181015183820152602001614954565b506000838581615af757fe5b0495945050505050565b603280615b0e8339019056fe60806040526040516032380380603283398181016040526020811015602357600080fd5b50516001600160a01b038116fffe536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f775361666545524332303a204552433230206f7065726174696f6e20646964206e6f742073756363656564a2646970667358221220274caa5a6a0c729be67fdcf4620871c021af258e3a717eac25b66f36cb17ee4964736f6c63430007050033", + "linkReferences": {}, + "deployedLinkReferences": {} +} \ No newline at end of file diff --git a/operate/data/contracts/foreign_omnibridge/contract.py b/operate/data/contracts/foreign_omnibridge/contract.py new file mode 100644 index 000000000..6228dad6b --- /dev/null +++ b/operate/data/contracts/foreign_omnibridge/contract.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the class to connect to the `ForeignOmnibridge` contract.""" + +from math import ceil + +from aea.common import JSONLike +from aea.configurations.base import PublicId +from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi + + +PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" # nosec + +#DEFAULT_GAS_BRIDGE_ETH_TO = 800_000 +DEFAULT_GAS_RELAY_TOKENS = 800_000 + +# By simulations, nonzero-ERC20-bridge gas ~ 1.05 zero-ERC20-bridge gas +NONZERO_ERC20_GAS_FACTOR = 1.15 + + +class ForeignOmnibridge(Contract): + """ForeignOmnibridge.""" + + contract_id = PublicId.from_str("valory/foreign_omnibridge:0.1.0") + + # @classmethod + # def build_bridge_eth_to_tx( + # cls, + # ledger_api: LedgerApi, + # contract_address: str, + # sender: str, + # to: str, + # amount: int, + # min_gas_limit: int, + # extra_data: bytes, + # raise_on_try: bool = False, + # ) -> JSONLike: + # """Build bridgeETHTo tx.""" + # contract_instance = cls.get_instance( + # ledger_api=ledger_api, contract_address=contract_address + # ) + # tx = contract_instance.functions.bridgeETHTo( + # to, min_gas_limit, extra_data + # ).build_transaction( + # { + # "from": sender, + # "value": amount, + # "gas": DEFAULT_GAS_BRIDGE_ETH_TO, + # "gasPrice": ledger_api.api.eth.gas_price, + # "nonce": ledger_api.api.eth.get_transaction_count(sender), + # } + # ) + # return ledger_api.update_with_gas_estimate( + # transaction=tx, + # raise_on_try=raise_on_try, + # ) + + @classmethod + def build_relay_tokens_tx( + cls, + ledger_api: LedgerApi, + contract_address: str, + sender: str, + token: str, + receiver: str, + amount: int, + raise_on_try: bool = False, + ) -> JSONLike: + """Build bridgeERC20To tx.""" + contract_instance = cls.get_instance( + ledger_api=ledger_api, contract_address=contract_address + ) + tx = contract_instance.functions.relayTokens( + token, receiver, amount + ).build_transaction( + { + "from": sender, + "gas": 1, + "gasPrice": ledger_api.api.eth.gas_price, + "nonce": ledger_api.api.eth.get_transaction_count(sender), + } + ) + + ledger_api.update_with_gas_estimate( + transaction=tx, + raise_on_try=raise_on_try, + ) + + if tx["gas"] > 1: + return tx + + tx_zero = contract_instance.functions.relayTokens( + token, receiver, 0 + ).build_transaction( + { + "from": PLACEHOLDER_NATIVE_TOKEN_ADDRESS, + "gas": 1, + "gasPrice": ledger_api.api.eth.gas_price, + "nonce": ledger_api.api.eth.get_transaction_count(sender), + } + ) + + ledger_api.update_with_gas_estimate( + transaction=tx_zero, + raise_on_try=raise_on_try, + ) + + if tx_zero["gas"] > 1: + tx["gas"] = ceil(tx_zero["gas"] * NONZERO_ERC20_GAS_FACTOR) + return tx + + tx["gas"] = DEFAULT_GAS_RELAY_TOKENS + return tx diff --git a/operate/data/contracts/foreign_omnibridge/contract.yaml b/operate/data/contracts/foreign_omnibridge/contract.yaml new file mode 100644 index 000000000..7eb5b01c9 --- /dev/null +++ b/operate/data/contracts/foreign_omnibridge/contract.yaml @@ -0,0 +1,23 @@ +name: foreign_omnibridge +author: valory +version: 0.1.0 +type: contract +description: ForeignOmnibridge +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye + build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m + contract.py: bafybeib2kiztts3436vccvraeura3jb23oihgzxgrt3biakbed24edcaai +fingerprint_ignore_patterns: [] +contracts: [] +class_name: ForeignOmnibridge +contract_interface_paths: + ethereum: build/ForeignOmnibridge.json +dependencies: + open-aea-ledger-ethereum: + version: ==1.60.0 + open-aea-test-autonomy: + version: ==0.18.3 + web3: + version: <7,>=6.0.0 diff --git a/operate/data/contracts/home_omnibridge/__init__.py b/operate/data/contracts/home_omnibridge/__init__.py new file mode 100644 index 000000000..5a31957df --- /dev/null +++ b/operate/data/contracts/home_omnibridge/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the `HomeOmnibridge` contract.""" diff --git a/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json b/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json new file mode 100644 index 000000000..a3ada7d7d --- /dev/null +++ b/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json @@ -0,0 +1,10 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "HomeOmnibridge", + "sourceName": "HomeOmnibridge.sol", + "abi": [{"inputs":[{"internalType":"string","name":"_suffix","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"newLimit","type":"uint256"}],"name":"DailyLimitChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"newLimit","type":"uint256"}],"name":"ExecutionDailyLimitChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"},{"indexed":false,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"FailedMessageFixed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"}],"name":"FeeDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"}],"name":"FeeDistributionFailed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"nativeToken","type":"address"},{"indexed":true,"internalType":"address","name":"bridgedToken","type":"address"}],"name":"NewTokenRegistered","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"}],"name":"TokensBridged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"}],"name":"TokensBridgingInitiated","type":"event"},{"inputs":[],"name":"bridgeContract","outputs":[{"internalType":"contract IAMB","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_nativeToken","type":"address"}],"name":"bridgedTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_to","type":"address"}],"name":"claimTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgedToken","type":"address"},{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_to","type":"address"}],"name":"claimTokensFromTokenContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"dailyLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"deployAndHandleBridgedTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"deployAndHandleBridgedTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"executionDailyLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"executionMaxPerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeManager","outputs":[{"internalType":"contract OmnibridgeFeeManager","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_messageId","type":"bytes32"}],"name":"fixFailedMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_receiver","type":"address"}],"name":"fixMediatorBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_homeToken","type":"address"}],"name":"foreignTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"forwardingRulesManager","outputs":[{"internalType":"contract MultiTokenForwardingRulesManager","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"gasLimitManager","outputs":[{"internalType":"contract SelectorTokenGasLimitManager","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBridgeInterfacesVersion","outputs":[{"internalType":"uint64","name":"major","type":"uint64"},{"internalType":"uint64","name":"minor","type":"uint64"},{"internalType":"uint64","name":"patch","type":"uint64"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getBridgeMode","outputs":[{"internalType":"bytes4","name":"_data","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getCurrentDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"handleBridgedTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"handleBridgedTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"handleNativeTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"handleNativeTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_foreignToken","type":"address"}],"name":"homeTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgeContract","type":"address"},{"internalType":"address","name":"_mediatorContract","type":"address"},{"internalType":"uint256[3]","name":"_dailyLimitMaxPerTxMinPerTxArray","type":"uint256[3]"},{"internalType":"uint256[2]","name":"_executionDailyLimitExecutionMaxPerTxArray","type":"uint256[2]"},{"internalType":"address","name":"_gasLimitManager","type":"address"},{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_tokenFactory","type":"address"},{"internalType":"address","name":"_feeManager","type":"address"},{"internalType":"address","name":"_forwardingRulesManager","type":"address"}],"name":"initialize","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"isBridgedTokenDeployAcknowledged","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isInitialized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"isRegisteredAsNativeToken","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"isTokenRegistered","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"maxAvailablePerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"maxPerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"mediatorBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mediatorContractOnOtherSide","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_messageId","type":"bytes32"}],"name":"messageFixed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenFactory","type":"address"},{"internalType":"address","name":"_forwardingRulesManager","type":"address"},{"internalType":"address","name":"_gasLimitManager","type":"address"},{"internalType":"address","name":"_feeManager","type":"address"}],"name":"migrateTo_3_3_0","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"minPerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgedToken","type":"address"}],"name":"nativeTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"onTokenTransfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract IERC677","name":"token","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"relayTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC677","name":"token","type":"address"},{"internalType":"address","name":"_receiver","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"relayTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC677","name":"token","type":"address"},{"internalType":"address","name":"_receiver","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"relayTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_messageId","type":"bytes32"}],"name":"requestFailedMessageFix","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgeContract","type":"address"}],"name":"setBridgeContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_nativeToken","type":"address"},{"internalType":"address","name":"_bridgedToken","type":"address"}],"name":"setCustomTokenAddressPair","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_dailyLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_dailyLimit","type":"uint256"}],"name":"setExecutionDailyLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_maxPerTx","type":"uint256"}],"name":"setExecutionMaxPerTx","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_feeManager","type":"address"}],"name":"setFeeManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_manager","type":"address"}],"name":"setForwardingRulesManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_manager","type":"address"}],"name":"setGasLimitManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_maxPerTx","type":"uint256"}],"name":"setMaxPerTx","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mediatorContract","type":"address"}],"name":"setMediatorContractOnOtherSide","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_minPerTx","type":"uint256"}],"name":"setMinPerTx","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenFactory","type":"address"}],"name":"setTokenFactory","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tokenFactory","outputs":[{"internalType":"contract TokenFactory","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_day","type":"uint256"}],"name":"totalExecutedPerDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_day","type":"uint256"}],"name":"totalSpentPerDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withinExecutionLimit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withinLimit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}], + "bytecode": "", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106103a45760003560e01c80636e5d6bea116101e9578063c2173d431161010f578063d7405481116100ad578063ec47de2a1161007c578063ec47de2a14611047578063f2c54fe814611073578063f2fde38b1461109f578063f3f51415146110c5576103a4565b8063d740548114610f29578063db6fff8c14610fed578063dfbe4ae014611019578063e77772fe1461103f576103a4565b8063cd596583116100e9578063cd59658314610daf578063d0342acd14610db7578063d0fb020314610de5578063d522cfd714610ded576103a4565b8063c2173d4314610cbd578063c534576114610ce3578063c722b1be14610da7576103a4565b80639a4a439511610187578063a4c0ed3611610156578063a4c0ed3614610b7c578063ab3a25d914610c35578063ad58bdd114610c61578063ae813e9f14610c97576103a4565b80639a4a439514610afd5780639cb7595a14610b1a5780639d4051ae14610b4e578063a4b1c24314610b56576103a4565b8063867f7a4d116101c3578063867f7a4d14610a03578063871c076014610ac75780638da5cb5b14610acf57806390ad84a814610ad7576103a4565b80636e5d6bea1461098b5780637610722f146109b15780637837cf91146109d7576103a4565b80632d70061f116102ce578063437764df1161026c578063613fa2f21161023b578063613fa2f2146108d957806361c04f84146108ff57806364696f971461092557806369ffa08a1461095d576103a4565b8063437764df1461084b578063472d35b91461087057806347ac7d6a1461089657806359339982146108bc576103a4565b80633a50bc87116102a85780633a50bc871461078a5780633da98c8b146107b65780633e6968b61461081d57806340f8dd8614610825576103a4565b80632d70061f1461071a5780632f73a9f81461075c578063392e53cd14610782576103a4565b8063125e4cfb1161034657806326aa101f1161031557806326aa101f146105ac578063272255bb146105d25780632803212f146106085780632ae87cdd14610634576103a4565b8063125e4cfb146104ec57806316ef191314610522578063194153d3146105485780631ba9212d1461056e576103a4565b80630950d515116103825780630950d5151461043b5780630b26cf66146104585780630b71a4a71461047e57806310775238146104ac576103a4565b806301e4f53a146103a957806301fcc1d3146103d7578063032f693f14610403575b600080fd5b6103d5600480360360408110156103bf57600080fd5b506001600160a01b0381351690602001356110eb565b005b6103d5600480360360408110156103ed57600080fd5b506001600160a01b03813516906020013561112a565b6104296004803603602081101561041957600080fd5b50356001600160a01b03166111c9565b60408051918252519081900360200190f35b6103d56004803603602081101561045157600080fd5b5035611221565b6103d56004803603602081101561046e57600080fd5b50356001600160a01b03166112ca565b6103d56004803603604081101561049457600080fd5b506001600160a01b03813581169160200135166112de565b6104d8600480360360408110156104c257600080fd5b506001600160a01b0381351690602001356113b3565b604080519115158252519081900360200190f35b6103d56004803603606081101561050257600080fd5b506001600160a01b03813581169160208101359091169060400135611426565b6104296004803603602081101561053857600080fd5b50356001600160a01b0316611460565b6104296004803603602081101561055e57600080fd5b50356001600160a01b03166114bd565b6103d56004803603608081101561058457600080fd5b506001600160a01b038135811691602081013582169160408201358116916060013516611518565b6104d8600480360360208110156105c257600080fd5b50356001600160a01b031661164b565b6103d5600480360360608110156105e857600080fd5b506001600160a01b0381358116916020810135909116906040013561165e565b6103d56004803603604081101561061e57600080fd5b506001600160a01b038135169060200135611681565b6103d5600480360360c081101561064a57600080fd5b6001600160a01b038235169190810190604081016020820135600160201b81111561067457600080fd5b82018360208201111561068657600080fd5b803590602001918460018302840111600160201b831117156106a757600080fd5b919390929091602081019035600160201b8111156106c457600080fd5b8201836020820111156106d657600080fd5b803590602001918460018302840111600160201b831117156106f757600080fd5b919350915060ff813516906001600160a01b036020820135169060400135611750565b6107406004803603602081101561073057600080fd5b50356001600160a01b0316611782565b604080516001600160a01b039092168252519081900360200190f35b6103d56004803603602081101561077257600080fd5b50356001600160a01b03166117e8565b6104d86117f9565b6104d8600480360360408110156107a057600080fd5b506001600160a01b03813516906020013561184a565b6104d860048036036101808110156107cd57600080fd5b506001600160a01b0381358116916020810135821691604082019160a081019160e082013581169161010081013582169161012082013581169161014081013582169161016090910135166118a0565b610429611a88565b6104296004803603602081101561083b57600080fd5b50356001600160a01b0316611a91565b610853611af0565b604080516001600160e01b03199092168252519081900360200190f35b6103d56004803603602081101561088657600080fd5b50356001600160a01b0316611afb565b610740600480360360208110156108ac57600080fd5b50356001600160a01b0316611b0c565b6104d8600480360360208110156108d257600080fd5b5035611b17565b6103d5600480360360208110156108ef57600080fd5b50356001600160a01b0316611b6a565b6107406004803603602081101561091557600080fd5b50356001600160a01b0316611b7b565b6103d56004803603606081101561093b57600080fd5b506001600160a01b038135811691602081013582169160409091013516611be4565b6103d56004803603604081101561097357600080fd5b506001600160a01b0381358116916020013516611c69565b6103d5600480360360208110156109a157600080fd5b50356001600160a01b0316611ca0565b610429600480360360208110156109c757600080fd5b50356001600160a01b0316611cb1565b6103d5600480360360408110156109ed57600080fd5b506001600160a01b038135169060200135611d0b565b6103d560048036036080811015610a1957600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610a5357600080fd5b820183602082011115610a6557600080fd5b803590602001918460018302840111600160201b83111715610a8657600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550611de3945050505050565b610740611e0d565b610740611e64565b6103d560048036036020811015610aed57600080fd5b50356001600160a01b0316611ebb565b6103d560048036036020811015610b1357600080fd5b5035611ecc565b610b226120c1565b6040805167ffffffffffffffff9485168152928416602084015292168183015290519081900360600190f35b6107406120cb565b61042960048036036020811015610b6c57600080fd5b50356001600160a01b0316612122565b6104d860048036036060811015610b9257600080fd5b6001600160a01b0382351691602081013591810190606081016040820135600160201b811115610bc157600080fd5b820183602082011115610bd357600080fd5b803590602001918460018302840111600160201b83111715610bf457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506121a7945050505050565b61042960048036036040811015610c4b57600080fd5b506001600160a01b03813516906020013561220e565b6103d560048036036060811015610c7757600080fd5b506001600160a01b03813581169160208101359091169060400135612274565b6104d860048036036020811015610cad57600080fd5b50356001600160a01b0316612282565b6104d860048036036020811015610cd357600080fd5b50356001600160a01b03166122db565b6103d560048036036080811015610cf957600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610d3357600080fd5b820183602082011115610d4557600080fd5b803590602001918460018302840111600160201b83111715610d6657600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612308945050505050565b610740612348565b61074061239f565b6103d560048036036040811015610dcd57600080fd5b506001600160a01b03813581169160200135166123f6565b6107406124b5565b6103d5600480360360e0811015610e0357600080fd5b6001600160a01b038235169190810190604081016020820135600160201b811115610e2d57600080fd5b820183602082011115610e3f57600080fd5b803590602001918460018302840111600160201b83111715610e6057600080fd5b919390929091602081019035600160201b811115610e7d57600080fd5b820183602082011115610e8f57600080fd5b803590602001918460018302840111600160201b83111715610eb057600080fd5b9193909260ff833516926001600160a01b03602082013516926040820135929091608081019060600135600160201b811115610eeb57600080fd5b820183602082011115610efd57600080fd5b803590602001918460018302840111600160201b83111715610f1e57600080fd5b50909250905061250c565b6103d560048036036080811015610f3f57600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610f7957600080fd5b820183602082011115610f8b57600080fd5b803590602001918460018302840111600160201b83111715610fac57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612582945050505050565b6103d56004803603604081101561100357600080fd5b506001600160a01b03813516906020013561258e565b6107406004803603602081101561102f57600080fd5b50356001600160a01b031661262b565b610740612636565b6103d56004803603604081101561105d57600080fd5b506001600160a01b03813516906020013561268d565b6104296004803603604081101561108957600080fd5b506001600160a01b03813516906020013561272e565b6103d5600480360360208110156110b557600080fd5b50356001600160a01b0316612797565b610429600480360360208110156110db57600080fd5b50356001600160a01b03166127a8565b6111268233836000805b506040519080825280601f01601f191660200182016040528015611120576020820181803683370190505b506127fe565b5050565b61113261295f565b61113b8261164b565b61114457600080fd5b8015806111635750600081118015611163575061116082611a91565b81105b61116c57600080fd5b60408051700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b60208083019190915260609490941b6001600160601b0319166031820152815180820360250181526045909101825280519084012060009081529283905290912055565b60408051670dac2f0a0cae4a8f60c31b6020808301919091526001600160601b0319606085901b1660288301528251601c818403018152603c909201835281519181019190912060009081529081905220545b919050565b611229612986565b61123281611b17565b1561123c57600080fd5b600061124782612a30565b9050600061125483612a89565b9050600061126184612ae6565b905061126c84612b35565b611277838383612b8e565b604080516001600160a01b03808616825284166020820152808201839052905185917f07b5483b8e4bd8ea240a474d5117738350e7d431e3668c48a97910b0b397796a919081900360600190a250505050565b6112d261295f565b6112db81612baf565b50565b6112e661295f565b6112ef8161164b565b156112f957600080fd5b600061130482611b7b565b6001600160a01b03161461131757600080fd5b600061132283611782565b6001600160a01b03161461133557600080fd5b61134a6001600160a01b038216306001612c2b565b806001600160a01b03166342966c6860016040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b15801561139157600080fd5b505af11580156113a5573d6000803e3d6000fd5b505050506111268282612cb7565b6000806113d1836113cb866113c6611a88565b61220e565b90612db2565b905060006113df60006127a8565b1180156113f45750806113f1856127a8565b10155b80156114085750611404846111c9565b8311155b801561141c575061141884612122565b8310155b9150505b92915050565b61142e612986565b600061143984611782565b90506114448161164b565b61144d57600080fd5b61145a8160008585612e13565b50505050565b60408051700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b60208083019190915260609390931b6001600160601b0319166031820152815180820360250181526045909101825280519083012060009081529182905290205490565b604080516e6d65646961746f7242616c616e636560881b60208083019190915260609390931b6001600160601b031916602f820152815180820360230181526043909101825280519083012060009081529182905290205490565b7f1ba9212d27ad775f3f89cdd276d3829dd95d169a049be071de905db3bbca633e600081905260046020527f39ed9a5a925de329b4b8d3797a51d757351f7bb58fe4f46a37c1af393b814a685460ff161561157257600080fd5b33301461157e57600080fd5b61158785612f3d565b61159084612fb9565b61159983613046565b6115a2826130d3565b604080516919185a5b1e531a5b5a5d60b21b6020808301919091526000602a83018190528351601e818503018152603e84018086528151918401919091208252918190528381206ec097ce7bc90715b34b9f10000000009081905590915291517fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd259415991819003605e0190a26000908152600460205260409020805460ff1916600117905550505050565b60008061165783612122565b1192915050565b611666612986565b61166f83613160565b61167c8360018484612e13565b505050565b61168961295f565b6116928261164b565b61169b57600080fd5b6116a4826111c9565b8111806116af575080155b6116b857600080fd5b604080516919185a5b1e531a5b5a5d60b21b6020808301919091526001600160601b0319606086901b16602a8301528251601e818403018152603e83018085528151918301919091206000908152918290529083902084905583905290516001600160a01b038416917fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd25941599190819003605e0190a25050565b611758612986565b600061176889898989898961321c565b90506117778160008585612e13565b505050505050505050565b604080516f686f6d65546f6b656e4164647265737360801b60208083019190915260609390931b6001600160601b03191660308201528151808203602401815260449091018252805190830120600090815260029092529020546001600160a01b031690565b6117f061295f565b6112db81612f3d565b7f0a6f646cd611241d8073675e00d1a1ff700fbf1b53fcf473de56d1e6e4b714ba60005260046020527f078d888f9b66f3f8bfa10909e31f1e16240db73449f0500afdbbe3a70da457cc5460ff1690565b600080611862836113cb8661185d611a88565b61272e565b905060006118706000611a91565b11801561188557508061188285611a91565b10155b801561141c575061189584611460565b909211159392505050565b60408051600481526024810182526020810180516001600160e01b03166337ef410160e11b1781529151815160009384936060933093919290918291908083835b602083106119005780518252601f1990920191602091820191016118e1565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114611960576040519150601f19603f3d011682016040523d82523d6000602084013e611965565b606091505b50915091508115806119a15750805160201480156119a1575080806020019051602081101561199357600080fd5b50516001600160a01b031633145b806119ab57503330145b6119b457600080fd5b6119bc6117f9565b156119c657600080fd5b6119cf8c612baf565b6119d88b61356a565b611a0c60008b60038060200260405190810160405280929190826003602002808284376000920191909152506135d4915050565b604080518082018252611a3b91600091908c906002908390839080828437600092019190915250613728915050565b611a4488613046565b611a4d87613817565b611a5686612f3d565b611a5f856130d3565b611a6884612fb9565b611a706138df565b611a786117f9565b9c9b505050505050505050505050565b62015180420490565b6040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b60208083019190915260609390931b6001600160601b0319166033820152815180820360270181526047909101825280519083012060009081529182905290205490565b6358a8b61360e11b90565b611b0361295f565b6112db816130d3565b600061142082611b7b565b604080516b1b595cdcd859d9519a5e195960a21b602080830191909152602c80830185905283518084039091018152604c909201835281519181019190912060009081526004909152205460ff16919050565b611b7261295f565b6112db81613046565b6040805172666f726569676e546f6b656e4164647265737360681b60208083019190915260609390931b6001600160601b03191660338201528151808203602701815260479091018252805190830120600090815260029092529020546001600160a01b031690565b611bec613936565b826001600160a01b03166369ffa08a83836040518363ffffffff1660e01b815260040180836001600160a01b03168152602001826001600160a01b0316815260200192505050600060405180830381600087803b158015611c4c57600080fd5b505af1158015611c60573d6000803e3d6000fd5b50505050505050565b611c71613936565b6001600160a01b0382161580611c8d5750611c8b8261164b565b155b611c9657600080fd5b61112682826139af565b611ca861295f565b6112db8161356a565b600080611cbd836111c9565b90506000611cca846127a8565b90506000611cda856113c6611a88565b90506000818311611cec576000611cf0565b8183035b9050808410611cff5780611d01565b835b9695505050505050565b611d1361295f565b611d1c8261164b565b611d2557600080fd5b611d2e82611460565b811180611d39575080155b611d4257600080fd5b6040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b6020808301919091526001600160601b0319606086901b16603383015282516027818403018152604783018085528151918301919091206000908152918290529083902084905583905290516001600160a01b038416917f4c177b42dbe934b3abbc0208c11a42e46589983431616f1710ab19969c5ed62e919081900360670190a25050565b611deb612986565b611df484613160565b611e018460018585612e13565b61145a838584846139e9565b7f98aa806e31e94a687a31c65769cb99670064dd7f5a87526da075c5fb4eab988060005260026020527f0c1206883be66049a02d4937078367c00b3d71dd1a9465df969363c6ddeac96d546001600160a01b031690565b7f02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c060005260026020527fb7802e97e87ef2842a6cce7da7ffaeaedaa2f61a6a7870b23d9d01fc9b73712e546001600160a01b031690565b611ec361295f565b6112db81612fb9565b6000611ed661239f565b9050806001600160a01b031663cb08a10c836040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b158015611f1c57600080fd5b505afa158015611f30573d6000803e3d6000fd5b505050506040513d6020811015611f4657600080fd5b505115611f5257600080fd5b306001600160a01b0316816001600160a01b0316633f9a8e7e846040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b158015611fa057600080fd5b505afa158015611fb4573d6000803e3d6000fd5b505050506040513d6020811015611fca57600080fd5b50516001600160a01b031614611fdf57600080fd5b611fe7611e0d565b6001600160a01b0316816001600160a01b0316634a610b04846040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b15801561203457600080fd5b505afa158015612048573d6000803e3d6000fd5b505050506040513d602081101561205e57600080fd5b50516001600160a01b03161461207357600080fd5b6040805160248082018590528251808303909101815260449091019091526020810180516001600160e01b0316630950d51560e01b908117909152906120ba816001613b56565b5050505050565b6003806001909192565b7f5f5bc4e0b888be22a35f2166061a04607296c26861006b9b8e089a172696a82260005260026020527f60072fd9ffad01d76b1d1421ce17a3613dc06795e4b113745995ad1d84a52121546001600160a01b031690565b60408051670dad2dca0cae4a8f60c31b6020808301919091526001600160601b0319606085901b1660288301528251601c818403018152603c9092018352815191810191909120600090815290819052908120546001600160a01b03831661218b57905061121c565b6000811161219a57600061219d565b60015b60ff169392505050565b60006121b1613d6a565b61220457604080516000815260208101909152825185906014116121f4576121d884613d8f565b90506014845111156121f4578351601319016014850190815291505b6122013387838886613d96565b50505b5060019392505050565b604080516f746f74616c5370656e7450657244617960801b60208083019190915260609490941b6001600160601b031916603082015260448082019390935281518082039093018352606401815281519183019190912060009081529182905290205490565b61167c8383836000806110f5565b604080516861636b4465706c6f7960b81b60208083019190915260609390931b6001600160601b03191660298201528151808203601d018152603d90910182528051908301206000908152600490925290205460ff1690565b60006122e68261164b565b8015611420575060006122f883611b7b565b6001600160a01b03161492915050565b612310612986565b600061231b85611782565b90506123268161164b565b61232f57600080fd5b61233c8160008686612e13565b6120ba848285856139e9565b7f5f86f226cd489cc09187d5f5e0adfb94308af0d4ceac482dd8a8adea9d80daf460005260026020527fab9e97adef29adb9492a44df89badb4a706f8f35202918df21ca61ed056c4868546001600160a01b031690565b7f811bbb11e8899da471f0e69a3ed55090fc90215227fc5fb1cb0d6e962ea7b74f60005260026020527fb4ed64697d3ef8518241966f7c6f28b0d72f20f51198717d198d2d55076c593d546001600160a01b031690565b6123fe613936565b806001600160a01b03811661241257600080fd5b61241b836122db565b61242457600080fd5b600061242f84613ef6565b90506000811161243e57600080fd5b600061244985611cb1565b90506000811161245857600080fd5b80821115612464578091505b61247685612470611a88565b84613f4a565b6040805160008082526020820190925260609161249891889088908790613fc8565b905060006124a7826001613b56565b9050611c6081888887614643565b7f779a349c5bee7817f04c960f525ee3e2f2516078c38c68a3149787976ee837e560005260026020527fc155b21a14c4592b97825e495fbe0d2705fb46420018cac5bfa7a09c43fae517546001600160a01b031690565b612514612986565b60006125248b8b8b8b8b8b61321c565b90506125338160008787612e13565b61257585828686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506139e992505050565b5050505050505050505050565b61145a848484846127fe565b61259661295f565b61259f8261164b565b6125a857600080fd5b8015806125ce57506125b982612122565b811180156125ce57506125cb826127a8565b81105b6125d757600080fd5b60408051670dac2f0a0cae4a8f60c31b60208083019190915260609490941b6001600160601b03191660288201528151808203601c018152603c909101825280519084012060009081529283905290912055565b600061142082611782565b7f269c5905f777ee6391c7a361d17039a7d62f52ba9fffeb98c5ade342705731a360005260026020527f15c764a0cd4bb3d72a49abedd3d6793c3b93c0d57f43174a348b443be86f79c1546001600160a01b031690565b61269561295f565b61269e8261164b565b6126a757600080fd5b6000811180156126be57506126bb826127a8565b81105b80156126d157506126ce826111c9565b81105b6126da57600080fd5b60408051670dad2dca0cae4a8f60c31b60208083019190915260609490941b6001600160601b03191660288201528151808203601c018152603c909101825280519084012060009081529283905290912055565b6040805172746f74616c457865637574656450657244617960681b60208083019190915260609490941b6001600160601b031916603382015260478082019390935281518082039093018352606701815281519183019190912060009081529182905290205490565b61279f61295f565b6112db81613817565b604080516919185a5b1e531a5b5a5d60b21b60208083019190915260609390931b6001600160601b031916602a8201528151808203601e018152603e909101825280519083012060009081529182905290205490565b612806613d6a565b1561281057600080fd5b6000846001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561285f57600080fd5b505afa158015612873573d6000803e3d6000fd5b505050506040513d602081101561288957600080fd5b5051905061289760016146b3565b6128ac6001600160a01b0386163330866146d7565b6128b660006146b3565b600061293b82876001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561290957600080fd5b505afa15801561291d573d6000803e3d6000fd5b505050506040513d602081101561293357600080fd5b505190614731565b90508381111561294a57600080fd5b6129578633878487613d96565b505050505050565b612967611e64565b6001600160a01b0316336001600160a01b03161461298457600080fd5b565b600061299061239f565b9050336001600160a01b038216146129a757600080fd5b6129af611e0d565b6001600160a01b0316816001600160a01b031663d67bdd256040518163ffffffff1660e01b815260040160206040518083038186803b1580156129f157600080fd5b505afa158015612a05573d6000803e3d6000fd5b505050506040513d6020811015612a1b57600080fd5b50516001600160a01b0316146112db57600080fd5b604080516b36b2b9b9b0b3b2aa37b5b2b760a11b602080830191909152602c80830185905283518084039091018152604c90920183528151918101919091206000908152600290915220546001600160a01b0316919050565b604080516f1b595cdcd859d9549958da5c1a595b9d60821b602080830191909152603080830185905283518084039091018152605090920183528151918101919091206000908152600290915220546001600160a01b0316919050565b604080516b6d65737361676556616c756560a01b602080830191909152602c80830185905283518084039091018152604c90920183528151918101919091206000908152908190522054919050565b604080516b1b595cdcd859d9519a5e195960a21b602080830191909152602c8083019490945282518083039094018452604c9091018252825192810192909220600090815260049092529020805460ff19166001179055565b61167c6000612b9c85611b7b565b6001600160a01b03161484848485614773565b612bb8816147c7565b612bc157600080fd5b7f811bbb11e8899da471f0e69a3ed55090fc90215227fc5fb1cb0d6e962ea7b74f60005260026020527fb4ed64697d3ef8518241966f7c6f28b0d72f20f51198717d198d2d55076c593d80546001600160a01b0319166001600160a01b0392909216919091179055565b826001600160a01b03166340c10f1983836040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050602060405180830381600087803b158015612c8257600080fd5b505af1158015612c96573d6000803e3d6000fd5b505050506040513d6020811015612cac57600080fd5b505161167c57600080fd5b604080516f686f6d65546f6b656e4164647265737360801b6020808301919091526001600160601b0319606086811b82166030850152845160248186030181526044850186528051908401206000908152600280855286822080546001600160a01b03808b166001600160a01b0319928316811790935572666f726569676e546f6b656e4164647265737360681b60648a0152948a901b90951660778801528751606b818903018152608b909701808952875197870197909720835294529485208054909216908716908117909155909290917f78d063210f4fb6b4cc932390bb8045fa2465e51349590182dab8b9e84c57a6ee9190a35050565b600082820183811015612e0c576040805162461bcd60e51b815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b9392505050565b612e1b613d6a565b15612e2557600080fd5b612e2f848261184a565b612e3857600080fd5b612e4a84612e44611a88565b83614803565b806000612e7a7f03be2b2875cb41e0e77355e802a16769bb8dfcf825061cde185c73bf94f1262586838986614884565b90506000612e86614c2f565b90508115612edb5760408051838152905182916001600160a01b038a16917fd560a522f77cfb4924d6fe51be1615e540a48a8931c48fe0349c7f47ebabe7479181900360200190a3612ed88383614731565b92505b612ee88688878688614773565b80856001600160a01b0316886001600160a01b03167f9afd47907e25028cdaca89d193518c302bbb128617d5a992c5abd45815526593866040518082815260200191505060405180910390a450505050505050565b612f46816147c7565b612f4f57600080fd5b7f269c5905f777ee6391c7a361d17039a7d62f52ba9fffeb98c5ade342705731a360005260026020527f15c764a0cd4bb3d72a49abedd3d6793c3b93c0d57f43174a348b443be86f79c180546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b0381161580612fd35750612fd3816147c7565b612fdc57600080fd5b7f5f86f226cd489cc09187d5f5e0adfb94308af0d4ceac482dd8a8adea9d80daf460005260026020527fab9e97adef29adb9492a44df89badb4a706f8f35202918df21ca61ed056c486880546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b03811615806130605750613060816147c7565b61306957600080fd5b7f5f5bc4e0b888be22a35f2166061a04607296c26861006b9b8e089a172696a82260005260026020527f60072fd9ffad01d76b1d1421ce17a3613dc06795e4b113745995ad1d84a5212180546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b03811615806130ed57506130ed816147c7565b6130f657600080fd5b7f779a349c5bee7817f04c960f525ee3e2f2516078c38c68a3149787976ee837e560005260026020527fc155b21a14c4592b97825e495fbe0d2705fb46420018cac5bfa7a09c43fae51780546001600160a01b0319166001600160a01b0392909216919091179055565b604080516861636b4465706c6f7960b81b6020808301919091526001600160601b0319606085901b1660298301528251601d818403018152603d909201835281519181019190912060009081526004909152205460ff166112db57604080516861636b4465706c6f7960b81b6020808301919091526001600160601b0319606085901b1660298301528251601d818403018152603d90920183528151918101919091206000908152600490915220805460ff1916600117905550565b60008061322888611782565b90506001600160a01b0381166134cf57606087878080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050604080516020601f8b01819004810282018101909252898152939450606093925089915088908190840183828082843760009201919091525050845192935050501515806132bd575060008151115b6132c657600080fd5b81516132d4578091506132dd565b80516132dd5750805b6132e682614ca2565b91506132f0612636565b6001600160a01b031663a39d6acf83838861330961239f565b6001600160a01b0316631544298e6040518163ffffffff1660e01b815260040160206040518083038186803b15801561334157600080fd5b505afa158015613355573d6000803e3d6000fd5b505050506040513d602081101561336b57600080fd5b50516040516001600160e01b031960e087901b16815260ff831660448201526064810182905260806004820190815285516084830152855190918291602482019160a40190602089019080838360005b838110156133d35781810151838201526020016133bb565b50505050905090810190601f1680156134005780820380516001836020036101000a031916815260200191505b50838103825286518152865160209182019188019080838360005b8381101561343357818101518382015260200161341b565b50505050905090810190601f1680156134605780820380516001836020036101000a031916815260200191505b509650505050505050602060405180830381600087803b15801561348357600080fd5b505af1158015613497573d6000803e3d6000fd5b505050506040513d60208110156134ad57600080fd5b505192506134bb8a84612cb7565b6134c8838660ff16614d57565b505061355f565b6134d88161164b565b61355f578260ff16816001600160a01b031663313ce5676040518163ffffffff1660e01b815260040160206040518083038186803b15801561351957600080fd5b505afa15801561352d573d6000803e3d6000fd5b505050506040513d602081101561354357600080fd5b505160ff161461355257600080fd5b61355f818460ff16614d57565b979650505050505050565b7f98aa806e31e94a687a31c65769cb99670064dd7f5a87526da075c5fb4eab988060005260026020527f0c1206883be66049a02d4937078367c00b3d71dd1a9465df969363c6ddeac96d80546001600160a01b0319166001600160a01b0392909216919091179055565b6040810151158015906135ee575060408101516020820151115b80156135fe575060208101518151115b61360757600080fd5b8051604080516919185a5b1e531a5b5a5d60b21b602082810191909152606086901b6001600160601b031916602a83018190528351808403601e018152603e8401855280519083012060009081528083528481209590955581860151670dac2f0a0cae4a8f60c31b605e850152606684018290528451605a818603018152607a8501865280519084012086528583528486205583860151670dad2dca0cae4a8f60c31b609a85015260a28401919091528351609681850301815260b690930184528251928201929092208452839052908220556001600160a01b038316907fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd25941599083905b60200201516040518082815260200191505060405180910390a25050565b805160208201511061373957600080fd5b80516040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b602082810191909152606086901b6001600160601b031916603383018190528351808403602701815260478401855280519083012060009081528083528481209590955581860151700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b606785015260788401919091528351606c818503018152608c90930184528251928201929092208452839052908220556001600160a01b038316907f4c177b42dbe934b3abbc0208c11a42e46589983431616f1710ab19969c5ed62e90839061370a565b6001600160a01b03811661382a57600080fd5b7f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0613853611e64565b604080516001600160a01b03928316815291841660208301528051918290030190a17f02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c060005260026020527fb7802e97e87ef2842a6cce7da7ffaeaedaa2f61a6a7870b23d9d01fc9b73712e80546001600160a01b0319166001600160a01b0392909216919091179055565b7f0a6f646cd611241d8073675e00d1a1ff700fbf1b53fcf473de56d1e6e4b714ba60005260046020527f078d888f9b66f3f8bfa10909e31f1e16240db73449f0500afdbbe3a70da457cc805460ff19166001179055565b306001600160a01b0316636fde82026040518163ffffffff1660e01b815260040160206040518083038186803b15801561396f57600080fd5b505afa158015613983573d6000803e3d6000fd5b505050506040513d602081101561399957600080fd5b50516001600160a01b0316331461298457600080fd5b806001600160a01b0381166139c357600080fd5b6001600160a01b0383166139df576139da82614edb565b61167c565b61167c8383614ee6565b6139f2846147c7565b1561145a57836001600160a01b031663db7af85460e01b84848460405160240180846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015613a61578181015183820152602001613a49565b50505050905090810190601f168015613a8e5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529181526020820180516001600160e01b03166001600160e01b031990991698909817885251815191979096508695509350915081905083835b60208310613af45780518252601f199092019160209182019101613ad5565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114611c60576040519150601f19603f3d011682016040523d82523d6000602084013e611c60565b600080613b61611e0d565b90506000613b6e85614f73565b90506000613b7a61239f565b905084613c7357806001600160a01b03166394643f718488856040518463ffffffff1660e01b815260040180846001600160a01b0316815260200180602001838152602001828103825284818151815260200191508051906020019080838360005b83811015613bf4578181015183820152602001613bdc565b50505050905090810190601f168015613c215780820380516001836020036101000a031916815260200191505b50945050505050602060405180830381600087803b158015613c4257600080fd5b505af1158015613c56573d6000803e3d6000fd5b505050506040513d6020811015613c6c57600080fd5b5051611d01565b806001600160a01b031663dc8601b38488856040518463ffffffff1660e01b815260040180846001600160a01b0316815260200180602001838152602001828103825284818151815260200191508051906020019080838360005b83811015613ce6578181015183820152602001613cce565b50505050905090810190601f168015613d135780820380516001836020036101000a031916815260200191505b50945050505050602060405180830381600087803b158015613d3457600080fd5b505af1158015613d48573d6000803e3d6000fd5b505050506040513d6020811015613d5e57600080fd5b50519695505050505050565b7f6168652c307c1e813ca11cfb3a601f1cf3b22452021a5052d8b05f1f1f8a3e925490565b6014015190565b6001600160a01b03831615801590613dc75750613db1611e0d565b6001600160a01b0316836001600160a01b031614155b613dd057600080fd5b613dd98561164b565b613df9576000613de8866150a7565b9050613df7868260ff16614d57565b505b613e0385836113b3565b613e0c57600080fd5b613e1885612470611a88565b6000613e2386611b7b565b90506000613e5e7f741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee266001600160a01b03841615888a88614884565b90506000613e6c8583614731565b90506060613e7d848a898589613fc8565b90506000613e9582613e908c8c8c615275565b613b56565b9050613ea3818b8b86614643565b8315613eea5760408051858152905182916001600160a01b038d16917fd560a522f77cfb4924d6fe51be1615e540a48a8931c48fe0349c7f47ebabe7479181900360200190a35b50505050505050505050565b6000611420613f04836114bd565b604080516370a0823160e01b815230600482015290516001600160a01b038616916370a08231916024808301926020929190829003018186803b15801561290957600080fd5b613f58816113cb858561220e565b600080858560405160200180806f746f74616c5370656e7450657244617960801b815250601001836001600160a01b031660601b81526014018281526020019250505060405160208183030381529060405280519060200120815260200190815260200160002081905550505050565b606060008083511180613fed57506000356001600160e01b03191663d740548160e01b145b90506001600160a01b0387166144a8576140138661400e866113cb8a6114bd565b615324565b61401c86612282565b15614159578061407a57604080516001600160a01b0380891660248301528716604482015260648082018790528251808303909101815260849091019091526020810180516001600160e01b031663125e4cfb60e01b179052614151565b63c534576160e01b8686868660405160240180856001600160a01b03168152602001846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156140ea5781810151838201526020016140d2565b50505050905090810190601f1680156141175780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909916989098179097525050505050505b91505061463a565b6000614164876150a7565b905060606141718861537f565b9050606061417e89615545565b9050600082511180614191575060008151115b61419a57600080fd5b836142ed57632ae87cdd60e01b898383868c8c60405160240180876001600160a01b0316815260200180602001806020018660ff168152602001856001600160a01b03168152602001848152602001838103835288818151815260200191508051906020019080838360005b8381101561421e578181015183820152602001614206565b50505050905090810190601f16801561424b5780820380516001836020036101000a031916815260200191505b50838103825287518152875160209182019189019080838360005b8381101561427e578181015183820152602001614266565b50505050905090810190601f1680156142ab5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909c169b909b17909a525061449d975050505050505050565b63d522cfd760e01b898383868c8c8c60405160240180886001600160a01b0316815260200180602001806020018760ff168152602001866001600160a01b031681526020018581526020018060200184810384528a818151815260200191508051906020019080838360005b83811015614371578181015183820152602001614359565b50505050905090810190601f16801561439e5780820380516001836020036101000a031916815260200191505b5084810383528951815289516020918201918b019080838360005b838110156143d15781810151838201526020016143b9565b50505050905090810190601f1680156143fe5780820380516001836020036101000a031916815260200191505b50848103825285518152855160209182019187019080838360005b83811015614431578181015183820152602001614419565b50505050905090810190601f16801561445e5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909e169d909d17909c5250505050505050505050505b94505050505061463a565b856001600160a01b03166342966c68856040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156144ee57600080fd5b505af1158015614502573d6000803e3d6000fd5b505050508061455f57604080516001600160a01b03808a1660248301528716604482015260648082018790528251808303909101815260849091019091526020810180516001600160e01b031663272255bb60e01b179052614636565b63867f7a4d60e01b8786868660405160240180856001600160a01b03168152602001846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156145cf5781810151838201526020016145b7565b50505050905090810190601f1680156145fc5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909916989098179097525050505050505b9150505b95945050505050565b61464d8484615678565b61465784836156e4565b6146618482615754565b83826001600160a01b0316846001600160a01b03167f59a9a8027b9c87b961e254899821c9a276b5efc35d1f7409ea4f291470f1629a846040518082815260200191505060405180910390a450505050565b7f6168652c307c1e813ca11cfb3a601f1cf3b22452021a5052d8b05f1f1f8a3e9255565b604080516001600160a01b0380861660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b17905261145a9085906157a3565b6000612e0c83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250615854565b84156147a95761478d6001600160a01b03851684846158eb565b6147a48461400e8361479e886114bd565b90614731565b6120ba565b6120ba83836147b78761593d565b6001600160a01b03169190612c2b565b6000813f7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708181148015906147fb57508115155b949350505050565b614811816113cb858561272e565b6000808585604051602001808072746f74616c457865637574656450657244617960681b815250601301836001600160a01b031660601b81526014018281526020019250505060405160208183030381529060405280519060200120815260200190815260200160002081905550505050565b60008061488f6124b5565b90506001600160a01b03811615614c22577f741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee26871480156149445750806001600160a01b031663071664c5866040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561491757600080fd5b505afa15801561492b573d6000803e3d6000fd5b505050506040513d602081101561494157600080fd5b50515b1561495357600091505061463a565b6000816001600160a01b031663710c60138987876040518463ffffffff1660e01b815260040180848152602001836001600160a01b03168152602001828152602001935050505060206040518083038186803b1580156149b257600080fd5b505afa1580156149c6573d6000803e3d6000fd5b505050506040513d60208110156149dc57600080fd5b505190508015614c19577f741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee26881415614a2757614a226001600160a01b03861683836158eb565b614bb1565b600087614a3b576340c10f1960e01b614a44565b63a9059cbb60e01b5b604080516001600160a01b038681166024830152604480830187905283518084039091018152606490920183526020820180516001600160e01b03166001600160e01b0319861617815292518251949550600094606094928c16939282918083835b60208310614ac55780518252601f199092019160209182019101614aa6565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114614b27576040519150601f19603f3d011682016040523d82523d6000602084013e614b2c565b606091505b509150915081614b83576040805185815290516001600160a01b038a16917fb8842ee9d1603ef0f5620c01feb6cf2e7921091eba728cbce562041a86ee109a919081900360200190a260009550505050505061463a565b80511580614ba45750808060200190516020811015614ba157600080fd5b50515b614bad57600080fd5b5050505b816001600160a01b0316634e281a7b866040518263ffffffff1660e01b815260040180826001600160a01b03168152602001915050600060405180830381600087803b158015614c0057600080fd5b505af1158015614c14573d6000803e3d6000fd5b505050505b915061463a9050565b5060009695505050505050565b6000614c3961239f565b6001600160a01b031663669f618b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614c7157600080fd5b505afa158015614c85573d6000803e3d6000fd5b505050506040513d6020811015614c9b57600080fd5b5051905090565b606080827f2066726f6d204d61696e6e6574000000000000000000000000000000000000006040516020018083805190602001908083835b60208310614cf95780518252601f199092019160209182019101614cda565b51815160209384036101000a60001901801990921691161790529201938452506040805180850381529390910190525093517f000000000000000000000000000000000000000000000000000000000000000d018452509192915050565b60006012821015614e4b5781601203600a0a90506000614d8182614d7b6000612122565b90615983565b90506000614d9383614d7b60006111c9565b90506000614da584614d7b60006127a8565b90506000614db785614d7b6000611460565b90506000614dc986614d7b6000611a91565b905084614dff5760019450848411614dff5760649350606491508383111580614df25750818111155b15614dff57506127109150815b614e23886040518060600160405280868152602001878152602001888152506135d4565b614e4188604051806040016040528084815260200185815250613728565b505050505061167c565b60128203600a0a9050614ea4836040518060600160405280614e7785614e7160006127a8565b906159c5565b8152602001614e8a85614e7160006111c9565b8152602001614e9d85614e716000612122565b90526135d4565b61167c836040518060400160405280614ec185614e716000611a91565b8152602001614ed485614e716000611460565b9052613728565b476111268282615a1e565b604080516370a0823160e01b8152306004820152905183916000916001600160a01b038416916370a08231916024808301926020929190829003018186803b158015614f3157600080fd5b505afa158015614f45573d6000803e3d6000fd5b505050506040513d6020811015614f5b57600080fd5b5051905061145a6001600160a01b03831684836158eb565b600080614f7e6120cb565b90506001600160a01b03811661500357614f9661239f565b6001600160a01b031663e5789d036040518163ffffffff1660e01b815260040160206040518083038186803b158015614fce57600080fd5b505afa158015614fe2573d6000803e3d6000fd5b505050506040513d6020811015614ff857600080fd5b5051915061121c9050565b60405163fb47201960e01b81526020600482018181528551602484015285516001600160a01b0385169363fb4720199388939283926044019185019080838360005b8381101561505d578181015183820152602001615045565b50505050905090810190601f16801561508a5780820380516001836020036101000a031916815260200191505b509250505060206040518083038186803b158015614fce57600080fd5b60408051600481526024810182526020810180516001600160e01b031663313ce56760e01b1781529151815160009384936060936001600160a01b03881693919290918291908083835b602083106151105780518252601f1990920191602091820191016150f1565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615170576040519150601f19603f3d011682016040523d82523d6000602084013e615175565b606091505b5091509150816152565760408051600481526024810182526020810180516001600160e01b0316632e0f262560e01b178152915181516001600160a01b0388169382918083835b602083106151db5780518252601f1990920191602091820191016151bc565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461523b576040519150601f19603f3d011682016040523d82523d6000602084013e615240565b606091505b509092509050816152565760009250505061121c565b80806020019051602081101561526b57600080fd5b5051949350505050565b600080615280612348565b90506001600160a01b038116158061463a57506040805163f7baa04960e01b81526001600160a01b03878116600483015286811660248301528581166044830152915160009284169163f7baa049916064808301926020929190829003018186803b1580156152ee57600080fd5b505afa158015615302573d6000803e3d6000fd5b505050506040513d602081101561531857600080fd5b50511395945050505050565b604080516e6d65646961746f7242616c616e636560881b60208083019190915260609490941b6001600160601b031916602f820152815180820360230181526043909101825280519084012060009081529283905290912055565b60408051600481526024810182526020810180516001600160e01b03166306fdde0360e01b1781529151815160609360009385936001600160a01b03881693919290918291908083835b602083106153e85780518252601f1990920191602091820191016153c9565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615448576040519150601f19603f3d011682016040523d82523d6000602084013e61544d565b606091505b50915091508161553c5760408051600481526024810182526020810180516001600160e01b03166351fa6fbf60e11b178152915181516001600160a01b0388169382918083835b602083106154b35780518252601f199092019160209182019101615494565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615513576040519150601f19603f3d011682016040523d82523d6000602084013e615518565b606091505b5090925090508161553c57604051806020016040528060008152509250505061121c565b6147fb81615a83565b60408051600481526024810182526020810180516001600160e01b03166395d89b4160e01b1781529151815160609360009385936001600160a01b03881693919290918291908083835b602083106155ae5780518252601f19909201916020918201910161558f565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461560e576040519150601f19603f3d011682016040523d82523d6000602084013e615613565b606091505b50915091508161553c5760408051600481526024810182526020810180516001600160e01b0316631eedf1af60e31b178152915181516001600160a01b038816938291808383602083106154b35780518252601f199092019160209182019101615494565b604080516b36b2b9b9b0b3b2aa37b5b2b760a11b602080830191909152602c8083019590955282518083039095018552604c90910182528351938101939093206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b604080516f1b595cdcd859d9549958da5c1a595b9d60821b60208083019190915260308083019590955282518083039095018552605090910182528351938101939093206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b604080516b6d65737361676556616c756560a01b602080830191909152602c8083019590955282518083039095018552604c909101825283519381019390932060009081529283905290912055565b60606157f8826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b0316615bdf9092919063ffffffff16565b80519091501561167c5780806020019051602081101561581757600080fd5b505161167c5760405162461bcd60e51b815260040180806020018281038252602a815260200180615e20602a913960400191505060405180910390fd5b600081848411156158e35760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b838110156158a8578181015183820152602001615890565b50505050905090810190601f1680156158d55780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b17905261167c9084906157a3565b60006001600160a01b03821673b7d311e2eb55f2f68a9440da38e7989210b9a05e141561597f575073857dd07866c1e19eb2cdfcef7ae655ce7f9e560d61121c565b5090565b6000612e0c83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250615bee565b6000826159d457506000611420565b828202828482816159e157fe5b0414612e0c5760405162461bcd60e51b8152600401808060200182810382526021815260200180615dff6021913960400191505060405180910390fd5b6040516001600160a01b0383169082156108fc029083906000818181858888f19350505050611126578082604051615a5590615dc0565b6001600160a01b039091168152604051908190036020019082f090508015801561145a573d6000803e3d6000fd5b6060602082511115615b5957818060200190516020811015615aa457600080fd5b8101908080516040519392919084600160201b821115615ac357600080fd5b908301906020820185811115615ad857600080fd5b8251600160201b811182820188101715615af157600080fd5b82525081516020918201929091019080838360005b83811015615b1e578181015183820152602001615b06565b50505050905090810190601f168015615b4b5780820380516001836020036101000a031916815260200191505b50604052505050905061121c565b815160201415615bca576000828060200190516020811015615b7a57600080fd5b50516040805160208082528183019092529192506060919060208201818036833701905050905060008260208301525b8215615bbf5760089290921b91600101615baa565b8152915061121c9050565b5060408051602081019091526000815261121c565b60606147fb8484600085615c53565b60008183615c3d5760405162461bcd60e51b81526020600482018181528351602484015283519092839260449091019190850190808383600083156158a8578181015183820152602001615890565b506000838581615c4957fe5b0495945050505050565b6060615c5e856147c7565b615caf576040805162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b60006060866001600160a01b031685876040518082805190602001908083835b60208310615cee5780518252601f199092019160209182019101615ccf565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d8060008114615d50576040519150601f19603f3d011682016040523d82523d6000602084013e615d55565b606091505b50915091508115615d695791506147fb9050565b805115615d795780518082602001fd5b60405162461bcd60e51b81526020600482018181528651602484015286518793919283926044019190850190808383600083156158a8578181015183820152602001615890565b603280615dcd8339019056fe60806040526040516032380380603283398181016040526020811015602357600080fd5b50516001600160a01b038116fffe536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f775361666545524332303a204552433230206f7065726174696f6e20646964206e6f742073756363656564a264697066735822122016ad8a7dfc627988e4caf299120e2273aae2be383102b8f744ab97c4389ae59364736f6c63430007050033", + "linkReferences": {}, + "deployedLinkReferences": {} +} \ No newline at end of file diff --git a/operate/data/contracts/home_omnibridge/contract.py b/operate/data/contracts/home_omnibridge/contract.py new file mode 100644 index 000000000..291671605 --- /dev/null +++ b/operate/data/contracts/home_omnibridge/contract.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the class to connect to the `ForeignOmnibridge` contract.""" + +from typing import Optional, cast + +from aea.configurations.base import PublicId +from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi +from aea_ledger_ethereum import EthereumApi +from web3.types import BlockIdentifier + + +class HomeOmnibridge(Contract): + """HomeOmnibridge.""" + + contract_id = PublicId.from_str("valory/home_omnibridge:0.1.0") + + @classmethod + def find_tokens_bridged_tx( + cls, + ledger_api: LedgerApi, + contract_address: str, + token: str, + recipient: str, + value: int, + message_id: bytes, + from_block: BlockIdentifier = "earliest", + to_block: BlockIdentifier = "latest", + ) -> Optional[str]: + """Return the transaction hash of the matching TokensBridged event in the given block range.""" + ledger_api = cast(EthereumApi, ledger_api) + contract_instance = cls.get_instance(ledger_api, contract_address) + entries = contract_instance.events.TokensBridged.create_filter( + fromBlock=from_block, + toBlock=to_block, + argument_filters={ + "token": token, + "recipient": recipient, + "messageId": message_id, + }, + ).get_all_entries() + + for entry in entries: + args = entry["args"] + if args["value"] == value: + return entry["transactionHash"].hex() + return None diff --git a/operate/data/contracts/home_omnibridge/contract.yaml b/operate/data/contracts/home_omnibridge/contract.yaml new file mode 100644 index 000000000..6c59b1964 --- /dev/null +++ b/operate/data/contracts/home_omnibridge/contract.yaml @@ -0,0 +1,23 @@ +name: home_omnibridge +author: valory +version: 0.1.0 +type: contract +description: HomeOmnibridge +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeigrlrmwk3dhnoe4o5ovygtmudik7ybhsx2etqqjpvdi65rtlfpuhe + build/HomeOmnibridge.json: bafybeihmey7vxdyvviuzifiyvrsfjn35j2sbub7njvgann5juqzca5x4zy + contract.py: bafybeigj6morbr2bkuqtzvrunrsgefnpvegrlycxafjmb7quo42wvltmhi +fingerprint_ignore_patterns: [] +contracts: [] +class_name: HomeOmnibridge +contract_interface_paths: + ethereum: build/HomeOmnibridge.json +dependencies: + open-aea-ledger-ethereum: + version: ==1.60.0 + open-aea-test-autonomy: + version: ==0.18.3 + web3: + version: <7,>=6.0.0 From 6be1e9430dd2ac138c2b73c4a282cb8663decbed Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 11:08:29 +0200 Subject: [PATCH 126/173] chore: update --- operate/bridge/bridge.py | 22 ++++++++--- operate/bridge/providers/bridge_provider.py | 13 +++---- .../providers/native_bridge_provider.py | 36 +++++++++-------- tests/test_bridge_providers.py | 39 +++++++++++-------- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index c4d41cdd0..b15ab5e01 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -36,6 +36,7 @@ from operate.bridge.providers.lifi_bridge_provider import LiFiBridgeProvider from operate.bridge.providers.native_bridge_provider import ( NativeBridgeProvider, + OmnibridgeContractAdaptor, OptimismContractAdaptor, ) from operate.constants import ZERO_ADDRESS @@ -143,9 +144,20 @@ def __init__( BridgeManagerData, BridgeManagerData.load(path) ) self._bridge_providers = { - LiFiBridgeProvider.id(): LiFiBridgeProvider(wallet_manager, logger), - NativeBridgeProvider.id(): NativeBridgeProvider( - OptimismContractAdaptor(), wallet_manager, logger + "LiFiBridgeProvider": LiFiBridgeProvider( + wallet_manager, "LiFiBridgeProvider", logger + ), + "NativeBridgeProvider.Optimism": NativeBridgeProvider( + OptimismContractAdaptor(), + "NativeBridgeProvider.Optimism", + wallet_manager, + logger, + ), + "NativeBridgeProvider.Omnibridge": NativeBridgeProvider( + OmnibridgeContractAdaptor(), + "NativeBridgeProvider.Omnibridge", + wallet_manager, + logger, ), } @@ -181,8 +193,8 @@ def _get_updated_bundle( self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.") bridge_providers = [ # Sorted in order of preference - self._bridge_providers[NativeBridgeProvider.id()], - self._bridge_providers[LiFiBridgeProvider.id()], + self._bridge_providers["NativeBridgeProvider.Optimism"], + self._bridge_providers["LiFiBridgeProvider"], ] bridge_requests = [] for params in requests_params: diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index b9205e93d..e32ff903e 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -140,26 +140,23 @@ class BridgeProvider(ABC): def __init__( self, wallet_manager: MasterWalletManager, + provider_id: str, logger: t.Optional[logging.Logger] = None, ) -> None: """Initialize the bridge provider.""" self.wallet_manager = wallet_manager + self.provider_id = provider_id self.logger = logger or setup_logger(name="operate.bridge.BridgeProvider") - @classmethod - def id(cls) -> str: - """Get the id of the bridge provider.""" - return f"{cls.__module__}.{cls.__qualname__}" - def description(self) -> str: """Get a human-readable description of the bridge provider.""" return self.__class__.__name__ def _validate(self, bridge_request: BridgeRequest) -> None: """Validate theat the bridge request was created by this bridge.""" - if bridge_request.bridge_provider_id != self.id(): + if bridge_request.bridge_provider_id != self.provider_id: raise ValueError( - f"Bridge request provider id {bridge_request.bridge_provider_id} does not match the bridge provider id {self.id()}" + f"Bridge request provider id {bridge_request.bridge_provider_id} does not match the bridge provider id {self.provider_id}" ) def can_handle_request(self, params: t.Dict) -> bool: @@ -210,7 +207,7 @@ def create_request(self, params: t.Dict) -> BridgeRequest: return BridgeRequest( params=params, - bridge_provider_id=self.id(), + bridge_provider_id=self.provider_id, id=f"{BRIDGE_REQUEST_PREFIX}{uuid.uuid4()}", quote_data=None, execution_data=None, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 9a7d86fb0..13036dc4e 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -243,24 +243,21 @@ def find_erc20_bridge_finalized_tx( ) -class OmnibridgeContractAdaptor(ABC): +class OmnibridgeContractAdaptor(BridgeContractAdaptor): """Adaptor class for Omnibridge contract packages.""" - @abstractmethod def build_bridge_native_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge native asset transaction.""" raise NotImplementedError() - @abstractmethod def build_bridge_erc20_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge ERC20 asset transaction.""" raise NotImplementedError() - @abstractmethod def find_native_bridge_finalized_tx( self, ledger_api: LedgerApi, @@ -271,7 +268,6 @@ def find_native_bridge_finalized_tx( """Return the transaction hash of the event indicating native bridge completion.""" raise NotImplementedError() - @abstractmethod def find_erc20_bridge_finalized_tx( self, ledger_api: LedgerApi, @@ -283,19 +279,21 @@ def find_erc20_bridge_finalized_tx( raise NotImplementedError() - class NativeBridgeProvider(BridgeProvider): """Native bridge provider""" def __init__( self, bridge_contract_adaptor: BridgeContractAdaptor, + provider_id: str, wallet_manager: MasterWalletManager, logger: t.Optional[logging.Logger] = None, ) -> None: """Initialize the bridge provider.""" self.bridge_contract_adaptor = bridge_contract_adaptor - super().__init__(wallet_manager=wallet_manager, logger=logger) + super().__init__( + wallet_manager=wallet_manager, provider_id=provider_id, logger=logger + ) def can_handle_request(self, params: t.Dict) -> bool: """Returns 'true' if the bridge can handle a request for 'params'.""" @@ -525,18 +523,22 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) if from_token == ZERO_ADDRESS: - to_tx_hash = self.bridge_contract_adaptor.find_native_bridge_finalized_tx( - ledger_api=to_ledger_api, - bridge_request=bridge_request, - from_block=from_block, - to_block=to_block, + to_tx_hash = ( + self.bridge_contract_adaptor.find_native_bridge_finalized_tx( + ledger_api=to_ledger_api, + bridge_request=bridge_request, + from_block=from_block, + to_block=to_block, + ) ) else: - to_tx_hash = self.bridge_contract_adaptor.find_erc20_bridge_finalized_tx( - ledger_api=to_ledger_api, - bridge_request=bridge_request, - from_block=from_block, - to_block=to_block, + to_tx_hash = ( + self.bridge_contract_adaptor.find_erc20_bridge_finalized_tx( + ledger_api=to_ledger_api, + bridge_request=bridge_request, + from_block=from_block, + to_block=to_block, + ) ) if to_tx_hash: self.logger.info( diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index a8bf11a9a..4df9f38c6 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -44,8 +44,8 @@ QuoteData, ) from operate.bridge.providers.native_bridge_provider import ( - OPTIMISM_BRIDGE_DATA, NativeBridgeProvider, + OPTIMISM_BRIDGE_DATA, OptimismContractAdaptor, ) from operate.cli import OperateApp @@ -95,11 +95,13 @@ def test_bridge_zero( }, } - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge = LiFiBridgeProvider( + provider_id="LiFiBridgeProvider", wallet_manager=operate.wallet_manager + ) bridge_request = BridgeRequest( params=params, - bridge_provider_id=bridge.id(), id="test-id", + bridge_provider_id=bridge.provider_id, quote_data=None, execution_data=None, status=BridgeRequestStatus.CREATED, @@ -230,11 +232,13 @@ def test_bridge_error( }, } - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge = LiFiBridgeProvider( + provider_id="LiFiBridgeProvider", wallet_manager=operate.wallet_manager + ) bridge_request = BridgeRequest( params=params, - bridge_provider_id=bridge.id(), id="test-id", + bridge_provider_id=bridge.provider_id, quote_data=None, execution_data=None, status=BridgeRequestStatus.CREATED, @@ -376,11 +380,13 @@ def test_bridge_quote( }, } - bridge = LiFiBridgeProvider(wallet_manager=operate.wallet_manager) + bridge = LiFiBridgeProvider( + provider_id="LiFiBridgeProvider", wallet_manager=operate.wallet_manager + ) bridge_request = BridgeRequest( params=params, - bridge_provider_id=bridge.id(), id="test-id", + bridge_provider_id=bridge.provider_id, quote_data=None, execution_data=None, status=BridgeRequestStatus.CREATED, @@ -498,6 +504,7 @@ def test_bridge_zero( } bridge = NativeBridgeProvider( + provider_id="NativeBridgeProvider", bridge_contract_adaptor=OptimismContractAdaptor(), wallet_manager=operate.wallet_manager, ) @@ -518,7 +525,7 @@ def test_bridge_zero( "amount": 0, }, }, - bridge_provider_id=NativeBridgeProvider.id(), + bridge_provider_id=bridge.provider_id, id=bridge_request.id, status=BridgeRequestStatus.CREATED, quote_data=None, @@ -538,9 +545,7 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE][ - "bridge_eta" - ], + bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], elapsed_time=0, message=MESSAGE_QUOTE_ZERO, provider_data=None, @@ -643,6 +648,7 @@ def test_bridge_execute_error( } bridge = NativeBridgeProvider( + provider_id="NativeBridgeProvider", bridge_contract_adaptor=OptimismContractAdaptor(), wallet_manager=operate.wallet_manager, ) @@ -663,8 +669,8 @@ def test_bridge_execute_error( "amount": 1000000000000000000, }, }, - bridge_provider_id=NativeBridgeProvider.id(), id=bridge_request.id, + bridge_provider_id=bridge.provider_id, status=BridgeRequestStatus.CREATED, quote_data=None, execution_data=None, @@ -683,9 +689,7 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE][ - "bridge_eta" - ], + bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], elapsed_time=0, message=None, provider_data=None, @@ -1028,11 +1032,12 @@ def test_update_execution_status( if bridge_provider_class == NativeBridgeProvider: bridge: BridgeProvider = NativeBridgeProvider( + provider_id="NativeBridgeProvider", bridge_contract_adaptor=OptimismContractAdaptor(), wallet_manager=operate.wallet_manager, ) else: - bridge = bridge_provider_class(wallet_manager=operate.wallet_manager) + bridge = bridge_provider_class(provider_id="", wallet_manager=operate.wallet_manager) quote_data = QuoteData( bridge_eta=0, @@ -1052,7 +1057,7 @@ def test_update_execution_status( bridge_request = BridgeRequest( params=params, - bridge_provider_id=bridge_provider_class.id(), + bridge_provider_id=bridge.provider_id, id=request_id, status=BridgeRequestStatus.EXECUTION_PENDING, quote_data=quote_data, From 9830ce4ed8e3b265a13e3a5b8c376738cf86cb70 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 13:32:59 +0200 Subject: [PATCH 127/173] chore: update --- .../providers/native_bridge_provider.py | 93 +- .../foreign_omnibridge/contract.yaml | 6 +- .../home_omnibridge/build/HomeOmnibridge.json | 1413 ++++++++++++++++- .../contracts/home_omnibridge/contract.yaml | 2 +- 4 files changed, 1476 insertions(+), 38 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 13036dc4e..7d3c443fe 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -45,6 +45,8 @@ ) from operate.constants import ZERO_ADDRESS from operate.data import DATA_DIR +from operate.data.contracts.foreign_omnibridge.contract import ForeignOmnibridge +from operate.data.contracts.home_omnibridge.contract import HomeOmnibridge from operate.data.contracts.l1_standard_bridge.contract import ( DEFAULT_BRIDGE_MIN_GAS_LIMIT, L1StandardBridge, @@ -57,7 +59,7 @@ BLOCK_CHUNK_SIZE = 5000 -OPTIMISM_BRIDGE_DATA: t.Dict[t.Any, t.Dict[str, t.Any]] = { +BRIDGE_DATA: t.Dict[t.Any, t.Dict[str, t.Any]] = { (Chain.ETHEREUM, Chain.BASE): { "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "to_bridge": "0x4200000000000000000000000000000000000010", @@ -73,6 +75,11 @@ "to_bridge": "0x4200000000000000000000000000000000000010", "bridge_eta": 5 * 60, }, + (Chain.ETHEREUM, Chain.GNOSIS): { + "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671", + "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d", + "bridge_eta": 30 * 60, + }, } @@ -119,13 +126,13 @@ def find_erc20_bridge_finalized_tx( class OptimismContractAdaptor(BridgeContractAdaptor): """Adaptor class for Optimism contract packages.""" - l1_standard_bridge_contract = t.cast( + _l1_standard_bridge_contract = t.cast( L1StandardBridge, L1StandardBridge.from_dir( directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"), ), ) - l2_standard_bridge_contract = t.cast( + _l2_standard_bridge_contract = t.cast( L2StandardBridge, L2StandardBridge.from_dir( directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"), @@ -141,11 +148,9 @@ def build_bridge_native_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] + from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] extra_data = Web3.keccak(text=bridge_request.id) - return self.l1_standard_bridge_contract.build_bridge_eth_to_tx( + return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( ledger_api=ledger_api, contract_address=from_bridge, sender=from_address, @@ -166,11 +171,9 @@ def build_bridge_erc20_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] + from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] extra_data = Web3.keccak(text=bridge_request.id) - return self.l1_standard_bridge_contract.build_bridge_erc20_to_tx( + return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx( ledger_api=ledger_api, contract_address=from_bridge, sender=from_address, @@ -195,11 +198,9 @@ def find_native_bridge_finalized_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "to_bridge" - ] + to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] extra_data = Web3.keccak(text=bridge_request.id) - return self.l2_standard_bridge_contract.find_eth_bridge_finalized_tx( + return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( ledger_api=ledger_api, contract_address=to_bridge, from_=from_address, @@ -225,11 +226,9 @@ def find_erc20_bridge_finalized_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "to_bridge" - ] + to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] extra_data = Web3.keccak(text=bridge_request.id) - return self.l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( + return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( ledger_api=ledger_api, contract_address=to_bridge, local_token=to_token, @@ -246,17 +245,31 @@ def find_erc20_bridge_finalized_tx( class OmnibridgeContractAdaptor(BridgeContractAdaptor): """Adaptor class for Omnibridge contract packages.""" + _foreign_omnibridge = t.cast( + ForeignOmnibridge, + ForeignOmnibridge.from_dir( + directory=str(DATA_DIR / "contracts" / "foreign_omnibridge"), + ), + ) + + _home_omnibridge = t.cast( + HomeOmnibridge, + HomeOmnibridge.from_dir( + directory=str(DATA_DIR / "contracts" / "home_omnibridge"), + ), + ) + def build_bridge_native_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge native asset transaction.""" - raise NotImplementedError() + raise NotImplementedError("NotImplementedError") def build_bridge_erc20_tx( self, ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge ERC20 asset transaction.""" - raise NotImplementedError() + raise NotImplementedError("NotImplementedError") def find_native_bridge_finalized_tx( self, @@ -266,7 +279,7 @@ def find_native_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating native bridge completion.""" - raise NotImplementedError() + raise NotImplementedError("NotImplementedError") def find_erc20_bridge_finalized_tx( self, @@ -276,7 +289,27 @@ def find_erc20_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating ERC20 bridge completion.""" - raise NotImplementedError() + from_chain = bridge_request.params["from"]["chain"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_token = bridge_request.params["to"]["token"] + to_amount = bridge_request.params["to"]["amount"] + to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] + + message_id = bytes.fromhex( + "000500004AC82B41BD819DD871590B510316F2385CB196FB000000000002AA74" + ) + + return self._home_omnibridge.find_tokens_bridged_tx( + ledger_api=ledger_api, + contract_address=to_bridge, + token=to_token, + recipient=to_address, + value=to_amount, + message_id=message_id, + from_block=from_block, + to_block=to_block, + ) class NativeBridgeProvider(BridgeProvider): @@ -306,7 +339,7 @@ def can_handle_request(self, params: t.Dict) -> bool: to_chain = Chain(params["to"]["chain"]) to_token = params["to"]["token"] - if (from_chain, to_chain) not in OPTIMISM_BRIDGE_DATA: + if (from_chain, to_chain) not in BRIDGE_DATA: self.logger.warning( f"[NATIVE BRIDGE] Unsupported bridge from {from_chain} to {to_chain}." ) @@ -354,9 +387,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - bridge_eta = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "bridge_eta" - ] + bridge_eta = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["bridge_eta"] message = None if to_amount == 0: @@ -413,9 +444,7 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] + from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] from_ledger_api = self._from_ledger_api(bridge_request) if from_token == ZERO_ADDRESS: @@ -492,9 +521,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - bridge_eta = OPTIMISM_BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))][ - "bridge_eta" - ] + bridge_eta = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["bridge_eta"] try: from_ledger_api = self._from_ledger_api(bridge_request) diff --git a/operate/data/contracts/foreign_omnibridge/contract.yaml b/operate/data/contracts/foreign_omnibridge/contract.yaml index 7eb5b01c9..f3d294196 100644 --- a/operate/data/contracts/foreign_omnibridge/contract.yaml +++ b/operate/data/contracts/foreign_omnibridge/contract.yaml @@ -6,9 +6,9 @@ description: ForeignOmnibridge license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: bafybeifsbxn6hlccnpgqnpvaz3ph6ajl4is4mcyerr6aqp7heggagcphye - build/L1StandardBridge.json: bafybeidq6jt7zmedtuxbbyggiqhu7w6543bunyd2vrbibg6y2svxsi2q5m - contract.py: bafybeib2kiztts3436vccvraeura3jb23oihgzxgrt3biakbed24edcaai + __init__.py: bafybeibsmumov3s36vfo24xp2niilcp3ywju2d4yqfadllyjncqtgtndly + build/ForeignOmnibridge.json: bafybeibmcflt7w5p5szgii7glbtrvjahweclowz2k7e6qjq7yfbvszy6em + contract.py: bafybeieldprlj4civ7bxsgt4xi5qhpqoj7z6tqeop23mqbd6ervr5szjda fingerprint_ignore_patterns: [] contracts: [] class_name: ForeignOmnibridge diff --git a/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json b/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json index a3ada7d7d..9f150921b 100644 --- a/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +++ b/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json @@ -2,7 +2,1418 @@ "_format": "hh-sol-artifact-1", "contractName": "HomeOmnibridge", "sourceName": "HomeOmnibridge.sol", - "abi": [{"inputs":[{"internalType":"string","name":"_suffix","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"newLimit","type":"uint256"}],"name":"DailyLimitChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"newLimit","type":"uint256"}],"name":"ExecutionDailyLimitChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"},{"indexed":false,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"FailedMessageFixed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"}],"name":"FeeDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"}],"name":"FeeDistributionFailed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"nativeToken","type":"address"},{"indexed":true,"internalType":"address","name":"bridgedToken","type":"address"}],"name":"NewTokenRegistered","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"}],"name":"TokensBridged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":true,"internalType":"bytes32","name":"messageId","type":"bytes32"}],"name":"TokensBridgingInitiated","type":"event"},{"inputs":[],"name":"bridgeContract","outputs":[{"internalType":"contract IAMB","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_nativeToken","type":"address"}],"name":"bridgedTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_to","type":"address"}],"name":"claimTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgedToken","type":"address"},{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_to","type":"address"}],"name":"claimTokensFromTokenContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"dailyLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"deployAndHandleBridgedTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"deployAndHandleBridgedTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"executionDailyLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"executionMaxPerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeManager","outputs":[{"internalType":"contract OmnibridgeFeeManager","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_messageId","type":"bytes32"}],"name":"fixFailedMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_receiver","type":"address"}],"name":"fixMediatorBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_homeToken","type":"address"}],"name":"foreignTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"forwardingRulesManager","outputs":[{"internalType":"contract MultiTokenForwardingRulesManager","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"gasLimitManager","outputs":[{"internalType":"contract SelectorTokenGasLimitManager","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBridgeInterfacesVersion","outputs":[{"internalType":"uint64","name":"major","type":"uint64"},{"internalType":"uint64","name":"minor","type":"uint64"},{"internalType":"uint64","name":"patch","type":"uint64"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getBridgeMode","outputs":[{"internalType":"bytes4","name":"_data","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"getCurrentDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"handleBridgedTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"handleBridgedTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"handleNativeTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"handleNativeTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_foreignToken","type":"address"}],"name":"homeTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgeContract","type":"address"},{"internalType":"address","name":"_mediatorContract","type":"address"},{"internalType":"uint256[3]","name":"_dailyLimitMaxPerTxMinPerTxArray","type":"uint256[3]"},{"internalType":"uint256[2]","name":"_executionDailyLimitExecutionMaxPerTxArray","type":"uint256[2]"},{"internalType":"address","name":"_gasLimitManager","type":"address"},{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_tokenFactory","type":"address"},{"internalType":"address","name":"_feeManager","type":"address"},{"internalType":"address","name":"_forwardingRulesManager","type":"address"}],"name":"initialize","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"isBridgedTokenDeployAcknowledged","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isInitialized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"isRegisteredAsNativeToken","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"isTokenRegistered","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"maxAvailablePerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"maxPerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"mediatorBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mediatorContractOnOtherSide","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_messageId","type":"bytes32"}],"name":"messageFixed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenFactory","type":"address"},{"internalType":"address","name":"_forwardingRulesManager","type":"address"},{"internalType":"address","name":"_gasLimitManager","type":"address"},{"internalType":"address","name":"_feeManager","type":"address"}],"name":"migrateTo_3_3_0","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"minPerTx","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgedToken","type":"address"}],"name":"nativeTokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"onTokenTransfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract IERC677","name":"token","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"relayTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC677","name":"token","type":"address"},{"internalType":"address","name":"_receiver","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"relayTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC677","name":"token","type":"address"},{"internalType":"address","name":"_receiver","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"relayTokensAndCall","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_messageId","type":"bytes32"}],"name":"requestFailedMessageFix","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_bridgeContract","type":"address"}],"name":"setBridgeContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_nativeToken","type":"address"},{"internalType":"address","name":"_bridgedToken","type":"address"}],"name":"setCustomTokenAddressPair","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_dailyLimit","type":"uint256"}],"name":"setDailyLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_dailyLimit","type":"uint256"}],"name":"setExecutionDailyLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_maxPerTx","type":"uint256"}],"name":"setExecutionMaxPerTx","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_feeManager","type":"address"}],"name":"setFeeManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_manager","type":"address"}],"name":"setForwardingRulesManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_manager","type":"address"}],"name":"setGasLimitManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_maxPerTx","type":"uint256"}],"name":"setMaxPerTx","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_mediatorContract","type":"address"}],"name":"setMediatorContractOnOtherSide","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_minPerTx","type":"uint256"}],"name":"setMinPerTx","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenFactory","type":"address"}],"name":"setTokenFactory","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tokenFactory","outputs":[{"internalType":"contract TokenFactory","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_day","type":"uint256"}],"name":"totalExecutedPerDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_day","type":"uint256"}],"name":"totalSpentPerDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withinExecutionLimit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withinLimit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}], + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "_suffix", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLimit", + "type": "uint256" + } + ], + "name": "DailyLimitChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLimit", + "type": "uint256" + } + ], + "name": "ExecutionDailyLimitChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "FailedMessageFixed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + } + ], + "name": "FeeDistributed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "FeeDistributionFailed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "nativeToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "bridgedToken", + "type": "address" + } + ], + "name": "NewTokenRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + } + ], + "name": "TokensBridged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "messageId", + "type": "bytes32" + } + ], + "name": "TokensBridgingInitiated", + "type": "event" + }, + { + "inputs": [], + "name": "bridgeContract", + "outputs": [ + { + "internalType": "contract IAMB", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_nativeToken", + "type": "address" + } + ], + "name": "bridgedTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + } + ], + "name": "claimTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgedToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + } + ], + "name": "claimTokensFromTokenContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "dailyLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "deployAndHandleBridgedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "deployAndHandleBridgedTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "executionDailyLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "executionMaxPerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeManager", + "outputs": [ + { + "internalType": "contract OmnibridgeFeeManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_messageId", + "type": "bytes32" + } + ], + "name": "fixFailedMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "fixMediatorBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_homeToken", + "type": "address" + } + ], + "name": "foreignTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "forwardingRulesManager", + "outputs": [ + { + "internalType": "contract MultiTokenForwardingRulesManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasLimitManager", + "outputs": [ + { + "internalType": "contract SelectorTokenGasLimitManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBridgeInterfacesVersion", + "outputs": [ + { + "internalType": "uint64", + "name": "major", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minor", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "patch", + "type": "uint64" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getBridgeMode", + "outputs": [ + { + "internalType": "bytes4", + "name": "_data", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentDay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "handleBridgedTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "handleBridgedTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "handleNativeTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "handleNativeTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_foreignToken", + "type": "address" + } + ], + "name": "homeTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgeContract", + "type": "address" + }, + { + "internalType": "address", + "name": "_mediatorContract", + "type": "address" + }, + { + "internalType": "uint256[3]", + "name": "_dailyLimitMaxPerTxMinPerTxArray", + "type": "uint256[3]" + }, + { + "internalType": "uint256[2]", + "name": "_executionDailyLimitExecutionMaxPerTxArray", + "type": "uint256[2]" + }, + { + "internalType": "address", + "name": "_gasLimitManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "address", + "name": "_tokenFactory", + "type": "address" + }, + { + "internalType": "address", + "name": "_feeManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_forwardingRulesManager", + "type": "address" + } + ], + "name": "initialize", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "isBridgedTokenDeployAcknowledged", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isInitialized", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "isRegisteredAsNativeToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "isTokenRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "maxAvailablePerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "maxPerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "mediatorBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mediatorContractOnOtherSide", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_messageId", + "type": "bytes32" + } + ], + "name": "messageFixed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenFactory", + "type": "address" + }, + { + "internalType": "address", + "name": "_forwardingRulesManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_gasLimitManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_feeManager", + "type": "address" + } + ], + "name": "migrateTo_3_3_0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + } + ], + "name": "minPerTx", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgedToken", + "type": "address" + } + ], + "name": "nativeTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "onTokenTransfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC677", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "relayTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC677", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "relayTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC677", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "relayTokensAndCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_messageId", + "type": "bytes32" + } + ], + "name": "requestFailedMessageFix", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bridgeContract", + "type": "address" + } + ], + "name": "setBridgeContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_nativeToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_bridgedToken", + "type": "address" + } + ], + "name": "setCustomTokenAddressPair", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_dailyLimit", + "type": "uint256" + } + ], + "name": "setDailyLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_dailyLimit", + "type": "uint256" + } + ], + "name": "setExecutionDailyLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxPerTx", + "type": "uint256" + } + ], + "name": "setExecutionMaxPerTx", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeManager", + "type": "address" + } + ], + "name": "setFeeManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_manager", + "type": "address" + } + ], + "name": "setForwardingRulesManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_manager", + "type": "address" + } + ], + "name": "setGasLimitManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxPerTx", + "type": "uint256" + } + ], + "name": "setMaxPerTx", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_mediatorContract", + "type": "address" + } + ], + "name": "setMediatorContractOnOtherSide", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minPerTx", + "type": "uint256" + } + ], + "name": "setMinPerTx", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tokenFactory", + "type": "address" + } + ], + "name": "setTokenFactory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tokenFactory", + "outputs": [ + { + "internalType": "contract TokenFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_day", + "type": "uint256" + } + ], + "name": "totalExecutedPerDay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_day", + "type": "uint256" + } + ], + "name": "totalSpentPerDay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withinExecutionLimit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withinLimit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], "bytecode": "", "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106103a45760003560e01c80636e5d6bea116101e9578063c2173d431161010f578063d7405481116100ad578063ec47de2a1161007c578063ec47de2a14611047578063f2c54fe814611073578063f2fde38b1461109f578063f3f51415146110c5576103a4565b8063d740548114610f29578063db6fff8c14610fed578063dfbe4ae014611019578063e77772fe1461103f576103a4565b8063cd596583116100e9578063cd59658314610daf578063d0342acd14610db7578063d0fb020314610de5578063d522cfd714610ded576103a4565b8063c2173d4314610cbd578063c534576114610ce3578063c722b1be14610da7576103a4565b80639a4a439511610187578063a4c0ed3611610156578063a4c0ed3614610b7c578063ab3a25d914610c35578063ad58bdd114610c61578063ae813e9f14610c97576103a4565b80639a4a439514610afd5780639cb7595a14610b1a5780639d4051ae14610b4e578063a4b1c24314610b56576103a4565b8063867f7a4d116101c3578063867f7a4d14610a03578063871c076014610ac75780638da5cb5b14610acf57806390ad84a814610ad7576103a4565b80636e5d6bea1461098b5780637610722f146109b15780637837cf91146109d7576103a4565b80632d70061f116102ce578063437764df1161026c578063613fa2f21161023b578063613fa2f2146108d957806361c04f84146108ff57806364696f971461092557806369ffa08a1461095d576103a4565b8063437764df1461084b578063472d35b91461087057806347ac7d6a1461089657806359339982146108bc576103a4565b80633a50bc87116102a85780633a50bc871461078a5780633da98c8b146107b65780633e6968b61461081d57806340f8dd8614610825576103a4565b80632d70061f1461071a5780632f73a9f81461075c578063392e53cd14610782576103a4565b8063125e4cfb1161034657806326aa101f1161031557806326aa101f146105ac578063272255bb146105d25780632803212f146106085780632ae87cdd14610634576103a4565b8063125e4cfb146104ec57806316ef191314610522578063194153d3146105485780631ba9212d1461056e576103a4565b80630950d515116103825780630950d5151461043b5780630b26cf66146104585780630b71a4a71461047e57806310775238146104ac576103a4565b806301e4f53a146103a957806301fcc1d3146103d7578063032f693f14610403575b600080fd5b6103d5600480360360408110156103bf57600080fd5b506001600160a01b0381351690602001356110eb565b005b6103d5600480360360408110156103ed57600080fd5b506001600160a01b03813516906020013561112a565b6104296004803603602081101561041957600080fd5b50356001600160a01b03166111c9565b60408051918252519081900360200190f35b6103d56004803603602081101561045157600080fd5b5035611221565b6103d56004803603602081101561046e57600080fd5b50356001600160a01b03166112ca565b6103d56004803603604081101561049457600080fd5b506001600160a01b03813581169160200135166112de565b6104d8600480360360408110156104c257600080fd5b506001600160a01b0381351690602001356113b3565b604080519115158252519081900360200190f35b6103d56004803603606081101561050257600080fd5b506001600160a01b03813581169160208101359091169060400135611426565b6104296004803603602081101561053857600080fd5b50356001600160a01b0316611460565b6104296004803603602081101561055e57600080fd5b50356001600160a01b03166114bd565b6103d56004803603608081101561058457600080fd5b506001600160a01b038135811691602081013582169160408201358116916060013516611518565b6104d8600480360360208110156105c257600080fd5b50356001600160a01b031661164b565b6103d5600480360360608110156105e857600080fd5b506001600160a01b0381358116916020810135909116906040013561165e565b6103d56004803603604081101561061e57600080fd5b506001600160a01b038135169060200135611681565b6103d5600480360360c081101561064a57600080fd5b6001600160a01b038235169190810190604081016020820135600160201b81111561067457600080fd5b82018360208201111561068657600080fd5b803590602001918460018302840111600160201b831117156106a757600080fd5b919390929091602081019035600160201b8111156106c457600080fd5b8201836020820111156106d657600080fd5b803590602001918460018302840111600160201b831117156106f757600080fd5b919350915060ff813516906001600160a01b036020820135169060400135611750565b6107406004803603602081101561073057600080fd5b50356001600160a01b0316611782565b604080516001600160a01b039092168252519081900360200190f35b6103d56004803603602081101561077257600080fd5b50356001600160a01b03166117e8565b6104d86117f9565b6104d8600480360360408110156107a057600080fd5b506001600160a01b03813516906020013561184a565b6104d860048036036101808110156107cd57600080fd5b506001600160a01b0381358116916020810135821691604082019160a081019160e082013581169161010081013582169161012082013581169161014081013582169161016090910135166118a0565b610429611a88565b6104296004803603602081101561083b57600080fd5b50356001600160a01b0316611a91565b610853611af0565b604080516001600160e01b03199092168252519081900360200190f35b6103d56004803603602081101561088657600080fd5b50356001600160a01b0316611afb565b610740600480360360208110156108ac57600080fd5b50356001600160a01b0316611b0c565b6104d8600480360360208110156108d257600080fd5b5035611b17565b6103d5600480360360208110156108ef57600080fd5b50356001600160a01b0316611b6a565b6107406004803603602081101561091557600080fd5b50356001600160a01b0316611b7b565b6103d56004803603606081101561093b57600080fd5b506001600160a01b038135811691602081013582169160409091013516611be4565b6103d56004803603604081101561097357600080fd5b506001600160a01b0381358116916020013516611c69565b6103d5600480360360208110156109a157600080fd5b50356001600160a01b0316611ca0565b610429600480360360208110156109c757600080fd5b50356001600160a01b0316611cb1565b6103d5600480360360408110156109ed57600080fd5b506001600160a01b038135169060200135611d0b565b6103d560048036036080811015610a1957600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610a5357600080fd5b820183602082011115610a6557600080fd5b803590602001918460018302840111600160201b83111715610a8657600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550611de3945050505050565b610740611e0d565b610740611e64565b6103d560048036036020811015610aed57600080fd5b50356001600160a01b0316611ebb565b6103d560048036036020811015610b1357600080fd5b5035611ecc565b610b226120c1565b6040805167ffffffffffffffff9485168152928416602084015292168183015290519081900360600190f35b6107406120cb565b61042960048036036020811015610b6c57600080fd5b50356001600160a01b0316612122565b6104d860048036036060811015610b9257600080fd5b6001600160a01b0382351691602081013591810190606081016040820135600160201b811115610bc157600080fd5b820183602082011115610bd357600080fd5b803590602001918460018302840111600160201b83111715610bf457600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506121a7945050505050565b61042960048036036040811015610c4b57600080fd5b506001600160a01b03813516906020013561220e565b6103d560048036036060811015610c7757600080fd5b506001600160a01b03813581169160208101359091169060400135612274565b6104d860048036036020811015610cad57600080fd5b50356001600160a01b0316612282565b6104d860048036036020811015610cd357600080fd5b50356001600160a01b03166122db565b6103d560048036036080811015610cf957600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610d3357600080fd5b820183602082011115610d4557600080fd5b803590602001918460018302840111600160201b83111715610d6657600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612308945050505050565b610740612348565b61074061239f565b6103d560048036036040811015610dcd57600080fd5b506001600160a01b03813581169160200135166123f6565b6107406124b5565b6103d5600480360360e0811015610e0357600080fd5b6001600160a01b038235169190810190604081016020820135600160201b811115610e2d57600080fd5b820183602082011115610e3f57600080fd5b803590602001918460018302840111600160201b83111715610e6057600080fd5b919390929091602081019035600160201b811115610e7d57600080fd5b820183602082011115610e8f57600080fd5b803590602001918460018302840111600160201b83111715610eb057600080fd5b9193909260ff833516926001600160a01b03602082013516926040820135929091608081019060600135600160201b811115610eeb57600080fd5b820183602082011115610efd57600080fd5b803590602001918460018302840111600160201b83111715610f1e57600080fd5b50909250905061250c565b6103d560048036036080811015610f3f57600080fd5b6001600160a01b03823581169260208101359091169160408201359190810190608081016060820135600160201b811115610f7957600080fd5b820183602082011115610f8b57600080fd5b803590602001918460018302840111600160201b83111715610fac57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550612582945050505050565b6103d56004803603604081101561100357600080fd5b506001600160a01b03813516906020013561258e565b6107406004803603602081101561102f57600080fd5b50356001600160a01b031661262b565b610740612636565b6103d56004803603604081101561105d57600080fd5b506001600160a01b03813516906020013561268d565b6104296004803603604081101561108957600080fd5b506001600160a01b03813516906020013561272e565b6103d5600480360360208110156110b557600080fd5b50356001600160a01b0316612797565b610429600480360360208110156110db57600080fd5b50356001600160a01b03166127a8565b6111268233836000805b506040519080825280601f01601f191660200182016040528015611120576020820181803683370190505b506127fe565b5050565b61113261295f565b61113b8261164b565b61114457600080fd5b8015806111635750600081118015611163575061116082611a91565b81105b61116c57600080fd5b60408051700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b60208083019190915260609490941b6001600160601b0319166031820152815180820360250181526045909101825280519084012060009081529283905290912055565b60408051670dac2f0a0cae4a8f60c31b6020808301919091526001600160601b0319606085901b1660288301528251601c818403018152603c909201835281519181019190912060009081529081905220545b919050565b611229612986565b61123281611b17565b1561123c57600080fd5b600061124782612a30565b9050600061125483612a89565b9050600061126184612ae6565b905061126c84612b35565b611277838383612b8e565b604080516001600160a01b03808616825284166020820152808201839052905185917f07b5483b8e4bd8ea240a474d5117738350e7d431e3668c48a97910b0b397796a919081900360600190a250505050565b6112d261295f565b6112db81612baf565b50565b6112e661295f565b6112ef8161164b565b156112f957600080fd5b600061130482611b7b565b6001600160a01b03161461131757600080fd5b600061132283611782565b6001600160a01b03161461133557600080fd5b61134a6001600160a01b038216306001612c2b565b806001600160a01b03166342966c6860016040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b15801561139157600080fd5b505af11580156113a5573d6000803e3d6000fd5b505050506111268282612cb7565b6000806113d1836113cb866113c6611a88565b61220e565b90612db2565b905060006113df60006127a8565b1180156113f45750806113f1856127a8565b10155b80156114085750611404846111c9565b8311155b801561141c575061141884612122565b8310155b9150505b92915050565b61142e612986565b600061143984611782565b90506114448161164b565b61144d57600080fd5b61145a8160008585612e13565b50505050565b60408051700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b60208083019190915260609390931b6001600160601b0319166031820152815180820360250181526045909101825280519083012060009081529182905290205490565b604080516e6d65646961746f7242616c616e636560881b60208083019190915260609390931b6001600160601b031916602f820152815180820360230181526043909101825280519083012060009081529182905290205490565b7f1ba9212d27ad775f3f89cdd276d3829dd95d169a049be071de905db3bbca633e600081905260046020527f39ed9a5a925de329b4b8d3797a51d757351f7bb58fe4f46a37c1af393b814a685460ff161561157257600080fd5b33301461157e57600080fd5b61158785612f3d565b61159084612fb9565b61159983613046565b6115a2826130d3565b604080516919185a5b1e531a5b5a5d60b21b6020808301919091526000602a83018190528351601e818503018152603e84018086528151918401919091208252918190528381206ec097ce7bc90715b34b9f10000000009081905590915291517fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd259415991819003605e0190a26000908152600460205260409020805460ff1916600117905550505050565b60008061165783612122565b1192915050565b611666612986565b61166f83613160565b61167c8360018484612e13565b505050565b61168961295f565b6116928261164b565b61169b57600080fd5b6116a4826111c9565b8111806116af575080155b6116b857600080fd5b604080516919185a5b1e531a5b5a5d60b21b6020808301919091526001600160601b0319606086901b16602a8301528251601e818403018152603e83018085528151918301919091206000908152918290529083902084905583905290516001600160a01b038416917fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd25941599190819003605e0190a25050565b611758612986565b600061176889898989898961321c565b90506117778160008585612e13565b505050505050505050565b604080516f686f6d65546f6b656e4164647265737360801b60208083019190915260609390931b6001600160601b03191660308201528151808203602401815260449091018252805190830120600090815260029092529020546001600160a01b031690565b6117f061295f565b6112db81612f3d565b7f0a6f646cd611241d8073675e00d1a1ff700fbf1b53fcf473de56d1e6e4b714ba60005260046020527f078d888f9b66f3f8bfa10909e31f1e16240db73449f0500afdbbe3a70da457cc5460ff1690565b600080611862836113cb8661185d611a88565b61272e565b905060006118706000611a91565b11801561188557508061188285611a91565b10155b801561141c575061189584611460565b909211159392505050565b60408051600481526024810182526020810180516001600160e01b03166337ef410160e11b1781529151815160009384936060933093919290918291908083835b602083106119005780518252601f1990920191602091820191016118e1565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114611960576040519150601f19603f3d011682016040523d82523d6000602084013e611965565b606091505b50915091508115806119a15750805160201480156119a1575080806020019051602081101561199357600080fd5b50516001600160a01b031633145b806119ab57503330145b6119b457600080fd5b6119bc6117f9565b156119c657600080fd5b6119cf8c612baf565b6119d88b61356a565b611a0c60008b60038060200260405190810160405280929190826003602002808284376000920191909152506135d4915050565b604080518082018252611a3b91600091908c906002908390839080828437600092019190915250613728915050565b611a4488613046565b611a4d87613817565b611a5686612f3d565b611a5f856130d3565b611a6884612fb9565b611a706138df565b611a786117f9565b9c9b505050505050505050505050565b62015180420490565b6040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b60208083019190915260609390931b6001600160601b0319166033820152815180820360270181526047909101825280519083012060009081529182905290205490565b6358a8b61360e11b90565b611b0361295f565b6112db816130d3565b600061142082611b7b565b604080516b1b595cdcd859d9519a5e195960a21b602080830191909152602c80830185905283518084039091018152604c909201835281519181019190912060009081526004909152205460ff16919050565b611b7261295f565b6112db81613046565b6040805172666f726569676e546f6b656e4164647265737360681b60208083019190915260609390931b6001600160601b03191660338201528151808203602701815260479091018252805190830120600090815260029092529020546001600160a01b031690565b611bec613936565b826001600160a01b03166369ffa08a83836040518363ffffffff1660e01b815260040180836001600160a01b03168152602001826001600160a01b0316815260200192505050600060405180830381600087803b158015611c4c57600080fd5b505af1158015611c60573d6000803e3d6000fd5b50505050505050565b611c71613936565b6001600160a01b0382161580611c8d5750611c8b8261164b565b155b611c9657600080fd5b61112682826139af565b611ca861295f565b6112db8161356a565b600080611cbd836111c9565b90506000611cca846127a8565b90506000611cda856113c6611a88565b90506000818311611cec576000611cf0565b8183035b9050808410611cff5780611d01565b835b9695505050505050565b611d1361295f565b611d1c8261164b565b611d2557600080fd5b611d2e82611460565b811180611d39575080155b611d4257600080fd5b6040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b6020808301919091526001600160601b0319606086901b16603383015282516027818403018152604783018085528151918301919091206000908152918290529083902084905583905290516001600160a01b038416917f4c177b42dbe934b3abbc0208c11a42e46589983431616f1710ab19969c5ed62e919081900360670190a25050565b611deb612986565b611df484613160565b611e018460018585612e13565b61145a838584846139e9565b7f98aa806e31e94a687a31c65769cb99670064dd7f5a87526da075c5fb4eab988060005260026020527f0c1206883be66049a02d4937078367c00b3d71dd1a9465df969363c6ddeac96d546001600160a01b031690565b7f02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c060005260026020527fb7802e97e87ef2842a6cce7da7ffaeaedaa2f61a6a7870b23d9d01fc9b73712e546001600160a01b031690565b611ec361295f565b6112db81612fb9565b6000611ed661239f565b9050806001600160a01b031663cb08a10c836040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b158015611f1c57600080fd5b505afa158015611f30573d6000803e3d6000fd5b505050506040513d6020811015611f4657600080fd5b505115611f5257600080fd5b306001600160a01b0316816001600160a01b0316633f9a8e7e846040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b158015611fa057600080fd5b505afa158015611fb4573d6000803e3d6000fd5b505050506040513d6020811015611fca57600080fd5b50516001600160a01b031614611fdf57600080fd5b611fe7611e0d565b6001600160a01b0316816001600160a01b0316634a610b04846040518263ffffffff1660e01b81526004018082815260200191505060206040518083038186803b15801561203457600080fd5b505afa158015612048573d6000803e3d6000fd5b505050506040513d602081101561205e57600080fd5b50516001600160a01b03161461207357600080fd5b6040805160248082018590528251808303909101815260449091019091526020810180516001600160e01b0316630950d51560e01b908117909152906120ba816001613b56565b5050505050565b6003806001909192565b7f5f5bc4e0b888be22a35f2166061a04607296c26861006b9b8e089a172696a82260005260026020527f60072fd9ffad01d76b1d1421ce17a3613dc06795e4b113745995ad1d84a52121546001600160a01b031690565b60408051670dad2dca0cae4a8f60c31b6020808301919091526001600160601b0319606085901b1660288301528251601c818403018152603c9092018352815191810191909120600090815290819052908120546001600160a01b03831661218b57905061121c565b6000811161219a57600061219d565b60015b60ff169392505050565b60006121b1613d6a565b61220457604080516000815260208101909152825185906014116121f4576121d884613d8f565b90506014845111156121f4578351601319016014850190815291505b6122013387838886613d96565b50505b5060019392505050565b604080516f746f74616c5370656e7450657244617960801b60208083019190915260609490941b6001600160601b031916603082015260448082019390935281518082039093018352606401815281519183019190912060009081529182905290205490565b61167c8383836000806110f5565b604080516861636b4465706c6f7960b81b60208083019190915260609390931b6001600160601b03191660298201528151808203601d018152603d90910182528051908301206000908152600490925290205460ff1690565b60006122e68261164b565b8015611420575060006122f883611b7b565b6001600160a01b03161492915050565b612310612986565b600061231b85611782565b90506123268161164b565b61232f57600080fd5b61233c8160008686612e13565b6120ba848285856139e9565b7f5f86f226cd489cc09187d5f5e0adfb94308af0d4ceac482dd8a8adea9d80daf460005260026020527fab9e97adef29adb9492a44df89badb4a706f8f35202918df21ca61ed056c4868546001600160a01b031690565b7f811bbb11e8899da471f0e69a3ed55090fc90215227fc5fb1cb0d6e962ea7b74f60005260026020527fb4ed64697d3ef8518241966f7c6f28b0d72f20f51198717d198d2d55076c593d546001600160a01b031690565b6123fe613936565b806001600160a01b03811661241257600080fd5b61241b836122db565b61242457600080fd5b600061242f84613ef6565b90506000811161243e57600080fd5b600061244985611cb1565b90506000811161245857600080fd5b80821115612464578091505b61247685612470611a88565b84613f4a565b6040805160008082526020820190925260609161249891889088908790613fc8565b905060006124a7826001613b56565b9050611c6081888887614643565b7f779a349c5bee7817f04c960f525ee3e2f2516078c38c68a3149787976ee837e560005260026020527fc155b21a14c4592b97825e495fbe0d2705fb46420018cac5bfa7a09c43fae517546001600160a01b031690565b612514612986565b60006125248b8b8b8b8b8b61321c565b90506125338160008787612e13565b61257585828686868080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506139e992505050565b5050505050505050505050565b61145a848484846127fe565b61259661295f565b61259f8261164b565b6125a857600080fd5b8015806125ce57506125b982612122565b811180156125ce57506125cb826127a8565b81105b6125d757600080fd5b60408051670dac2f0a0cae4a8f60c31b60208083019190915260609490941b6001600160601b03191660288201528151808203601c018152603c909101825280519084012060009081529283905290912055565b600061142082611782565b7f269c5905f777ee6391c7a361d17039a7d62f52ba9fffeb98c5ade342705731a360005260026020527f15c764a0cd4bb3d72a49abedd3d6793c3b93c0d57f43174a348b443be86f79c1546001600160a01b031690565b61269561295f565b61269e8261164b565b6126a757600080fd5b6000811180156126be57506126bb826127a8565b81105b80156126d157506126ce826111c9565b81105b6126da57600080fd5b60408051670dad2dca0cae4a8f60c31b60208083019190915260609490941b6001600160601b03191660288201528151808203601c018152603c909101825280519084012060009081529283905290912055565b6040805172746f74616c457865637574656450657244617960681b60208083019190915260609490941b6001600160601b031916603382015260478082019390935281518082039093018352606701815281519183019190912060009081529182905290205490565b61279f61295f565b6112db81613817565b604080516919185a5b1e531a5b5a5d60b21b60208083019190915260609390931b6001600160601b031916602a8201528151808203601e018152603e909101825280519083012060009081529182905290205490565b612806613d6a565b1561281057600080fd5b6000846001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561285f57600080fd5b505afa158015612873573d6000803e3d6000fd5b505050506040513d602081101561288957600080fd5b5051905061289760016146b3565b6128ac6001600160a01b0386163330866146d7565b6128b660006146b3565b600061293b82876001600160a01b03166370a08231306040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561290957600080fd5b505afa15801561291d573d6000803e3d6000fd5b505050506040513d602081101561293357600080fd5b505190614731565b90508381111561294a57600080fd5b6129578633878487613d96565b505050505050565b612967611e64565b6001600160a01b0316336001600160a01b03161461298457600080fd5b565b600061299061239f565b9050336001600160a01b038216146129a757600080fd5b6129af611e0d565b6001600160a01b0316816001600160a01b031663d67bdd256040518163ffffffff1660e01b815260040160206040518083038186803b1580156129f157600080fd5b505afa158015612a05573d6000803e3d6000fd5b505050506040513d6020811015612a1b57600080fd5b50516001600160a01b0316146112db57600080fd5b604080516b36b2b9b9b0b3b2aa37b5b2b760a11b602080830191909152602c80830185905283518084039091018152604c90920183528151918101919091206000908152600290915220546001600160a01b0316919050565b604080516f1b595cdcd859d9549958da5c1a595b9d60821b602080830191909152603080830185905283518084039091018152605090920183528151918101919091206000908152600290915220546001600160a01b0316919050565b604080516b6d65737361676556616c756560a01b602080830191909152602c80830185905283518084039091018152604c90920183528151918101919091206000908152908190522054919050565b604080516b1b595cdcd859d9519a5e195960a21b602080830191909152602c8083019490945282518083039094018452604c9091018252825192810192909220600090815260049092529020805460ff19166001179055565b61167c6000612b9c85611b7b565b6001600160a01b03161484848485614773565b612bb8816147c7565b612bc157600080fd5b7f811bbb11e8899da471f0e69a3ed55090fc90215227fc5fb1cb0d6e962ea7b74f60005260026020527fb4ed64697d3ef8518241966f7c6f28b0d72f20f51198717d198d2d55076c593d80546001600160a01b0319166001600160a01b0392909216919091179055565b826001600160a01b03166340c10f1983836040518363ffffffff1660e01b815260040180836001600160a01b0316815260200182815260200192505050602060405180830381600087803b158015612c8257600080fd5b505af1158015612c96573d6000803e3d6000fd5b505050506040513d6020811015612cac57600080fd5b505161167c57600080fd5b604080516f686f6d65546f6b656e4164647265737360801b6020808301919091526001600160601b0319606086811b82166030850152845160248186030181526044850186528051908401206000908152600280855286822080546001600160a01b03808b166001600160a01b0319928316811790935572666f726569676e546f6b656e4164647265737360681b60648a0152948a901b90951660778801528751606b818903018152608b909701808952875197870197909720835294529485208054909216908716908117909155909290917f78d063210f4fb6b4cc932390bb8045fa2465e51349590182dab8b9e84c57a6ee9190a35050565b600082820183811015612e0c576040805162461bcd60e51b815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b9392505050565b612e1b613d6a565b15612e2557600080fd5b612e2f848261184a565b612e3857600080fd5b612e4a84612e44611a88565b83614803565b806000612e7a7f03be2b2875cb41e0e77355e802a16769bb8dfcf825061cde185c73bf94f1262586838986614884565b90506000612e86614c2f565b90508115612edb5760408051838152905182916001600160a01b038a16917fd560a522f77cfb4924d6fe51be1615e540a48a8931c48fe0349c7f47ebabe7479181900360200190a3612ed88383614731565b92505b612ee88688878688614773565b80856001600160a01b0316886001600160a01b03167f9afd47907e25028cdaca89d193518c302bbb128617d5a992c5abd45815526593866040518082815260200191505060405180910390a450505050505050565b612f46816147c7565b612f4f57600080fd5b7f269c5905f777ee6391c7a361d17039a7d62f52ba9fffeb98c5ade342705731a360005260026020527f15c764a0cd4bb3d72a49abedd3d6793c3b93c0d57f43174a348b443be86f79c180546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b0381161580612fd35750612fd3816147c7565b612fdc57600080fd5b7f5f86f226cd489cc09187d5f5e0adfb94308af0d4ceac482dd8a8adea9d80daf460005260026020527fab9e97adef29adb9492a44df89badb4a706f8f35202918df21ca61ed056c486880546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b03811615806130605750613060816147c7565b61306957600080fd5b7f5f5bc4e0b888be22a35f2166061a04607296c26861006b9b8e089a172696a82260005260026020527f60072fd9ffad01d76b1d1421ce17a3613dc06795e4b113745995ad1d84a5212180546001600160a01b0319166001600160a01b0392909216919091179055565b6001600160a01b03811615806130ed57506130ed816147c7565b6130f657600080fd5b7f779a349c5bee7817f04c960f525ee3e2f2516078c38c68a3149787976ee837e560005260026020527fc155b21a14c4592b97825e495fbe0d2705fb46420018cac5bfa7a09c43fae51780546001600160a01b0319166001600160a01b0392909216919091179055565b604080516861636b4465706c6f7960b81b6020808301919091526001600160601b0319606085901b1660298301528251601d818403018152603d909201835281519181019190912060009081526004909152205460ff166112db57604080516861636b4465706c6f7960b81b6020808301919091526001600160601b0319606085901b1660298301528251601d818403018152603d90920183528151918101919091206000908152600490915220805460ff1916600117905550565b60008061322888611782565b90506001600160a01b0381166134cf57606087878080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525050604080516020601f8b01819004810282018101909252898152939450606093925089915088908190840183828082843760009201919091525050845192935050501515806132bd575060008151115b6132c657600080fd5b81516132d4578091506132dd565b80516132dd5750805b6132e682614ca2565b91506132f0612636565b6001600160a01b031663a39d6acf83838861330961239f565b6001600160a01b0316631544298e6040518163ffffffff1660e01b815260040160206040518083038186803b15801561334157600080fd5b505afa158015613355573d6000803e3d6000fd5b505050506040513d602081101561336b57600080fd5b50516040516001600160e01b031960e087901b16815260ff831660448201526064810182905260806004820190815285516084830152855190918291602482019160a40190602089019080838360005b838110156133d35781810151838201526020016133bb565b50505050905090810190601f1680156134005780820380516001836020036101000a031916815260200191505b50838103825286518152865160209182019188019080838360005b8381101561343357818101518382015260200161341b565b50505050905090810190601f1680156134605780820380516001836020036101000a031916815260200191505b509650505050505050602060405180830381600087803b15801561348357600080fd5b505af1158015613497573d6000803e3d6000fd5b505050506040513d60208110156134ad57600080fd5b505192506134bb8a84612cb7565b6134c8838660ff16614d57565b505061355f565b6134d88161164b565b61355f578260ff16816001600160a01b031663313ce5676040518163ffffffff1660e01b815260040160206040518083038186803b15801561351957600080fd5b505afa15801561352d573d6000803e3d6000fd5b505050506040513d602081101561354357600080fd5b505160ff161461355257600080fd5b61355f818460ff16614d57565b979650505050505050565b7f98aa806e31e94a687a31c65769cb99670064dd7f5a87526da075c5fb4eab988060005260026020527f0c1206883be66049a02d4937078367c00b3d71dd1a9465df969363c6ddeac96d80546001600160a01b0319166001600160a01b0392909216919091179055565b6040810151158015906135ee575060408101516020820151115b80156135fe575060208101518151115b61360757600080fd5b8051604080516919185a5b1e531a5b5a5d60b21b602082810191909152606086901b6001600160601b031916602a83018190528351808403601e018152603e8401855280519083012060009081528083528481209590955581860151670dac2f0a0cae4a8f60c31b605e850152606684018290528451605a818603018152607a8501865280519084012086528583528486205583860151670dad2dca0cae4a8f60c31b609a85015260a28401919091528351609681850301815260b690930184528251928201929092208452839052908220556001600160a01b038316907fca0b3dabefdbd8c72c0a9cf4a6e9d107da897abf036ef3f3f3b010cdd25941599083905b60200201516040518082815260200191505060405180910390a25050565b805160208201511061373957600080fd5b80516040805172195e1958dd5d1a5bdb91185a5b1e531a5b5a5d606a1b602082810191909152606086901b6001600160601b031916603383018190528351808403602701815260478401855280519083012060009081528083528481209590955581860151700caf0cac6eae8d2dedc9ac2f0a0cae4a8f607b1b606785015260788401919091528351606c818503018152608c90930184528251928201929092208452839052908220556001600160a01b038316907f4c177b42dbe934b3abbc0208c11a42e46589983431616f1710ab19969c5ed62e90839061370a565b6001600160a01b03811661382a57600080fd5b7f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0613853611e64565b604080516001600160a01b03928316815291841660208301528051918290030190a17f02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c060005260026020527fb7802e97e87ef2842a6cce7da7ffaeaedaa2f61a6a7870b23d9d01fc9b73712e80546001600160a01b0319166001600160a01b0392909216919091179055565b7f0a6f646cd611241d8073675e00d1a1ff700fbf1b53fcf473de56d1e6e4b714ba60005260046020527f078d888f9b66f3f8bfa10909e31f1e16240db73449f0500afdbbe3a70da457cc805460ff19166001179055565b306001600160a01b0316636fde82026040518163ffffffff1660e01b815260040160206040518083038186803b15801561396f57600080fd5b505afa158015613983573d6000803e3d6000fd5b505050506040513d602081101561399957600080fd5b50516001600160a01b0316331461298457600080fd5b806001600160a01b0381166139c357600080fd5b6001600160a01b0383166139df576139da82614edb565b61167c565b61167c8383614ee6565b6139f2846147c7565b1561145a57836001600160a01b031663db7af85460e01b84848460405160240180846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015613a61578181015183820152602001613a49565b50505050905090810190601f168015613a8e5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529181526020820180516001600160e01b03166001600160e01b031990991698909817885251815191979096508695509350915081905083835b60208310613af45780518252601f199092019160209182019101613ad5565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114611c60576040519150601f19603f3d011682016040523d82523d6000602084013e611c60565b600080613b61611e0d565b90506000613b6e85614f73565b90506000613b7a61239f565b905084613c7357806001600160a01b03166394643f718488856040518463ffffffff1660e01b815260040180846001600160a01b0316815260200180602001838152602001828103825284818151815260200191508051906020019080838360005b83811015613bf4578181015183820152602001613bdc565b50505050905090810190601f168015613c215780820380516001836020036101000a031916815260200191505b50945050505050602060405180830381600087803b158015613c4257600080fd5b505af1158015613c56573d6000803e3d6000fd5b505050506040513d6020811015613c6c57600080fd5b5051611d01565b806001600160a01b031663dc8601b38488856040518463ffffffff1660e01b815260040180846001600160a01b0316815260200180602001838152602001828103825284818151815260200191508051906020019080838360005b83811015613ce6578181015183820152602001613cce565b50505050905090810190601f168015613d135780820380516001836020036101000a031916815260200191505b50945050505050602060405180830381600087803b158015613d3457600080fd5b505af1158015613d48573d6000803e3d6000fd5b505050506040513d6020811015613d5e57600080fd5b50519695505050505050565b7f6168652c307c1e813ca11cfb3a601f1cf3b22452021a5052d8b05f1f1f8a3e925490565b6014015190565b6001600160a01b03831615801590613dc75750613db1611e0d565b6001600160a01b0316836001600160a01b031614155b613dd057600080fd5b613dd98561164b565b613df9576000613de8866150a7565b9050613df7868260ff16614d57565b505b613e0385836113b3565b613e0c57600080fd5b613e1885612470611a88565b6000613e2386611b7b565b90506000613e5e7f741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee266001600160a01b03841615888a88614884565b90506000613e6c8583614731565b90506060613e7d848a898589613fc8565b90506000613e9582613e908c8c8c615275565b613b56565b9050613ea3818b8b86614643565b8315613eea5760408051858152905182916001600160a01b038d16917fd560a522f77cfb4924d6fe51be1615e540a48a8931c48fe0349c7f47ebabe7479181900360200190a35b50505050505050505050565b6000611420613f04836114bd565b604080516370a0823160e01b815230600482015290516001600160a01b038616916370a08231916024808301926020929190829003018186803b15801561290957600080fd5b613f58816113cb858561220e565b600080858560405160200180806f746f74616c5370656e7450657244617960801b815250601001836001600160a01b031660601b81526014018281526020019250505060405160208183030381529060405280519060200120815260200190815260200160002081905550505050565b606060008083511180613fed57506000356001600160e01b03191663d740548160e01b145b90506001600160a01b0387166144a8576140138661400e866113cb8a6114bd565b615324565b61401c86612282565b15614159578061407a57604080516001600160a01b0380891660248301528716604482015260648082018790528251808303909101815260849091019091526020810180516001600160e01b031663125e4cfb60e01b179052614151565b63c534576160e01b8686868660405160240180856001600160a01b03168152602001846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156140ea5781810151838201526020016140d2565b50505050905090810190601f1680156141175780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909916989098179097525050505050505b91505061463a565b6000614164876150a7565b905060606141718861537f565b9050606061417e89615545565b9050600082511180614191575060008151115b61419a57600080fd5b836142ed57632ae87cdd60e01b898383868c8c60405160240180876001600160a01b0316815260200180602001806020018660ff168152602001856001600160a01b03168152602001848152602001838103835288818151815260200191508051906020019080838360005b8381101561421e578181015183820152602001614206565b50505050905090810190601f16801561424b5780820380516001836020036101000a031916815260200191505b50838103825287518152875160209182019189019080838360005b8381101561427e578181015183820152602001614266565b50505050905090810190601f1680156142ab5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909c169b909b17909a525061449d975050505050505050565b63d522cfd760e01b898383868c8c8c60405160240180886001600160a01b0316815260200180602001806020018760ff168152602001866001600160a01b031681526020018581526020018060200184810384528a818151815260200191508051906020019080838360005b83811015614371578181015183820152602001614359565b50505050905090810190601f16801561439e5780820380516001836020036101000a031916815260200191505b5084810383528951815289516020918201918b019080838360005b838110156143d15781810151838201526020016143b9565b50505050905090810190601f1680156143fe5780820380516001836020036101000a031916815260200191505b50848103825285518152855160209182019187019080838360005b83811015614431578181015183820152602001614419565b50505050905090810190601f16801561445e5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909e169d909d17909c5250505050505050505050505b94505050505061463a565b856001600160a01b03166342966c68856040518263ffffffff1660e01b815260040180828152602001915050600060405180830381600087803b1580156144ee57600080fd5b505af1158015614502573d6000803e3d6000fd5b505050508061455f57604080516001600160a01b03808a1660248301528716604482015260648082018790528251808303909101815260849091019091526020810180516001600160e01b031663272255bb60e01b179052614636565b63867f7a4d60e01b8786868660405160240180856001600160a01b03168152602001846001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b838110156145cf5781810151838201526020016145b7565b50505050905090810190601f1680156145fc5780820380516001836020036101000a031916815260200191505b5060408051601f198184030181529190526020810180516001600160e01b03166001600160e01b0319909916989098179097525050505050505b9150505b95945050505050565b61464d8484615678565b61465784836156e4565b6146618482615754565b83826001600160a01b0316846001600160a01b03167f59a9a8027b9c87b961e254899821c9a276b5efc35d1f7409ea4f291470f1629a846040518082815260200191505060405180910390a450505050565b7f6168652c307c1e813ca11cfb3a601f1cf3b22452021a5052d8b05f1f1f8a3e9255565b604080516001600160a01b0380861660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b17905261145a9085906157a3565b6000612e0c83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250615854565b84156147a95761478d6001600160a01b03851684846158eb565b6147a48461400e8361479e886114bd565b90614731565b6120ba565b6120ba83836147b78761593d565b6001600160a01b03169190612c2b565b6000813f7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708181148015906147fb57508115155b949350505050565b614811816113cb858561272e565b6000808585604051602001808072746f74616c457865637574656450657244617960681b815250601301836001600160a01b031660601b81526014018281526020019250505060405160208183030381529060405280519060200120815260200190815260200160002081905550505050565b60008061488f6124b5565b90506001600160a01b03811615614c22577f741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee26871480156149445750806001600160a01b031663071664c5866040518263ffffffff1660e01b815260040180826001600160a01b0316815260200191505060206040518083038186803b15801561491757600080fd5b505afa15801561492b573d6000803e3d6000fd5b505050506040513d602081101561494157600080fd5b50515b1561495357600091505061463a565b6000816001600160a01b031663710c60138987876040518463ffffffff1660e01b815260040180848152602001836001600160a01b03168152602001828152602001935050505060206040518083038186803b1580156149b257600080fd5b505afa1580156149c6573d6000803e3d6000fd5b505050506040513d60208110156149dc57600080fd5b505190508015614c19577f741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee26881415614a2757614a226001600160a01b03861683836158eb565b614bb1565b600087614a3b576340c10f1960e01b614a44565b63a9059cbb60e01b5b604080516001600160a01b038681166024830152604480830187905283518084039091018152606490920183526020820180516001600160e01b03166001600160e01b0319861617815292518251949550600094606094928c16939282918083835b60208310614ac55780518252601f199092019160209182019101614aa6565b6001836020036101000a0380198251168184511680821785525050505050509050019150506000604051808303816000865af19150503d8060008114614b27576040519150601f19603f3d011682016040523d82523d6000602084013e614b2c565b606091505b509150915081614b83576040805185815290516001600160a01b038a16917fb8842ee9d1603ef0f5620c01feb6cf2e7921091eba728cbce562041a86ee109a919081900360200190a260009550505050505061463a565b80511580614ba45750808060200190516020811015614ba157600080fd5b50515b614bad57600080fd5b5050505b816001600160a01b0316634e281a7b866040518263ffffffff1660e01b815260040180826001600160a01b03168152602001915050600060405180830381600087803b158015614c0057600080fd5b505af1158015614c14573d6000803e3d6000fd5b505050505b915061463a9050565b5060009695505050505050565b6000614c3961239f565b6001600160a01b031663669f618b6040518163ffffffff1660e01b815260040160206040518083038186803b158015614c7157600080fd5b505afa158015614c85573d6000803e3d6000fd5b505050506040513d6020811015614c9b57600080fd5b5051905090565b606080827f2066726f6d204d61696e6e6574000000000000000000000000000000000000006040516020018083805190602001908083835b60208310614cf95780518252601f199092019160209182019101614cda565b51815160209384036101000a60001901801990921691161790529201938452506040805180850381529390910190525093517f000000000000000000000000000000000000000000000000000000000000000d018452509192915050565b60006012821015614e4b5781601203600a0a90506000614d8182614d7b6000612122565b90615983565b90506000614d9383614d7b60006111c9565b90506000614da584614d7b60006127a8565b90506000614db785614d7b6000611460565b90506000614dc986614d7b6000611a91565b905084614dff5760019450848411614dff5760649350606491508383111580614df25750818111155b15614dff57506127109150815b614e23886040518060600160405280868152602001878152602001888152506135d4565b614e4188604051806040016040528084815260200185815250613728565b505050505061167c565b60128203600a0a9050614ea4836040518060600160405280614e7785614e7160006127a8565b906159c5565b8152602001614e8a85614e7160006111c9565b8152602001614e9d85614e716000612122565b90526135d4565b61167c836040518060400160405280614ec185614e716000611a91565b8152602001614ed485614e716000611460565b9052613728565b476111268282615a1e565b604080516370a0823160e01b8152306004820152905183916000916001600160a01b038416916370a08231916024808301926020929190829003018186803b158015614f3157600080fd5b505afa158015614f45573d6000803e3d6000fd5b505050506040513d6020811015614f5b57600080fd5b5051905061145a6001600160a01b03831684836158eb565b600080614f7e6120cb565b90506001600160a01b03811661500357614f9661239f565b6001600160a01b031663e5789d036040518163ffffffff1660e01b815260040160206040518083038186803b158015614fce57600080fd5b505afa158015614fe2573d6000803e3d6000fd5b505050506040513d6020811015614ff857600080fd5b5051915061121c9050565b60405163fb47201960e01b81526020600482018181528551602484015285516001600160a01b0385169363fb4720199388939283926044019185019080838360005b8381101561505d578181015183820152602001615045565b50505050905090810190601f16801561508a5780820380516001836020036101000a031916815260200191505b509250505060206040518083038186803b158015614fce57600080fd5b60408051600481526024810182526020810180516001600160e01b031663313ce56760e01b1781529151815160009384936060936001600160a01b03881693919290918291908083835b602083106151105780518252601f1990920191602091820191016150f1565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615170576040519150601f19603f3d011682016040523d82523d6000602084013e615175565b606091505b5091509150816152565760408051600481526024810182526020810180516001600160e01b0316632e0f262560e01b178152915181516001600160a01b0388169382918083835b602083106151db5780518252601f1990920191602091820191016151bc565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461523b576040519150601f19603f3d011682016040523d82523d6000602084013e615240565b606091505b509092509050816152565760009250505061121c565b80806020019051602081101561526b57600080fd5b5051949350505050565b600080615280612348565b90506001600160a01b038116158061463a57506040805163f7baa04960e01b81526001600160a01b03878116600483015286811660248301528581166044830152915160009284169163f7baa049916064808301926020929190829003018186803b1580156152ee57600080fd5b505afa158015615302573d6000803e3d6000fd5b505050506040513d602081101561531857600080fd5b50511395945050505050565b604080516e6d65646961746f7242616c616e636560881b60208083019190915260609490941b6001600160601b031916602f820152815180820360230181526043909101825280519084012060009081529283905290912055565b60408051600481526024810182526020810180516001600160e01b03166306fdde0360e01b1781529151815160609360009385936001600160a01b03881693919290918291908083835b602083106153e85780518252601f1990920191602091820191016153c9565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615448576040519150601f19603f3d011682016040523d82523d6000602084013e61544d565b606091505b50915091508161553c5760408051600481526024810182526020810180516001600160e01b03166351fa6fbf60e11b178152915181516001600160a01b0388169382918083835b602083106154b35780518252601f199092019160209182019101615494565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d8060008114615513576040519150601f19603f3d011682016040523d82523d6000602084013e615518565b606091505b5090925090508161553c57604051806020016040528060008152509250505061121c565b6147fb81615a83565b60408051600481526024810182526020810180516001600160e01b03166395d89b4160e01b1781529151815160609360009385936001600160a01b03881693919290918291908083835b602083106155ae5780518252601f19909201916020918201910161558f565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d806000811461560e576040519150601f19603f3d011682016040523d82523d6000602084013e615613565b606091505b50915091508161553c5760408051600481526024810182526020810180516001600160e01b0316631eedf1af60e31b178152915181516001600160a01b038816938291808383602083106154b35780518252601f199092019160209182019101615494565b604080516b36b2b9b9b0b3b2aa37b5b2b760a11b602080830191909152602c8083019590955282518083039095018552604c90910182528351938101939093206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b604080516f1b595cdcd859d9549958da5c1a595b9d60821b60208083019190915260308083019590955282518083039095018552605090910182528351938101939093206000908152600290935290912080546001600160a01b0319166001600160a01b03909216919091179055565b604080516b6d65737361676556616c756560a01b602080830191909152602c8083019590955282518083039095018552604c909101825283519381019390932060009081529283905290912055565b60606157f8826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b0316615bdf9092919063ffffffff16565b80519091501561167c5780806020019051602081101561581757600080fd5b505161167c5760405162461bcd60e51b815260040180806020018281038252602a815260200180615e20602a913960400191505060405180910390fd5b600081848411156158e35760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b838110156158a8578181015183820152602001615890565b50505050905090810190601f1680156158d55780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b17905261167c9084906157a3565b60006001600160a01b03821673b7d311e2eb55f2f68a9440da38e7989210b9a05e141561597f575073857dd07866c1e19eb2cdfcef7ae655ce7f9e560d61121c565b5090565b6000612e0c83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250615bee565b6000826159d457506000611420565b828202828482816159e157fe5b0414612e0c5760405162461bcd60e51b8152600401808060200182810382526021815260200180615dff6021913960400191505060405180910390fd5b6040516001600160a01b0383169082156108fc029083906000818181858888f19350505050611126578082604051615a5590615dc0565b6001600160a01b039091168152604051908190036020019082f090508015801561145a573d6000803e3d6000fd5b6060602082511115615b5957818060200190516020811015615aa457600080fd5b8101908080516040519392919084600160201b821115615ac357600080fd5b908301906020820185811115615ad857600080fd5b8251600160201b811182820188101715615af157600080fd5b82525081516020918201929091019080838360005b83811015615b1e578181015183820152602001615b06565b50505050905090810190601f168015615b4b5780820380516001836020036101000a031916815260200191505b50604052505050905061121c565b815160201415615bca576000828060200190516020811015615b7a57600080fd5b50516040805160208082528183019092529192506060919060208201818036833701905050905060008260208301525b8215615bbf5760089290921b91600101615baa565b8152915061121c9050565b5060408051602081019091526000815261121c565b60606147fb8484600085615c53565b60008183615c3d5760405162461bcd60e51b81526020600482018181528351602484015283519092839260449091019190850190808383600083156158a8578181015183820152602001615890565b506000838581615c4957fe5b0495945050505050565b6060615c5e856147c7565b615caf576040805162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015290519081900360640190fd5b60006060866001600160a01b031685876040518082805190602001908083835b60208310615cee5780518252601f199092019160209182019101615ccf565b6001836020036101000a03801982511681845116808217855250505050505090500191505060006040518083038185875af1925050503d8060008114615d50576040519150601f19603f3d011682016040523d82523d6000602084013e615d55565b606091505b50915091508115615d695791506147fb9050565b805115615d795780518082602001fd5b60405162461bcd60e51b81526020600482018181528651602484015286518793919283926044019190850190808383600083156158a8578181015183820152602001615890565b603280615dcd8339019056fe60806040526040516032380380603283398181016040526020811015602357600080fd5b50516001600160a01b038116fffe536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f775361666545524332303a204552433230206f7065726174696f6e20646964206e6f742073756363656564a264697066735822122016ad8a7dfc627988e4caf299120e2273aae2be383102b8f744ab97c4389ae59364736f6c63430007050033", "linkReferences": {}, diff --git a/operate/data/contracts/home_omnibridge/contract.yaml b/operate/data/contracts/home_omnibridge/contract.yaml index 6c59b1964..027ad6399 100644 --- a/operate/data/contracts/home_omnibridge/contract.yaml +++ b/operate/data/contracts/home_omnibridge/contract.yaml @@ -7,7 +7,7 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeigrlrmwk3dhnoe4o5ovygtmudik7ybhsx2etqqjpvdi65rtlfpuhe - build/HomeOmnibridge.json: bafybeihmey7vxdyvviuzifiyvrsfjn35j2sbub7njvgann5juqzca5x4zy + build/HomeOmnibridge.json: bafybeihm4ff4mhoca2iwcogg5srqe2x6p674uxxc3v5dcci4emxfgc7qpm contract.py: bafybeigj6morbr2bkuqtzvrunrsgefnpvegrlycxafjmb7quo42wvltmhi fingerprint_ignore_patterns: [] contracts: [] From d8e6ac52485430097ec25335d5f474d072dfc17c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 13:37:24 +0200 Subject: [PATCH 128/173] chore: fix test --- tests/test_bridge_providers.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 4df9f38c6..81ef9da04 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -44,8 +44,10 @@ QuoteData, ) from operate.bridge.providers.native_bridge_provider import ( + BRIDGE_DATA, + BridgeContractAdaptor, NativeBridgeProvider, - OPTIMISM_BRIDGE_DATA, + OmnibridgeContractAdaptor, OptimismContractAdaptor, ) from operate.cli import OperateApp @@ -545,7 +547,7 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], + bridge_eta=BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], elapsed_time=0, message=MESSAGE_QUOTE_ZERO, provider_data=None, @@ -689,7 +691,7 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=OPTIMISM_BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], + bridge_eta=BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], elapsed_time=0, message=None, provider_data=None, @@ -805,6 +807,7 @@ class TestBridgeProvider: @pytest.mark.parametrize( ( "bridge_provider_class", + "contract_adaptor_class", "params", "request_id", "from_tx_hash", @@ -813,9 +816,11 @@ class TestBridgeProvider: "expected_elapsed_time", ), [ + # NativeBridgeProvider (Omnibridge) - EXECUTION_DONE tests # LiFiBridgeProvider - EXECUTION_DONE tests ( LiFiBridgeProvider, + None, { "from": { "chain": "gnosis", @@ -835,9 +840,10 @@ class TestBridgeProvider: "0x6cd9176f1da953e4464adb8bdc81fbe4133ebcd1bb6aeac49946a38ff025e623", 374, ), - # NativeBridgeProvider - EXECUTION_DONE tests + # NativeBridgeProvider (Optimism bridge) - EXECUTION_DONE tests ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -859,6 +865,7 @@ class TestBridgeProvider: ), ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -880,6 +887,7 @@ class TestBridgeProvider: ), ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -901,6 +909,7 @@ class TestBridgeProvider: ), ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -920,9 +929,10 @@ class TestBridgeProvider: "0xf4ccb5f6547c188e638ac3d84f80158e3d7462211e15bc3657f8585b0bbffb68", 186, ), - # NativeBridgeProvider - EXECUTION_FAILED tests + # NativeBridgeProvider (Optimism bridge) - EXECUTION_FAILED tests ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -944,6 +954,7 @@ class TestBridgeProvider: ), ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -965,6 +976,7 @@ class TestBridgeProvider: ), ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -986,6 +998,7 @@ class TestBridgeProvider: ), ( NativeBridgeProvider, + OptimismContractAdaptor, { "from": { "chain": "ethereum", @@ -1012,6 +1025,7 @@ def test_update_execution_status( tmp_path: Path, password: str, bridge_provider_class: t.Type[BridgeProvider], + contract_adaptor_class: t.Optional[t.Type[BridgeContractAdaptor]], params: dict, request_id: str, from_tx_hash: str, @@ -1030,14 +1044,16 @@ def test_update_execution_status( operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - if bridge_provider_class == NativeBridgeProvider: + if contract_adaptor_class is not None: bridge: BridgeProvider = NativeBridgeProvider( provider_id="NativeBridgeProvider", - bridge_contract_adaptor=OptimismContractAdaptor(), + bridge_contract_adaptor=contract_adaptor_class(), wallet_manager=operate.wallet_manager, ) else: - bridge = bridge_provider_class(provider_id="", wallet_manager=operate.wallet_manager) + bridge = bridge_provider_class( + provider_id="", wallet_manager=operate.wallet_manager + ) quote_data = QuoteData( bridge_eta=0, From 631917783501f5c04bf35594e7482b2fb27816d5 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 14:01:21 +0200 Subject: [PATCH 129/173] fix: linters --- tests/test_bridge_providers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 81ef9da04..5ea62e7c6 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -47,7 +47,6 @@ BRIDGE_DATA, BridgeContractAdaptor, NativeBridgeProvider, - OmnibridgeContractAdaptor, OptimismContractAdaptor, ) from operate.cli import OperateApp From 3ec99101f06fe09ce41683ce8ccb65e20294adf3 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 16:08:13 +0200 Subject: [PATCH 130/173] fix: sanitize wrong addresses --- operate/bridge/providers/bridge_provider.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index e32ff903e..9125f6c05 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -32,6 +32,7 @@ from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger from autonomy.chain.tx import TxSettler +from web3 import Web3 from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, @@ -202,7 +203,12 @@ def create_request(self, params: t.Dict) -> BridgeRequest: if not self.can_handle_request(params): raise ValueError("Invalid input: Cannot process bridge request.") + w3 = Web3() params = copy.deepcopy(params) + params["from"]["address"] = w3.to_checksum_address(params["from"]["address"]) + params["from"]["token"] = w3.to_checksum_address(params["from"]["token"]) + params["to"]["address"] = w3.to_checksum_address(params["to"]["address"]) + params["to"]["token"] = w3.to_checksum_address(params["to"]["token"]) params["to"]["amount"] = int(params["to"]["amount"]) return BridgeRequest( From 951f6f3d131ad2965454dbe2a1bb6cd21157d42c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 17:44:51 +0200 Subject: [PATCH 131/173] chore: omnibridge get event --- .../providers/native_bridge_provider.py | 70 ++++++++++++------- .../contracts/foreign_omnibridge/contract.py | 63 ++++++++--------- .../foreign_omnibridge/contract.yaml | 2 +- 3 files changed, 77 insertions(+), 58 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 7d3c443fe..a1826ea77 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -88,14 +88,14 @@ class BridgeContractAdaptor(ABC): @abstractmethod def build_bridge_native_tx( - self, ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge native asset transaction.""" raise NotImplementedError() @abstractmethod def build_bridge_erc20_tx( - self, ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge ERC20 asset transaction.""" raise NotImplementedError() @@ -103,7 +103,8 @@ def build_bridge_erc20_tx( @abstractmethod def find_native_bridge_finalized_tx( self, - ledger_api: LedgerApi, + from_ledger_api: LedgerApi, + to_ledger_api: LedgerApi, bridge_request: BridgeRequest, from_block: BlockIdentifier, to_block: BlockIdentifier, @@ -114,7 +115,8 @@ def find_native_bridge_finalized_tx( @abstractmethod def find_erc20_bridge_finalized_tx( self, - ledger_api: LedgerApi, + from_ledger_api: LedgerApi, + to_ledger_api: LedgerApi, bridge_request: BridgeRequest, from_block: BlockIdentifier, to_block: BlockIdentifier, @@ -140,7 +142,7 @@ class OptimismContractAdaptor(BridgeContractAdaptor): ) def build_bridge_native_tx( - self, ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge native asset transaction.""" from_chain = bridge_request.params["from"]["chain"] @@ -151,7 +153,7 @@ def build_bridge_native_tx( from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] extra_data = Web3.keccak(text=bridge_request.id) return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( - ledger_api=ledger_api, + ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, to=to_address, @@ -161,7 +163,7 @@ def build_bridge_native_tx( ) def build_bridge_erc20_tx( - self, ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge ERC20 asset transaction.""" from_chain = bridge_request.params["from"]["chain"] @@ -174,7 +176,7 @@ def build_bridge_erc20_tx( from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] extra_data = Web3.keccak(text=bridge_request.id) return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx( - ledger_api=ledger_api, + ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, local_token=from_token, @@ -187,7 +189,8 @@ def build_bridge_erc20_tx( def find_native_bridge_finalized_tx( self, - ledger_api: LedgerApi, + from_ledger_api: LedgerApi, + to_ledger_api: LedgerApi, bridge_request: BridgeRequest, from_block: BlockIdentifier, to_block: BlockIdentifier, @@ -201,7 +204,7 @@ def find_native_bridge_finalized_tx( to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] extra_data = Web3.keccak(text=bridge_request.id) return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( - ledger_api=ledger_api, + ledger_api=to_ledger_api, contract_address=to_bridge, from_=from_address, to=to_address, @@ -213,7 +216,8 @@ def find_native_bridge_finalized_tx( def find_erc20_bridge_finalized_tx( self, - ledger_api: LedgerApi, + from_ledger_api: LedgerApi, + to_ledger_api: LedgerApi, bridge_request: BridgeRequest, from_block: BlockIdentifier, to_block: BlockIdentifier, @@ -229,7 +233,7 @@ def find_erc20_bridge_finalized_tx( to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] extra_data = Web3.keccak(text=bridge_request.id) return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( - ledger_api=ledger_api, + ledger_api=to_ledger_api, contract_address=to_bridge, local_token=to_token, remote_token=from_token, @@ -260,48 +264,62 @@ class OmnibridgeContractAdaptor(BridgeContractAdaptor): ) def build_bridge_native_tx( - self, ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge native asset transaction.""" - raise NotImplementedError("NotImplementedError") + raise NotImplementedError(f"{self.__class__.__name__} does not support this method.") def build_bridge_erc20_tx( - self, ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge ERC20 asset transaction.""" raise NotImplementedError("NotImplementedError") def find_native_bridge_finalized_tx( self, - ledger_api: LedgerApi, + from_ledger_api: LedgerApi, + to_ledger_api: LedgerApi, bridge_request: BridgeRequest, from_block: BlockIdentifier, to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating native bridge completion.""" - raise NotImplementedError("NotImplementedError") + raise NotImplementedError(f"{self.__class__.__name__} does not support this method.") def find_erc20_bridge_finalized_tx( self, - ledger_api: LedgerApi, + from_ledger_api: LedgerApi, + to_ledger_api: LedgerApi, bridge_request: BridgeRequest, from_block: BlockIdentifier, to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating ERC20 bridge completion.""" from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + from_tx_hash = bridge_request.execution_data.from_tx_hash to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] + from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] - message_id = bytes.fromhex( - "000500004AC82B41BD819DD871590B510316F2385CB196FB000000000002AA74" + message_id = self._foreign_omnibridge.get_tokens_bridging_initiated_message_id( + ledger_api=from_ledger_api, + contract_address=from_bridge, + tx_hash=from_tx_hash, + token=from_token, + sender=from_address, + value=to_amount, ) + if not message_id: + raise RuntimeError(f"Cannot find 'messageId' in transaction {from_tx_hash}.") + return self._home_omnibridge.find_tokens_bridged_tx( - ledger_api=ledger_api, + ledger_api=to_ledger_api, contract_address=to_bridge, token=to_token, recipient=to_address, @@ -418,11 +436,11 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: if from_token == ZERO_ADDRESS: bridge_tx = self.bridge_contract_adaptor.build_bridge_native_tx( - ledger_api=from_ledger_api, bridge_request=bridge_request + from_ledger_api=from_ledger_api, bridge_request=bridge_request ) else: bridge_tx = self.bridge_contract_adaptor.build_bridge_erc20_tx( - ledger_api=from_ledger_api, bridge_request=bridge_request + from_ledger_api=from_ledger_api, bridge_request=bridge_request ) BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) @@ -552,7 +570,8 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: if from_token == ZERO_ADDRESS: to_tx_hash = ( self.bridge_contract_adaptor.find_native_bridge_finalized_tx( - ledger_api=to_ledger_api, + from_ledger_api=from_ledger_api, + to_ledger_api=to_ledger_api, bridge_request=bridge_request, from_block=from_block, to_block=to_block, @@ -561,7 +580,8 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: else: to_tx_hash = ( self.bridge_contract_adaptor.find_erc20_bridge_finalized_tx( - ledger_api=to_ledger_api, + from_ledger_api=from_ledger_api, + to_ledger_api=to_ledger_api, bridge_request=bridge_request, from_block=from_block, to_block=to_block, diff --git a/operate/data/contracts/foreign_omnibridge/contract.py b/operate/data/contracts/foreign_omnibridge/contract.py index 6228dad6b..7bd2337db 100644 --- a/operate/data/contracts/foreign_omnibridge/contract.py +++ b/operate/data/contracts/foreign_omnibridge/contract.py @@ -20,6 +20,7 @@ """This module contains the class to connect to the `ForeignOmnibridge` contract.""" from math import ceil +from typing import Optional from aea.common import JSONLike from aea.configurations.base import PublicId @@ -41,38 +42,6 @@ class ForeignOmnibridge(Contract): contract_id = PublicId.from_str("valory/foreign_omnibridge:0.1.0") - # @classmethod - # def build_bridge_eth_to_tx( - # cls, - # ledger_api: LedgerApi, - # contract_address: str, - # sender: str, - # to: str, - # amount: int, - # min_gas_limit: int, - # extra_data: bytes, - # raise_on_try: bool = False, - # ) -> JSONLike: - # """Build bridgeETHTo tx.""" - # contract_instance = cls.get_instance( - # ledger_api=ledger_api, contract_address=contract_address - # ) - # tx = contract_instance.functions.bridgeETHTo( - # to, min_gas_limit, extra_data - # ).build_transaction( - # { - # "from": sender, - # "value": amount, - # "gas": DEFAULT_GAS_BRIDGE_ETH_TO, - # "gasPrice": ledger_api.api.eth.gas_price, - # "nonce": ledger_api.api.eth.get_transaction_count(sender), - # } - # ) - # return ledger_api.update_with_gas_estimate( - # transaction=tx, - # raise_on_try=raise_on_try, - # ) - @classmethod def build_relay_tokens_tx( cls, @@ -129,3 +98,33 @@ def build_relay_tokens_tx( tx["gas"] = DEFAULT_GAS_RELAY_TOKENS return tx + + @classmethod + def get_tokens_bridging_initiated_message_id( + cls, + ledger_api: LedgerApi, + contract_address: str, + tx_hash: str, + token: str, + sender: str, + value: int, + raise_on_try: bool = False, + ) -> Optional[bytes]: + """Get the 'messageId' for the matching 'TokensBridgingInitiated' within the transaction 'tx_hash'.""" + contract_instance = cls.get_instance( + ledger_api=ledger_api, contract_address=contract_address + ) + receipt = ledger_api.api.eth.get_transaction_receipt(tx_hash) + event = contract_instance.events.TokensBridgingInitiated() + events = event.process_receipt(receipt) + + for e in events: + args = e["args"] + if ( + args["token"].lower() == token.lower() + and args["sender"].lower() == sender.lower() + and int(args["value"]) == value + ): + return args["messageId"] + + return None diff --git a/operate/data/contracts/foreign_omnibridge/contract.yaml b/operate/data/contracts/foreign_omnibridge/contract.yaml index f3d294196..4e9eba2b4 100644 --- a/operate/data/contracts/foreign_omnibridge/contract.yaml +++ b/operate/data/contracts/foreign_omnibridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeibsmumov3s36vfo24xp2niilcp3ywju2d4yqfadllyjncqtgtndly build/ForeignOmnibridge.json: bafybeibmcflt7w5p5szgii7glbtrvjahweclowz2k7e6qjq7yfbvszy6em - contract.py: bafybeieldprlj4civ7bxsgt4xi5qhpqoj7z6tqeop23mqbd6ervr5szjda + contract.py: bafybeidjjdhjr2rj3q2q27fhm366kt4qpsvjqg3ytpa2titic5sari6r24 fingerprint_ignore_patterns: [] contracts: [] class_name: ForeignOmnibridge From 4afefdf60efab44579eb55a2c29b12bf6c4160c8 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 19:57:11 +0200 Subject: [PATCH 132/173] chore: update --- operate/bridge/bridge.py | 1 + operate/bridge/providers/bridge_provider.py | 2 + .../bridge/providers/lifi_bridge_provider.py | 7 +- .../providers/native_bridge_provider.py | 196 ++++++++++++------ 4 files changed, 142 insertions(+), 64 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index b15ab5e01..ae4e93783 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -194,6 +194,7 @@ def _get_updated_bundle( bridge_providers = [ # Sorted in order of preference self._bridge_providers["NativeBridgeProvider.Optimism"], + self._bridge_providers["NativeBridgeProvider.Omnibridge"], self._bridge_providers["LiFiBridgeProvider"], ] bridge_requests = [] diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 9125f6c05..a5cec9663 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -457,6 +457,7 @@ def status_json(self, bridge_request: BridgeRequest) -> t.Dict: explorer_link = self._get_explorer_link(tx_hash) return { + "eta": bridge_request.quote_data.bridge_eta, "explorer_link": explorer_link, "message": bridge_request.execution_data.message, "status": bridge_request.status.value, @@ -464,6 +465,7 @@ def status_json(self, bridge_request: BridgeRequest) -> t.Dict: } if bridge_request.quote_data: return { + "eta": bridge_request.quote_data.bridge_eta, "message": bridge_request.quote_data.message, "status": bridge_request.status.value, } diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 68b8040a1..c984239ec 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -43,6 +43,9 @@ from operate.operate_types import Chain +LIFI_DEFAULT_ETA = 5 * 60 + + class LiFiTransactionStatus(str, enum.Enum): """LI.FI transaction status.""" @@ -94,7 +97,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: if to_amount == 0: self.logger.info(f"[LI.FI BRIDGE] {MESSAGE_QUOTE_ZERO}") quote_data = QuoteData( - bridge_eta=None, + bridge_eta=0, elapsed_time=0, message=MESSAGE_QUOTE_ZERO, provider_data=None, @@ -126,7 +129,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: response.raise_for_status() response_json = response.json() quote_data = QuoteData( - bridge_eta=None, + bridge_eta=LIFI_DEFAULT_ETA, elapsed_time=time.time() - start, message=None, provider_data={ diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index a1826ea77..77b5aeb77 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -52,40 +52,45 @@ L1StandardBridge, ) from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge -from operate.ledger.profiles import ERC20_TOKENS +from operate.ledger.profiles import ERC20_TOKENS, OLAS, USDC, WRAPPED_NATIVE_ASSET from operate.operate_types import Chain from operate.wallet.master import MasterWalletManager BLOCK_CHUNK_SIZE = 5000 -BRIDGE_DATA: t.Dict[t.Any, t.Dict[str, t.Any]] = { - (Chain.ETHEREUM, Chain.BASE): { - "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", - "to_bridge": "0x4200000000000000000000000000000000000010", - "bridge_eta": 5 * 60, - }, - (Chain.ETHEREUM, Chain.MODE): { - "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21", - "to_bridge": "0x4200000000000000000000000000000000000010", - "bridge_eta": 5 * 60, - }, - (Chain.ETHEREUM, Chain.OPTIMISTIC): { - "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", - "to_bridge": "0x4200000000000000000000000000000000000010", - "bridge_eta": 5 * 60, - }, - (Chain.ETHEREUM, Chain.GNOSIS): { - "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671", - "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d", - "bridge_eta": 30 * 60, - }, -} - class BridgeContractAdaptor(ABC): """Adaptor class for bridge contract packages.""" + BRIDGE_PARAMS: t.Dict + + def can_handle_request(self, params: t.Dict) -> bool: + """Returns 'true' if the contract adaptor can handle a request for 'params'.""" + from_chain = Chain(params["from"]["chain"]) + from_token = params["from"]["token"] + to_chain = Chain(params["to"]["chain"]) + to_token = params["to"]["token"] + + if (from_chain, to_chain) not in self.BRIDGE_PARAMS: + return False + + bridge_params = self.BRIDGE_PARAMS[(from_chain, to_chain)] + + if from_token not in bridge_params["supported_from_tokens"]: + return False + + for token_map in ERC20_TOKENS: + if ( + from_chain in token_map + and to_chain in token_map + and token_map[from_chain].lower() == from_token.lower() + and token_map[to_chain].lower() == to_token.lower() + ): + return True + + return False + @abstractmethod def build_bridge_native_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest @@ -128,6 +133,42 @@ def find_erc20_bridge_finalized_tx( class OptimismContractAdaptor(BridgeContractAdaptor): """Adaptor class for Optimism contract packages.""" + BRIDGE_PARAMS: t.Dict[t.Any, t.Dict[str, t.Any]] = { + (Chain.ETHEREUM, Chain.BASE): { + "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 5 * 60, + "supported_from_tokens": ( + ZERO_ADDRESS, + WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], + OLAS[Chain.ETHEREUM], + USDC[Chain.ETHEREUM], + ), + }, + (Chain.ETHEREUM, Chain.MODE): { + "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 5 * 60, + "supported_from_tokens": ( + ZERO_ADDRESS, + WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], + OLAS[Chain.ETHEREUM], + USDC[Chain.ETHEREUM], + ), + }, + (Chain.ETHEREUM, Chain.OPTIMISTIC): { + "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 5 * 60, + "supported_from_tokens": ( + ZERO_ADDRESS, + WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], + OLAS[Chain.ETHEREUM], + USDC[Chain.ETHEREUM], + ), + }, + } + _l1_standard_bridge_contract = t.cast( L1StandardBridge, L1StandardBridge.from_dir( @@ -150,7 +191,9 @@ def build_bridge_native_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] + from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] extra_data = Web3.keccak(text=bridge_request.id) return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( ledger_api=from_ledger_api, @@ -173,7 +216,9 @@ def build_bridge_erc20_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] + from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] extra_data = Web3.keccak(text=bridge_request.id) return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx( ledger_api=from_ledger_api, @@ -201,7 +246,9 @@ def find_native_bridge_finalized_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] + to_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "to_bridge" + ] extra_data = Web3.keccak(text=bridge_request.id) return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( ledger_api=to_ledger_api, @@ -230,7 +277,9 @@ def find_erc20_bridge_finalized_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] + to_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "to_bridge" + ] extra_data = Web3.keccak(text=bridge_request.id) return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( ledger_api=to_ledger_api, @@ -249,6 +298,19 @@ def find_erc20_bridge_finalized_tx( class OmnibridgeContractAdaptor(BridgeContractAdaptor): """Adaptor class for Omnibridge contract packages.""" + BRIDGE_PARAMS: t.Dict[t.Any, t.Dict[str, t.Any]] = { + (Chain.ETHEREUM, Chain.GNOSIS): { + "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671", + "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d", + "bridge_eta": 30 * 60, + "supported_from_tokens": ( + WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], + OLAS[Chain.ETHEREUM], + USDC[Chain.ETHEREUM], + ), + }, + } + _foreign_omnibridge = t.cast( ForeignOmnibridge, ForeignOmnibridge.from_dir( @@ -267,13 +329,32 @@ def build_bridge_native_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge native asset transaction.""" - raise NotImplementedError(f"{self.__class__.__name__} does not support this method.") + raise NotImplementedError( + f"{self.__class__.__name__} does not support this method." + ) def build_bridge_erc20_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge ERC20 asset transaction.""" - raise NotImplementedError("NotImplementedError") + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + to_chain = bridge_request.params["to"]["chain"] + to_address = bridge_request.params["to"]["address"] + to_amount = bridge_request.params["to"]["amount"] + from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] + + return self._foreign_omnibridge.build_relay_tokens_tx( + ledger_api=from_ledger_api, + contract_address=from_bridge, + sender=from_address, + token=from_token, + receiver=to_address, + amount=to_amount, + ) def find_native_bridge_finalized_tx( self, @@ -284,7 +365,9 @@ def find_native_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating native bridge completion.""" - raise NotImplementedError(f"{self.__class__.__name__} does not support this method.") + raise NotImplementedError( + f"{self.__class__.__name__} does not support this method." + ) def find_erc20_bridge_finalized_tx( self, @@ -303,8 +386,12 @@ def find_erc20_bridge_finalized_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] - to_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["to_bridge"] + from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] + to_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "to_bridge" + ] message_id = self._foreign_omnibridge.get_tokens_bridging_initiated_message_id( ledger_api=from_ledger_api, @@ -316,7 +403,9 @@ def find_erc20_bridge_finalized_tx( ) if not message_id: - raise RuntimeError(f"Cannot find 'messageId' in transaction {from_tx_hash}.") + raise RuntimeError( + f"Cannot find 'messageId' in transaction {from_tx_hash}." + ) return self._home_omnibridge.find_tokens_bridged_tx( ledger_api=to_ledger_api, @@ -352,33 +441,10 @@ def can_handle_request(self, params: t.Dict) -> bool: if not super().can_handle_request(params): return False - from_chain = Chain(params["from"]["chain"]) - from_token = params["from"]["token"] - to_chain = Chain(params["to"]["chain"]) - to_token = params["to"]["token"] - - if (from_chain, to_chain) not in BRIDGE_DATA: - self.logger.warning( - f"[NATIVE BRIDGE] Unsupported bridge from {from_chain} to {to_chain}." - ) + if not self.bridge_contract_adaptor.can_handle_request(params): return False - if from_token == ZERO_ADDRESS and to_token == ZERO_ADDRESS: - return True - - for token_map in ERC20_TOKENS: - if ( - from_chain in token_map - and to_chain in token_map - and token_map[from_chain].lower() == from_token.lower() - and token_map[to_chain].lower() == to_token.lower() - ): - return True - - self.logger.warning( - f"[NATIVE BRIDGE] Unsupported token pair: {from_chain} {from_token} -> {to_chain} {to_token}" - ) - return False + return True def description(self) -> str: """Get a human-readable description of the bridge provider.""" @@ -405,7 +471,9 @@ def quote(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - bridge_eta = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["bridge_eta"] + bridge_eta = self.bridge_contract_adaptor.BRIDGE_PARAMS[ + (Chain(from_chain), Chain(to_chain)) + ]["bridge_eta"] message = None if to_amount == 0: @@ -462,7 +530,9 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["from_bridge"] + from_bridge = self.bridge_contract_adaptor.BRIDGE_PARAMS[ + (Chain(from_chain), Chain(to_chain)) + ]["from_bridge"] from_ledger_api = self._from_ledger_api(bridge_request) if from_token == ZERO_ADDRESS: @@ -539,7 +609,9 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] - bridge_eta = BRIDGE_DATA[(Chain(from_chain), Chain(to_chain))]["bridge_eta"] + bridge_eta = self.bridge_contract_adaptor.BRIDGE_PARAMS[ + (Chain(from_chain), Chain(to_chain)) + ]["bridge_eta"] try: from_ledger_api = self._from_ledger_api(bridge_request) From 74ba423f184e2e0a9bab8de9ca725b2803237d97 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 23:12:39 +0200 Subject: [PATCH 133/173] chore: remove redundant code --- .../providers/native_bridge_provider.py | 193 ++++++------------ 1 file changed, 60 insertions(+), 133 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 77b5aeb77..bf8ac379e 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -92,33 +92,14 @@ def can_handle_request(self, params: t.Dict) -> bool: return False @abstractmethod - def build_bridge_native_tx( + def build_bridge_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: - """Build bridge native asset transaction.""" + """Build bridge transaction.""" raise NotImplementedError() @abstractmethod - def build_bridge_erc20_tx( - self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest - ) -> JSONLike: - """Build bridge ERC20 asset transaction.""" - raise NotImplementedError() - - @abstractmethod - def find_native_bridge_finalized_tx( - self, - from_ledger_api: LedgerApi, - to_ledger_api: LedgerApi, - bridge_request: BridgeRequest, - from_block: BlockIdentifier, - to_block: BlockIdentifier, - ) -> t.Optional[str]: - """Return the transaction hash of the event indicating native bridge completion.""" - raise NotImplementedError() - - @abstractmethod - def find_erc20_bridge_finalized_tx( + def find_bridge_finalized_tx( self, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, @@ -126,7 +107,7 @@ def find_erc20_bridge_finalized_tx( from_block: BlockIdentifier, to_block: BlockIdentifier, ) -> t.Optional[str]: - """Return the transaction hash of the event indicating ERC20 bridge completion.""" + """Return the transaction hash of the event indicating bridge completion.""" raise NotImplementedError() @@ -182,33 +163,10 @@ class OptimismContractAdaptor(BridgeContractAdaptor): ), ) - def build_bridge_native_tx( - self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest - ) -> JSONLike: - """Build bridge native asset transaction.""" - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] - to_chain = bridge_request.params["to"]["chain"] - to_address = bridge_request.params["to"]["address"] - to_amount = bridge_request.params["to"]["amount"] - from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] - extra_data = Web3.keccak(text=bridge_request.id) - return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( - ledger_api=from_ledger_api, - contract_address=from_bridge, - sender=from_address, - to=to_address, - amount=int(to_amount), - min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, - extra_data=extra_data, - ) - - def build_bridge_erc20_tx( + def build_bridge_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: - """Build bridge ERC20 asset transaction.""" + """Build bridge transaction.""" from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -220,6 +178,18 @@ def build_bridge_erc20_tx( "from_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) + + if from_token == ZERO_ADDRESS: + return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( + ledger_api=from_ledger_api, + contract_address=from_bridge, + sender=from_address, + to=to_address, + amount=int(to_amount), + min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT, + extra_data=extra_data, + ) + return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx( ledger_api=from_ledger_api, contract_address=from_bridge, @@ -232,36 +202,7 @@ def build_bridge_erc20_tx( extra_data=extra_data, ) - def find_native_bridge_finalized_tx( - self, - from_ledger_api: LedgerApi, - to_ledger_api: LedgerApi, - bridge_request: BridgeRequest, - from_block: BlockIdentifier, - to_block: BlockIdentifier, - ) -> t.Optional[str]: - """Return the transaction hash of the event indicating native bridge completion.""" - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] - to_chain = bridge_request.params["to"]["chain"] - to_address = bridge_request.params["to"]["address"] - to_amount = bridge_request.params["to"]["amount"] - to_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "to_bridge" - ] - extra_data = Web3.keccak(text=bridge_request.id) - return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( - ledger_api=to_ledger_api, - contract_address=to_bridge, - from_=from_address, - to=to_address, - amount=to_amount, - extra_data=extra_data, - from_block=from_block, - to_block=to_block, - ) - - def find_erc20_bridge_finalized_tx( + def find_bridge_finalized_tx( self, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, @@ -269,7 +210,7 @@ def find_erc20_bridge_finalized_tx( from_block: BlockIdentifier, to_block: BlockIdentifier, ) -> t.Optional[str]: - """Return the transaction hash of the event indicating ERC20 bridge completion.""" + """Return the transaction hash of the event indicating bridge completion.""" from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -281,6 +222,19 @@ def find_erc20_bridge_finalized_tx( "to_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) + + if from_token == ZERO_ADDRESS: + return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( + ledger_api=to_ledger_api, + contract_address=to_bridge, + from_=from_address, + to=to_address, + amount=to_amount, + extra_data=extra_data, + from_block=from_block, + to_block=to_block, + ) + return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, @@ -325,18 +279,10 @@ class OmnibridgeContractAdaptor(BridgeContractAdaptor): ), ) - def build_bridge_native_tx( - self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest - ) -> JSONLike: - """Build bridge native asset transaction.""" - raise NotImplementedError( - f"{self.__class__.__name__} does not support this method." - ) - - def build_bridge_erc20_tx( + def build_bridge_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: - """Build bridge ERC20 asset transaction.""" + """Build bridge transaction.""" from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -347,6 +293,11 @@ def build_bridge_erc20_tx( "from_bridge" ] + if from_token == ZERO_ADDRESS: + raise NotImplementedError( + f"{self.__class__.__name__} does not support bridge native tokens." + ) + return self._foreign_omnibridge.build_relay_tokens_tx( ledger_api=from_ledger_api, contract_address=from_bridge, @@ -356,20 +307,7 @@ def build_bridge_erc20_tx( amount=to_amount, ) - def find_native_bridge_finalized_tx( - self, - from_ledger_api: LedgerApi, - to_ledger_api: LedgerApi, - bridge_request: BridgeRequest, - from_block: BlockIdentifier, - to_block: BlockIdentifier, - ) -> t.Optional[str]: - """Return the transaction hash of the event indicating native bridge completion.""" - raise NotImplementedError( - f"{self.__class__.__name__} does not support this method." - ) - - def find_erc20_bridge_finalized_tx( + def find_bridge_finalized_tx( self, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, @@ -377,7 +315,7 @@ def find_erc20_bridge_finalized_tx( from_block: BlockIdentifier, to_block: BlockIdentifier, ) -> t.Optional[str]: - """Return the transaction hash of the event indicating ERC20 bridge completion.""" + """Return the transaction hash of the event indicating bridge completion.""" from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] @@ -393,6 +331,11 @@ def find_erc20_bridge_finalized_tx( "to_bridge" ] + if from_token == ZERO_ADDRESS: + raise NotImplementedError( + f"{self.__class__.__name__} does not support bridge native tokens." + ) + message_id = self._foreign_omnibridge.get_tokens_bridging_initiated_message_id( ledger_api=from_ledger_api, contract_address=from_bridge, @@ -478,6 +421,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: message = None if to_amount == 0: self.logger.info(f"[NATIVE BRIDGE] {MESSAGE_QUOTE_ZERO}") + bridge_eta = 0 message = MESSAGE_QUOTE_ZERO quote_data = QuoteData( @@ -499,17 +443,10 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: if not quote_data: return None - from_token = bridge_request.params["from"]["token"] from_ledger_api = self._from_ledger_api(bridge_request) - - if from_token == ZERO_ADDRESS: - bridge_tx = self.bridge_contract_adaptor.build_bridge_native_tx( - from_ledger_api=from_ledger_api, bridge_request=bridge_request - ) - else: - bridge_tx = self.bridge_contract_adaptor.build_bridge_erc20_tx( - from_ledger_api=from_ledger_api, bridge_request=bridge_request - ) + bridge_tx = self.bridge_contract_adaptor.build_bridge_tx( + from_ledger_api=from_ledger_api, bridge_request=bridge_request + ) BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) @@ -639,26 +576,16 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) - if from_token == ZERO_ADDRESS: - to_tx_hash = ( - self.bridge_contract_adaptor.find_native_bridge_finalized_tx( - from_ledger_api=from_ledger_api, - to_ledger_api=to_ledger_api, - bridge_request=bridge_request, - from_block=from_block, - to_block=to_block, - ) - ) - else: - to_tx_hash = ( - self.bridge_contract_adaptor.find_erc20_bridge_finalized_tx( - from_ledger_api=from_ledger_api, - to_ledger_api=to_ledger_api, - bridge_request=bridge_request, - from_block=from_block, - to_block=to_block, - ) + to_tx_hash = ( + self.bridge_contract_adaptor.find_bridge_finalized_tx( + from_ledger_api=from_ledger_api, + to_ledger_api=to_ledger_api, + bridge_request=bridge_request, + from_block=from_block, + to_block=to_block, ) + ) + if to_tx_hash: self.logger.info( f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." From 0d514d345900e6e4b9c81a41d6c75b45c56daf5f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 23:40:13 +0200 Subject: [PATCH 134/173] chore: update --- .../providers/native_bridge_provider.py | 15 +++--- tests/test_bridge_bridge.py | 17 +++++- tests/test_bridge_providers.py | 54 +++++++++++++++---- 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index bf8ac379e..1a1c7013d 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -544,7 +544,6 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: return from_chain = bridge_request.params["from"]["chain"] - from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] bridge_eta = self.bridge_contract_adaptor.BRIDGE_PARAMS[ (Chain(from_chain), Chain(to_chain)) @@ -576,14 +575,12 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE): to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block) - to_tx_hash = ( - self.bridge_contract_adaptor.find_bridge_finalized_tx( - from_ledger_api=from_ledger_api, - to_ledger_api=to_ledger_api, - bridge_request=bridge_request, - from_block=from_block, - to_block=to_block, - ) + to_tx_hash = self.bridge_contract_adaptor.find_bridge_finalized_tx( + from_ledger_api=from_ledger_api, + to_ledger_api=to_ledger_api, + bridge_request=bridge_request, + from_block=from_block, + to_block=to_block, ) if to_tx_hash: diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 791a6966e..b920d8171 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -31,6 +31,7 @@ BridgeRequestStatus, MESSAGE_QUOTE_ZERO, ) +from operate.bridge.providers.lifi_bridge_provider import LIFI_DEFAULT_ETA from operate.cli import OperateApp from operate.constants import ZERO_ADDRESS from operate.ledger.profiles import OLAS @@ -107,10 +108,12 @@ def test_bundle_zero( "bridge_refill_requirements": brr["bridge_refill_requirements"], "bridge_request_status": [ { + "eta": 0, "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, }, { + "eta": 0, "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, }, @@ -212,10 +215,12 @@ def test_bundle_error( "bridge_refill_requirements": brr["bridge_refill_requirements"], "bridge_request_status": [ { + "eta": None, "message": brr["bridge_request_status"][0]["message"], "status": BridgeRequestStatus.QUOTE_FAILED.value, }, { + "eta": 0, "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, }, @@ -334,8 +339,16 @@ def test_bundle_quote( }, "bridge_refill_requirements": brr["bridge_refill_requirements"], "bridge_request_status": [ - {"message": None, "status": BridgeRequestStatus.QUOTE_DONE.value}, - {"message": None, "status": BridgeRequestStatus.QUOTE_DONE.value}, + { + "eta": LIFI_DEFAULT_ETA, + "message": None, + "status": BridgeRequestStatus.QUOTE_DONE.value, + }, + { + "eta": LIFI_DEFAULT_ETA, + "message": None, + "status": BridgeRequestStatus.QUOTE_DONE.value, + }, ], "bridge_total_requirements": brr["bridge_total_requirements"], "expiration_timestamp": brr["expiration_timestamp"], diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 5ea62e7c6..9136ff875 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -43,10 +43,11 @@ MESSAGE_QUOTE_ZERO, QuoteData, ) +from operate.bridge.providers.lifi_bridge_provider import LIFI_DEFAULT_ETA from operate.bridge.providers.native_bridge_provider import ( - BRIDGE_DATA, BridgeContractAdaptor, NativeBridgeProvider, + OmnibridgeContractAdaptor, OptimismContractAdaptor, ) from operate.cli import OperateApp @@ -124,7 +125,7 @@ def test_bridge_zero( bridge.quote(bridge_request=bridge_request) qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.bridge_eta is None, "Wrong quote data." + assert qd.bridge_eta == 0, "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.provider_data is None, "Wrong quote data." @@ -136,6 +137,7 @@ def test_bridge_zero( sj = bridge.status_json(bridge_request) expected_sj = { + "eta": 0, "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, } @@ -158,7 +160,7 @@ def test_bridge_zero( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.bridge_eta is None, "Wrong quote data." + assert qd.bridge_eta == 0, "Wrong quote data." assert qd.elapsed_time == 0, "Wrong quote data." assert qd.message == MESSAGE_QUOTE_ZERO, "Wrong quote data." assert qd.provider_data is None, "Wrong quote data." @@ -193,6 +195,7 @@ def test_bridge_zero( sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { + "eta": 0, "explorer_link": sj["explorer_link"], "tx_hash": None, # type: ignore "message": sj["message"], @@ -276,6 +279,7 @@ def test_bridge_error( assert bridge_request.quote_data is not None, "Wrong quote data." sj = bridge.status_json(bridge_request) expected_sj = { + "eta": None, "message": bridge_request.quote_data.message, "status": BridgeRequestStatus.QUOTE_FAILED.value, } @@ -340,6 +344,7 @@ def test_bridge_error( MESSAGE_EXECUTION_FAILED_QUOTE_FAILED in sj["message"] ), "Wrong execution data." expected_sj = { + "eta": None, "explorer_link": sj["explorer_link"], "tx_hash": None, "message": sj["message"], @@ -409,7 +414,7 @@ def test_bridge_quote( bridge.quote(bridge_request=bridge_request) qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.bridge_eta is None, "Wrong quote data." + assert qd.bridge_eta == LIFI_DEFAULT_ETA, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.provider_data is not None, "Wrong quote data." @@ -424,6 +429,7 @@ def test_bridge_quote( assert bridge_request.quote_data is not None, "Wrong quote data." sj = bridge.status_json(bridge_request) expected_sj = { + "eta": LIFI_DEFAULT_ETA, "message": bridge_request.quote_data.message, "status": BridgeRequestStatus.QUOTE_DONE.value, } @@ -454,7 +460,7 @@ def test_bridge_quote( qd = bridge_request.quote_data assert qd is not None, "Missing quote data." - assert qd.bridge_eta is None, "Wrong quote data." + assert qd.bridge_eta == LIFI_DEFAULT_ETA, "Wrong quote data." assert qd.elapsed_time > 0, "Wrong quote data." assert qd.message is None, "Wrong quote data." assert qd.provider_data is not None, "Wrong quote data." @@ -473,8 +479,6 @@ def test_bridge_quote( class TestNativeBridge: """Tests for bridge.providers.NativeBridgeProvider class.""" - # TODO: test existing executions: failed and done - def test_bridge_zero( self, tmp_path: Path, @@ -546,7 +550,7 @@ def test_bridge_zero( # Quote expected_quote_data = QuoteData( - bridge_eta=BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], + bridge_eta=0, elapsed_time=0, message=MESSAGE_QUOTE_ZERO, provider_data=None, @@ -562,6 +566,7 @@ def test_bridge_zero( assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.status_json(bridge_request) expected_sj = { + "eta": 0, "message": MESSAGE_QUOTE_ZERO, "status": BridgeRequestStatus.QUOTE_DONE.value, } @@ -603,6 +608,7 @@ def test_bridge_zero( sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { + "eta": 0, "explorer_link": sj["explorer_link"], "tx_hash": None, # type: ignore "message": sj["message"], @@ -690,7 +696,9 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=BRIDGE_DATA[Chain.ETHEREUM, Chain.BASE]["bridge_eta"], + bridge_eta=bridge.bridge_contract_adaptor.BRIDGE_PARAMS[ + Chain.ETHEREUM, Chain.BASE + ]["bridge_eta"], elapsed_time=0, message=None, provider_data=None, @@ -706,6 +714,9 @@ def test_bridge_execute_error( assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.status_json(bridge_request) expected_sj = { + "eta": bridge.bridge_contract_adaptor.BRIDGE_PARAMS[ + Chain.ETHEREUM, Chain.BASE + ]["bridge_eta"], "message": None, "status": BridgeRequestStatus.QUOTE_DONE.value, } @@ -759,6 +770,9 @@ def test_bridge_execute_error( sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_FAILED in sj["message"], "Wrong execution data." expected_sj = { + "eta": bridge.bridge_contract_adaptor.BRIDGE_PARAMS[ + Chain.ETHEREUM, Chain.BASE + ]["bridge_eta"], "explorer_link": sj["explorer_link"], "tx_hash": None, # type: ignore "message": sj["message"], @@ -816,6 +830,28 @@ class TestBridgeProvider: ), [ # NativeBridgeProvider (Omnibridge) - EXECUTION_DONE tests + ( + NativeBridgeProvider, + OmnibridgeContractAdaptor, + { + "from": { + "chain": "ethereum", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0x0001A500A6B18995B03f44bb040A5fFc28E45CB0", + }, + "to": { + "chain": "gnosis", + "address": "0x308508F09F81A6d28679db6da73359c72f8e22C5", + "token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", + "amount": 1000000000000000000, + }, + }, + "b-b5648bc4-15c0-4792-9970-cc692851ce50", + "0x8b7646a46e06b3fd4f61f592f6da0f66b5f29a71b13cfce4ce8a8dd2095b7827", + BridgeRequestStatus.EXECUTION_DONE, + "0xc8905a2193d86a58bb92f2bbe3640ef340aacfe4273efc50ad80115f3ed20206", + 2195, + ), # LiFiBridgeProvider - EXECUTION_DONE tests ( LiFiBridgeProvider, From 0740900eda06a198f270fc51a35aad6f497ea936 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 19 May 2025 23:51:34 +0200 Subject: [PATCH 135/173] fix: linters --- operate/bridge/providers/bridge_provider.py | 3 ++- operate/bridge/providers/native_bridge_provider.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index a5cec9663..30bde5f6d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -34,6 +34,7 @@ from autonomy.chain.tx import TxSettler from web3 import Web3 +from operate.bridge import bridge from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, ON_CHAIN_INTERACT_SLEEP, @@ -448,7 +449,7 @@ def status_json(self, bridge_request: BridgeRequest) -> t.Dict: """JSON representation of the status.""" self._validate(bridge_request) - if bridge_request.execution_data: + if bridge_request.execution_data and bridge_request.quote_data: self._update_execution_status(bridge_request) tx_hash = None explorer_link = None diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 1a1c7013d..0a4ab8d82 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -316,6 +316,12 @@ def find_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating bridge completion.""" + if not bridge_request.execution_data: + raise RuntimeError("Execution data not present") + + if not bridge_request.execution_data.from_tx_hash: + raise RuntimeError("Source chain transaction hash not present") + from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] From 87d3f5758d9eda8f82f06a05d8c80ec5244d2c78 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 11:07:39 +0200 Subject: [PATCH 136/173] chore: update adaptors --- operate/bridge/providers/bridge_provider.py | 12 +- .../bridge/providers/lifi_bridge_provider.py | 6 +- .../providers/native_bridge_provider.py | 137 ++++++++++++------ operate/data/README.md | 2 + tests/test_bridge_providers.py | 3 + 5 files changed, 109 insertions(+), 51 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 30bde5f6d..7f2ce2904 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -34,7 +34,6 @@ from autonomy.chain.tx import TxSettler from web3 import Web3 -from operate.bridge import bridge from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, ON_CHAIN_INTERACT_SLEEP, @@ -89,6 +88,7 @@ class ExecutionData(LocalResource): timestamp: int from_tx_hash: t.Optional[str] to_tx_hash: t.Optional[str] + provider_data: t.Optional[t.Dict] # Provider-specific data class BridgeRequestStatus(str, enum.Enum): @@ -340,6 +340,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: timestamp=int(time.time()), from_tx_hash=None, to_tx_hash=None, + provider_data=None, ) bridge_request.execution_data = execution_data bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @@ -370,6 +371,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: timestamp=int(time.time()), from_tx_hash=None, to_tx_hash=None, + provider_data=None, ) bridge_request.execution_data = execution_data bridge_request.status = BridgeRequestStatus.EXECUTION_DONE @@ -413,6 +415,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: timestamp=int(timestamp), from_tx_hash=tx_hashes[-1], to_tx_hash=None, + provider_data=None, ) bridge_request.execution_data = execution_data if len(tx_hashes) == len(txs): @@ -431,6 +434,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: timestamp=int(time.time()), from_tx_hash=None, to_tx_hash=None, + provider_data=None, ) bridge_request.execution_data = execution_data bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @@ -441,7 +445,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: raise NotImplementedError() @abstractmethod - def _get_explorer_link(self, tx_hash: str) -> str: + def _get_explorer_link(self, bridge_request: BridgeRequest) -> str: """Get the explorer link for a transaction.""" raise NotImplementedError() @@ -452,14 +456,12 @@ def status_json(self, bridge_request: BridgeRequest) -> t.Dict: if bridge_request.execution_data and bridge_request.quote_data: self._update_execution_status(bridge_request) tx_hash = None - explorer_link = None if bridge_request.execution_data.from_tx_hash: tx_hash = bridge_request.execution_data.from_tx_hash - explorer_link = self._get_explorer_link(tx_hash) return { "eta": bridge_request.quote_data.bridge_eta, - "explorer_link": explorer_link, + "explorer_link": self._get_explorer_link(bridge_request), "message": bridge_request.execution_data.message, "status": bridge_request.status.value, "tx_hash": tx_hash, diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index c984239ec..b6397a50a 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -359,6 +359,10 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: else: bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN - def _get_explorer_link(self, tx_hash: str) -> str: + def _get_explorer_link(self, bridge_request: BridgeRequest) -> str: """Get the explorer link for a transaction.""" + tx_hash = None + if bridge_request.execution_data and bridge_request.execution_data.from_tx_hash: + tx_hash = bridge_request.execution_data.from_tx_hash + return f"https://scan.li.fi/tx/{tx_hash}" diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 0a4ab8d82..77adb8daa 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -65,17 +65,18 @@ class BridgeContractAdaptor(ABC): BRIDGE_PARAMS: t.Dict - def can_handle_request(self, params: t.Dict) -> bool: + @classmethod + def can_handle_request(cls, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" from_chain = Chain(params["from"]["chain"]) from_token = params["from"]["token"] to_chain = Chain(params["to"]["chain"]) to_token = params["to"]["token"] - if (from_chain, to_chain) not in self.BRIDGE_PARAMS: + if (from_chain, to_chain) not in cls.BRIDGE_PARAMS: return False - bridge_params = self.BRIDGE_PARAMS[(from_chain, to_chain)] + bridge_params = cls.BRIDGE_PARAMS[(from_chain, to_chain)] if from_token not in bridge_params["supported_from_tokens"]: return False @@ -91,16 +92,18 @@ def can_handle_request(self, params: t.Dict) -> bool: return False + @classmethod @abstractmethod def build_bridge_tx( - self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge transaction.""" raise NotImplementedError() + @classmethod @abstractmethod def find_bridge_finalized_tx( - self, + cls, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -163,8 +166,9 @@ class OptimismContractAdaptor(BridgeContractAdaptor): ), ) + @classmethod def build_bridge_tx( - self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge transaction.""" from_chain = bridge_request.params["from"]["chain"] @@ -174,13 +178,13 @@ def build_bridge_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + from_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ "from_bridge" ] extra_data = Web3.keccak(text=bridge_request.id) if from_token == ZERO_ADDRESS: - return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( + return cls._l1_standard_bridge_contract.build_bridge_eth_to_tx( ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, @@ -190,7 +194,7 @@ def build_bridge_tx( extra_data=extra_data, ) - return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx( + return cls._l1_standard_bridge_contract.build_bridge_erc20_to_tx( ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, @@ -202,8 +206,9 @@ def build_bridge_tx( extra_data=extra_data, ) + @classmethod def find_bridge_finalized_tx( - self, + cls, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -218,13 +223,11 @@ def find_bridge_finalized_tx( to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "to_bridge" - ] + to_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))]["to_bridge"] extra_data = Web3.keccak(text=bridge_request.id) if from_token == ZERO_ADDRESS: - return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( + return cls._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, from_=from_address, @@ -235,7 +238,7 @@ def find_bridge_finalized_tx( to_block=to_block, ) - return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( + return cls._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, local_token=to_token, @@ -279,8 +282,9 @@ class OmnibridgeContractAdaptor(BridgeContractAdaptor): ), ) + @classmethod def build_bridge_tx( - self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge transaction.""" from_chain = bridge_request.params["from"]["chain"] @@ -289,16 +293,16 @@ def build_bridge_tx( to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + from_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ "from_bridge" ] if from_token == ZERO_ADDRESS: raise NotImplementedError( - f"{self.__class__.__name__} does not support bridge native tokens." + f"{cls.__class__.__name__} does not support bridge native tokens." ) - return self._foreign_omnibridge.build_relay_tokens_tx( + return cls._foreign_omnibridge.build_relay_tokens_tx( ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, @@ -307,8 +311,9 @@ def build_bridge_tx( amount=to_amount, ) + @classmethod def find_bridge_finalized_tx( - self, + cls, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -316,47 +321,30 @@ def find_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating bridge completion.""" - if not bridge_request.execution_data: - raise RuntimeError("Execution data not present") - - if not bridge_request.execution_data.from_tx_hash: - raise RuntimeError("Source chain transaction hash not present") - from_chain = bridge_request.params["from"]["chain"] - from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] - from_tx_hash = bridge_request.execution_data.from_tx_hash to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] - to_bridge = self.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "to_bridge" - ] + to_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))]["to_bridge"] if from_token == ZERO_ADDRESS: raise NotImplementedError( - f"{self.__class__.__name__} does not support bridge native tokens." + f"{cls.__class__.__name__} does not support bridge native tokens." ) - message_id = self._foreign_omnibridge.get_tokens_bridging_initiated_message_id( - ledger_api=from_ledger_api, - contract_address=from_bridge, - tx_hash=from_tx_hash, - token=from_token, - sender=from_address, - value=to_amount, + message_id = cls.get_message_id( + from_ledger_api=from_ledger_api, + bridge_request=bridge_request, ) if not message_id: raise RuntimeError( - f"Cannot find 'messageId' in transaction {from_tx_hash}." + f"Cannot find 'messageId' for bridge request {bridge_request.id}." ) - return self._home_omnibridge.find_tokens_bridged_tx( + return cls._home_omnibridge.find_tokens_bridged_tx( ledger_api=to_ledger_api, contract_address=to_bridge, token=to_token, @@ -367,6 +355,48 @@ def find_bridge_finalized_tx( to_block=to_block, ) + @classmethod + def get_message_id( + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> t.Optional[bytes]: + """Get the bridge message id.""" + if not bridge_request.execution_data: + raise RuntimeError("Execution data not present") + + if not bridge_request.execution_data.from_tx_hash: + raise RuntimeError("Source chain transaction hash not present") + + if ( + bridge_request.execution_data.provider_data + and "message_id" in bridge_request.execution_data.provider_data + ): + return bridge_request.execution_data.provider_data.get("message_id", None) + + from_chain = bridge_request.params["from"]["chain"] + from_address = bridge_request.params["from"]["address"] + from_token = bridge_request.params["from"]["token"] + from_tx_hash = bridge_request.execution_data.from_tx_hash + to_chain = bridge_request.params["to"]["chain"] + to_amount = bridge_request.params["to"]["amount"] + from_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ + "from_bridge" + ] + + message_id = cls._foreign_omnibridge.get_tokens_bridging_initiated_message_id( + ledger_api=from_ledger_api, + contract_address=from_bridge, + tx_hash=from_tx_hash, + token=from_token, + sender=from_address, + value=to_amount, + ) + + if not bridge_request.execution_data.provider_data: + bridge_request.execution_data.provider_data = {} + + bridge_request.execution_data.provider_data["message_id"] = message_id + return message_id + class NativeBridgeProvider(BridgeProvider): """Native bridge provider""" @@ -611,6 +641,9 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: except Exception as e: self.logger.error(f"Error updating execution status: {e}") + import traceback + + traceback.print_exc() execution_data.message = f"{MESSAGE_EXECUTION_FAILED} {str(e)}" bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED @@ -630,6 +663,20 @@ def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: high = mid - 1 return best - def _get_explorer_link(self, tx_hash: str) -> str: + def _get_explorer_link(self, bridge_request: BridgeRequest) -> str: """Get the explorer link for a transaction.""" + + if isinstance(self.bridge_contract_adaptor, OmnibridgeContractAdaptor): + from_ledger_api = self._from_ledger_api(bridge_request) + message_id = self.bridge_contract_adaptor.get_message_id( + from_ledger_api, bridge_request + ) + if not message_id: + return "https://bridge.gnosischain.com/bridge-explorer/" + return f"https://bridge.gnosischain.com/bridge-explorer/transaction/0x{message_id.hex()}" + + tx_hash = None + if bridge_request.execution_data and bridge_request.execution_data.from_tx_hash: + tx_hash = bridge_request.execution_data.from_tx_hash + return f"https://etherscan.io/tx/{tx_hash}" # TODO this bridge should return None here - discuss with FE diff --git a/operate/data/README.md b/operate/data/README.md index a7c757859..7cce87c21 100644 --- a/operate/data/README.md +++ b/operate/data/README.md @@ -9,6 +9,8 @@ Reused packages from other repositories: New packages from this repository: - `contracts/dual_staking_token` +- `contracts/foreign_omnibridge` +- `contracts/home_omnibridge` - `contracts/l1_standard_bridge` - `contracts/l2_standard_bridge` - `contracts/requester_activity_checker` diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 9136ff875..be8a44ed6 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -596,6 +596,7 @@ def test_bridge_zero( timestamp=0, from_tx_hash=None, to_tx_hash=None, + provider_data=None, ) expected_request.execution_data = expected_execution_data expected_request.status = BridgeRequestStatus.EXECUTION_DONE @@ -754,6 +755,7 @@ def test_bridge_execute_error( timestamp=0, from_tx_hash=None, to_tx_hash=None, + provider_data=None, ) expected_request.execution_data = expected_execution_data expected_request.status = BridgeRequestStatus.EXECUTION_FAILED @@ -1104,6 +1106,7 @@ def test_update_execution_status( timestamp=0, from_tx_hash=from_tx_hash, to_tx_hash=None, + provider_data=None, ) bridge_request = BridgeRequest( From a81862eaee897923e22eeb733e523ff4e8311938 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 13:11:12 +0200 Subject: [PATCH 137/173] fix: remove eth filter --- .../contracts/home_omnibridge/contract.py | 41 ++++++--- .../contracts/home_omnibridge/contract.yaml | 2 +- .../contracts/l2_standard_bridge/contract.py | 89 ++++++++++++------- .../l2_standard_bridge/contract.yaml | 2 +- 4 files changed, 85 insertions(+), 49 deletions(-) diff --git a/operate/data/contracts/home_omnibridge/contract.py b/operate/data/contracts/home_omnibridge/contract.py index 291671605..73010a49d 100644 --- a/operate/data/contracts/home_omnibridge/contract.py +++ b/operate/data/contracts/home_omnibridge/contract.py @@ -25,7 +25,9 @@ from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi +import eth_abi from web3.types import BlockIdentifier +from web3 import Web3 class HomeOmnibridge(Contract): @@ -47,19 +49,30 @@ def find_tokens_bridged_tx( ) -> Optional[str]: """Return the transaction hash of the matching TokensBridged event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) - contract_instance = cls.get_instance(ledger_api, contract_address) - entries = contract_instance.events.TokensBridged.create_filter( - fromBlock=from_block, - toBlock=to_block, - argument_filters={ - "token": token, - "recipient": recipient, - "messageId": message_id, - }, - ).get_all_entries() + event_signature = "TokensBridged(address,address,uint256,bytes32)" + event_signature_hash = Web3.keccak(text=event_signature).hex() + + topics = [ + event_signature_hash, # TokensBridged + "0x" + token.lower()[2:].rjust(64, "0"), # token + "0x" + recipient.lower()[2:].rjust(64, "0"), # recipient + "0x" + message_id.hex().rjust(64, "0") + ] + non_indexed_types = ["uint256"] + non_indexed_values = [ + value, # value + ] + + logs = ledger_api.api.eth.get_logs({ + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + }) + + for log in logs: + decoded = eth_abi.decode(non_indexed_types, log["data"]) + if all(a == b for a, b in zip(decoded, non_indexed_values)): + return log["transactionHash"].hex() - for entry in entries: - args = entry["args"] - if args["value"] == value: - return entry["transactionHash"].hex() return None diff --git a/operate/data/contracts/home_omnibridge/contract.yaml b/operate/data/contracts/home_omnibridge/contract.yaml index 027ad6399..db55504ad 100644 --- a/operate/data/contracts/home_omnibridge/contract.yaml +++ b/operate/data/contracts/home_omnibridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeigrlrmwk3dhnoe4o5ovygtmudik7ybhsx2etqqjpvdi65rtlfpuhe build/HomeOmnibridge.json: bafybeihm4ff4mhoca2iwcogg5srqe2x6p674uxxc3v5dcci4emxfgc7qpm - contract.py: bafybeigj6morbr2bkuqtzvrunrsgefnpvegrlycxafjmb7quo42wvltmhi + contract.py: bafybeifl6g6lrujk6gmuckgyhh4gxxf5iucholbwdkduk4rqp62y5rhavm fingerprint_ignore_patterns: [] contracts: [] class_name: HomeOmnibridge diff --git a/operate/data/contracts/l2_standard_bridge/contract.py b/operate/data/contracts/l2_standard_bridge/contract.py index 89bd5b32b..538e40f62 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.py +++ b/operate/data/contracts/l2_standard_bridge/contract.py @@ -25,6 +25,8 @@ from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi +import eth_abi +from web3 import Web3 from web3.types import BlockIdentifier @@ -47,20 +49,32 @@ def find_eth_bridge_finalized_tx( ) -> Optional[str]: """Return the transaction hash of the matching ETHBridgeFinalized event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) - contract_instance = cls.get_instance(ledger_api, contract_address) - entries = contract_instance.events.ETHBridgeFinalized.create_filter( - fromBlock=from_block, - toBlock=to_block, - argument_filters={ - "from": from_, - "to": to, - }, - ).get_all_entries() - - for entry in entries: - args = entry["args"] - if args["amount"] == amount and args["extraData"] == extra_data: - return entry["transactionHash"].hex() + event_signature = "ETHBridgeFinalized(address,address,uint256,bytes)" + event_signature_hash = Web3.keccak(text=event_signature).hex() + + topics = [ + event_signature_hash, # ETHBridgeFinalized + "0x" + from_.lower()[2:].rjust(64, "0"), # from + "0x" + to.lower()[2:].rjust(64, "0"), # to + ] + non_indexed_types = ["uint256", "bytes"] + non_indexed_values = [ + amount, # amount + extra_data, # extraData + ] + + logs = ledger_api.api.eth.get_logs({ + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + }) + + for log in logs: + decoded = eth_abi.decode(non_indexed_types, log["data"]) + if all(a == b for a, b in zip(decoded, non_indexed_values)): + return log["transactionHash"].hex() + return None @classmethod @@ -79,23 +93,32 @@ def find_erc20_bridge_finalized_tx( ) -> Optional[str]: """Return the transaction hash of the matching ERC20BridgeFinalized event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) - contract_instance = cls.get_instance(ledger_api, contract_address) - entries = contract_instance.events.ERC20BridgeFinalized.create_filter( - fromBlock=from_block, - toBlock=to_block, - argument_filters={ - "localToken": local_token, - "remoteToken": remote_token, - "from": from_, - }, - ).get_all_entries() - - for entry in entries: - args = entry["args"] - if ( - args["to"].lower() == to.lower() - and args["amount"] == amount - and args["extraData"] == extra_data - ): - return entry["transactionHash"].hex() + event_signature = "ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" + event_signature_hash = Web3.keccak(text=event_signature).hex() + + topics = [ + event_signature_hash, # ERC20BridgeFinalized + "0x" + local_token.lower()[2:].rjust(64, "0"), # localToken + "0x" + remote_token.lower()[2:].rjust(64, "0"), # remoteToken + "0x" + from_.lower()[2:].rjust(64, "0"), # from + ] + non_indexed_types = ["address", "uint256", "bytes"] + non_indexed_values = [ + to.lower(), # to + amount, # amount + extra_data, # extraData + ] + + logs = ledger_api.api.eth.get_logs({ + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + }) + + for log in logs: + decoded = eth_abi.decode(non_indexed_types, log["data"]) + if all(a == b for a, b in zip(decoded, non_indexed_values)): + return log["transactionHash"].hex() + return None diff --git a/operate/data/contracts/l2_standard_bridge/contract.yaml b/operate/data/contracts/l2_standard_bridge/contract.yaml index 595e1c85b..71e87bc8d 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.yaml +++ b/operate/data/contracts/l2_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihp3ucqaxnuozotqcmvm6bjdds4rfbtr56sj7jeb6sbxlzikvtxyq build/L2StandardBridge.json: bafybeid2zqptx4awocztkhmifqsdnqrqcib73hbsp53snmfd24u7k3q6me - contract.py: bafybeievv75gwvgj6w42ng6pkrewkns6mwnyzeckufpbyhkq7npkbn35ve + contract.py: bafybeidacvflojrpqh55jpirh223r5q4slljw6xbs7zrn4auzspw5h3fw4 fingerprint_ignore_patterns: [] contracts: [] class_name: L2StandardBridge From 5842fe4a8311a980d4d4faca921e6571bb4d89f8 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 13:13:48 +0200 Subject: [PATCH 138/173] fix: linters --- .../contracts/home_omnibridge/contract.py | 20 ++++++----- .../contracts/home_omnibridge/contract.yaml | 2 +- .../contracts/l2_standard_bridge/contract.py | 34 +++++++++++-------- .../l2_standard_bridge/contract.yaml | 2 +- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/operate/data/contracts/home_omnibridge/contract.py b/operate/data/contracts/home_omnibridge/contract.py index 73010a49d..bf2080ceb 100644 --- a/operate/data/contracts/home_omnibridge/contract.py +++ b/operate/data/contracts/home_omnibridge/contract.py @@ -21,13 +21,13 @@ from typing import Optional, cast +import eth_abi from aea.configurations.base import PublicId from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi -import eth_abi -from web3.types import BlockIdentifier from web3 import Web3 +from web3.types import BlockIdentifier class HomeOmnibridge(Contract): @@ -56,19 +56,21 @@ def find_tokens_bridged_tx( event_signature_hash, # TokensBridged "0x" + token.lower()[2:].rjust(64, "0"), # token "0x" + recipient.lower()[2:].rjust(64, "0"), # recipient - "0x" + message_id.hex().rjust(64, "0") + "0x" + message_id.hex().rjust(64, "0"), # messageId ] non_indexed_types = ["uint256"] non_indexed_values = [ value, # value ] - logs = ledger_api.api.eth.get_logs({ - "fromBlock": from_block, - "toBlock": to_block, - "address": contract_address, - "topics": topics, - }) + logs = ledger_api.api.eth.get_logs( + { + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + } + ) for log in logs: decoded = eth_abi.decode(non_indexed_types, log["data"]) diff --git a/operate/data/contracts/home_omnibridge/contract.yaml b/operate/data/contracts/home_omnibridge/contract.yaml index db55504ad..a628b2fde 100644 --- a/operate/data/contracts/home_omnibridge/contract.yaml +++ b/operate/data/contracts/home_omnibridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeigrlrmwk3dhnoe4o5ovygtmudik7ybhsx2etqqjpvdi65rtlfpuhe build/HomeOmnibridge.json: bafybeihm4ff4mhoca2iwcogg5srqe2x6p674uxxc3v5dcci4emxfgc7qpm - contract.py: bafybeifl6g6lrujk6gmuckgyhh4gxxf5iucholbwdkduk4rqp62y5rhavm + contract.py: bafybeignqvstktpvbliopzztan4a46crvf3ig5o4yt4fxhjpgljtlyrzga fingerprint_ignore_patterns: [] contracts: [] class_name: HomeOmnibridge diff --git a/operate/data/contracts/l2_standard_bridge/contract.py b/operate/data/contracts/l2_standard_bridge/contract.py index 538e40f62..48149d529 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.py +++ b/operate/data/contracts/l2_standard_bridge/contract.py @@ -21,11 +21,11 @@ from typing import Optional, cast +import eth_abi from aea.configurations.base import PublicId from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi -import eth_abi from web3 import Web3 from web3.types import BlockIdentifier @@ -63,12 +63,14 @@ def find_eth_bridge_finalized_tx( extra_data, # extraData ] - logs = ledger_api.api.eth.get_logs({ - "fromBlock": from_block, - "toBlock": to_block, - "address": contract_address, - "topics": topics, - }) + logs = ledger_api.api.eth.get_logs( + { + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + } + ) for log in logs: decoded = eth_abi.decode(non_indexed_types, log["data"]) @@ -93,7 +95,9 @@ def find_erc20_bridge_finalized_tx( ) -> Optional[str]: """Return the transaction hash of the matching ERC20BridgeFinalized event in the given block range.""" ledger_api = cast(EthereumApi, ledger_api) - event_signature = "ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" + event_signature = ( + "ERC20BridgeFinalized(address,address,address,address,uint256,bytes)" + ) event_signature_hash = Web3.keccak(text=event_signature).hex() topics = [ @@ -109,12 +113,14 @@ def find_erc20_bridge_finalized_tx( extra_data, # extraData ] - logs = ledger_api.api.eth.get_logs({ - "fromBlock": from_block, - "toBlock": to_block, - "address": contract_address, - "topics": topics, - }) + logs = ledger_api.api.eth.get_logs( + { + "fromBlock": from_block, + "toBlock": to_block, + "address": contract_address, + "topics": topics, + } + ) for log in logs: decoded = eth_abi.decode(non_indexed_types, log["data"]) diff --git a/operate/data/contracts/l2_standard_bridge/contract.yaml b/operate/data/contracts/l2_standard_bridge/contract.yaml index 71e87bc8d..74191f553 100644 --- a/operate/data/contracts/l2_standard_bridge/contract.yaml +++ b/operate/data/contracts/l2_standard_bridge/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihp3ucqaxnuozotqcmvm6bjdds4rfbtr56sj7jeb6sbxlzikvtxyq build/L2StandardBridge.json: bafybeid2zqptx4awocztkhmifqsdnqrqcib73hbsp53snmfd24u7k3q6me - contract.py: bafybeidacvflojrpqh55jpirh223r5q4slljw6xbs7zrn4auzspw5h3fw4 + contract.py: bafybeifah3y4wr4mkqdu4yuv364gxssdybuqf32wcszvrgwc33lucrjdw4 fingerprint_ignore_patterns: [] contracts: [] class_name: L2StandardBridge From 6fef95fa277d8e64979c48746d80b4d4c2e81297 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 13:32:36 +0200 Subject: [PATCH 139/173] fix: checksum addresses --- operate/bridge/providers/native_bridge_provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 77adb8daa..4b068a412 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -69,9 +69,9 @@ class BridgeContractAdaptor(ABC): def can_handle_request(cls, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" from_chain = Chain(params["from"]["chain"]) - from_token = params["from"]["token"] + from_token = Web3.to_checksum_address(params["from"]["token"]) to_chain = Chain(params["to"]["chain"]) - to_token = params["to"]["token"] + to_token = Web3.to_checksum_address(params["to"]["token"]) if (from_chain, to_chain) not in cls.BRIDGE_PARAMS: return False From 44f4e7a6b7ef74363c84d2aa2afcd58069a10c05 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 16:46:00 +0200 Subject: [PATCH 140/173] chore: fix links --- operate/bridge/providers/bridge_provider.py | 2 +- .../bridge/providers/lifi_bridge_provider.py | 11 ++-- .../providers/native_bridge_provider.py | 57 +++++++++++++------ tests/test_bridge_providers.py | 4 +- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 7f2ce2904..e92dd0fb7 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -445,7 +445,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: raise NotImplementedError() @abstractmethod - def _get_explorer_link(self, bridge_request: BridgeRequest) -> str: + def _get_explorer_link(self, bridge_request: BridgeRequest) -> t.Optional[str]: """Get the explorer link for a transaction.""" raise NotImplementedError() diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index b6397a50a..c39e4524b 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -359,10 +359,13 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: else: bridge_request.status = BridgeRequestStatus.EXECUTION_UNKNOWN - def _get_explorer_link(self, bridge_request: BridgeRequest) -> str: + def _get_explorer_link(self, bridge_request: BridgeRequest) -> t.Optional[str]: """Get the explorer link for a transaction.""" - tx_hash = None - if bridge_request.execution_data and bridge_request.execution_data.from_tx_hash: - tx_hash = bridge_request.execution_data.from_tx_hash + if not bridge_request.execution_data: + return None + + tx_hash = bridge_request.execution_data.from_tx_hash + if not tx_hash: + return None return f"https://scan.li.fi/tx/{tx_hash}" diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 4b068a412..8d07cd285 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -113,6 +113,14 @@ def find_bridge_finalized_tx( """Return the transaction hash of the event indicating bridge completion.""" raise NotImplementedError() + @classmethod + @abstractmethod + def get_explorer_link( + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> t.Optional[str]: + """Get the explorer link for a transaction.""" + raise NotImplementedError() + class OptimismContractAdaptor(BridgeContractAdaptor): """Adaptor class for Optimism contract packages.""" @@ -251,6 +259,20 @@ def find_bridge_finalized_tx( to_block=to_block, ) + @classmethod + def get_explorer_link( + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> t.Optional[str]: + """Get the explorer link for a transaction.""" + if not bridge_request.execution_data: + return None + + tx_hash = bridge_request.execution_data.from_tx_hash + if not tx_hash: + return None + + return f"https://etherscan.io/tx/{tx_hash}" + class OmnibridgeContractAdaptor(BridgeContractAdaptor): """Adaptor class for Omnibridge contract packages.""" @@ -361,10 +383,10 @@ def get_message_id( ) -> t.Optional[bytes]: """Get the bridge message id.""" if not bridge_request.execution_data: - raise RuntimeError("Execution data not present") + return None if not bridge_request.execution_data.from_tx_hash: - raise RuntimeError("Source chain transaction hash not present") + return None if ( bridge_request.execution_data.provider_data @@ -397,6 +419,16 @@ def get_message_id( bridge_request.execution_data.provider_data["message_id"] = message_id return message_id + @classmethod + def get_explorer_link( + cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + ) -> t.Optional[str]: + """Get the explorer link for a transaction.""" + message_id = cls.get_message_id(from_ledger_api, bridge_request) + if not message_id: + return None + return f"https://bridge.gnosischain.com/bridge-explorer/transaction/0x{message_id.hex()}" + class NativeBridgeProvider(BridgeProvider): """Native bridge provider""" @@ -663,20 +695,9 @@ def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: high = mid - 1 return best - def _get_explorer_link(self, bridge_request: BridgeRequest) -> str: + def _get_explorer_link(self, bridge_request: BridgeRequest) -> t.Optional[str]: """Get the explorer link for a transaction.""" - - if isinstance(self.bridge_contract_adaptor, OmnibridgeContractAdaptor): - from_ledger_api = self._from_ledger_api(bridge_request) - message_id = self.bridge_contract_adaptor.get_message_id( - from_ledger_api, bridge_request - ) - if not message_id: - return "https://bridge.gnosischain.com/bridge-explorer/" - return f"https://bridge.gnosischain.com/bridge-explorer/transaction/0x{message_id.hex()}" - - tx_hash = None - if bridge_request.execution_data and bridge_request.execution_data.from_tx_hash: - tx_hash = bridge_request.execution_data.from_tx_hash - - return f"https://etherscan.io/tx/{tx_hash}" # TODO this bridge should return None here - discuss with FE + from_ledger_api = self._from_ledger_api(bridge_request) + return self.bridge_contract_adaptor.get_explorer_link( + from_ledger_api=from_ledger_api, bridge_request=bridge_request + ) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index be8a44ed6..3fbf5b103 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -345,7 +345,7 @@ def test_bridge_error( ), "Wrong execution data." expected_sj = { "eta": None, - "explorer_link": sj["explorer_link"], + "explorer_link": None, "tx_hash": None, "message": sj["message"], "status": BridgeRequestStatus.EXECUTION_FAILED.value, @@ -610,7 +610,7 @@ def test_bridge_zero( assert MESSAGE_EXECUTION_SKIPPED in sj["message"], "Wrong execution data." expected_sj = { "eta": 0, - "explorer_link": sj["explorer_link"], + "explorer_link": None, "tx_hash": None, # type: ignore "message": sj["message"], "status": BridgeRequestStatus.EXECUTION_DONE.value, From 0c04929d1395a155163183df02b8142c689c5593 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 18:05:25 +0200 Subject: [PATCH 141/173] feat: add last requested bundle id endpoint --- docs/api.md | 17 +++++++++++++++++ operate/bridge/bridge.py | 9 ++++++++- operate/bridge/providers/bridge_provider.py | 2 +- .../bridge/providers/lifi_bridge_provider.py | 2 +- operate/cli.py | 7 +++++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 2d9341d40..f90c9407b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1027,6 +1027,23 @@ Gets the status of a quote bundle. Individual bridge request status: --- +### `GET /api/bridge/last_executed_bundle_id` + +Gets the last executed quote bundle id. + +
+ Response + + ```json + { + "id": "br-52e3f945-b495-49bd-8ea2-cafa67468ed3" + } + ``` + +
+ +--- + ## Unused endpoints ### `POST /api/services/{service}/onchain/deploy` diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index ae4e93783..c608c7918 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -49,7 +49,7 @@ DEFAULT_BUNDLE_VALIDITY_PERIOD = 3 * 60 EXECUTED_BUNDLES_PATH = "executed" -BRIDGE_REQUEST_BUNDLE_PREFIX = "br-" +BRIDGE_REQUEST_BUNDLE_PREFIX = "rb-" @dataclass @@ -93,6 +93,7 @@ class BridgeManagerData(LocalResource): path: Path version: int = 1 last_requested_bundle: t.Optional[BridgeRequestBundle] = None + last_executed_bundle_id: t.Optional[str] = None _file = "bridge.json" @@ -303,8 +304,10 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: requirements = self.bridge_refill_requirements(bundle.requests_params) self.data.last_requested_bundle = None + self.data.last_executed_bundle_id = bundle_id bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json" bundle.path = bundle_path + self._store_data() bundle.store() if requirements["is_refill_required"]: @@ -372,3 +375,7 @@ def quote_bundle(self, bundle: BridgeRequestBundle) -> None: bridge = self._bridge_providers[bridge_request.bridge_provider_id] bridge.quote(bridge_request) bundle.timestamp = int(time.time()) + + def last_executed_bundle_id(self) -> t.Optional[str]: + """Get the last executed bundle id.""" + return self.data.last_executed_bundle_id diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index e92dd0fb7..57ca48d86 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -48,7 +48,7 @@ PLACEHOLDER_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" # nosec DEFAULT_MAX_QUOTE_RETRIES = 3 -BRIDGE_REQUEST_PREFIX = "b-" +BRIDGE_REQUEST_PREFIX = "r-" MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." MESSAGE_EXECUTION_SKIPPED = "Execution skipped." MESSAGE_EXECUTION_FAILED = "Execution failed:" diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index c39e4524b..352faaeee 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -368,4 +368,4 @@ def _get_explorer_link(self, bridge_request: BridgeRequest) -> t.Optional[str]: if not tx_hash: return None - return f"https://scan.li.fi/tx/{tx_hash}" + return f"https://scan.li.fi/tx/S" diff --git a/operate/cli.py b/operate/cli.py index b5954d59b..7c2d14e5f 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -1007,6 +1007,13 @@ async def _bridge_execute(request: Request) -> JSONResponse: status_code=HTTPStatus.INTERNAL_SERVER_ERROR, ) + @app.get("/api/bridge/last_executed_bundle_id") + @with_retries + async def _bridge_last_executed_bundle_id(request: Request) -> t.List[t.Dict]: + """Get last executed bundle id.""" + content = {"id": operate.bridge_manager().last_executed_bundle_id()} + return JSONResponse(content=content, status_code=HTTPStatus.OK) + @app.get("/api/bridge/status/{id}") @with_retries async def _bridge_status(request: Request) -> JSONResponse: From 6a41fdd2eb128e9f345541cfb8eb67ba1e107d70 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 18:07:59 +0200 Subject: [PATCH 142/173] fix: explorer link --- operate/bridge/providers/lifi_bridge_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 352faaeee..c39e4524b 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -368,4 +368,4 @@ def _get_explorer_link(self, bridge_request: BridgeRequest) -> t.Optional[str]: if not tx_hash: return None - return f"https://scan.li.fi/tx/S" + return f"https://scan.li.fi/tx/{tx_hash}" From c737adcf63bc89d21141a505fcdce53fead4df65 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 21:26:08 +0200 Subject: [PATCH 143/173] chore: fix --- .../providers/native_bridge_provider.py | 12 ++ tests/test_bridge_bridge.py | 186 +++++++++++++++++- 2 files changed, 196 insertions(+), 2 deletions(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 8d07cd285..f066ee5a5 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -76,6 +76,9 @@ def can_handle_request(cls, params: t.Dict) -> bool: if (from_chain, to_chain) not in cls.BRIDGE_PARAMS: return False + if from_token == ZERO_ADDRESS: + return True + bridge_params = cls.BRIDGE_PARAMS[(from_chain, to_chain)] if from_token not in bridge_params["supported_from_tokens"]: @@ -304,6 +307,15 @@ class OmnibridgeContractAdaptor(BridgeContractAdaptor): ), ) + @classmethod + def can_handle_request(cls, params: t.Dict) -> bool: + """Returns 'true' if the contract adaptor can handle a request for 'params'.""" + from_token = Web3.to_checksum_address(params["from"]["token"]) + if from_token == ZERO_ADDRESS: + return False + + return super().can_handle_request(params) + @classmethod def build_bridge_tx( cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index b920d8171..cc1bb51bb 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -22,19 +22,30 @@ import os import time +import typing as t from pathlib import Path import pytest from deepdiff import DeepDiff from operate.bridge.providers.bridge_provider import ( + BridgeProvider, BridgeRequestStatus, MESSAGE_QUOTE_ZERO, ) -from operate.bridge.providers.lifi_bridge_provider import LIFI_DEFAULT_ETA +from operate.bridge.providers.lifi_bridge_provider import ( + LIFI_DEFAULT_ETA, + LiFiBridgeProvider, +) +from operate.bridge.providers.native_bridge_provider import ( + BridgeContractAdaptor, + NativeBridgeProvider, + OmnibridgeContractAdaptor, + OptimismContractAdaptor, +) from operate.cli import OperateApp from operate.constants import ZERO_ADDRESS -from operate.ledger.profiles import OLAS +from operate.ledger.profiles import OLAS, USDC from operate.operate_types import Chain, LedgerType @@ -387,3 +398,174 @@ def test_bundle_quote( print(diff) assert not diff, "Wrong bridge refill requirements." + + @pytest.mark.parametrize( + ( + "from_chain", + "from_token", + "to_chain", + "to_token", + "expected_provider_cls", + "expected_contract_adaptor_cls", + ), + [ + # Base + ( + Chain.ETHEREUM.value, + ZERO_ADDRESS, + "base", + ZERO_ADDRESS, + NativeBridgeProvider, + OptimismContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + OLAS[Chain.ETHEREUM], + "base", + OLAS[Chain.BASE], + NativeBridgeProvider, + OptimismContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + USDC[Chain.ETHEREUM], + "base", + USDC[Chain.BASE], + NativeBridgeProvider, + OptimismContractAdaptor, + ), + # Mode + ( + Chain.ETHEREUM.value, + ZERO_ADDRESS, + Chain.MODE.value, + ZERO_ADDRESS, + NativeBridgeProvider, + OptimismContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + OLAS[Chain.ETHEREUM], + Chain.MODE.value, + OLAS[Chain.MODE], + NativeBridgeProvider, + OptimismContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + USDC[Chain.ETHEREUM], + Chain.MODE.value, + USDC[Chain.MODE], + NativeBridgeProvider, + OptimismContractAdaptor, + ), + # Optimism + ( + Chain.ETHEREUM.value, + ZERO_ADDRESS, + Chain.OPTIMISTIC.value, + ZERO_ADDRESS, + NativeBridgeProvider, + OptimismContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + OLAS[Chain.ETHEREUM], + Chain.OPTIMISTIC.value, + OLAS[Chain.OPTIMISTIC], + NativeBridgeProvider, + OptimismContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + USDC[Chain.ETHEREUM], + Chain.OPTIMISTIC.value, + USDC[Chain.OPTIMISTIC], + NativeBridgeProvider, + OptimismContractAdaptor, + ), + # Gnosis + ( + Chain.ETHEREUM.value, + ZERO_ADDRESS, + Chain.GNOSIS.value, + ZERO_ADDRESS, + LiFiBridgeProvider, + None, + ), + ( + Chain.ETHEREUM.value, + OLAS[Chain.ETHEREUM], + Chain.GNOSIS.value, + OLAS[Chain.GNOSIS], + NativeBridgeProvider, + OmnibridgeContractAdaptor, + ), + ( + Chain.ETHEREUM.value, + USDC[Chain.ETHEREUM], + Chain.GNOSIS.value, + USDC[Chain.GNOSIS], + NativeBridgeProvider, + OmnibridgeContractAdaptor, + ), + ], + ) + def test_correct_providers( + self, + tmp_path: Path, + password: str, + from_chain: str, + from_token: str, + to_chain: str, + to_token: str, + expected_provider_cls: t.Type[BridgeProvider], + expected_contract_adaptor_cls: t.Type[BridgeContractAdaptor], + ) -> None: + """test_correct_providers""" + operate = OperateApp( + home=tmp_path / OPERATE, + ) + operate.setup() + operate.create_user_account(password=password) + operate.password = password + operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) + bridge_manager = operate.bridge_manager() + + wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + params = [ + { + "from": { + "chain": from_chain, + "address": wallet_address, + "token": from_token, + }, + "to": { + "chain": to_chain, + "address": wallet_address, + "token": to_token, + "amount": 0, + }, + }, + ] + + bundle = bridge_manager.data.last_requested_bundle + assert bundle is None, "Wrong bundle." + bridge_manager.bridge_refill_requirements( + requests_params=params, force_update=False + ) + + bundle = bridge_manager.data.last_requested_bundle + assert bundle is not None, "Wrong bundle." + assert len(bundle.bridge_requests) == 1, "Wrong bundle." + bridge_request = bundle.bridge_requests[0] + bridge = bridge_manager._bridge_providers[bridge_request.bridge_provider_id] + + assert isinstance( + bridge, expected_provider_cls + ), f"Expected provider {expected_provider_cls}, got {type(bridge)}" + + if isinstance(bridge, NativeBridgeProvider): + assert isinstance( + bridge.bridge_contract_adaptor, expected_contract_adaptor_cls + ), f"Expected adaptor {expected_contract_adaptor_cls}, got {type(bridge.contract_adaptor)}" From 7a0fdd520292effb18617b4bb305ed90fced3632 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 20 May 2025 21:57:03 +0200 Subject: [PATCH 144/173] fix: linters --- tests/test_bridge_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index cc1bb51bb..becbc024a 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -568,4 +568,4 @@ def test_correct_providers( if isinstance(bridge, NativeBridgeProvider): assert isinstance( bridge.bridge_contract_adaptor, expected_contract_adaptor_cls - ), f"Expected adaptor {expected_contract_adaptor_cls}, got {type(bridge.contract_adaptor)}" + ), f"Expected adaptor {expected_contract_adaptor_cls}, got {type(bridge.bridge_contract_adaptor)}" From 1d9329392e0baa2ce6510464d90ef173446e843a Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 21 May 2025 18:21:52 +0200 Subject: [PATCH 145/173] chore: update --- operate/bridge/bridge.py | 100 +++++++---- .../providers/native_bridge_provider.py | 168 ++++++------------ 2 files changed, 123 insertions(+), 145 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index c608c7918..106b5d733 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -52,6 +52,42 @@ BRIDGE_REQUEST_BUNDLE_PREFIX = "rb-" +BRIDGE_CONFIGS = { + "native-ethereum-to-base": { + "from_chain": "ethereum", + "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + "to_chain": "base", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 300, + "bridge_contract_adaptor_class": OptimismContractAdaptor, + }, + "native-ethereum-to-mode": { + "from_chain": "ethereum", + "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21", + "to_chain": "mode", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 300, + "bridge_contract_adaptor_class": OptimismContractAdaptor, + }, + "native-ethereum-to-optimism": { + "from_chain": "ethereum", + "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", + "to_chain": "optimistic", + "to_bridge": "0x4200000000000000000000000000000000000010", + "bridge_eta": 300, + "bridge_contract_adaptor_class": OptimismContractAdaptor, + }, + "native-ethereum-to-gnosis": { + "from_chain": "ethereum", + "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671", + "to_chain": "gnosis", + "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d", + "bridge_eta": 1800, + "bridge_contract_adaptor_class": OmnibridgeContractAdaptor, + }, +} + + @dataclass class BridgeRequestBundle(LocalResource): """BridgeRequestBundle""" @@ -125,8 +161,6 @@ def load(cls, path: Path) -> "LocalResource": class BridgeManager: """BridgeManager""" - _bridge_providers: t.Dict[str, BridgeProvider] - def __init__( self, path: Path, @@ -144,22 +178,26 @@ def __init__( self.data: BridgeManagerData = cast( BridgeManagerData, BridgeManagerData.load(path) ) - self._bridge_providers = { - "LiFiBridgeProvider": LiFiBridgeProvider( - wallet_manager, "LiFiBridgeProvider", logger - ), - "NativeBridgeProvider.Optimism": NativeBridgeProvider( - OptimismContractAdaptor(), - "NativeBridgeProvider.Optimism", - wallet_manager, - logger, - ), - "NativeBridgeProvider.Omnibridge": NativeBridgeProvider( - OmnibridgeContractAdaptor(), - "NativeBridgeProvider.Omnibridge", + self._fallback_bridge_provider = LiFiBridgeProvider( + provider_id="LiFiBridgeProvider", + wallet_manager=wallet_manager, + logger=logger, + ) + + self._native_bridge_providers = { + bridge_id: NativeBridgeProvider( + config["bridge_contract_adaptor_class"]( + from_chain=config["from_chain"], + to_chain=config["to_chain"], + from_bridge=config["from_bridge"], + to_bridge=config["to_bridge"], + bridge_eta=config["bridge_eta"], + ), + bridge_id, wallet_manager, logger, - ), + ) + for bridge_id, config in BRIDGE_CONFIGS.items() } def _store_data(self) -> None: @@ -193,22 +231,17 @@ def _get_updated_bundle( if not bundle or create_new_bundle: self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.") - bridge_providers = [ # Sorted in order of preference - self._bridge_providers["NativeBridgeProvider.Optimism"], - self._bridge_providers["NativeBridgeProvider.Omnibridge"], - self._bridge_providers["LiFiBridgeProvider"], - ] bridge_requests = [] for params in requests_params: - for bridge_provider in bridge_providers: + for bridge_provider in self._native_bridge_providers.values(): if bridge_provider.can_handle_request(params): bridge_requests.append( bridge_provider.create_request(params=params) ) break else: - raise RuntimeError( - f"Cannot find an appropriate bridge provider for params {params}." + bridge_requests.append( + self._fallback_bridge_provider.create_request(params=params) ) bundle = BridgeRequestBundle( @@ -318,7 +351,7 @@ def execute_bundle(self, bundle_id: str) -> t.Dict: self.logger.info("[BRIDGE MANAGER] Executing quotes.") for request in bundle.bridge_requests: - bridge = self._bridge_providers[request.bridge_provider_id] + bridge = self._get_bridge_provider(request) bridge.execute(request) self._store_data() @@ -347,7 +380,7 @@ def get_status_json(self, bundle_id: str) -> t.Dict: bridge_request_status = [] for request in bundle.bridge_requests: - bridge = self._bridge_providers[request.bridge_provider_id] + bridge = self._get_bridge_provider(request) bridge_request_status.append(bridge.status_json(request)) updated_status = [request.status for request in bundle.bridge_requests] @@ -360,19 +393,26 @@ def get_status_json(self, bundle_id: str) -> t.Dict: "bridge_request_status": bridge_request_status, } + def _get_bridge_provider(self, request: BridgeRequest) -> BridgeProvider: + bridge = self._native_bridge_providers.get(request.bridge_provider_id, None) + if bridge: + return bridge + + return self._fallback_bridge_provider + def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: """Sum bridge requirements.""" requirements = [] - for request in bundle.bridge_requests: - bridge = self._bridge_providers[request.bridge_provider_id] - requirements.append(bridge.bridge_requirements(request)) + for bridge_request in bundle.bridge_requests: + bridge = self._get_bridge_provider(bridge_request) + requirements.append(bridge.bridge_requirements(bridge_request)) return merge_sum_dicts(*requirements) def quote_bundle(self, bundle: BridgeRequestBundle) -> None: """Update the bundle with the quotes.""" for bridge_request in bundle.bridge_requests: - bridge = self._bridge_providers[bridge_request.bridge_provider_id] + bridge = self._get_bridge_provider(bridge_request) bridge.quote(bridge_request) bundle.timestamp = int(time.time()) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index f066ee5a5..3068f2e8c 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -63,26 +63,43 @@ class BridgeContractAdaptor(ABC): """Adaptor class for bridge contract packages.""" - BRIDGE_PARAMS: t.Dict + # BRIDGE_PARAMS: t.Dict - @classmethod - def can_handle_request(cls, params: t.Dict) -> bool: + def __init__( + self, + from_chain: str, + from_bridge: str, + to_chain: str, + to_bridge: str, + bridge_eta: int, + ) -> None: + super().__init__() + self.from_chain = from_chain + self.from_bridge = from_bridge + self.to_chain = to_chain + self.to_bridge = to_bridge + self.bridge_eta = bridge_eta + + # def can_handle_request(self, from_chain: str, from_token:str, to_chain:str, to_token: str) -> bool: + # return True + + def can_handle_request(self, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" from_chain = Chain(params["from"]["chain"]) from_token = Web3.to_checksum_address(params["from"]["token"]) to_chain = Chain(params["to"]["chain"]) to_token = Web3.to_checksum_address(params["to"]["token"]) - if (from_chain, to_chain) not in cls.BRIDGE_PARAMS: - return False + # if (from_chain, to_chain) not in self.BRIDGE_PARAMS: + # return False if from_token == ZERO_ADDRESS: return True - bridge_params = cls.BRIDGE_PARAMS[(from_chain, to_chain)] + # bridge_params = self.BRIDGE_PARAMS[(from_chain, to_chain)] - if from_token not in bridge_params["supported_from_tokens"]: - return False + # if from_token not in bridge_params["supported_from_tokens"]: + # return False for token_map in ERC20_TOKENS: if ( @@ -95,18 +112,16 @@ def can_handle_request(cls, params: t.Dict) -> bool: return False - @classmethod @abstractmethod def build_bridge_tx( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge transaction.""" raise NotImplementedError() - @classmethod @abstractmethod def find_bridge_finalized_tx( - cls, + self, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -116,10 +131,9 @@ def find_bridge_finalized_tx( """Return the transaction hash of the event indicating bridge completion.""" raise NotImplementedError() - @classmethod @abstractmethod def get_explorer_link( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> t.Optional[str]: """Get the explorer link for a transaction.""" raise NotImplementedError() @@ -128,42 +142,6 @@ def get_explorer_link( class OptimismContractAdaptor(BridgeContractAdaptor): """Adaptor class for Optimism contract packages.""" - BRIDGE_PARAMS: t.Dict[t.Any, t.Dict[str, t.Any]] = { - (Chain.ETHEREUM, Chain.BASE): { - "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", - "to_bridge": "0x4200000000000000000000000000000000000010", - "bridge_eta": 5 * 60, - "supported_from_tokens": ( - ZERO_ADDRESS, - WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], - OLAS[Chain.ETHEREUM], - USDC[Chain.ETHEREUM], - ), - }, - (Chain.ETHEREUM, Chain.MODE): { - "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21", - "to_bridge": "0x4200000000000000000000000000000000000010", - "bridge_eta": 5 * 60, - "supported_from_tokens": ( - ZERO_ADDRESS, - WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], - OLAS[Chain.ETHEREUM], - USDC[Chain.ETHEREUM], - ), - }, - (Chain.ETHEREUM, Chain.OPTIMISTIC): { - "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", - "to_bridge": "0x4200000000000000000000000000000000000010", - "bridge_eta": 5 * 60, - "supported_from_tokens": ( - ZERO_ADDRESS, - WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], - OLAS[Chain.ETHEREUM], - USDC[Chain.ETHEREUM], - ), - }, - } - _l1_standard_bridge_contract = t.cast( L1StandardBridge, L1StandardBridge.from_dir( @@ -177,25 +155,20 @@ class OptimismContractAdaptor(BridgeContractAdaptor): ), ) - @classmethod def build_bridge_tx( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge transaction.""" - from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] + from_bridge = self.from_bridge extra_data = Web3.keccak(text=bridge_request.id) if from_token == ZERO_ADDRESS: - return cls._l1_standard_bridge_contract.build_bridge_eth_to_tx( + return self._l1_standard_bridge_contract.build_bridge_eth_to_tx( ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, @@ -205,7 +178,7 @@ def build_bridge_tx( extra_data=extra_data, ) - return cls._l1_standard_bridge_contract.build_bridge_erc20_to_tx( + return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx( ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, @@ -217,9 +190,8 @@ def build_bridge_tx( extra_data=extra_data, ) - @classmethod def find_bridge_finalized_tx( - cls, + self, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -227,18 +199,16 @@ def find_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating bridge completion.""" - from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))]["to_bridge"] + to_bridge = self.to_bridge extra_data = Web3.keccak(text=bridge_request.id) if from_token == ZERO_ADDRESS: - return cls._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( + return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, from_=from_address, @@ -249,7 +219,7 @@ def find_bridge_finalized_tx( to_block=to_block, ) - return cls._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( + return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx( ledger_api=to_ledger_api, contract_address=to_bridge, local_token=to_token, @@ -262,9 +232,8 @@ def find_bridge_finalized_tx( to_block=to_block, ) - @classmethod def get_explorer_link( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> t.Optional[str]: """Get the explorer link for a transaction.""" if not bridge_request.execution_data: @@ -280,19 +249,6 @@ def get_explorer_link( class OmnibridgeContractAdaptor(BridgeContractAdaptor): """Adaptor class for Omnibridge contract packages.""" - BRIDGE_PARAMS: t.Dict[t.Any, t.Dict[str, t.Any]] = { - (Chain.ETHEREUM, Chain.GNOSIS): { - "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671", - "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d", - "bridge_eta": 30 * 60, - "supported_from_tokens": ( - WRAPPED_NATIVE_ASSET[Chain.ETHEREUM], - OLAS[Chain.ETHEREUM], - USDC[Chain.ETHEREUM], - ), - }, - } - _foreign_omnibridge = t.cast( ForeignOmnibridge, ForeignOmnibridge.from_dir( @@ -307,8 +263,7 @@ class OmnibridgeContractAdaptor(BridgeContractAdaptor): ), ) - @classmethod - def can_handle_request(cls, params: t.Dict) -> bool: + def can_handle_request(self, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" from_token = Web3.to_checksum_address(params["from"]["token"]) if from_token == ZERO_ADDRESS: @@ -316,27 +271,22 @@ def can_handle_request(cls, params: t.Dict) -> bool: return super().can_handle_request(params) - @classmethod def build_bridge_tx( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: """Build bridge transaction.""" - from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] + from_bridge = self.from_bridge if from_token == ZERO_ADDRESS: raise NotImplementedError( - f"{cls.__class__.__name__} does not support bridge native tokens." + f"{self.__class__.__name__} does not support bridge native tokens." ) - return cls._foreign_omnibridge.build_relay_tokens_tx( + return self._foreign_omnibridge.build_relay_tokens_tx( ledger_api=from_ledger_api, contract_address=from_bridge, sender=from_address, @@ -345,9 +295,8 @@ def build_bridge_tx( amount=to_amount, ) - @classmethod def find_bridge_finalized_tx( - cls, + self, from_ledger_api: LedgerApi, to_ledger_api: LedgerApi, bridge_request: BridgeRequest, @@ -355,20 +304,18 @@ def find_bridge_finalized_tx( to_block: BlockIdentifier, ) -> t.Optional[str]: """Return the transaction hash of the event indicating bridge completion.""" - from_chain = bridge_request.params["from"]["chain"] from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] to_address = bridge_request.params["to"]["address"] to_token = bridge_request.params["to"]["token"] to_amount = bridge_request.params["to"]["amount"] - to_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))]["to_bridge"] + to_bridge = self.to_bridge if from_token == ZERO_ADDRESS: raise NotImplementedError( - f"{cls.__class__.__name__} does not support bridge native tokens." + f"{self.__class__.__name__} does not support bridge native tokens." ) - message_id = cls.get_message_id( + message_id = self.get_message_id( from_ledger_api=from_ledger_api, bridge_request=bridge_request, ) @@ -378,7 +325,7 @@ def find_bridge_finalized_tx( f"Cannot find 'messageId' for bridge request {bridge_request.id}." ) - return cls._home_omnibridge.find_tokens_bridged_tx( + return self._home_omnibridge.find_tokens_bridged_tx( ledger_api=to_ledger_api, contract_address=to_bridge, token=to_token, @@ -389,9 +336,8 @@ def find_bridge_finalized_tx( to_block=to_block, ) - @classmethod def get_message_id( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> t.Optional[bytes]: """Get the bridge message id.""" if not bridge_request.execution_data: @@ -412,11 +358,9 @@ def get_message_id( from_tx_hash = bridge_request.execution_data.from_tx_hash to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = cls.BRIDGE_PARAMS[(Chain(from_chain), Chain(to_chain))][ - "from_bridge" - ] + from_bridge = self.from_bridge - message_id = cls._foreign_omnibridge.get_tokens_bridging_initiated_message_id( + message_id = self._foreign_omnibridge.get_tokens_bridging_initiated_message_id( ledger_api=from_ledger_api, contract_address=from_bridge, tx_hash=from_tx_hash, @@ -494,9 +438,7 @@ def quote(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - bridge_eta = self.bridge_contract_adaptor.BRIDGE_PARAMS[ - (Chain(from_chain), Chain(to_chain)) - ]["bridge_eta"] + bridge_eta = self.bridge_contract_adaptor.bridge_eta message = None if to_amount == 0: @@ -547,9 +489,7 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: from_token = bridge_request.params["from"]["token"] to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] - from_bridge = self.bridge_contract_adaptor.BRIDGE_PARAMS[ - (Chain(from_chain), Chain(to_chain)) - ]["from_bridge"] + from_bridge = self.bridge_contract_adaptor.from_bridge from_ledger_api = self._from_ledger_api(bridge_request) if from_token == ZERO_ADDRESS: @@ -625,9 +565,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_chain = bridge_request.params["from"]["chain"] to_chain = bridge_request.params["to"]["chain"] - bridge_eta = self.bridge_contract_adaptor.BRIDGE_PARAMS[ - (Chain(from_chain), Chain(to_chain)) - ]["bridge_eta"] + bridge_eta = self.bridge_contract_adaptor.bridge_eta try: from_ledger_api = self._from_ledger_api(bridge_request) From 52841c1e060f9c50661c97e991aa3bef1fff9d51 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 21 May 2025 19:17:07 +0200 Subject: [PATCH 146/173] chore: update --- operate/bridge/bridge.py | 2 +- .../providers/native_bridge_provider.py | 45 +++++++------------ tests/test_bridge_bridge.py | 6 +-- tests/test_bridge_providers.py | 40 ++++++++++++----- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 106b5d733..32161e9fd 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -52,7 +52,7 @@ BRIDGE_REQUEST_BUNDLE_PREFIX = "rb-" -BRIDGE_CONFIGS = { +BRIDGE_CONFIGS: t.Dict[str, t.Any] = { "native-ethereum-to-base": { "from_chain": "ethereum", "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 3068f2e8c..5da509443 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -52,7 +52,7 @@ L1StandardBridge, ) from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge -from operate.ledger.profiles import ERC20_TOKENS, OLAS, USDC, WRAPPED_NATIVE_ASSET +from operate.ledger.profiles import ERC20_TOKENS from operate.operate_types import Chain from operate.wallet.master import MasterWalletManager @@ -63,8 +63,6 @@ class BridgeContractAdaptor(ABC): """Adaptor class for bridge contract packages.""" - # BRIDGE_PARAMS: t.Dict - def __init__( self, from_chain: str, @@ -73,6 +71,7 @@ def __init__( to_bridge: str, bridge_eta: int, ) -> None: + """Initialize the bridge contract adaptor.""" super().__init__() self.from_chain = from_chain self.from_bridge = from_bridge @@ -80,33 +79,28 @@ def __init__( self.to_bridge = to_bridge self.bridge_eta = bridge_eta - # def can_handle_request(self, from_chain: str, from_token:str, to_chain:str, to_token: str) -> bool: - # return True - def can_handle_request(self, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" - from_chain = Chain(params["from"]["chain"]) + from_chain = params["from"]["chain"] from_token = Web3.to_checksum_address(params["from"]["token"]) - to_chain = Chain(params["to"]["chain"]) + to_chain = params["to"]["chain"] to_token = Web3.to_checksum_address(params["to"]["token"]) - # if (from_chain, to_chain) not in self.BRIDGE_PARAMS: - # return False + if from_chain != self.from_chain: + return False + + if to_chain != self.to_chain: + return False if from_token == ZERO_ADDRESS: return True - # bridge_params = self.BRIDGE_PARAMS[(from_chain, to_chain)] - - # if from_token not in bridge_params["supported_from_tokens"]: - # return False - for token_map in ERC20_TOKENS: if ( - from_chain in token_map - and to_chain in token_map - and token_map[from_chain].lower() == from_token.lower() - and token_map[to_chain].lower() == to_token.lower() + Chain(from_chain) in token_map + and Chain(to_chain) in token_map + and token_map[Chain(from_chain)].lower() == from_token.lower() + and token_map[Chain(to_chain)].lower() == to_token.lower() ): return True @@ -352,11 +346,9 @@ def get_message_id( ): return bridge_request.execution_data.provider_data.get("message_id", None) - from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] from_tx_hash = bridge_request.execution_data.from_tx_hash - to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] from_bridge = self.from_bridge @@ -375,12 +367,11 @@ def get_message_id( bridge_request.execution_data.provider_data["message_id"] = message_id return message_id - @classmethod def get_explorer_link( - cls, from_ledger_api: LedgerApi, bridge_request: BridgeRequest + self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> t.Optional[str]: """Get the explorer link for a transaction.""" - message_id = cls.get_message_id(from_ledger_api, bridge_request) + message_id = self.get_message_id(from_ledger_api, bridge_request) if not message_id: return None return f"https://bridge.gnosischain.com/bridge-explorer/transaction/0x{message_id.hex()}" @@ -435,8 +426,6 @@ def quote(self, bridge_request: BridgeRequest) -> None: f"Cannot quote bridge request {bridge_request.id}: execution already present." ) - from_chain = bridge_request.params["from"]["chain"] - to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] bridge_eta = self.bridge_contract_adaptor.bridge_eta @@ -484,10 +473,8 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: if not quote_data: return None - from_chain = bridge_request.params["from"]["chain"] from_address = bridge_request.params["from"]["address"] from_token = bridge_request.params["from"]["token"] - to_chain = bridge_request.params["to"]["chain"] to_amount = bridge_request.params["to"]["amount"] from_bridge = self.bridge_contract_adaptor.from_bridge from_ledger_api = self._from_ledger_api(bridge_request) @@ -563,8 +550,6 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: bridge_request.status = BridgeRequestStatus.EXECUTION_FAILED return - from_chain = bridge_request.params["from"]["chain"] - to_chain = bridge_request.params["to"]["chain"] bridge_eta = self.bridge_contract_adaptor.bridge_eta try: diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index becbc024a..f0f6cced6 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -333,12 +333,12 @@ def test_bundle_quote( assert bundle is not None, "Unexpected bundle." request = bundle.bridge_requests[0] - bridge = bridge_manager._bridge_providers[request.bridge_provider_id] + bridge = bridge_manager._get_bridge_provider(request) assert ( len(bridge._get_transactions(request)) == 1 ), "Wrong number of transactions." request = bundle.bridge_requests[1] - bridge = bridge_manager._bridge_providers[request.bridge_provider_id] + bridge = bridge_manager._get_bridge_provider(request) assert ( len(bridge._get_transactions(request)) == 2 ), "Wrong number of transactions." @@ -559,7 +559,7 @@ def test_correct_providers( assert bundle is not None, "Wrong bundle." assert len(bundle.bridge_requests) == 1, "Wrong bundle." bridge_request = bundle.bridge_requests[0] - bridge = bridge_manager._bridge_providers[bridge_request.bridge_provider_id] + bridge = bridge_manager._get_bridge_provider(bridge_request) assert isinstance( bridge, expected_provider_cls diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 3fbf5b103..f93cb4c5c 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -30,6 +30,7 @@ from web3 import Web3 from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, + BRIDGE_CONFIGS, BridgeRequest, LiFiBridgeProvider, ) @@ -508,9 +509,16 @@ def test_bridge_zero( }, } + bridge_key = "native-ethereum-to-base" bridge = NativeBridgeProvider( provider_id="NativeBridgeProvider", - bridge_contract_adaptor=OptimismContractAdaptor(), + bridge_contract_adaptor=OptimismContractAdaptor( + from_chain=BRIDGE_CONFIGS[bridge_key]["from_chain"], + from_bridge=BRIDGE_CONFIGS[bridge_key]["from_bridge"], + to_chain=BRIDGE_CONFIGS[bridge_key]["to_chain"], + to_bridge=BRIDGE_CONFIGS[bridge_key]["to_bridge"], + bridge_eta=BRIDGE_CONFIGS[bridge_key]["bridge_eta"], + ), wallet_manager=operate.wallet_manager, ) @@ -655,9 +663,16 @@ def test_bridge_execute_error( }, } + bridge_key = "native-ethereum-to-base" bridge = NativeBridgeProvider( provider_id="NativeBridgeProvider", - bridge_contract_adaptor=OptimismContractAdaptor(), + bridge_contract_adaptor=OptimismContractAdaptor( + from_chain=BRIDGE_CONFIGS[bridge_key]["from_chain"], + from_bridge=BRIDGE_CONFIGS[bridge_key]["from_bridge"], + to_chain=BRIDGE_CONFIGS[bridge_key]["to_chain"], + to_bridge=BRIDGE_CONFIGS[bridge_key]["to_bridge"], + bridge_eta=BRIDGE_CONFIGS[bridge_key]["bridge_eta"], + ), wallet_manager=operate.wallet_manager, ) @@ -697,9 +712,7 @@ def test_bridge_execute_error( # Quote expected_quote_data = QuoteData( - bridge_eta=bridge.bridge_contract_adaptor.BRIDGE_PARAMS[ - Chain.ETHEREUM, Chain.BASE - ]["bridge_eta"], + bridge_eta=bridge.bridge_contract_adaptor.bridge_eta, elapsed_time=0, message=None, provider_data=None, @@ -715,9 +728,7 @@ def test_bridge_execute_error( assert bridge_request == expected_request, "Wrong bridge request." sj = bridge.status_json(bridge_request) expected_sj = { - "eta": bridge.bridge_contract_adaptor.BRIDGE_PARAMS[ - Chain.ETHEREUM, Chain.BASE - ]["bridge_eta"], + "eta": bridge.bridge_contract_adaptor.bridge_eta, "message": None, "status": BridgeRequestStatus.QUOTE_DONE.value, } @@ -772,9 +783,7 @@ def test_bridge_execute_error( sj = bridge.status_json(bridge_request) assert MESSAGE_EXECUTION_FAILED in sj["message"], "Wrong execution data." expected_sj = { - "eta": bridge.bridge_contract_adaptor.BRIDGE_PARAMS[ - Chain.ETHEREUM, Chain.BASE - ]["bridge_eta"], + "eta": bridge.bridge_contract_adaptor.bridge_eta, "explorer_link": sj["explorer_link"], "tx_hash": None, # type: ignore "message": sj["message"], @@ -1082,9 +1091,16 @@ def test_update_execution_status( operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) if contract_adaptor_class is not None: + bridge_key = "native-ethereum-to-base" bridge: BridgeProvider = NativeBridgeProvider( provider_id="NativeBridgeProvider", - bridge_contract_adaptor=contract_adaptor_class(), + bridge_contract_adaptor=OptimismContractAdaptor( + from_chain=BRIDGE_CONFIGS[bridge_key]["from_chain"], + from_bridge=BRIDGE_CONFIGS[bridge_key]["from_bridge"], + to_chain=BRIDGE_CONFIGS[bridge_key]["to_chain"], + to_bridge=BRIDGE_CONFIGS[bridge_key]["to_bridge"], + bridge_eta=BRIDGE_CONFIGS[bridge_key]["bridge_eta"], + ), wallet_manager=operate.wallet_manager, ) else: From 0779c5c1933b598dc27c67c193877da5476c6542 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 21 May 2025 19:27:37 +0200 Subject: [PATCH 147/173] fix: tests --- tests/test_bridge_providers.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index f93cb4c5c..2e0d14967 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -1091,15 +1091,17 @@ def test_update_execution_status( operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) if contract_adaptor_class is not None: - bridge_key = "native-ethereum-to-base" + from_chain = params["from"]["chain"] + to_chain = params["to"]["chain"] + provider_id = f"native-{from_chain}-to-{to_chain}" bridge: BridgeProvider = NativeBridgeProvider( - provider_id="NativeBridgeProvider", - bridge_contract_adaptor=OptimismContractAdaptor( - from_chain=BRIDGE_CONFIGS[bridge_key]["from_chain"], - from_bridge=BRIDGE_CONFIGS[bridge_key]["from_bridge"], - to_chain=BRIDGE_CONFIGS[bridge_key]["to_chain"], - to_bridge=BRIDGE_CONFIGS[bridge_key]["to_bridge"], - bridge_eta=BRIDGE_CONFIGS[bridge_key]["bridge_eta"], + provider_id=provider_id, + bridge_contract_adaptor=contract_adaptor_class( + from_chain=BRIDGE_CONFIGS[provider_id]["from_chain"], + from_bridge=BRIDGE_CONFIGS[provider_id]["from_bridge"], + to_chain=BRIDGE_CONFIGS[provider_id]["to_chain"], + to_bridge=BRIDGE_CONFIGS[provider_id]["to_bridge"], + bridge_eta=BRIDGE_CONFIGS[provider_id]["bridge_eta"], ), wallet_manager=operate.wallet_manager, ) From 3ecab40821ce4e09ce62fce8aeb4bfb17ca809f1 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 21 May 2025 21:35:08 +0200 Subject: [PATCH 148/173] fix: tests --- tests/test_bridge_providers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 2e0d14967..2760395c4 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -375,9 +375,9 @@ def test_bridge_quote( wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address params = { "from": { - "chain": "gnosis", + "chain": Chain.ETHEREUM.value, "address": wallet_address, - "token": OLAS[Chain.GNOSIS], + "token": ZERO_ADDRESS, }, "to": { "chain": "base", @@ -446,10 +446,9 @@ def test_bridge_quote( quote = bridge_request.quote_data.provider_data.get("response") br = bridge.bridge_requirements(bridge_request) expected_br = { - "gnosis": { + "ethereum": { wallet_address: { - ZERO_ADDRESS: br["gnosis"][wallet_address][ZERO_ADDRESS], - OLAS[Chain.GNOSIS]: int(quote["action"]["fromAmount"]), # type: ignore + ZERO_ADDRESS: br["ethereum"][wallet_address][ZERO_ADDRESS], } } } From bc051d22de3da79782aaddb7190d9b57530641b6 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 21 May 2025 21:47:12 +0200 Subject: [PATCH 149/173] chore: rename bridge configs to native_bridge_configs --- operate/bridge/bridge.py | 4 +-- .../providers/native_bridge_provider.py | 2 +- tests/test_bridge_providers.py | 32 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 32161e9fd..e430ae861 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -52,7 +52,7 @@ BRIDGE_REQUEST_BUNDLE_PREFIX = "rb-" -BRIDGE_CONFIGS: t.Dict[str, t.Any] = { +NATIVE_BRIDGE_CONFIGS: t.Dict[str, t.Any] = { "native-ethereum-to-base": { "from_chain": "ethereum", "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", @@ -197,7 +197,7 @@ def __init__( wallet_manager, logger, ) - for bridge_id, config in BRIDGE_CONFIGS.items() + for bridge_id, config in NATIVE_BRIDGE_CONFIGS.items() } def _store_data(self) -> None: diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 5da509443..69d6e58a2 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -623,7 +623,7 @@ def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int: while low <= high: mid = (low + high) // 2 block = w3.eth.get_block(mid) - if block.timestamp < timestamp: + if block["timestamp"] < timestamp: best = mid low = mid + 1 else: diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 2760395c4..063cb2f99 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -30,9 +30,9 @@ from web3 import Web3 from operate.bridge.bridge import ( # MESSAGE_EXECUTION_SKIPPED,; MESSAGE_QUOTE_ZERO, - BRIDGE_CONFIGS, BridgeRequest, LiFiBridgeProvider, + NATIVE_BRIDGE_CONFIGS, ) from operate.bridge.providers.bridge_provider import ( BridgeProvider, @@ -512,11 +512,11 @@ def test_bridge_zero( bridge = NativeBridgeProvider( provider_id="NativeBridgeProvider", bridge_contract_adaptor=OptimismContractAdaptor( - from_chain=BRIDGE_CONFIGS[bridge_key]["from_chain"], - from_bridge=BRIDGE_CONFIGS[bridge_key]["from_bridge"], - to_chain=BRIDGE_CONFIGS[bridge_key]["to_chain"], - to_bridge=BRIDGE_CONFIGS[bridge_key]["to_bridge"], - bridge_eta=BRIDGE_CONFIGS[bridge_key]["bridge_eta"], + from_chain=NATIVE_BRIDGE_CONFIGS[bridge_key]["from_chain"], + from_bridge=NATIVE_BRIDGE_CONFIGS[bridge_key]["from_bridge"], + to_chain=NATIVE_BRIDGE_CONFIGS[bridge_key]["to_chain"], + to_bridge=NATIVE_BRIDGE_CONFIGS[bridge_key]["to_bridge"], + bridge_eta=NATIVE_BRIDGE_CONFIGS[bridge_key]["bridge_eta"], ), wallet_manager=operate.wallet_manager, ) @@ -666,11 +666,11 @@ def test_bridge_execute_error( bridge = NativeBridgeProvider( provider_id="NativeBridgeProvider", bridge_contract_adaptor=OptimismContractAdaptor( - from_chain=BRIDGE_CONFIGS[bridge_key]["from_chain"], - from_bridge=BRIDGE_CONFIGS[bridge_key]["from_bridge"], - to_chain=BRIDGE_CONFIGS[bridge_key]["to_chain"], - to_bridge=BRIDGE_CONFIGS[bridge_key]["to_bridge"], - bridge_eta=BRIDGE_CONFIGS[bridge_key]["bridge_eta"], + from_chain=NATIVE_BRIDGE_CONFIGS[bridge_key]["from_chain"], + from_bridge=NATIVE_BRIDGE_CONFIGS[bridge_key]["from_bridge"], + to_chain=NATIVE_BRIDGE_CONFIGS[bridge_key]["to_chain"], + to_bridge=NATIVE_BRIDGE_CONFIGS[bridge_key]["to_bridge"], + bridge_eta=NATIVE_BRIDGE_CONFIGS[bridge_key]["bridge_eta"], ), wallet_manager=operate.wallet_manager, ) @@ -1096,11 +1096,11 @@ def test_update_execution_status( bridge: BridgeProvider = NativeBridgeProvider( provider_id=provider_id, bridge_contract_adaptor=contract_adaptor_class( - from_chain=BRIDGE_CONFIGS[provider_id]["from_chain"], - from_bridge=BRIDGE_CONFIGS[provider_id]["from_bridge"], - to_chain=BRIDGE_CONFIGS[provider_id]["to_chain"], - to_bridge=BRIDGE_CONFIGS[provider_id]["to_bridge"], - bridge_eta=BRIDGE_CONFIGS[provider_id]["bridge_eta"], + from_chain=NATIVE_BRIDGE_CONFIGS[provider_id]["from_chain"], + from_bridge=NATIVE_BRIDGE_CONFIGS[provider_id]["from_bridge"], + to_chain=NATIVE_BRIDGE_CONFIGS[provider_id]["to_chain"], + to_bridge=NATIVE_BRIDGE_CONFIGS[provider_id]["to_bridge"], + bridge_eta=NATIVE_BRIDGE_CONFIGS[provider_id]["bridge_eta"], ), wallet_manager=operate.wallet_manager, ) From 44b001228d9e886ad81bc0cd438aab6672a5f035 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 21 May 2025 21:48:50 +0200 Subject: [PATCH 150/173] fix: linters --- tests/test_bridge_providers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_bridge_providers.py b/tests/test_bridge_providers.py index 063cb2f99..ffb81fcf5 100644 --- a/tests/test_bridge_providers.py +++ b/tests/test_bridge_providers.py @@ -443,7 +443,6 @@ def test_bridge_quote( bridge_request.quote_data.provider_data is not None ), "Missing quote data." - quote = bridge_request.quote_data.provider_data.get("response") br = bridge.bridge_requirements(bridge_request) expected_br = { "ethereum": { From 3df6aa5cb4b1c7683b56142821dc27f5d739bb96 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 22 May 2025 16:28:05 +0200 Subject: [PATCH 151/173] refactor: remove _get_transactions --- .../bridge/providers/lifi_bridge_provider.py | 96 +++++++------------ .../providers/native_bridge_provider.py | 60 +++--------- 2 files changed, 48 insertions(+), 108 deletions(-) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index c39e4524b..af0f61fc0 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -185,48 +185,8 @@ def quote(self, bridge_request: BridgeRequest) -> None: time.sleep(2) - def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: - self.logger.info( - f"[LI.FI BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." - ) - - quote_data = bridge_request.quote_data - if not quote_data: - return None - - if not quote_data.provider_data: - return None - - quote = quote_data.provider_data.get("response") - if not quote: - return None - - if "action" not in quote: - return None - - transaction_request = quote.get("transactionRequest") - if not transaction_request: - return None - - from_ledger_api = self._from_ledger_api(bridge_request) - - bridge_tx = { - "value": int(transaction_request["value"], 16), - "to": transaction_request["to"], - "data": transaction_request["data"], # TODO remove bytes? - "from": transaction_request["from"], - "chainId": transaction_request["chainId"], - "gasPrice": int(transaction_request["gasPrice"], 16), - "gas": int(transaction_request["gasLimit"], 16), - "nonce": from_ledger_api.api.eth.get_transaction_count( - transaction_request["from"] - ), - } - BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) - BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) - return bridge_tx - def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + """Get the approve transaction.""" self.logger.info( f"[LI.FI BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." ) @@ -269,37 +229,47 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: approve_tx["gas"] = ceil(approve_tx["gas"] * GAS_ESTIMATE_BUFFER) return approve_tx - def _get_transactions( - self, bridge_request: BridgeRequest - ) -> t.List[t.Tuple[str, t.Dict]]: - """Get the sorted list of transactions to execute the bridge request.""" + def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + """Get the bridge transaction.""" self.logger.info( - f"[LI.FI BRIDGE] Get transactions for bridge request {bridge_request.id}." + f"[LI.FI BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." ) - if not bridge_request.quote_data: - return [] + quote_data = bridge_request.quote_data + if not quote_data: + return None - if bridge_request.params["to"]["amount"] == 0: - return [] + if not quote_data.provider_data: + return None - bridge_tx = self._get_bridge_tx(bridge_request) + quote = quote_data.provider_data.get("response") + if not quote: + return None - if not bridge_tx: - return [] + if "action" not in quote: + return None - approve_tx = self._get_approve_tx(bridge_request) + transaction_request = quote.get("transactionRequest") + if not transaction_request: + return None - if not approve_tx: - return [ - ("bridge_tx", bridge_tx), - ] + from_ledger_api = self._from_ledger_api(bridge_request) - bridge_tx["nonce"] = approve_tx["nonce"] + 1 - return [ - ("approve_tx", approve_tx), - ("bridge_tx", bridge_tx), - ] + bridge_tx = { + "value": int(transaction_request["value"], 16), + "to": transaction_request["to"], + "data": transaction_request["data"], # TODO remove bytes? + "from": transaction_request["from"], + "chainId": transaction_request["chainId"], + "gasPrice": int(transaction_request["gasPrice"], 16), + "gas": int(transaction_request["gasLimit"], 16), + "nonce": from_ledger_api.api.eth.get_transaction_count( + transaction_request["from"] + ), + } + BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) + BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) + return bridge_tx def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 69d6e58a2..4e268d0ca 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -445,26 +445,8 @@ def quote(self, bridge_request: BridgeRequest) -> None: bridge_request.quote_data = quote_data bridge_request.status = BridgeRequestStatus.QUOTE_DONE - def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: - self.logger.info( - f"[NATIVE BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." - ) - - quote_data = bridge_request.quote_data - if not quote_data: - return None - - from_ledger_api = self._from_ledger_api(bridge_request) - bridge_tx = self.bridge_contract_adaptor.build_bridge_tx( - from_ledger_api=from_ledger_api, bridge_request=bridge_request - ) - - BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) - BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) - bridge_tx["gas"] = ceil(bridge_tx["gas"] * GAS_ESTIMATE_BUFFER) - return bridge_tx - def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + """Get the approve transaction.""" self.logger.info( f"[NATIVE BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." ) @@ -495,37 +477,25 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: approve_tx["gas"] = ceil(approve_tx["gas"] * GAS_ESTIMATE_BUFFER) return approve_tx - def _get_transactions( - self, bridge_request: BridgeRequest - ) -> t.List[t.Tuple[str, t.Dict]]: - """Get the sorted list of transactions to execute the bridge request.""" + def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + """Get the bridge transaction.""" self.logger.info( - f"[NATIVE BRIDGE] Get transactions for bridge request {bridge_request.id}." + f"[NATIVE BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." ) - if not bridge_request.quote_data: - return [] - - if bridge_request.params["to"]["amount"] == 0: - return [] - - bridge_tx = self._get_bridge_tx(bridge_request) - - if not bridge_tx: - return [] - - approve_tx = self._get_approve_tx(bridge_request) + quote_data = bridge_request.quote_data + if not quote_data: + return None - if not approve_tx: - return [ - ("bridge_tx", bridge_tx), - ] + from_ledger_api = self._from_ledger_api(bridge_request) + bridge_tx = self.bridge_contract_adaptor.build_bridge_tx( + from_ledger_api=from_ledger_api, bridge_request=bridge_request + ) - bridge_tx["nonce"] = approve_tx["nonce"] + 1 - return [ - ("approve_tx", approve_tx), - ("bridge_tx", bridge_tx), - ] + BridgeProvider._update_with_gas_pricing(bridge_tx, from_ledger_api) + BridgeProvider._update_with_gas_estimate(bridge_tx, from_ledger_api) + bridge_tx["gas"] = ceil(bridge_tx["gas"] * GAS_ESTIMATE_BUFFER) + return bridge_tx def _update_execution_status(self, bridge_request: BridgeRequest) -> None: """Update the execution status. Returns `True` if the status changed.""" From e67d349681df755123059a0f73663ae2b76cbb82 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 22 May 2025 16:45:13 +0200 Subject: [PATCH 152/173] refactor: remove _get_transactions --- operate/bridge/providers/bridge_provider.py | 41 +++++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 57ca48d86..51843d623 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -61,9 +61,7 @@ f"{MESSAGE_EXECUTION_FAILED} transaction settlement failed." ) -ERC20_APPROVE_SELECTOR = ( - "0x095ea7b3" # First4 bytes of Keccak('approve(address,uint256)') -) +ERC20_APPROVE_SELECTOR = "0x095ea7b3" # First 4 bytes of Web3.keccak(text='approve(address,uint256)').hex()[:10] GAS_ESTIMATE_BUFFER = 1.10 @@ -135,7 +133,8 @@ class BridgeProvider(ABC): - description - quote - _update_execution_status - - _get_transactions + - _get_approve_tx + - _get_bridge_tx - _get_explorer_link """ @@ -243,10 +242,13 @@ def quote(self, bridge_request: BridgeRequest) -> None: raise NotImplementedError() @abstractmethod - def _get_transactions( - self, bridge_request: BridgeRequest - ) -> t.List[t.Tuple[str, t.Dict]]: - """Get the sorted list of transactions to execute the bridge request.""" + def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + """Get the approve transaction.""" + raise NotImplementedError() + + @abstractmethod + def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: + """Get the bridge transaction.""" raise NotImplementedError() def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: @@ -262,8 +264,16 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: from_token = bridge_request.params["from"]["token"] from_ledger_api = self._from_ledger_api(bridge_request) - transactions = self._get_transactions(bridge_request) - if not transactions: + txs = [] + + approve_tx = self._get_approve_tx(bridge_request) + if approve_tx: + txs.append(("approve_tx", approve_tx)) + bridge_tx = self._get_bridge_tx(bridge_request) + if bridge_tx: + txs.append(("bridge_tx", bridge_tx)) + + if not txs: return { from_chain: { from_address: { @@ -277,7 +287,7 @@ def bridge_requirements(self, bridge_request: BridgeRequest) -> t.Dict: total_gas_fees = 0 total_token = 0 - for tx_label, tx in transactions: + for tx_label, tx in txs: self.logger.debug( f"[BRIDGE PROVIDER] Processing transaction {tx_label} for bridge request {bridge_request.id}." ) @@ -359,7 +369,14 @@ def execute(self, bridge_request: BridgeRequest) -> None: f"Cannot execute bridge request {bridge_request.id}: execution data already present." ) - txs = self._get_transactions(bridge_request) + txs = [] + + approve_tx = self._get_approve_tx(bridge_request) + if approve_tx: + txs.append(("approve_tx", approve_tx)) + bridge_tx = self._get_bridge_tx(bridge_request) + if bridge_tx: + txs.append(("bridge_tx", bridge_tx)) if not txs: self.logger.info( From f911214eaaf87f531b6dbfd528d781a413ed1d16 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 22 May 2025 16:48:58 +0200 Subject: [PATCH 153/173] chore: fix tests --- tests/test_bridge_bridge.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index f0f6cced6..b5277b000 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -334,14 +334,10 @@ def test_bundle_quote( request = bundle.bridge_requests[0] bridge = bridge_manager._get_bridge_provider(request) - assert ( - len(bridge._get_transactions(request)) == 1 - ), "Wrong number of transactions." + assert bridge._get_approve_tx() is not None, "Wrong number of transactions." request = bundle.bridge_requests[1] bridge = bridge_manager._get_bridge_provider(request) - assert ( - len(bridge._get_transactions(request)) == 2 - ), "Wrong number of transactions." + assert bridge._get_approve_tx() is not None, "Wrong number of transactions." expected_brr = { "id": brr["id"], From f12f44126b05411fd8b32988d7eff7900a476309 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Thu, 22 May 2025 17:12:14 +0200 Subject: [PATCH 154/173] chore: use local TxSettler for bridge module --- operate/bridge/providers/bridge_provider.py | 2 +- operate/bridge/tx.py | 233 ++++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 operate/bridge/tx.py diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 51843d623..05a54d8eb 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -31,9 +31,9 @@ from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger -from autonomy.chain.tx import TxSettler from web3 import Web3 +from operate.bridge.tx import TxSettler from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, ON_CHAIN_INTERACT_SLEEP, diff --git a/operate/bridge/tx.py b/operate/bridge/tx.py new file mode 100644 index 000000000..9901261a4 --- /dev/null +++ b/operate/bridge/tx.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023-2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tx settlement helper.""" + +import time +from datetime import datetime +from typing import Callable, Dict, Optional, cast + +from aea.configurations.data_types import PublicId +from aea.crypto.base import Crypto, LedgerApi +from autonomy.chain.base import registry_contracts +from autonomy.chain.config import ChainType, ContractConfigs +from autonomy.chain.exceptions import ( + ChainInteractionError, + ChainTimeoutError, + RPCError, + TxBuildError, +) +from requests.exceptions import ConnectionError as RequestsConnectionError + + +DEFAULT_ON_CHAIN_INTERACT_TIMEOUT = 60.0 +DEFAULT_ON_CHAIN_INTERACT_RETRIES = 5.0 +DEFAULT_ON_CHAIN_INTERACT_SLEEP = 3.0 + +ERRORS_TO_RETRY = ( + "FeeTooLow", + "wrong transaction nonce", + "INTERNAL_ERROR: nonce too low", + "Got empty transaction", + "AlreadyKnown", + "ALREADY_EXISTS", + "already known", + "ReplacementNotAllowed", + "OldNonce", +) + + +def should_rebuild(error: str) -> bool: + """Check if we should rebuild the transaction.""" + for _error in ("wrong transaction nonce", "OldNonce"): + if _error in error: + return True + return False + + +def should_retry(error: str) -> bool: + """Check an error message to check if we should raise an error or retry the tx""" + if "Transaction with hash" in error and "not found" in error: + return True + for _error in ERRORS_TO_RETRY: + if _error in error: + return True + return False + + +def should_reprice(error: str) -> bool: + """Check an error message to check if we should reprice the transaction""" + return "FeeTooLow" in error or "ReplacementNotAllowed" in error + + +class TxSettler: + """Tx settlement helper""" + + tx: Optional[Dict] + + def __init__( # pylint: disable=too-many-arguments + self, + ledger_api: LedgerApi, + crypto: Crypto, + chain_type: ChainType, + timeout: Optional[float] = None, + retries: Optional[int] = None, + sleep: Optional[float] = None, + ) -> None: + """Initialize object.""" + self.chain_type = chain_type + self.ledger_api = ledger_api + self.crypto = crypto + self.tx = None + self.timeout = timeout or DEFAULT_ON_CHAIN_INTERACT_TIMEOUT + self.retries = retries or DEFAULT_ON_CHAIN_INTERACT_RETRIES + self.sleep = sleep or DEFAULT_ON_CHAIN_INTERACT_SLEEP + + def build( + self, + method: Callable[[], Dict], + contract: str, + kwargs: Dict, + ) -> Dict: + """Build transaction.""" + return method( # type: ignore + ledger_api=self.ledger_api, + contract_address=ContractConfigs.get(name=contract).contracts[ + self.chain_type + ], + raise_on_try=True, + **kwargs, + ) + + def _reprice(self, tx_dict: Dict) -> Optional[Dict]: + """Reprice transaction.""" + if "maxFeePerGas" not in tx_dict or "maxPriorityFeePerGas" not in tx_dict: + # This means something went wrong when building the transaction + # returning a None value to the main loop will tell the main loop + # to rebuild the transaction + return None + + old_price = { + "maxFeePerGas": tx_dict[ # pylint: disable=unsubscriptable-object + "maxFeePerGas" + ], + "maxPriorityFeePerGas": tx_dict[ # pylint: disable=unsubscriptable-object + "maxPriorityFeePerGas" + ], + } + tx_dict.update( + self.ledger_api.try_get_gas_pricing( + old_price=old_price, + ) + ) + return tx_dict + + @staticmethod + def _already_known(error: str) -> bool: + """Check if the transaction is alreade sent""" + return "AlreadyKnown" in error + + def transact( + self, + method: Callable[[], Dict], + contract: str, + kwargs: Dict, + dry_run: bool = False, + ) -> Dict: + """Make a transaction and return a receipt""" + retries = 0 + tx_dict = None + tx_digest = None + already_known = False + deadline = datetime.now().timestamp() + self.timeout + while retries < self.retries and deadline >= datetime.now().timestamp(): + retries += 1 + try: + if not already_known: + tx_dict = tx_dict or self.build( + method=method, + contract=contract, + kwargs=kwargs, + ) + if tx_dict is None: + raise TxBuildError("Got empty transaction") + + # Return transaction dict on dry-run + if dry_run: + return tx_dict + + tx_signed = self.crypto.sign_transaction(transaction=tx_dict) + tx_digest = self.ledger_api.send_signed_transaction( + tx_signed=tx_signed, + raise_on_try=True, + ) + tx_receipt = self.ledger_api.api.eth.get_transaction_receipt( + cast(str, tx_digest) + ) + if tx_receipt is not None: + return tx_receipt + except RequestsConnectionError as e: + raise RPCError("Cannot connect to the given RPC") from e + except Exception as e: # pylint: disable=broad-except + error = str(e) + if self._already_known(error): + already_known = True + continue # pragma: nocover + if not should_retry(error): + raise ChainInteractionError(error) from e + if should_reprice(error): + print(f"Low gas error: {e}; Repricing the transaction...") + tx_dict = self._reprice(cast(Dict, tx_dict)) + continue # pragma: nocover + if "Transaction with hash" in error and "not found" in error: + already_known = True + print( + f"Error getting transaction receipt: {e}; " + f"Will retry in {self.sleep}..." + ) + time.sleep(self.sleep) + continue # pragma: nocover + + if should_rebuild(error): + tx_dict = None + + tx_digest = None + already_known = False + print( + f"Error occured when interacting with chain: {e}; " + f"will retry in {self.sleep}..." + ) + time.sleep(self.sleep) + raise ChainTimeoutError("Timed out when waiting for transaction to go through") + + def process( + self, + event: str, + receipt: Dict, + contract: PublicId, + ) -> Dict: + """Process tx receipt.""" + return registry_contracts.get_contract(contract).get_events( + ledger_api=self.ledger_api, + contract_address=ContractConfigs.get(contract.name).contracts[ + self.chain_type + ], + receipt=receipt, + event=event, + ) From 396c14d6b33ff39a5aaf74ec0510970f2b3255ca Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 23 May 2025 12:23:42 +0200 Subject: [PATCH 155/173] chore: remove custom TxSettler --- operate/bridge/providers/bridge_provider.py | 2 +- operate/bridge/tx.py | 233 -------------------- 2 files changed, 1 insertion(+), 234 deletions(-) delete mode 100644 operate/bridge/tx.py diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 05a54d8eb..51843d623 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -31,9 +31,9 @@ from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger +from autonomy.chain.tx import TxSettler from web3 import Web3 -from operate.bridge.tx import TxSettler from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, ON_CHAIN_INTERACT_SLEEP, diff --git a/operate/bridge/tx.py b/operate/bridge/tx.py deleted file mode 100644 index 9901261a4..000000000 --- a/operate/bridge/tx.py +++ /dev/null @@ -1,233 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023-2024 Valory AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Tx settlement helper.""" - -import time -from datetime import datetime -from typing import Callable, Dict, Optional, cast - -from aea.configurations.data_types import PublicId -from aea.crypto.base import Crypto, LedgerApi -from autonomy.chain.base import registry_contracts -from autonomy.chain.config import ChainType, ContractConfigs -from autonomy.chain.exceptions import ( - ChainInteractionError, - ChainTimeoutError, - RPCError, - TxBuildError, -) -from requests.exceptions import ConnectionError as RequestsConnectionError - - -DEFAULT_ON_CHAIN_INTERACT_TIMEOUT = 60.0 -DEFAULT_ON_CHAIN_INTERACT_RETRIES = 5.0 -DEFAULT_ON_CHAIN_INTERACT_SLEEP = 3.0 - -ERRORS_TO_RETRY = ( - "FeeTooLow", - "wrong transaction nonce", - "INTERNAL_ERROR: nonce too low", - "Got empty transaction", - "AlreadyKnown", - "ALREADY_EXISTS", - "already known", - "ReplacementNotAllowed", - "OldNonce", -) - - -def should_rebuild(error: str) -> bool: - """Check if we should rebuild the transaction.""" - for _error in ("wrong transaction nonce", "OldNonce"): - if _error in error: - return True - return False - - -def should_retry(error: str) -> bool: - """Check an error message to check if we should raise an error or retry the tx""" - if "Transaction with hash" in error and "not found" in error: - return True - for _error in ERRORS_TO_RETRY: - if _error in error: - return True - return False - - -def should_reprice(error: str) -> bool: - """Check an error message to check if we should reprice the transaction""" - return "FeeTooLow" in error or "ReplacementNotAllowed" in error - - -class TxSettler: - """Tx settlement helper""" - - tx: Optional[Dict] - - def __init__( # pylint: disable=too-many-arguments - self, - ledger_api: LedgerApi, - crypto: Crypto, - chain_type: ChainType, - timeout: Optional[float] = None, - retries: Optional[int] = None, - sleep: Optional[float] = None, - ) -> None: - """Initialize object.""" - self.chain_type = chain_type - self.ledger_api = ledger_api - self.crypto = crypto - self.tx = None - self.timeout = timeout or DEFAULT_ON_CHAIN_INTERACT_TIMEOUT - self.retries = retries or DEFAULT_ON_CHAIN_INTERACT_RETRIES - self.sleep = sleep or DEFAULT_ON_CHAIN_INTERACT_SLEEP - - def build( - self, - method: Callable[[], Dict], - contract: str, - kwargs: Dict, - ) -> Dict: - """Build transaction.""" - return method( # type: ignore - ledger_api=self.ledger_api, - contract_address=ContractConfigs.get(name=contract).contracts[ - self.chain_type - ], - raise_on_try=True, - **kwargs, - ) - - def _reprice(self, tx_dict: Dict) -> Optional[Dict]: - """Reprice transaction.""" - if "maxFeePerGas" not in tx_dict or "maxPriorityFeePerGas" not in tx_dict: - # This means something went wrong when building the transaction - # returning a None value to the main loop will tell the main loop - # to rebuild the transaction - return None - - old_price = { - "maxFeePerGas": tx_dict[ # pylint: disable=unsubscriptable-object - "maxFeePerGas" - ], - "maxPriorityFeePerGas": tx_dict[ # pylint: disable=unsubscriptable-object - "maxPriorityFeePerGas" - ], - } - tx_dict.update( - self.ledger_api.try_get_gas_pricing( - old_price=old_price, - ) - ) - return tx_dict - - @staticmethod - def _already_known(error: str) -> bool: - """Check if the transaction is alreade sent""" - return "AlreadyKnown" in error - - def transact( - self, - method: Callable[[], Dict], - contract: str, - kwargs: Dict, - dry_run: bool = False, - ) -> Dict: - """Make a transaction and return a receipt""" - retries = 0 - tx_dict = None - tx_digest = None - already_known = False - deadline = datetime.now().timestamp() + self.timeout - while retries < self.retries and deadline >= datetime.now().timestamp(): - retries += 1 - try: - if not already_known: - tx_dict = tx_dict or self.build( - method=method, - contract=contract, - kwargs=kwargs, - ) - if tx_dict is None: - raise TxBuildError("Got empty transaction") - - # Return transaction dict on dry-run - if dry_run: - return tx_dict - - tx_signed = self.crypto.sign_transaction(transaction=tx_dict) - tx_digest = self.ledger_api.send_signed_transaction( - tx_signed=tx_signed, - raise_on_try=True, - ) - tx_receipt = self.ledger_api.api.eth.get_transaction_receipt( - cast(str, tx_digest) - ) - if tx_receipt is not None: - return tx_receipt - except RequestsConnectionError as e: - raise RPCError("Cannot connect to the given RPC") from e - except Exception as e: # pylint: disable=broad-except - error = str(e) - if self._already_known(error): - already_known = True - continue # pragma: nocover - if not should_retry(error): - raise ChainInteractionError(error) from e - if should_reprice(error): - print(f"Low gas error: {e}; Repricing the transaction...") - tx_dict = self._reprice(cast(Dict, tx_dict)) - continue # pragma: nocover - if "Transaction with hash" in error and "not found" in error: - already_known = True - print( - f"Error getting transaction receipt: {e}; " - f"Will retry in {self.sleep}..." - ) - time.sleep(self.sleep) - continue # pragma: nocover - - if should_rebuild(error): - tx_dict = None - - tx_digest = None - already_known = False - print( - f"Error occured when interacting with chain: {e}; " - f"will retry in {self.sleep}..." - ) - time.sleep(self.sleep) - raise ChainTimeoutError("Timed out when waiting for transaction to go through") - - def process( - self, - event: str, - receipt: Dict, - contract: PublicId, - ) -> Dict: - """Process tx receipt.""" - return registry_contracts.get_contract(contract).get_events( - ledger_api=self.ledger_api, - contract_address=ContractConfigs.get(contract.name).contracts[ - self.chain_type - ], - receipt=receipt, - event=event, - ) From df1e518ff54d1436e656956aa3f017ac99e27461 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 23 May 2025 12:23:56 +0200 Subject: [PATCH 156/173] fix: native bridge --- operate/bridge/providers/native_bridge_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 4e268d0ca..64dd8de6d 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -92,7 +92,7 @@ def can_handle_request(self, params: t.Dict) -> bool: if to_chain != self.to_chain: return False - if from_token == ZERO_ADDRESS: + if from_token == ZERO_ADDRESS and to_token == ZERO_ADDRESS: return True for token_map in ERC20_TOKENS: From 8be22384fc8ff244206e3116469e600331651dff Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 23 May 2025 12:51:42 +0200 Subject: [PATCH 157/173] fix: zero-amount bridges --- operate/bridge/providers/lifi_bridge_provider.py | 6 ++++++ operate/bridge/providers/native_bridge_provider.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index af0f61fc0..c4acec021 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -191,6 +191,9 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: f"[LI.FI BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." ) + if bridge_request.params["to"]["amount"] == 0: + return None + quote_data = bridge_request.quote_data if not quote_data: return None @@ -235,6 +238,9 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: f"[LI.FI BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." ) + if bridge_request.params["to"]["amount"] == 0: + return None + quote_data = bridge_request.quote_data if not quote_data: return None diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 64dd8de6d..22753ce9f 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -451,6 +451,9 @@ def _get_approve_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: f"[NATIVE BRIDGE] Get appprove transaction for bridge request {bridge_request.id}." ) + if bridge_request.params["to"]["amount"] == 0: + return None + quote_data = bridge_request.quote_data if not quote_data: return None @@ -483,6 +486,9 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: f"[NATIVE BRIDGE] Get bridge transaction for bridge request {bridge_request.id}." ) + if bridge_request.params["to"]["amount"] == 0: + return None + quote_data = bridge_request.quote_data if not quote_data: return None From eda9b9b5a25100bd026ea166dd27a91152463dd3 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Fri, 23 May 2025 17:03:48 +0200 Subject: [PATCH 158/173] test: bridge and swap tests --- tests/test_bridge_bridge.py | 337 +++++++++++++++++++++++------------- 1 file changed, 220 insertions(+), 117 deletions(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index b5277b000..f19ccf2fd 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -23,9 +23,11 @@ import os import time import typing as t +from functools import cache from pathlib import Path import pytest +import requests from deepdiff import DeepDiff from operate.bridge.providers.bridge_provider import ( @@ -56,10 +58,69 @@ or os.getenv("CI", "").lower() == "true" ) +COINGECKO_PLATFORM_IDS = { + "ethereum": "ethereum", + "polygon": "polygon-pos", + "arbitrum": "arbitrum-one", + "optimistic": "optimistic-ethereum", + "binance": "binance-smart-chain", + "avalanche": "avalanche", + "fantom": "fantom", + "base": "base", + "mode": "mode", + "gnosis": "xdai", +} + +COINGECKO_NATIVE_IDS = { + "ethereum": "ethereum", + "polygon": "matic-network", + "arbitrum": "ethereum", + "optimistic": "ethereum", + "binance": "binancecoin", + "avalanche": "avalanche-2", + "fantom": "fantom", + "base": "ethereum", + "mode": "ethereum", + "gnosis": "xdai", +} + class TestBridgeManager: """Tests for bridge.bridge.BridgeManager class.""" + @staticmethod + @cache + def _get_token_price_usd(chain: str, token_address: str) -> t.Optional[float]: + print(f"Calling _get_token_price_usd {chain=} {token_address=}") + chain = chain.lower() + if token_address == ZERO_ADDRESS: + coingecko_id = COINGECKO_NATIVE_IDS.get(chain) + if not coingecko_id: + return None + url = "https://api.coingecko.com/api/v3/simple/price" + print(f"Fetching {url}") + params = {"ids": coingecko_id, "vs_currencies": "usd"} + r = requests.get(url, params=params, timeout=30) + if r.status_code != 200: + return None + data = r.json() + print(r.json()) + return data.get(coingecko_id, {}).get("usd") + + platform_id = COINGECKO_PLATFORM_IDS.get(chain) + if not platform_id: + return None + token_address = token_address.lower() + url = f"https://api.coingecko.com/api/v3/simple/token_price/{platform_id}" + params = {"contract_addresses": token_address, "vs_currencies": "usd"} + print(f"Fetching {url}") + r = requests.get(url, params=params, timeout=30) + if r.status_code != 200: + return None + data = r.json() + print(r.json()) + return data.get(token_address, {}).get("usd") + def test_bundle_zero( self, tmp_path: Path, @@ -334,10 +395,19 @@ def test_bundle_quote( request = bundle.bridge_requests[0] bridge = bridge_manager._get_bridge_provider(request) - assert bridge._get_approve_tx() is not None, "Wrong number of transactions." + assert bridge._get_approve_tx(request) is None, "Wrong number of transactions." + assert ( + bridge._get_bridge_tx(request) is not None + ), "Wrong number of transactions." + request = bundle.bridge_requests[1] bridge = bridge_manager._get_bridge_provider(request) - assert bridge._get_approve_tx() is not None, "Wrong number of transactions." + assert ( + bridge._get_approve_tx(request) is not None + ), "Wrong number of transactions." + assert ( + bridge._get_approve_tx(request) is not None + ), "Wrong number of transactions." expected_brr = { "id": brr["id"], @@ -396,118 +466,98 @@ def test_bundle_quote( assert not diff, "Wrong bridge refill requirements." @pytest.mark.parametrize( - ( - "from_chain", - "from_token", - "to_chain", - "to_token", - "expected_provider_cls", - "expected_contract_adaptor_cls", - ), - [ - # Base - ( - Chain.ETHEREUM.value, - ZERO_ADDRESS, - "base", - ZERO_ADDRESS, - NativeBridgeProvider, - OptimismContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - OLAS[Chain.ETHEREUM], - "base", - OLAS[Chain.BASE], - NativeBridgeProvider, - OptimismContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - USDC[Chain.ETHEREUM], - "base", - USDC[Chain.BASE], - NativeBridgeProvider, - OptimismContractAdaptor, - ), - # Mode - ( - Chain.ETHEREUM.value, - ZERO_ADDRESS, - Chain.MODE.value, - ZERO_ADDRESS, - NativeBridgeProvider, - OptimismContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - OLAS[Chain.ETHEREUM], - Chain.MODE.value, - OLAS[Chain.MODE], - NativeBridgeProvider, - OptimismContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - USDC[Chain.ETHEREUM], - Chain.MODE.value, - USDC[Chain.MODE], - NativeBridgeProvider, - OptimismContractAdaptor, - ), - # Optimism - ( - Chain.ETHEREUM.value, - ZERO_ADDRESS, - Chain.OPTIMISTIC.value, - ZERO_ADDRESS, - NativeBridgeProvider, - OptimismContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - OLAS[Chain.ETHEREUM], - Chain.OPTIMISTIC.value, - OLAS[Chain.OPTIMISTIC], - NativeBridgeProvider, - OptimismContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - USDC[Chain.ETHEREUM], - Chain.OPTIMISTIC.value, - USDC[Chain.OPTIMISTIC], - NativeBridgeProvider, - OptimismContractAdaptor, - ), - # Gnosis - ( - Chain.ETHEREUM.value, - ZERO_ADDRESS, - Chain.GNOSIS.value, - ZERO_ADDRESS, - LiFiBridgeProvider, - None, - ), - ( - Chain.ETHEREUM.value, - OLAS[Chain.ETHEREUM], - Chain.GNOSIS.value, - OLAS[Chain.GNOSIS], - NativeBridgeProvider, - OmnibridgeContractAdaptor, - ), - ( - Chain.ETHEREUM.value, - USDC[Chain.ETHEREUM], - Chain.GNOSIS.value, - USDC[Chain.GNOSIS], - NativeBridgeProvider, - OmnibridgeContractAdaptor, - ), - ], + "to_chain_enum", [Chain.BASE, Chain.MODE, Chain.OPTIMISTIC] + ) + def test_correct_providers_bridge_native( + self, + tmp_path: Path, + password: str, + to_chain_enum: Chain, + ) -> None: + """test_correct_providers_bridge_native""" + self._main_test_correct_providers( + tmp_path=tmp_path, + password=password, + from_chain=Chain.ETHEREUM.value, + from_token=ZERO_ADDRESS, + to_chain=to_chain_enum.value, + to_token=ZERO_ADDRESS, + expected_provider_cls=NativeBridgeProvider, + expected_contract_adaptor_cls=OptimismContractAdaptor, + ) + + @pytest.mark.parametrize("to_chain_enum", [Chain.GNOSIS]) + def test_correct_providers_swap_native( + self, + tmp_path: Path, + password: str, + to_chain_enum: Chain, + ) -> None: + """test_correct_providers_swap_native""" + self._main_test_correct_providers( + tmp_path=tmp_path, + password=password, + from_chain=Chain.ETHEREUM.value, + from_token=ZERO_ADDRESS, + to_chain=to_chain_enum.value, + to_token=ZERO_ADDRESS, + expected_provider_cls=LiFiBridgeProvider, + expected_contract_adaptor_cls=None, + ) + + @pytest.mark.parametrize( + "to_chain_enum", [Chain.BASE, Chain.MODE, Chain.OPTIMISTIC, Chain.GNOSIS] ) - def test_correct_providers( + @pytest.mark.parametrize("token_dict", [USDC, OLAS]) + def test_correct_providers_bridge_token( + self, + tmp_path: Path, + password: str, + to_chain_enum: Chain, + token_dict: t.Dict, + ) -> None: + """test_correct_providers_bridge_token""" + expected_contract_adaptor_cls: type[BridgeContractAdaptor] + if to_chain_enum == Chain.GNOSIS: + expected_contract_adaptor_cls = OmnibridgeContractAdaptor + else: + expected_contract_adaptor_cls = OptimismContractAdaptor + + self._main_test_correct_providers( + tmp_path=tmp_path, + password=password, + from_chain=Chain.ETHEREUM.value, + from_token=token_dict[Chain.ETHEREUM], + to_chain=to_chain_enum.value, + to_token=token_dict[to_chain_enum], + expected_provider_cls=NativeBridgeProvider, + expected_contract_adaptor_cls=expected_contract_adaptor_cls, + ) + + @pytest.mark.parametrize( + "to_chain_enum", [Chain.BASE, Chain.MODE, Chain.OPTIMISTIC, Chain.GNOSIS] + ) + @pytest.mark.parametrize("token_dict", [USDC, OLAS]) + def test_correct_providers_swap_token( + self, + tmp_path: Path, + password: str, + to_chain_enum: Chain, + token_dict: t.Dict, + ) -> None: + """test_correct_providers_swap_token""" + self._main_test_correct_providers( + tmp_path=tmp_path, + password=password, + from_chain=Chain.ETHEREUM.value, + from_token=ZERO_ADDRESS, + to_chain=to_chain_enum.value, + to_token=token_dict[to_chain_enum], + expected_provider_cls=LiFiBridgeProvider, + expected_contract_adaptor_cls=None, + ) + + def _main_test_correct_providers( self, tmp_path: Path, password: str, @@ -516,9 +566,11 @@ def test_correct_providers( to_chain: str, to_token: str, expected_provider_cls: t.Type[BridgeProvider], - expected_contract_adaptor_cls: t.Type[BridgeContractAdaptor], + expected_contract_adaptor_cls: t.Optional[t.Type[BridgeContractAdaptor]], + check_price: bool = True, + margin: float = 0.15, ) -> None: - """test_correct_providers""" + """_main_test_correct_providers""" operate = OperateApp( home=tmp_path / OPERATE, ) @@ -529,6 +581,18 @@ def test_correct_providers( bridge_manager = operate.bridge_manager() wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address + + amount_unit = 100 + if to_token in USDC.values(): + to_decimals = 6 + else: + to_decimals = 18 + + if from_token in USDC.values(): + from_decimals = 6 + else: + from_decimals = 18 + params = [ { "from": { @@ -540,17 +604,22 @@ def test_correct_providers( "chain": to_chain, "address": wallet_address, "token": to_token, - "amount": 0, + "amount": amount_unit * (10**to_decimals), }, }, ] bundle = bridge_manager.data.last_requested_bundle assert bundle is None, "Wrong bundle." - bridge_manager.bridge_refill_requirements( + refill_requirements = bridge_manager.bridge_refill_requirements( requests_params=params, force_update=False ) + for request_status in refill_requirements["bridge_request_status"]: + assert ( + request_status["status"] == BridgeRequestStatus.QUOTE_DONE + ), f"Wrong bundle for params\n{params}" + bundle = bridge_manager.data.last_requested_bundle assert bundle is not None, "Wrong bundle." assert len(bundle.bridge_requests) == 1, "Wrong bundle." @@ -562,6 +631,40 @@ def test_correct_providers( ), f"Expected provider {expected_provider_cls}, got {type(bridge)}" if isinstance(bridge, NativeBridgeProvider): + assert expected_contract_adaptor_cls is not None, "Wrong contract adaptor." assert isinstance( bridge.bridge_contract_adaptor, expected_contract_adaptor_cls ), f"Expected adaptor {expected_contract_adaptor_cls}, got {type(bridge.bridge_contract_adaptor)}" + + if check_price: + to_price_usd = self._get_token_price_usd(to_chain, to_token) + from_price_usd = self._get_token_price_usd(from_chain, from_token) + print(f"{to_price_usd=}") + print(f"{from_price_usd=}") + + if to_price_usd is None or from_price_usd is None: + pytest.skip("Token price could not be retrieved; skipping price check.") + return + + refill_amount = refill_requirements["bridge_total_requirements"][ + from_chain + ][wallet_address][from_token] + + print(f"{refill_amount=}") + + quoted_from_cost_usd = ( + refill_amount * from_price_usd / (10**from_decimals) + ) + expected_to_cost_usd = amount_unit * to_price_usd + print(f"Expected cost on {to_chain}: {expected_to_cost_usd}") + print(f"Quoted cost on {from_chain}: {quoted_from_cost_usd}") + + overpaid_usd = max(quoted_from_cost_usd - expected_to_cost_usd, 0) + overpaid_percent = (overpaid_usd / expected_to_cost_usd) * 100 + print( + f"Overpaid {overpaid_usd:.2f} USD ({overpaid_percent:.2f}% < {margin*100:.2f}%)" + ) + + assert quoted_from_cost_usd <= expected_to_cost_usd * ( + 1.0 + margin + ), f"Quoted cost exceeds {margin*100:.2f}% margin" From 60f1c076f2c8b166c02b41dcfecc9f838db82b52 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 2 Jun 2025 17:36:44 +0200 Subject: [PATCH 159/173] fix: optimism middleware --- operate/bridge/bridge.py | 2 +- operate/bridge/providers/bridge_provider.py | 13 ++++++++++++- operate/bridge/providers/lifi_bridge_provider.py | 1 + operate/bridge/providers/native_bridge_provider.py | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index e430ae861..40e009e27 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -398,7 +398,7 @@ def _get_bridge_provider(self, request: BridgeRequest) -> BridgeProvider: if bridge: return bridge - return self._fallback_bridge_provider + raise RuntimeError(f"Bridge request {request.id} does not have a valid provider.") def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: """Sum bridge requirements.""" diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 51843d623..643d67d6d 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -28,6 +28,7 @@ import uuid from abc import ABC, abstractmethod from dataclasses import dataclass +from web3.middleware import geth_poa_middleware from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger @@ -52,7 +53,7 @@ MESSAGE_QUOTE_ZERO = "Zero-amount quote requested." MESSAGE_EXECUTION_SKIPPED = "Execution skipped." MESSAGE_EXECUTION_FAILED = "Execution failed:" -MESSAGE_EXECUTION_FAILED_ETA = f"{MESSAGE_EXECUTION_FAILED} ETA exceeded." +MESSAGE_EXECUTION_FAILED_ETA = f"{MESSAGE_EXECUTION_FAILED} bridge ETA exceeded." MESSAGE_EXECUTION_FAILED_QUOTE_FAILED = f"{MESSAGE_EXECUTION_FAILED} quote failed." MESSAGE_EXECUTION_FAILED_REVERTED = ( f"{MESSAGE_EXECUTION_FAILED} bridge transaction reverted." @@ -226,6 +227,11 @@ def _from_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: chain = Chain(from_chain) wallet = self.wallet_manager.load(chain.ledger_type) ledger_api = wallet.ledger_api(chain) + + # TODO: Backport to open aea/autonomy + if chain == Chain.OPTIMISTIC: + ledger_api.api.middleware_onion.inject(geth_poa_middleware, layer=0) + return ledger_api def _to_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: @@ -234,6 +240,11 @@ def _to_ledger_api(self, bridge_request: BridgeRequest) -> LedgerApi: chain = Chain(from_chain) wallet = self.wallet_manager.load(chain.ledger_type) ledger_api = wallet.ledger_api(chain) + + # TODO: Backport to open aea/autonomy + if chain == Chain.OPTIMISTIC: + ledger_api.api.middleware_onion.inject(geth_poa_middleware, layer=0) + return ledger_api @abstractmethod diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index c4acec021..f40e22f4c 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -323,6 +323,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: from_tx_hash = execution_data.from_tx_hash to_ledger_api = self._to_ledger_api(bridge_request) to_tx_hash = response_json.get("receiving", {}).get("txHash") + execution_data.message = None execution_data.to_tx_hash = to_tx_hash execution_data.elapsed_time = BridgeProvider._tx_timestamp( to_tx_hash, to_ledger_api diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 22753ce9f..80d2eef3a 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -566,6 +566,7 @@ def _update_execution_status(self, bridge_request: BridgeRequest) -> None: self.logger.info( f"[NATIVE BRIDGE] Execution done for {bridge_request.id}." ) + execution_data.message = None execution_data.to_tx_hash = to_tx_hash execution_data.elapsed_time = BridgeProvider._tx_timestamp( to_tx_hash, to_ledger_api From d707f283e74026ad5fc532814080ff0958af6f4c Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 2 Jun 2025 19:05:00 +0200 Subject: [PATCH 160/173] fix: check native bridge remote tokens --- operate/bridge/bridge.py | 7 +- operate/bridge/providers/bridge_provider.py | 2 +- .../providers/native_bridge_provider.py | 43 +- .../optimism_mintable_erc20/__init__.py | 20 + .../build/OptimismMintableERC20.json | 491 ++++++++++++++++++ .../optimism_mintable_erc20/contract.py | 45 ++ .../optimism_mintable_erc20/contract.yaml | 23 + 7 files changed, 625 insertions(+), 6 deletions(-) create mode 100644 operate/data/contracts/optimism_mintable_erc20/__init__.py create mode 100644 operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json create mode 100644 operate/data/contracts/optimism_mintable_erc20/contract.py create mode 100644 operate/data/contracts/optimism_mintable_erc20/contract.yaml diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index 40e009e27..d32baea5a 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -398,7 +398,12 @@ def _get_bridge_provider(self, request: BridgeRequest) -> BridgeProvider: if bridge: return bridge - raise RuntimeError(f"Bridge request {request.id} does not have a valid provider.") + if request.bridge_provider_id == self._fallback_bridge_provider.provider_id: + return self._fallback_bridge_provider + + raise RuntimeError( + f"Bridge request {request.id} does not have a valid provider." + ) def bridge_total_requirements(self, bundle: BridgeRequestBundle) -> t.Dict: """Sum bridge requirements.""" diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 643d67d6d..064493f6e 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -28,12 +28,12 @@ import uuid from abc import ABC, abstractmethod from dataclasses import dataclass -from web3.middleware import geth_poa_middleware from aea.crypto.base import LedgerApi from aea.helpers.logging import setup_logger from autonomy.chain.tx import TxSettler from web3 import Web3 +from web3.middleware import geth_poa_middleware from operate.constants import ( ON_CHAIN_INTERACT_RETRIES, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 80d2eef3a..a2f9b2252 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -52,6 +52,9 @@ L1StandardBridge, ) from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge +from operate.data.contracts.optimism_mintable_erc20.contract import ( + OptimismMintableERC20, +) from operate.ledger.profiles import ERC20_TOKENS from operate.operate_types import Chain from operate.wallet.master import MasterWalletManager @@ -79,7 +82,7 @@ def __init__( self.to_bridge = to_bridge self.bridge_eta = bridge_eta - def can_handle_request(self, params: t.Dict) -> bool: + def can_handle_request(self, to_ledger_api: LedgerApi, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" from_chain = params["from"]["chain"] from_token = Web3.to_checksum_address(params["from"]["token"]) @@ -149,6 +152,33 @@ class OptimismContractAdaptor(BridgeContractAdaptor): ), ) + _optimism_mintable_erc20_contract = t.cast( + OptimismMintableERC20, + OptimismMintableERC20.from_dir( + directory=str(DATA_DIR / "contracts" / "optimism_mintable_erc20"), + ), + ) + + def can_handle_request(self, to_ledger_api: LedgerApi, params: t.Dict) -> bool: + """Returns 'true' if the contract adaptor can handle a request for 'params'.""" + + from_token = Web3.to_checksum_address(params["from"]["token"]) + to_token = Web3.to_checksum_address(params["to"]["token"]) + + if to_token != ZERO_ADDRESS: + try: + l1_token = self._optimism_mintable_erc20_contract.l1_token( + ledger_api=to_ledger_api, + contract_address=to_token, + )["data"] + + if l1_token != from_token: + return False + except Exception: # pylint: disable=broad-except + return False + + return super().can_handle_request(to_ledger_api, params) + def build_bridge_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest ) -> JSONLike: @@ -257,13 +287,13 @@ class OmnibridgeContractAdaptor(BridgeContractAdaptor): ), ) - def can_handle_request(self, params: t.Dict) -> bool: + def can_handle_request(self, to_ledger_api: LedgerApi, params: t.Dict) -> bool: """Returns 'true' if the contract adaptor can handle a request for 'params'.""" from_token = Web3.to_checksum_address(params["from"]["token"]) if from_token == ZERO_ADDRESS: return False - return super().can_handle_request(params) + return super().can_handle_request(to_ledger_api, params) def build_bridge_tx( self, from_ledger_api: LedgerApi, bridge_request: BridgeRequest @@ -399,7 +429,12 @@ def can_handle_request(self, params: t.Dict) -> bool: if not super().can_handle_request(params): return False - if not self.bridge_contract_adaptor.can_handle_request(params): + to_chain = params["to"]["chain"] + chain = Chain(to_chain) + wallet = self.wallet_manager.load(chain.ledger_type) + to_ledger_api = wallet.ledger_api(chain) + + if not self.bridge_contract_adaptor.can_handle_request(to_ledger_api, params): return False return True diff --git a/operate/data/contracts/optimism_mintable_erc20/__init__.py b/operate/data/contracts/optimism_mintable_erc20/__init__.py new file mode 100644 index 000000000..3cc8edd56 --- /dev/null +++ b/operate/data/contracts/optimism_mintable_erc20/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the `OptimismMintableERC20` contract.""" diff --git a/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json b/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json new file mode 100644 index 000000000..1a12425e1 --- /dev/null +++ b/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json @@ -0,0 +1,491 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "OptimismMintableERC20", + "sourceName": "OptimismMintableERC20.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_bridge", + "type": "address" + }, + { + "internalType": "address", + "name": "_remoteToken", + "type": "address" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "string", + "name": "_symbol", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "BRIDGE", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "REMOTE_TOKEN", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bridge", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l1Token", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l2Bridge", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "remoteToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "_interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "", + "deployedBytecode": "", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/operate/data/contracts/optimism_mintable_erc20/contract.py b/operate/data/contracts/optimism_mintable_erc20/contract.py new file mode 100644 index 000000000..dc880537b --- /dev/null +++ b/operate/data/contracts/optimism_mintable_erc20/contract.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the class to connect to the `OptimismMintableERC20` contract.""" + + +from aea.common import JSONLike +from aea.configurations.base import PublicId +from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi + + +class OptimismMintableERC20(Contract): + """Optimism OptimismMintableERC20.""" + + contract_id = PublicId.from_str("valory/optimism_mintable_erc20:0.1.0") + + @classmethod + def l1_token( + cls, + ledger_api: LedgerApi, + contract_address: str, + ) -> JSONLike: + """l1Token""" + contract_instance = cls.get_instance( + ledger_api=ledger_api, contract_address=contract_address + ) + l1_token = contract_instance.functions.l1Token().call() + return dict(data=l1_token) diff --git a/operate/data/contracts/optimism_mintable_erc20/contract.yaml b/operate/data/contracts/optimism_mintable_erc20/contract.yaml new file mode 100644 index 000000000..54c202f9d --- /dev/null +++ b/operate/data/contracts/optimism_mintable_erc20/contract.yaml @@ -0,0 +1,23 @@ +name: optimism_mintable_erc20 +author: valory +version: 0.1.0 +type: contract +description: Optimism Mintable ERC20 +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeiffqotbwpjde3htgfuplg62x4btoekvitqrkrlx2vkjpciwzov6sy + build/OptimismMintableERC20.json: bafybeieuzn55dh5zlg4ygtuifqso6hwbhsjvidxkjkh2vnah5psxqup62i + contract.py: bafybeiauq7vas7xuyb2zt4m5mb6i2g2nrwtptv6muivur5u3c5lwckys4q +fingerprint_ignore_patterns: [] +contracts: [] +class_name: OptimismMintableERC20 +contract_interface_paths: + ethereum: build/OptimismMintableERC20.json +dependencies: + open-aea-ledger-ethereum: + version: ==1.60.0 + open-aea-test-autonomy: + version: ==0.18.3 + web3: + version: <7,>=6.0.0 From c3cdc1f2c4f65d4620d0f9c9a05edef51d2beae4 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 2 Jun 2025 19:32:42 +0200 Subject: [PATCH 161/173] tests: fix --- tests/test_bridge_bridge.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index f19ccf2fd..bd27f4c3d 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -517,12 +517,21 @@ def test_correct_providers_bridge_token( token_dict: t.Dict, ) -> None: """test_correct_providers_bridge_token""" + expected_provider_cls = NativeBridgeProvider expected_contract_adaptor_cls: type[BridgeContractAdaptor] if to_chain_enum == Chain.GNOSIS: expected_contract_adaptor_cls = OmnibridgeContractAdaptor else: expected_contract_adaptor_cls = OptimismContractAdaptor + if to_chain_enum == Chain.BASE and token_dict == USDC: + expected_provider_cls = LiFiBridgeProvider + expected_contract_adaptor_cls = None + + if to_chain_enum == Chain.OPTIMISTIC and token_dict == USDC: + expected_provider_cls = LiFiBridgeProvider + expected_contract_adaptor_cls = None + self._main_test_correct_providers( tmp_path=tmp_path, password=password, @@ -530,7 +539,7 @@ def test_correct_providers_bridge_token( from_token=token_dict[Chain.ETHEREUM], to_chain=to_chain_enum.value, to_token=token_dict[to_chain_enum], - expected_provider_cls=NativeBridgeProvider, + expected_provider_cls=expected_provider_cls, expected_contract_adaptor_cls=expected_contract_adaptor_cls, ) From 7c1ab6f81c676f1a479964f1a4ee4b7d159c0341 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 2 Jun 2025 19:42:56 +0200 Subject: [PATCH 162/173] fix: linters --- tests/test_bridge_bridge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index bd27f4c3d..7386493d4 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -517,7 +517,7 @@ def test_correct_providers_bridge_token( token_dict: t.Dict, ) -> None: """test_correct_providers_bridge_token""" - expected_provider_cls = NativeBridgeProvider + expected_provider_cls: type[BridgeProvider] = NativeBridgeProvider expected_contract_adaptor_cls: type[BridgeContractAdaptor] if to_chain_enum == Chain.GNOSIS: expected_contract_adaptor_cls = OmnibridgeContractAdaptor @@ -526,11 +526,11 @@ def test_correct_providers_bridge_token( if to_chain_enum == Chain.BASE and token_dict == USDC: expected_provider_cls = LiFiBridgeProvider - expected_contract_adaptor_cls = None + expected_contract_adaptor_cls = None # type: ignore if to_chain_enum == Chain.OPTIMISTIC and token_dict == USDC: expected_provider_cls = LiFiBridgeProvider - expected_contract_adaptor_cls = None + expected_contract_adaptor_cls = None # type: ignore self._main_test_correct_providers( tmp_path=tmp_path, From 32b073317ef1e159d049acf113d51ff5dbb37b73 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Mon, 2 Jun 2025 22:55:24 +0200 Subject: [PATCH 163/173] chore: review updates --- operate/bridge/bridge.py | 2 +- .../bridge/providers/lifi_bridge_provider.py | 2 +- .../providers/native_bridge_provider.py | 13 +++++++---- operate/ledger/profiles.py | 23 +++++++++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/operate/bridge/bridge.py b/operate/bridge/bridge.py index d32baea5a..3649d31ba 100644 --- a/operate/bridge/bridge.py +++ b/operate/bridge/bridge.py @@ -137,7 +137,7 @@ class BridgeManagerData(LocalResource): # It can be inconvenient that all local resources create an empty resource # if the file is corrupted. For example, if a service configuration is # corrupted, we might want to halt execution, because otherwise, the application - # could continue as if the user is creatig a service from scratch. + # could continue as if the user is creating a service from scratch. # For the bridge manager data, it's harmless, because its memory # is limited to the process of getting and executing a quote. @classmethod # Overrides from LocalResource diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index f40e22f4c..9495a4c76 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -278,7 +278,7 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: return bridge_tx def _update_execution_status(self, bridge_request: BridgeRequest) -> None: - """Update the execution status. Returns `True` if the status changed.""" + """Update the execution status.""" if bridge_request.status not in ( BridgeRequestStatus.EXECUTION_PENDING, diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index a2f9b2252..29726f531 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -55,7 +55,7 @@ from operate.data.contracts.optimism_mintable_erc20.contract import ( OptimismMintableERC20, ) -from operate.ledger.profiles import ERC20_TOKENS +from operate.ledger.profiles import ERC20_TOKENS, EXPLORER_URL from operate.operate_types import Chain from operate.wallet.master import MasterWalletManager @@ -175,6 +175,9 @@ def can_handle_request(self, to_ledger_api: LedgerApi, params: t.Dict) -> bool: if l1_token != from_token: return False except Exception: # pylint: disable=broad-except + import traceback + + traceback.print_exc() return False return super().can_handle_request(to_ledger_api, params) @@ -267,7 +270,9 @@ def get_explorer_link( if not tx_hash: return None - return f"https://etherscan.io/tx/{tx_hash}" + chain = Chain(bridge_request.params["from"]["chain"]) + url = EXPLORER_URL[chain]["tx"] + return url.format(tx_hash=tx_hash) class OmnibridgeContractAdaptor(BridgeContractAdaptor): @@ -539,9 +544,9 @@ def _get_bridge_tx(self, bridge_request: BridgeRequest) -> t.Optional[t.Dict]: return bridge_tx def _update_execution_status(self, bridge_request: BridgeRequest) -> None: - """Update the execution status. Returns `True` if the status changed.""" + """Update the execution status.""" - if bridge_request.status not in (BridgeRequestStatus.EXECUTION_PENDING,): + if bridge_request.status != BridgeRequestStatus.EXECUTION_PENDING: return self.logger.info( diff --git a/operate/ledger/profiles.py b/operate/ledger/profiles.py index a755880ff..53b936d02 100644 --- a/operate/ledger/profiles.py +++ b/operate/ledger/profiles.py @@ -203,6 +203,29 @@ Chain.OPTIMISTIC: {ZERO_ADDRESS: 5_000_000_000_000_000}, } +EXPLORER_URL = { + Chain.BASE: { + "tx": "https://basescan.org/tx/{tx_hash}", + "address": "https://basescan.org/address/{address}", + }, + Chain.ETHEREUM: { + "tx": "https://etherscan.io/tx/{tx_hash}", + "address": "https://etherscan.io/address/{address}", + }, + Chain.GNOSIS: { + "tx": "https://gnosisscan.io/tx/{tx_hash}", + "address": "https://gnosisscan.io/address/{address}", + }, + Chain.MODE: { + "tx": "https://modescan.io/tx/{tx_hash}", + "address": "https://modescan.io/address/{address}", + }, + Chain.OPTIMISTIC: { + "tx": "https://optimistic.etherscan.io/tx/{tx_hash}", + "address": "https://optimistic.etherscan.io/address/{address}", + }, +} + def get_staking_contract( chain: str, staking_program_id: t.Optional[str] From 2eab7bbd8f79fcc8b6c05fa2282f4dbd520c191f Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 3 Jun 2025 10:44:55 +0200 Subject: [PATCH 164/173] chore: remove comment --- operate/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/operate/cli.py b/operate/cli.py index ed6b30b38..520451509 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -183,7 +183,6 @@ def bridge_manager(self) -> BridgeManager: manager = BridgeManager( path=self._path / "bridge", wallet_manager=self.wallet_manager, - # remove: quote_validity_period=24 * 60 * 60, # TODO remove ) return manager From 4484fc5a9129403fc6c508ab8f9f17e3b8a473a6 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 3 Jun 2025 10:47:46 +0200 Subject: [PATCH 165/173] chore: refactor bridge_manager as a property --- operate/cli.py | 9 +++++---- tests/test_bridge_bridge.py | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/operate/cli.py b/operate/cli.py index 520451509..87153adcd 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -178,6 +178,7 @@ def wallet_manager(self) -> MasterWalletManager: manager.setup() return manager + @property def bridge_manager(self) -> BridgeManager: """Load master wallet.""" manager = BridgeManager( @@ -963,7 +964,7 @@ async def _bridge_refill_requirements(request: Request) -> JSONResponse: try: data = await request.json() - output = operate.bridge_manager().bridge_refill_requirements( + output = operate.bridge_manager.bridge_refill_requirements( requests_params=data["bridge_requests"], force_update=data.get("force_update", False), ) @@ -991,7 +992,7 @@ async def _bridge_execute(request: Request) -> JSONResponse: try: data = await request.json() - output = operate.bridge_manager().execute_bundle(bundle_id=data["id"]) + output = operate.bridge_manager.execute_bundle(bundle_id=data["id"]) return JSONResponse( content=output, @@ -1011,7 +1012,7 @@ async def _bridge_execute(request: Request) -> JSONResponse: @with_retries async def _bridge_last_executed_bundle_id(request: Request) -> t.List[t.Dict]: """Get last executed bundle id.""" - content = {"id": operate.bridge_manager().last_executed_bundle_id()} + content = {"id": operate.bridge_manager.last_executed_bundle_id()} return JSONResponse(content=content, status_code=HTTPStatus.OK) @app.get("/api/bridge/status/{id}") @@ -1022,7 +1023,7 @@ async def _bridge_status(request: Request) -> JSONResponse: quote_bundle_id = request.path_params["id"] try: - output = operate.bridge_manager().get_status_json(bundle_id=quote_bundle_id) + output = operate.bridge_manager.get_status_json(bundle_id=quote_bundle_id) return JSONResponse( content=output, diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 7386493d4..bb462ca63 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -135,7 +135,7 @@ def test_bundle_zero( operate.create_user_account(password=password) operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - bridge_manager = operate.bridge_manager() + bridge_manager = operate.bridge_manager wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address params = [ @@ -242,7 +242,7 @@ def test_bundle_error( operate.create_user_account(password=password) operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - bridge_manager = operate.bridge_manager() + bridge_manager = operate.bridge_manager wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address params = [ @@ -350,7 +350,7 @@ def test_bundle_quote( operate.create_user_account(password=password) operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - bridge_manager = operate.bridge_manager() + bridge_manager = operate.bridge_manager wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address params = [ @@ -587,7 +587,7 @@ def _main_test_correct_providers( operate.create_user_account(password=password) operate.password = password operate.wallet_manager.create(ledger_type=LedgerType.ETHEREUM) - bridge_manager = operate.bridge_manager() + bridge_manager = operate.bridge_manager wallet_address = operate.wallet_manager.load(LedgerType.ETHEREUM).address From 5d55ac0cafe87e9d5523707c4747d3179f99aa08 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 3 Jun 2025 11:07:18 +0200 Subject: [PATCH 166/173] chore: catch general exception --- operate/bridge/providers/lifi_bridge_provider.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/operate/bridge/providers/lifi_bridge_provider.py b/operate/bridge/providers/lifi_bridge_provider.py index 9495a4c76..756478852 100644 --- a/operate/bridge/providers/lifi_bridge_provider.py +++ b/operate/bridge/providers/lifi_bridge_provider.py @@ -175,6 +175,21 @@ def quote(self, bridge_request: BridgeRequest) -> None: }, timestamp=int(time.time()), ) + except Exception as e: # pylint:disable=broad-except + self.logger.warning( + f"[LI.FI BRIDGE] Request failed on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}." + ) + quote_data = QuoteData( + bridge_eta=None, + elapsed_time=time.time() - start, + message=str(e), + provider_data={ + "attempts": attempt, + "response": None, + "response_status": HTTPStatus.INTERNAL_SERVER_ERROR, + }, + timestamp=int(time.time()), + ) if attempt >= DEFAULT_MAX_QUOTE_RETRIES: self.logger.error( f"[LI.FI BRIDGE] Request failed after {DEFAULT_MAX_QUOTE_RETRIES} attempts." From f8b7186a4e79a03d5c0f056ee70476fd126c6d59 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 3 Jun 2025 12:14:33 +0200 Subject: [PATCH 167/173] fix: nonce --- operate/bridge/providers/bridge_provider.py | 3 +++ operate/bridge/providers/native_bridge_provider.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/operate/bridge/providers/bridge_provider.py b/operate/bridge/providers/bridge_provider.py index 064493f6e..ceb2bc3c2 100644 --- a/operate/bridge/providers/bridge_provider.py +++ b/operate/bridge/providers/bridge_provider.py @@ -411,6 +411,7 @@ def execute(self, bridge_request: BridgeRequest) -> None: ) timestamp = time.time() chain = Chain(bridge_request.params["from"]["chain"]) + from_address = bridge_request.params["from"]["address"] wallet = self.wallet_manager.load(chain.ledger_type) from_ledger_api = self._from_ledger_api(bridge_request) tx_settler = TxSettler( @@ -425,6 +426,8 @@ def execute(self, bridge_request: BridgeRequest) -> None: for tx_label, tx in txs: self.logger.info(f"[BRIDGE] Executing transaction {tx_label}.") + nonce = from_ledger_api.api.eth.get_transaction_count(from_address) + tx["nonce"] = nonce # TODO: backport to TxSettler setattr( # noqa: B010 tx_settler, "build", lambda *args, **kwargs: tx # noqa: B023 ) diff --git a/operate/bridge/providers/native_bridge_provider.py b/operate/bridge/providers/native_bridge_provider.py index 29726f531..016f01f4d 100644 --- a/operate/bridge/providers/native_bridge_provider.py +++ b/operate/bridge/providers/native_bridge_provider.py @@ -175,9 +175,6 @@ def can_handle_request(self, to_ledger_api: LedgerApi, params: t.Dict) -> bool: if l1_token != from_token: return False except Exception: # pylint: disable=broad-except - import traceback - - traceback.print_exc() return False return super().can_handle_request(to_ledger_api, params) From 06af632016325c5c64fc68305e6f83f318518009 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Tue, 3 Jun 2025 12:27:56 +0200 Subject: [PATCH 168/173] chore: update README.md --- operate/data/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/operate/data/README.md b/operate/data/README.md index 7cce87c21..9f4365d62 100644 --- a/operate/data/README.md +++ b/operate/data/README.md @@ -13,6 +13,7 @@ New packages from this repository: - `contracts/home_omnibridge` - `contracts/l1_standard_bridge` - `contracts/l2_standard_bridge` +- `contracts/optimism_mintable_erc20` - `contracts/requester_activity_checker` TODO: Have a better way to import and reuse packages in the middleware. From 5688df4f7f648352ff4d4183da192df00d34d434 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 4 Jun 2025 11:07:21 +0200 Subject: [PATCH 169/173] chore: fix mode rpc endpoint --- operate/ledger/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/operate/ledger/__init__.py b/operate/ledger/__init__.py index 85087284c..3e1c43275 100644 --- a/operate/ledger/__init__.py +++ b/operate/ledger/__init__.py @@ -34,7 +34,7 @@ BASE_PUBLIC_RPC = os.environ.get("BASE_RPC", "https://mainnet.base.org") CELO_PUBLIC_RPC = os.environ.get("CELO_RPC", "https://forno.celo.org") OPTIMISM_PUBLIC_RPC = os.environ.get("OPTIMISM_RPC", "https://mainnet.optimism.io") -MODE_PUBLIC_RPC = os.environ.get("MODE_RPC", "https://rpc.mode.network") +MODE_PUBLIC_RPC = os.environ.get("MODE_RPC", "https://mainnet.mode.network/") ETHEREUM_RPC = os.environ.get("ETHEREUM_RPC", "https://ethereum.publicnode.com") GNOSIS_RPC = os.environ.get("GNOSIS_RPC", "https://rpc-gate.autonolas.tech/gnosis-rpc/") @@ -42,7 +42,7 @@ BASE_RPC = os.environ.get("BASE_RPC", "https://mainnet.base.org") CELO_RPC = os.environ.get("CELO_RPC", "https://forno.celo.org") OPTIMISM_RPC = os.environ.get("OPTIMISM_RPC", "https://mainnet.optimism.io") -MODE_RPC = os.environ.get("MODE_RPC", "https://rpc.mode.network") +MODE_RPC = os.environ.get("MODE_RPC", "https://mainnet.mode.network/") PUBLIC_RPCS = { Chain.ETHEREUM: ETHEREUM_PUBLIC_RPC, From 308bacf81f454e2bfa9e999c932101ee4b9df8b5 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 4 Jun 2025 11:28:42 +0200 Subject: [PATCH 170/173] tests: skip --- tests/test_bridge_bridge.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index bb462ca63..97a453af1 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -543,6 +543,7 @@ def test_correct_providers_bridge_token( expected_contract_adaptor_cls=expected_contract_adaptor_cls, ) + @pytest.mark.skipif(RUNNING_IN_CI, reason="Skip test on CI.") @pytest.mark.parametrize( "to_chain_enum", [Chain.BASE, Chain.MODE, Chain.OPTIMISTIC, Chain.GNOSIS] ) From b6f391c5facfa135c661c9173db30cbf1957f4df Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 4 Jun 2025 11:52:04 +0200 Subject: [PATCH 171/173] chore: github actions --- .github/workflows/common_checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/common_checks.yml b/.github/workflows/common_checks.yml index a88a82fd7..25bfb0657 100644 --- a/.github/workflows/common_checks.yml +++ b/.github/workflows/common_checks.yml @@ -71,5 +71,7 @@ jobs: - name: Run unit tests run: | + echo "GITHUB_ACTIONS=${GITHUB_ACTIONS}" + echo "CI=${CI}" pip install tomte[tox]==0.2.15 tox -e unit-tests From 02cc3f1b2295cbfa28b1dad33d866915c53fcd07 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 4 Jun 2025 12:17:40 +0200 Subject: [PATCH 172/173] chore: tests --- tests/test_bridge_bridge.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index 97a453af1..f8ab079f0 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -556,6 +556,11 @@ def test_correct_providers_swap_token( token_dict: t.Dict, ) -> None: """test_correct_providers_swap_token""" + + print(f"GITHUB_ACTIONS={os.getenv('GITHUB_ACTIONS', '')}") + print(f"CI={os.getenv('CI', '')}") + print(f"RUNNING_IN_CI={RUNNING_IN_CI}") + self._main_test_correct_providers( tmp_path=tmp_path, password=password, From ecdb6228ccc4dcf379b9e26632adbeefabce5745 Mon Sep 17 00:00:00 2001 From: jmoreira-valory Date: Wed, 4 Jun 2025 12:31:33 +0200 Subject: [PATCH 173/173] tests: fix --- .github/workflows/common_checks.yml | 2 -- tests/test_bridge_bridge.py | 5 ----- tox.ini | 2 ++ 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/common_checks.yml b/.github/workflows/common_checks.yml index 25bfb0657..a88a82fd7 100644 --- a/.github/workflows/common_checks.yml +++ b/.github/workflows/common_checks.yml @@ -71,7 +71,5 @@ jobs: - name: Run unit tests run: | - echo "GITHUB_ACTIONS=${GITHUB_ACTIONS}" - echo "CI=${CI}" pip install tomte[tox]==0.2.15 tox -e unit-tests diff --git a/tests/test_bridge_bridge.py b/tests/test_bridge_bridge.py index f8ab079f0..97a453af1 100644 --- a/tests/test_bridge_bridge.py +++ b/tests/test_bridge_bridge.py @@ -556,11 +556,6 @@ def test_correct_providers_swap_token( token_dict: t.Dict, ) -> None: """test_correct_providers_swap_token""" - - print(f"GITHUB_ACTIONS={os.getenv('GITHUB_ACTIONS', '')}") - print(f"CI={os.getenv('CI', '')}") - print(f"RUNNING_IN_CI={RUNNING_IN_CI}") - self._main_test_correct_providers( tmp_path=tmp_path, password=password, diff --git a/tox.ini b/tox.ini index 5e429955b..2d4f929a3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,8 @@ envlist = isort skip_missing_interpreters = true isolated_build = True +[testenv] +passenv = * [testenv:bandit] skipsdist = True