olas-operate-middleware 0.1.0rc36__tar.gz → 0.13.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. olas_operate_middleware-0.13.5/PKG-INFO +56 -0
  2. olas_operate_middleware-0.13.5/README.md +25 -0
  3. olas_operate_middleware-0.1.0rc36/operate/ledger/base.py → olas_operate_middleware-0.13.5/operate/__init__.py +14 -14
  4. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/account/user.py +35 -9
  5. olas_operate_middleware-0.13.5/operate/bridge/bridge_manager.py +470 -0
  6. olas_operate_middleware-0.13.5/operate/bridge/providers/lifi_provider.py +377 -0
  7. olas_operate_middleware-0.13.5/operate/bridge/providers/native_bridge_provider.py +677 -0
  8. olas_operate_middleware-0.13.5/operate/bridge/providers/provider.py +461 -0
  9. olas_operate_middleware-0.13.5/operate/bridge/providers/relay_provider.py +457 -0
  10. olas_operate_middleware-0.13.5/operate/cli.py +1885 -0
  11. olas_operate_middleware-0.13.5/operate/constants.py +86 -0
  12. olas_operate_middleware-0.13.5/operate/data/README.md +19 -0
  13. olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/__init__.py +20 -0
  14. olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
  15. olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/contract.py +132 -0
  16. olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/contract.yaml +23 -0
  17. olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/__init__.py +20 -0
  18. olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
  19. olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/contract.py +130 -0
  20. olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
  21. olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/__init__.py +20 -0
  22. olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
  23. olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/contract.py +80 -0
  24. olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/contract.yaml +23 -0
  25. olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
  26. olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
  27. olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/contract.py +158 -0
  28. olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
  29. olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
  30. olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
  31. olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/contract.py +130 -0
  32. olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
  33. olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/__init__.py +20 -0
  34. olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
  35. olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/contract.py +44 -0
  36. olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/contract.yaml +23 -0
  37. olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
  38. olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
  39. olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
  40. olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
  41. olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/__init__.py +20 -0
  42. olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
  43. olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/contract.py +61 -0
  44. olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/contract.yaml +23 -0
  45. olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/__init__.py +20 -0
  46. olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
  47. olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/contract.py +33 -0
  48. olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
  49. {olas_operate_middleware-0.1.0rc36/operate/utils → olas_operate_middleware-0.13.5/operate/data/contracts/staking_token}/__init__.py +1 -1
  50. olas_operate_middleware-0.13.5/operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
  51. {olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token → olas_operate_middleware-0.13.5/operate/data/contracts/staking_token}/contract.py +30 -16
  52. olas_operate_middleware-0.13.5/operate/data/contracts/staking_token/contract.yaml +23 -0
  53. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
  54. {olas_operate_middleware-0.1.0rc36/operate → olas_operate_middleware-0.13.5/operate/data/contracts/uniswap_v2_erc20/tests}/__init__.py +2 -2
  55. olas_operate_middleware-0.13.5/operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
  56. olas_operate_middleware-0.13.5/operate/keys.py +193 -0
  57. olas_operate_middleware-0.13.5/operate/ledger/__init__.py +196 -0
  58. olas_operate_middleware-0.13.5/operate/ledger/profiles.py +348 -0
  59. olas_operate_middleware-0.13.5/operate/migration.py +555 -0
  60. {olas_operate_middleware-0.1.0rc36/operate/http → olas_operate_middleware-0.13.5/operate/operate_http}/__init__.py +3 -4
  61. {olas_operate_middleware-0.1.0rc36/operate/http → olas_operate_middleware-0.13.5/operate/operate_http}/exceptions.py +6 -4
  62. olas_operate_middleware-0.13.5/operate/operate_types.py +454 -0
  63. olas_operate_middleware-0.13.5/operate/pearl.py +50 -0
  64. olas_operate_middleware-0.13.5/operate/quickstart/analyse_logs.py +118 -0
  65. olas_operate_middleware-0.13.5/operate/quickstart/claim_staking_rewards.py +104 -0
  66. olas_operate_middleware-0.13.5/operate/quickstart/reset_configs.py +106 -0
  67. olas_operate_middleware-0.13.5/operate/quickstart/reset_password.py +70 -0
  68. olas_operate_middleware-0.13.5/operate/quickstart/reset_staking.py +145 -0
  69. olas_operate_middleware-0.13.5/operate/quickstart/run_service.py +784 -0
  70. olas_operate_middleware-0.13.5/operate/quickstart/stop_service.py +72 -0
  71. olas_operate_middleware-0.13.5/operate/quickstart/terminate_on_chain_service.py +83 -0
  72. olas_operate_middleware-0.13.5/operate/quickstart/utils.py +302 -0
  73. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/resource.py +64 -5
  74. olas_operate_middleware-0.13.5/operate/services/agent_runner.py +202 -0
  75. olas_operate_middleware-0.13.5/operate/services/deployment_runner.py +868 -0
  76. olas_operate_middleware-0.13.5/operate/services/funding_manager.py +929 -0
  77. olas_operate_middleware-0.13.5/operate/services/health_checker.py +280 -0
  78. olas_operate_middleware-0.13.5/operate/services/manage.py +2774 -0
  79. olas_operate_middleware-0.13.5/operate/services/protocol.py +2027 -0
  80. olas_operate_middleware-0.13.5/operate/services/service.py +1270 -0
  81. olas_operate_middleware-0.13.5/operate/services/utils/mech.py +103 -0
  82. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/services/utils/tendermint.py +96 -14
  83. olas_operate_middleware-0.13.5/operate/settings.py +70 -0
  84. olas_operate_middleware-0.13.5/operate/utils/__init__.py +155 -0
  85. olas_operate_middleware-0.13.5/operate/utils/gnosis.py +649 -0
  86. olas_operate_middleware-0.13.5/operate/utils/single_instance.py +226 -0
  87. olas_operate_middleware-0.13.5/operate/utils/ssl.py +133 -0
  88. olas_operate_middleware-0.13.5/operate/wallet/master.py +994 -0
  89. olas_operate_middleware-0.13.5/operate/wallet/wallet_recovery_manager.py +509 -0
  90. olas_operate_middleware-0.13.5/pyproject.toml +46 -0
  91. olas_operate_middleware-0.1.0rc36/PKG-INFO +0 -302
  92. olas_operate_middleware-0.1.0rc36/README.md +0 -262
  93. olas_operate_middleware-0.1.0rc36/operate/cli.py +0 -703
  94. olas_operate_middleware-0.1.0rc36/operate/constants.py +0 -37
  95. olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token/__init__.py +0 -20
  96. olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
  97. olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token/contract.yaml +0 -23
  98. olas_operate_middleware-0.1.0rc36/operate/keys.py +0 -106
  99. olas_operate_middleware-0.1.0rc36/operate/ledger/__init__.py +0 -99
  100. olas_operate_middleware-0.1.0rc36/operate/ledger/ethereum.py +0 -48
  101. olas_operate_middleware-0.1.0rc36/operate/ledger/profiles.py +0 -44
  102. olas_operate_middleware-0.1.0rc36/operate/ledger/solana.py +0 -38
  103. olas_operate_middleware-0.1.0rc36/operate/services/manage.py +0 -945
  104. olas_operate_middleware-0.1.0rc36/operate/services/protocol.py +0 -1167
  105. olas_operate_middleware-0.1.0rc36/operate/services/service.py +0 -934
  106. olas_operate_middleware-0.1.0rc36/operate/types.py +0 -260
  107. olas_operate_middleware-0.1.0rc36/operate/utils/gnosis.py +0 -347
  108. olas_operate_middleware-0.1.0rc36/operate/wallet/master.py +0 -411
  109. olas_operate_middleware-0.1.0rc36/pyproject.toml +0 -50
  110. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/LICENSE +0 -0
  111. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/account/__init__.py +0 -0
  112. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/__init__.py +0 -0
  113. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/__init__.py +0 -0
  114. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/__init__.py +0 -0
  115. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/build/IUniswapV2ERC20.json +0 -0
  116. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/contract.py +0 -0
  117. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/services/__init__.py +0 -0
  118. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/services/utils/__init__.py +0 -0
  119. {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/wallet/__init__.py +0 -0
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: olas-operate-middleware
3
+ Version: 0.13.5
4
+ Summary:
5
+ License-File: LICENSE
6
+ Author: David Vilela
7
+ Author-email: dvilelaf@gmail.com
8
+ Requires-Python: >=3.10,<3.12
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Requires-Dist: argon2-cffi (==23.1.0)
13
+ Requires-Dist: clea (==0.1.0rc4)
14
+ Requires-Dist: cryptography (>=46.0.3,<47.0.0)
15
+ Requires-Dist: cytoolz (==0.12.3)
16
+ Requires-Dist: deepdiff (>=8.6.1,<9.0.0)
17
+ Requires-Dist: fastapi (==0.110.3)
18
+ Requires-Dist: halo (==0.0.31)
19
+ Requires-Dist: multiaddr (==0.0.9)
20
+ Requires-Dist: open-aea-cli-ipfs (>=2.0.6,<3.0.0)
21
+ Requires-Dist: open-aea-ledger-cosmos (>=2.0.6,<3.0.0)
22
+ Requires-Dist: open-aea-ledger-ethereum (>=2.0.6,<3.0.0)
23
+ Requires-Dist: open-aea-ledger-ethereum-flashbots (>=2.0.6,<3.0.0)
24
+ Requires-Dist: open-autonomy (>=0.21.4,<0.22.0)
25
+ Requires-Dist: psutil (>=5.9.8,<6.0.0)
26
+ Requires-Dist: pyinstaller (>=6.8.0,<7.0.0)
27
+ Requires-Dist: requests-mock (>=1.12.1,<2.0.0)
28
+ Requires-Dist: uvicorn (==0.27.0)
29
+ Description-Content-Type: text/markdown
30
+
31
+ <h1 align="center">
32
+ <b>Olas Operate Middleware</b>
33
+ </h1>
34
+
35
+ A cross-platform python package used to run autonomous agents powered by the OLAS Network.
36
+
37
+ ## Getting Started
38
+
39
+ Install using
40
+ ```
41
+ pip install olas-operate-middleware
42
+ ```
43
+
44
+ Start the daemon service using
45
+ ```
46
+ python -m operate.cli daemon
47
+ ```
48
+
49
+ ## License
50
+
51
+ - [Apache 2.0](LICENSE)
52
+
53
+ ## Security
54
+
55
+ - [Security Policy](SECURITY.md)
56
+
@@ -0,0 +1,25 @@
1
+ <h1 align="center">
2
+ <b>Olas Operate Middleware</b>
3
+ </h1>
4
+
5
+ A cross-platform python package used to run autonomous agents powered by the OLAS Network.
6
+
7
+ ## Getting Started
8
+
9
+ Install using
10
+ ```
11
+ pip install olas-operate-middleware
12
+ ```
13
+
14
+ Start the daemon service using
15
+ ```
16
+ python -m operate.cli daemon
17
+ ```
18
+
19
+ ## License
20
+
21
+ - [Apache 2.0](LICENSE)
22
+
23
+ ## Security
24
+
25
+ - [Security Policy](SECURITY.md)
@@ -17,21 +17,21 @@
17
17
  #
18
18
  # ------------------------------------------------------------------------------
19
19
 
20
- """Base class."""
20
+ """Operate app."""
21
21
 
22
- import typing as t
23
- from abc import ABC, abstractmethod
22
+ import logging
23
+ from importlib.metadata import PackageNotFoundError, version
24
24
 
25
25
 
26
- class LedgerHelper(ABC): # pylint: disable=too-few-public-methods
27
- """Base ledger helper."""
26
+ try:
27
+ # Prefer the distribution name if installed; fall back to the module name
28
+ __version__ = version("olas-operate-middleware")
29
+ except PackageNotFoundError:
30
+ try:
31
+ __version__ = version("operate")
32
+ except PackageNotFoundError:
33
+ logger = logging.getLogger("operate")
34
+ logger.warning("Could not determine version, using 0.0.0+local")
35
+ __version__ = "0.0.0+local"
28
36
 
29
- api: t.Any
30
-
31
- def __init__(self, rpc: str) -> None:
32
- """Initialize object."""
33
- self.rpc = rpc
34
-
35
- @abstractmethod
36
- def create_key(self) -> t.Any:
37
- """Create key."""
37
+ logging.getLogger("aea").setLevel(logging.ERROR)
@@ -23,21 +23,22 @@ import hashlib
23
23
  from dataclasses import dataclass
24
24
  from pathlib import Path
25
25
 
26
+ import argon2
27
+
26
28
  from operate.resource import LocalResource
27
29
 
28
30
 
29
- def sha256(string: str) -> str:
30
- """Get SHA256 hexdigest of a string."""
31
- sh256 = hashlib.sha256()
32
- sh256.update(string.encode())
33
- return sh256.hexdigest()
31
+ def argon2id(password: str) -> str:
32
+ """Get Argon2id digest of a password."""
33
+ ph = argon2.PasswordHasher() # Defaults to Argon2id
34
+ return ph.hash(password)
34
35
 
35
36
 
36
37
  @dataclass
37
38
  class UserAccount(LocalResource):
38
39
  """User account."""
39
40
 
40
- password_sha: str
41
+ password_hash: str
41
42
  path: Path
42
43
 
43
44
  @classmethod
@@ -49,7 +50,7 @@ class UserAccount(LocalResource):
49
50
  def new(cls, password: str, path: Path) -> "UserAccount":
50
51
  """Create a new user."""
51
52
  user = UserAccount(
52
- password_sha=sha256(string=password),
53
+ password_hash=argon2id(password=password),
53
54
  path=path,
54
55
  )
55
56
  user.store()
@@ -57,11 +58,36 @@ class UserAccount(LocalResource):
57
58
 
58
59
  def is_valid(self, password: str) -> bool:
59
60
  """Check if a password string is valid."""
60
- return sha256(string=password) == self.password_sha
61
+ try:
62
+ ph = argon2.PasswordHasher()
63
+ valid = ph.verify(self.password_hash, password)
64
+
65
+ if valid and ph.check_needs_rehash(self.password_hash):
66
+ self.password_hash = argon2id(password)
67
+ self.store()
68
+
69
+ return valid
70
+ except argon2.exceptions.VerificationError:
71
+ return False
72
+ except argon2.exceptions.InvalidHashError:
73
+ # Verify legacy password hash and update it to Argon2id if valid
74
+ sha256 = hashlib.sha256()
75
+ sha256.update(password.encode())
76
+ if sha256.hexdigest() == self.password_hash:
77
+ self.password_hash = argon2id(password=password)
78
+ self.store()
79
+ return True
80
+
81
+ return False
61
82
 
62
83
  def update(self, old_password: str, new_password: str) -> None:
63
84
  """Update current password."""
64
85
  if not self.is_valid(password=old_password):
65
86
  raise ValueError("Old password is not valid")
66
- self.password_sha = sha256(string=new_password)
87
+ self.password_hash = argon2id(password=new_password)
88
+ self.store()
89
+
90
+ def force_update(self, new_password: str) -> None:
91
+ """Force update current password."""
92
+ self.password_hash = argon2id(password=new_password)
67
93
  self.store()
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ------------------------------------------------------------------------------
4
+ #
5
+ # Copyright 2024 Valory AG
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ # ------------------------------------------------------------------------------
20
+ """Bridge manager."""
21
+
22
+
23
+ import json
24
+ import logging
25
+ import time
26
+ import typing as t
27
+ import uuid
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import cast
31
+
32
+ from deepdiff import DeepDiff
33
+ from web3 import Web3
34
+
35
+ from operate.bridge.providers.lifi_provider import LiFiProvider
36
+ from operate.bridge.providers.native_bridge_provider import (
37
+ NativeBridgeProvider,
38
+ OmnibridgeContractAdaptor,
39
+ OptimismContractAdaptor,
40
+ )
41
+ from operate.bridge.providers.provider import Provider, ProviderRequest
42
+ from operate.bridge.providers.relay_provider import RelayProvider
43
+ from operate.constants import ZERO_ADDRESS
44
+ from operate.ledger.profiles import USDC
45
+ from operate.operate_types import Chain, ChainAmounts
46
+ from operate.resource import LocalResource
47
+ from operate.services.manage import get_assets_balances
48
+ from operate.wallet.master import MasterWalletManager
49
+
50
+
51
+ DEFAULT_BUNDLE_VALIDITY_PERIOD = 3 * 60
52
+ EXECUTED_BUNDLES_PATH = "executed"
53
+ BRIDGE_REQUEST_BUNDLE_PREFIX = "rb-"
54
+
55
+ LIFI_PROVIDER_ID = "lifi-provider"
56
+ RELAY_PROVIDER_ID = "relay-provider"
57
+
58
+ NATIVE_BRIDGE_PROVIDER_CONFIGS: t.Dict[str, t.Any] = {
59
+ "native-ethereum-to-base": {
60
+ "from_chain": Chain.ETHEREUM.value,
61
+ "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35",
62
+ "to_chain": Chain.BASE.value,
63
+ "to_bridge": "0x4200000000000000000000000000000000000010",
64
+ "bridge_eta": 300,
65
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
66
+ },
67
+ "native-ethereum-to-celo": {
68
+ "from_chain": Chain.ETHEREUM.value,
69
+ "from_bridge": "0x9C4955b92F34148dbcfDCD82e9c9eCe5CF2badfe",
70
+ "to_chain": Chain.CELO.value,
71
+ "to_bridge": "0x4200000000000000000000000000000000000010",
72
+ "bridge_eta": 300,
73
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
74
+ },
75
+ "native-ethereum-to-gnosis": {
76
+ "from_chain": Chain.ETHEREUM.value,
77
+ "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671",
78
+ "to_chain": Chain.GNOSIS.value,
79
+ "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d",
80
+ "bridge_eta": 1800,
81
+ "bridge_contract_adaptor_class": OmnibridgeContractAdaptor,
82
+ },
83
+ "native-ethereum-to-mode": {
84
+ "from_chain": Chain.ETHEREUM.value,
85
+ "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21",
86
+ "to_chain": Chain.MODE.value,
87
+ "to_bridge": "0x4200000000000000000000000000000000000010",
88
+ "bridge_eta": 300,
89
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
90
+ },
91
+ "native-ethereum-to-optimism": {
92
+ "from_chain": Chain.ETHEREUM.value,
93
+ "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
94
+ "to_chain": Chain.OPTIMISM.value,
95
+ "to_bridge": "0x4200000000000000000000000000000000000010",
96
+ "bridge_eta": 300,
97
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
98
+ },
99
+ }
100
+
101
+ # Routes are defined as the tuples (from_chain, from_token, to_chain, to_token)
102
+ PREFERRED_ROUTES = {
103
+ (
104
+ Chain.ETHEREUM,
105
+ USDC[Chain.ETHEREUM],
106
+ Chain.OPTIMISM,
107
+ USDC[Chain.OPTIMISM],
108
+ ): LIFI_PROVIDER_ID,
109
+ (
110
+ Chain.ETHEREUM,
111
+ USDC[Chain.ETHEREUM],
112
+ Chain.BASE,
113
+ USDC[Chain.BASE],
114
+ ): LIFI_PROVIDER_ID,
115
+ (Chain.ETHEREUM, ZERO_ADDRESS, Chain.GNOSIS, ZERO_ADDRESS): RELAY_PROVIDER_ID,
116
+ }
117
+
118
+
119
+ @dataclass
120
+ class ProviderRequestBundle(LocalResource):
121
+ """ProviderRequestBundle"""
122
+
123
+ requests_params: t.List[t.Dict]
124
+ provider_requests: t.List[ProviderRequest]
125
+ timestamp: int
126
+ id: str
127
+
128
+ def get_from_chains(self) -> set[Chain]:
129
+ """Get 'from' chains."""
130
+ return {
131
+ Chain(request.params["from"]["chain"]) for request in self.provider_requests
132
+ }
133
+
134
+ def get_from_addresses(self, chain: Chain) -> set[str]:
135
+ """Get 'from' addresses."""
136
+ chain_str = chain.value
137
+ return {
138
+ request.params["from"]["address"]
139
+ for request in self.provider_requests
140
+ if request.params["from"]["chain"] == chain_str
141
+ }
142
+
143
+ def get_from_tokens(self, chain: Chain) -> set[str]:
144
+ """Get 'from' tokens."""
145
+ chain_str = chain.value
146
+ return {
147
+ request.params["from"]["token"]
148
+ for request in self.provider_requests
149
+ if request.params["from"]["chain"] == chain_str
150
+ }
151
+
152
+
153
+ @dataclass
154
+ class BridgeManagerData(LocalResource):
155
+ """BridgeManagerData"""
156
+
157
+ path: Path
158
+ version: int = 1
159
+ last_requested_bundle: t.Optional[ProviderRequestBundle] = None
160
+ last_executed_bundle_id: t.Optional[str] = None
161
+
162
+ _file = "bridge.json"
163
+
164
+ # TODO Migrate to LocalResource?
165
+ # It can be inconvenient that all local resources create an empty resource
166
+ # if the file is corrupted. For example, if a service configuration is
167
+ # corrupted, we might want to halt execution, because otherwise, the application
168
+ # could continue as if the user is creating a service from scratch.
169
+ # For the bridge manager data, it's harmless, because its memory
170
+ # is limited to the process of getting and executing a quote.
171
+ @classmethod # Overrides from LocalResource
172
+ def load(cls, path: Path) -> "LocalResource":
173
+ """Load local resource."""
174
+
175
+ file = path / cls._file
176
+ if not file.exists():
177
+ BridgeManagerData(path=path).store()
178
+
179
+ try:
180
+ super().load(path=file)
181
+ except (json.JSONDecodeError, KeyError):
182
+ new_file = path / f"invalid_{int(time.time())}_{cls._file}"
183
+ file.rename(new_file)
184
+ BridgeManagerData(path=path).store()
185
+
186
+ return super().load(path)
187
+
188
+
189
+ class BridgeManager:
190
+ """BridgeManager"""
191
+
192
+ def __init__(
193
+ self,
194
+ path: Path,
195
+ wallet_manager: MasterWalletManager,
196
+ logger: logging.Logger,
197
+ bundle_validity_period: int = DEFAULT_BUNDLE_VALIDITY_PERIOD,
198
+ ) -> None:
199
+ """Initialize bridge manager."""
200
+ self.path = path
201
+ self.wallet_manager = wallet_manager
202
+ self.logger = logger
203
+ self.bundle_validity_period = bundle_validity_period
204
+ self.path.mkdir(exist_ok=True)
205
+ (self.path / EXECUTED_BUNDLES_PATH).mkdir(exist_ok=True)
206
+ self.data: BridgeManagerData = cast(
207
+ BridgeManagerData, BridgeManagerData.load(path)
208
+ )
209
+ self._native_bridge_providers = {
210
+ provider_id: NativeBridgeProvider(
211
+ config["bridge_contract_adaptor_class"](
212
+ from_chain=config["from_chain"],
213
+ to_chain=config["to_chain"],
214
+ from_bridge=config["from_bridge"],
215
+ to_bridge=config["to_bridge"],
216
+ bridge_eta=config["bridge_eta"],
217
+ ),
218
+ provider_id,
219
+ wallet_manager,
220
+ logger,
221
+ )
222
+ for provider_id, config in NATIVE_BRIDGE_PROVIDER_CONFIGS.items()
223
+ }
224
+
225
+ self._providers: t.Dict[str, Provider] = {}
226
+ self._providers.update(self._native_bridge_providers)
227
+ self._providers[LIFI_PROVIDER_ID] = LiFiProvider(
228
+ provider_id=LIFI_PROVIDER_ID,
229
+ wallet_manager=wallet_manager,
230
+ logger=logger,
231
+ )
232
+ self._providers[RELAY_PROVIDER_ID] = RelayProvider(
233
+ provider_id=RELAY_PROVIDER_ID,
234
+ wallet_manager=wallet_manager,
235
+ logger=logger,
236
+ )
237
+
238
+ def _store_data(self) -> None:
239
+ self.logger.info("[BRIDGE MANAGER] Storing data to file.")
240
+ self.data.store()
241
+
242
+ def _get_updated_bundle(
243
+ self, requests_params: t.List[t.Dict], force_update: bool
244
+ ) -> ProviderRequestBundle:
245
+ """Ensures to return a valid (non expired) bundle for the given inputs."""
246
+
247
+ now = int(time.time())
248
+ bundle = self.data.last_requested_bundle
249
+ create_new_bundle = False
250
+
251
+ if not bundle:
252
+ self.logger.info("[BRIDGE MANAGER] No last bundle.")
253
+ create_new_bundle = True
254
+ elif DeepDiff(requests_params, bundle.requests_params):
255
+ self.logger.info("[BRIDGE MANAGER] Different requests params.")
256
+ create_new_bundle = True
257
+ elif force_update:
258
+ self.logger.info("[BRIDGE MANAGER] Force bundle update.")
259
+ self.quote_bundle(bundle)
260
+ self._store_data()
261
+ elif now > bundle.timestamp + self.bundle_validity_period:
262
+ self.logger.info("[BRIDGE MANAGER] Bundle expired.")
263
+ self.quote_bundle(bundle)
264
+ self._store_data()
265
+
266
+ if not bundle or create_new_bundle:
267
+ self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.")
268
+
269
+ provider_requests = []
270
+ for params in requests_params:
271
+ route = (
272
+ Chain(params["from"]["chain"]),
273
+ params["from"]["token"],
274
+ Chain(params["to"]["chain"]),
275
+ params["to"]["token"],
276
+ )
277
+ provider_id = PREFERRED_ROUTES.get(route)
278
+
279
+ if not provider_id:
280
+ for provider in self._native_bridge_providers.values():
281
+ if provider.can_handle_request(params):
282
+ provider_id = provider.provider_id
283
+ break
284
+
285
+ if not provider_id:
286
+ provider_id = RELAY_PROVIDER_ID
287
+
288
+ provider_requests.append(
289
+ self._providers[provider_id].create_request(params=params)
290
+ )
291
+
292
+ bundle = ProviderRequestBundle(
293
+ id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}",
294
+ requests_params=requests_params,
295
+ provider_requests=provider_requests,
296
+ timestamp=now,
297
+ )
298
+
299
+ self.data.last_requested_bundle = bundle
300
+ self.quote_bundle(bundle)
301
+ self._store_data()
302
+
303
+ return bundle
304
+
305
+ def _sanitize(self, requests_params: t.List) -> None:
306
+ """Sanitize quote requests."""
307
+ w3 = Web3()
308
+ for params in requests_params:
309
+ params["from"]["address"] = w3.to_checksum_address(
310
+ params["from"]["address"]
311
+ )
312
+ params["from"]["token"] = w3.to_checksum_address(params["from"]["token"])
313
+ params["to"]["address"] = w3.to_checksum_address(params["to"]["address"])
314
+ params["to"]["token"] = w3.to_checksum_address(params["to"]["token"])
315
+ params["to"]["amount"] = int(params["to"]["amount"])
316
+
317
+ def _raise_if_invalid(self, requests_params: t.List) -> None:
318
+ """Preprocess quote requests."""
319
+
320
+ for params in requests_params:
321
+ from_chain = params["from"]["chain"]
322
+ from_address = params["from"]["address"]
323
+
324
+ wallet = self.wallet_manager.load(Chain(from_chain).ledger_type)
325
+ wallet_address = wallet.address
326
+ safe_address = wallet.safes.get(Chain(from_chain))
327
+
328
+ if from_address is None or not (
329
+ from_address == wallet_address or from_address == safe_address
330
+ ):
331
+ raise ValueError(
332
+ f"Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}."
333
+ )
334
+
335
+ def bridge_refill_requirements(
336
+ self, requests_params: t.List[t.Dict], force_update: bool = False
337
+ ) -> t.Dict:
338
+ """Get bridge refill requirements."""
339
+ self._sanitize(requests_params)
340
+ self._raise_if_invalid(requests_params)
341
+ self.logger.info(
342
+ f"[BRIDGE MANAGER] Quote requests count: {len(requests_params)}."
343
+ )
344
+
345
+ bundle = self._get_updated_bundle(requests_params, force_update)
346
+
347
+ balances = ChainAmounts()
348
+ for chain in bundle.get_from_chains():
349
+ ledger_api = self.wallet_manager.load(chain.ledger_type).ledger_api(chain)
350
+ balances[chain.value] = get_assets_balances(
351
+ ledger_api=ledger_api,
352
+ asset_addresses={ZERO_ADDRESS} | bundle.get_from_tokens(chain),
353
+ addresses=bundle.get_from_addresses(chain),
354
+ )
355
+
356
+ bridge_total_requirements = self.bridge_total_requirements(bundle)
357
+
358
+ bridge_refill_requirements = ChainAmounts.shortfalls(
359
+ bridge_total_requirements, balances
360
+ )
361
+
362
+ is_refill_required = any(
363
+ amount > 0
364
+ for from_addresses in bridge_refill_requirements.values()
365
+ for from_tokens in from_addresses.values()
366
+ for amount in from_tokens.values()
367
+ )
368
+
369
+ status_json = self.get_status_json(bundle.id)
370
+ status_json.update(
371
+ {
372
+ "balances": balances,
373
+ "bridge_refill_requirements": bridge_refill_requirements,
374
+ "bridge_total_requirements": bridge_total_requirements,
375
+ "expiration_timestamp": bundle.timestamp + self.bundle_validity_period,
376
+ "is_refill_required": is_refill_required,
377
+ }
378
+ )
379
+ return status_json
380
+
381
+ def execute_bundle(self, bundle_id: str) -> t.Dict:
382
+ """Execute the bundle"""
383
+
384
+ bundle = self.data.last_requested_bundle
385
+
386
+ if not bundle:
387
+ raise RuntimeError("[BRIDGE MANAGER] No bundle.")
388
+
389
+ if bundle.id != bundle_id:
390
+ raise RuntimeError(
391
+ f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}."
392
+ )
393
+
394
+ requirements = self.bridge_refill_requirements(bundle.requests_params)
395
+ self.data.last_requested_bundle = None
396
+ self.data.last_executed_bundle_id = bundle_id
397
+ bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json"
398
+ bundle.path = bundle_path
399
+ self._store_data()
400
+ bundle.store()
401
+
402
+ if requirements["is_refill_required"]:
403
+ self.logger.warning(
404
+ f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}."
405
+ )
406
+
407
+ self.logger.info("[BRIDGE MANAGER] Executing quotes.")
408
+
409
+ for request in bundle.provider_requests:
410
+ provider = self._providers[request.provider_id]
411
+ provider.execute(request)
412
+ self._store_data()
413
+
414
+ self._store_data()
415
+ bundle.store()
416
+ self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.")
417
+ return self.get_status_json(bundle_id)
418
+
419
+ def get_status_json(self, bundle_id: str) -> t.Dict:
420
+ """Get execution status of bundle."""
421
+ bundle = self.data.last_requested_bundle
422
+
423
+ if bundle is not None and bundle.id == bundle_id:
424
+ pass
425
+ else:
426
+ bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json"
427
+ if bundle_path.exists():
428
+ bundle = cast(
429
+ ProviderRequestBundle, ProviderRequestBundle.load(bundle_path)
430
+ )
431
+ bundle.path = bundle_path # TODO backport to resource.py ?
432
+ else:
433
+ raise FileNotFoundError(f"Bundle with ID {bundle_id} does not exist.")
434
+
435
+ initial_status = [request.status for request in bundle.provider_requests]
436
+
437
+ provider_request_status = []
438
+ for request in bundle.provider_requests:
439
+ provider = self._providers[request.provider_id]
440
+ provider_request_status.append(provider.status_json(request))
441
+
442
+ updated_status = [request.status for request in bundle.provider_requests]
443
+
444
+ if initial_status != updated_status and bundle.path is not None:
445
+ bundle.store()
446
+
447
+ return {
448
+ "id": bundle.id,
449
+ "bridge_request_status": provider_request_status,
450
+ }
451
+
452
+ def bridge_total_requirements(self, bundle: ProviderRequestBundle) -> ChainAmounts:
453
+ """Sum bridge requirements."""
454
+ requirements = []
455
+ for provider_request in bundle.provider_requests:
456
+ provider = self._providers[provider_request.provider_id]
457
+ requirements.append(provider.requirements(provider_request))
458
+
459
+ return ChainAmounts.add(*requirements)
460
+
461
+ def quote_bundle(self, bundle: ProviderRequestBundle) -> None:
462
+ """Update the bundle with the quotes."""
463
+ for provider_request in bundle.provider_requests:
464
+ provider = self._providers[provider_request.provider_id]
465
+ provider.quote(provider_request)
466
+ bundle.timestamp = int(time.time())
467
+
468
+ def last_executed_bundle_id(self) -> t.Optional[str]:
469
+ """Get the last executed bundle id."""
470
+ return self.data.last_executed_bundle_id