olas-operate-middleware 0.2.4__tar.gz → 0.11.4__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 (109) hide show
  1. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/PKG-INFO +17 -13
  2. olas_operate_middleware-0.2.4/operate/ledger/solana.py → olas_operate_middleware-0.11.4/operate/__init__.py +14 -15
  3. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/account/user.py +31 -10
  4. olas_operate_middleware-0.11.4/operate/bridge/bridge_manager.py +460 -0
  5. olas_operate_middleware-0.11.4/operate/bridge/providers/lifi_provider.py +377 -0
  6. olas_operate_middleware-0.11.4/operate/bridge/providers/native_bridge_provider.py +664 -0
  7. olas_operate_middleware-0.11.4/operate/bridge/providers/provider.py +469 -0
  8. olas_operate_middleware-0.11.4/operate/bridge/providers/relay_provider.py +457 -0
  9. olas_operate_middleware-0.11.4/operate/cli.py +1837 -0
  10. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/constants.py +42 -12
  11. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/README.md +11 -2
  12. olas_operate_middleware-0.11.4/operate/data/contracts/dual_staking_token/__init__.py +20 -0
  13. olas_operate_middleware-0.11.4/operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
  14. olas_operate_middleware-0.11.4/operate/data/contracts/dual_staking_token/contract.py +132 -0
  15. olas_operate_middleware-0.11.4/operate/data/contracts/dual_staking_token/contract.yaml +23 -0
  16. olas_operate_middleware-0.11.4/operate/data/contracts/foreign_omnibridge/__init__.py +20 -0
  17. olas_operate_middleware-0.11.4/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
  18. olas_operate_middleware-0.11.4/operate/data/contracts/foreign_omnibridge/contract.py +130 -0
  19. olas_operate_middleware-0.11.4/operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
  20. olas_operate_middleware-0.11.4/operate/data/contracts/home_omnibridge/__init__.py +20 -0
  21. olas_operate_middleware-0.11.4/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
  22. olas_operate_middleware-0.11.4/operate/data/contracts/home_omnibridge/contract.py +80 -0
  23. olas_operate_middleware-0.11.4/operate/data/contracts/home_omnibridge/contract.yaml +23 -0
  24. olas_operate_middleware-0.11.4/operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
  25. olas_operate_middleware-0.11.4/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
  26. olas_operate_middleware-0.11.4/operate/data/contracts/l1_standard_bridge/contract.py +138 -0
  27. olas_operate_middleware-0.11.4/operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
  28. {olas_operate_middleware-0.2.4/operate → olas_operate_middleware-0.11.4/operate/data/contracts/l2_standard_bridge}/__init__.py +2 -7
  29. olas_operate_middleware-0.11.4/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
  30. olas_operate_middleware-0.11.4/operate/data/contracts/l2_standard_bridge/contract.py +130 -0
  31. olas_operate_middleware-0.11.4/operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
  32. olas_operate_middleware-0.11.4/operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
  33. olas_operate_middleware-0.11.4/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
  34. olas_operate_middleware-0.2.4/operate/utils/__init__.py → olas_operate_middleware-0.11.4/operate/data/contracts/optimism_mintable_erc20/contract.py +20 -22
  35. olas_operate_middleware-0.11.4/operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
  36. olas_operate_middleware-0.11.4/operate/data/contracts/recovery_module/__init__.py +20 -0
  37. olas_operate_middleware-0.11.4/operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
  38. olas_operate_middleware-0.11.4/operate/data/contracts/recovery_module/contract.py +61 -0
  39. olas_operate_middleware-0.11.4/operate/data/contracts/recovery_module/contract.yaml +23 -0
  40. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/uniswap_v2_erc20/contract.py +0 -1
  41. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +1 -1
  42. olas_operate_middleware-0.11.4/operate/keys.py +180 -0
  43. olas_operate_middleware-0.11.4/operate/ledger/__init__.py +202 -0
  44. olas_operate_middleware-0.11.4/operate/ledger/profiles.py +336 -0
  45. olas_operate_middleware-0.11.4/operate/migration.py +456 -0
  46. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/operate_http/__init__.py +2 -1
  47. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/operate_http/exceptions.py +6 -4
  48. olas_operate_middleware-0.11.4/operate/operate_types.py +528 -0
  49. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/analyse_logs.py +3 -3
  50. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/claim_staking_rewards.py +11 -12
  51. olas_operate_middleware-0.11.4/operate/quickstart/reset_configs.py +106 -0
  52. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/reset_password.py +4 -3
  53. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/reset_staking.py +5 -3
  54. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/run_service.py +205 -227
  55. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/stop_service.py +3 -1
  56. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/terminate_on_chain_service.py +1 -1
  57. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/quickstart/utils.py +26 -26
  58. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/resource.py +76 -1
  59. olas_operate_middleware-0.11.4/operate/services/agent_runner.py +212 -0
  60. olas_operate_middleware-0.11.4/operate/services/deployment_runner.py +775 -0
  61. olas_operate_middleware-0.11.4/operate/services/funding_manager.py +924 -0
  62. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/health_checker.py +38 -26
  63. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/manage.py +1115 -907
  64. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/protocol.py +602 -196
  65. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/service.py +282 -315
  66. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/utils/mech.py +34 -26
  67. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/utils/tendermint.py +27 -8
  68. olas_operate_middleware-0.11.4/operate/settings.py +70 -0
  69. olas_operate_middleware-0.11.4/operate/utils/__init__.py +109 -0
  70. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/utils/gnosis.py +167 -105
  71. olas_operate_middleware-0.11.4/operate/utils/single_instance.py +226 -0
  72. olas_operate_middleware-0.11.4/operate/utils/ssl.py +133 -0
  73. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/wallet/master.py +315 -223
  74. olas_operate_middleware-0.11.4/operate/wallet/wallet_recovery_manager.py +210 -0
  75. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/pyproject.toml +15 -12
  76. olas_operate_middleware-0.2.4/operate/cli.py +0 -1185
  77. olas_operate_middleware-0.2.4/operate/data/contracts/service_staking_token/contract.yaml +0 -23
  78. olas_operate_middleware-0.2.4/operate/keys.py +0 -125
  79. olas_operate_middleware-0.2.4/operate/ledger/__init__.py +0 -127
  80. olas_operate_middleware-0.2.4/operate/ledger/base.py +0 -37
  81. olas_operate_middleware-0.2.4/operate/ledger/ethereum.py +0 -48
  82. olas_operate_middleware-0.2.4/operate/ledger/profiles.py +0 -188
  83. olas_operate_middleware-0.2.4/operate/operate_types.py +0 -333
  84. olas_operate_middleware-0.2.4/operate/services/deployment_runner.py +0 -452
  85. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/LICENSE +0 -0
  86. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/README.md +0 -0
  87. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/account/__init__.py +0 -0
  88. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/__init__.py +0 -0
  89. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/__init__.py +0 -0
  90. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/mech_activity/__init__.py +0 -0
  91. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/mech_activity/build/MechActivity.json +0 -0
  92. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/mech_activity/contract.py +0 -0
  93. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/mech_activity/contract.yaml +0 -0
  94. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/requester_activity_checker/__init__.py +0 -0
  95. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +0 -0
  96. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/requester_activity_checker/contract.py +0 -0
  97. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/requester_activity_checker/contract.yaml +0 -0
  98. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/staking_token/__init__.py +0 -0
  99. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/staking_token/build/StakingToken.json +0 -0
  100. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/staking_token/contract.py +0 -0
  101. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/staking_token/contract.yaml +0 -0
  102. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/uniswap_v2_erc20/__init__.py +0 -0
  103. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/uniswap_v2_erc20/build/IUniswapV2ERC20.json +0 -0
  104. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/uniswap_v2_erc20/contract.yaml +0 -0
  105. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +0 -0
  106. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/pearl.py +0 -0
  107. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/__init__.py +0 -0
  108. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/services/utils/__init__.py +0 -0
  109. {olas_operate_middleware-0.2.4 → olas_operate_middleware-0.11.4}/operate/wallet/__init__.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: olas-operate-middleware
3
- Version: 0.2.4
3
+ Version: 0.11.4
4
4
  Summary:
5
+ License-File: LICENSE
5
6
  Author: David Vilela
6
7
  Author-email: dvilelaf@gmail.com
7
8
  Requires-Python: >=3.9,<3.12
@@ -10,37 +11,40 @@ Classifier: Programming Language :: Python :: 3.9
10
11
  Classifier: Programming Language :: Python :: 3.10
11
12
  Classifier: Programming Language :: Python :: 3.11
12
13
  Requires-Dist: aiohttp (==3.9.5)
14
+ Requires-Dist: argon2-cffi (==23.1.0)
13
15
  Requires-Dist: clea (==0.1.0rc4)
14
16
  Requires-Dist: cytoolz (==0.12.3)
15
- Requires-Dist: deepdiff (>=8.0.1,<9.0.0)
17
+ Requires-Dist: deepdiff (>=8.6.1,<9.0.0)
16
18
  Requires-Dist: docker (==6.1.2)
17
- Requires-Dist: eth-abi (==5.1.0)
19
+ Requires-Dist: eth-abi (==4.0.0)
18
20
  Requires-Dist: eth-account (==0.8.0)
19
21
  Requires-Dist: eth-hash (==0.7.0)
20
22
  Requires-Dist: eth-keyfile (==0.6.1)
21
23
  Requires-Dist: eth-keys (==0.4.0)
22
24
  Requires-Dist: eth-rlp (==0.3.0)
23
25
  Requires-Dist: eth-typing (==3.5.2)
24
- Requires-Dist: eth-utils (==2.3.1)
25
- Requires-Dist: fastapi (==0.110.0)
26
+ Requires-Dist: eth-utils (==2.2.0)
27
+ Requires-Dist: fastapi (==0.110.3)
26
28
  Requires-Dist: frozenlist (==1.4.1)
27
29
  Requires-Dist: halo (==0.0.31)
28
30
  Requires-Dist: hexbytes (==0.3.1)
29
31
  Requires-Dist: ipfshttpclient (==0.8.0a2)
30
32
  Requires-Dist: jsonschema (==4.3.3)
33
+ Requires-Dist: multiaddr (==0.0.9)
31
34
  Requires-Dist: multidict (==6.0.5)
32
- Requires-Dist: open-aea-cli-ipfs (==1.64.0)
33
- Requires-Dist: open-aea-ledger-cosmos (==1.64.0)
34
- Requires-Dist: open-aea-ledger-ethereum (==1.64.0)
35
- Requires-Dist: open-aea-ledger-ethereum-flashbots (==1.64.0)
36
- Requires-Dist: open-autonomy (==0.19.4)
35
+ Requires-Dist: open-aea-cli-ipfs (==1.65.0)
36
+ Requires-Dist: open-aea-ledger-cosmos (==1.65.0)
37
+ Requires-Dist: open-aea-ledger-ethereum (==1.65.0)
38
+ Requires-Dist: open-aea-ledger-ethereum-flashbots (==1.65.0)
39
+ Requires-Dist: open-autonomy (>=0.20.2,<0.21.0)
37
40
  Requires-Dist: psutil (>=5.9.8,<6.0.0)
38
41
  Requires-Dist: pyinstaller (>=6.8.0,<7.0.0)
42
+ Requires-Dist: requests-mock (>=1.12.1,<2.0.0)
39
43
  Requires-Dist: requests-toolbelt (==1.0.0)
40
- Requires-Dist: starlette (==0.36.3)
44
+ Requires-Dist: starlette (==0.37.2)
41
45
  Requires-Dist: twikit (==2.2.0)
42
46
  Requires-Dist: uvicorn (==0.27.0)
43
- Requires-Dist: web3 (==6.1.0)
47
+ Requires-Dist: web3 (==6.20.4)
44
48
  Description-Content-Type: text/markdown
45
49
 
46
50
  <h1 align="center">
@@ -17,22 +17,21 @@
17
17
  #
18
18
  # ------------------------------------------------------------------------------
19
19
 
20
- """Solana ledger helpers."""
20
+ """Operate app."""
21
21
 
22
- import typing as t
22
+ import logging
23
+ from importlib.metadata import PackageNotFoundError, version
23
24
 
24
- from operate.ledger.base import LedgerHelper
25
- from operate.operate_types import LedgerType
26
25
 
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"
27
36
 
28
- class Solana(LedgerHelper):
29
- """Solana ledger helper."""
30
-
31
- def create_key(self) -> t.Dict:
32
- """Create key."""
33
- return {
34
- "address": "",
35
- "private_key": "",
36
- "encrypted": False,
37
- "ledger": LedgerType.SOLANA,
38
- }
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,16 +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)
67
88
  self.store()
68
89
 
69
90
  def force_update(self, new_password: str) -> None:
70
91
  """Force update current password."""
71
- self.password_sha = sha256(string=new_password)
92
+ self.password_hash = argon2id(password=new_password)
72
93
  self.store()
@@ -0,0 +1,460 @@
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": "ethereum",
61
+ "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35",
62
+ "to_chain": "base",
63
+ "to_bridge": "0x4200000000000000000000000000000000000010",
64
+ "bridge_eta": 300,
65
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
66
+ },
67
+ "native-ethereum-to-mode": {
68
+ "from_chain": "ethereum",
69
+ "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21",
70
+ "to_chain": "mode",
71
+ "to_bridge": "0x4200000000000000000000000000000000000010",
72
+ "bridge_eta": 300,
73
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
74
+ },
75
+ "native-ethereum-to-optimism": {
76
+ "from_chain": "ethereum",
77
+ "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
78
+ "to_chain": "optimism",
79
+ "to_bridge": "0x4200000000000000000000000000000000000010",
80
+ "bridge_eta": 300,
81
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
82
+ },
83
+ "native-ethereum-to-gnosis": {
84
+ "from_chain": "ethereum",
85
+ "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671",
86
+ "to_chain": "gnosis",
87
+ "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d",
88
+ "bridge_eta": 1800,
89
+ "bridge_contract_adaptor_class": OmnibridgeContractAdaptor,
90
+ },
91
+ }
92
+
93
+
94
+ ROUTES = {
95
+ (
96
+ Chain.ETHEREUM, # from_chain
97
+ USDC[Chain.ETHEREUM], # from_token
98
+ Chain.OPTIMISM, # to_chain
99
+ USDC[Chain.OPTIMISM], # to_token
100
+ ): LIFI_PROVIDER_ID,
101
+ (
102
+ Chain.ETHEREUM, # from_chain
103
+ USDC[Chain.ETHEREUM], # from_token
104
+ Chain.BASE, # to_chain
105
+ USDC[Chain.BASE], # to_token
106
+ ): LIFI_PROVIDER_ID,
107
+ (Chain.ETHEREUM, ZERO_ADDRESS, Chain.GNOSIS, ZERO_ADDRESS): RELAY_PROVIDER_ID,
108
+ }
109
+
110
+
111
+ @dataclass
112
+ class ProviderRequestBundle(LocalResource):
113
+ """ProviderRequestBundle"""
114
+
115
+ requests_params: t.List[t.Dict]
116
+ provider_requests: t.List[ProviderRequest]
117
+ timestamp: int
118
+ id: str
119
+
120
+ def get_from_chains(self) -> set[Chain]:
121
+ """Get 'from' chains."""
122
+ return {
123
+ Chain(request.params["from"]["chain"]) for request in self.provider_requests
124
+ }
125
+
126
+ def get_from_addresses(self, chain: Chain) -> set[str]:
127
+ """Get 'from' addresses."""
128
+ chain_str = chain.value
129
+ return {
130
+ request.params["from"]["address"]
131
+ for request in self.provider_requests
132
+ if request.params["from"]["chain"] == chain_str
133
+ }
134
+
135
+ def get_from_tokens(self, chain: Chain) -> set[str]:
136
+ """Get 'from' tokens."""
137
+ chain_str = chain.value
138
+ return {
139
+ request.params["from"]["token"]
140
+ for request in self.provider_requests
141
+ if request.params["from"]["chain"] == chain_str
142
+ }
143
+
144
+
145
+ @dataclass
146
+ class BridgeManagerData(LocalResource):
147
+ """BridgeManagerData"""
148
+
149
+ path: Path
150
+ version: int = 1
151
+ last_requested_bundle: t.Optional[ProviderRequestBundle] = None
152
+ last_executed_bundle_id: t.Optional[str] = None
153
+
154
+ _file = "bridge.json"
155
+
156
+ # TODO Migrate to LocalResource?
157
+ # It can be inconvenient that all local resources create an empty resource
158
+ # if the file is corrupted. For example, if a service configuration is
159
+ # corrupted, we might want to halt execution, because otherwise, the application
160
+ # could continue as if the user is creating a service from scratch.
161
+ # For the bridge manager data, it's harmless, because its memory
162
+ # is limited to the process of getting and executing a quote.
163
+ @classmethod # Overrides from LocalResource
164
+ def load(cls, path: Path) -> "LocalResource":
165
+ """Load local resource."""
166
+
167
+ file = path / cls._file
168
+ if not file.exists():
169
+ BridgeManagerData(path=path).store()
170
+
171
+ try:
172
+ super().load(path=file)
173
+ except (json.JSONDecodeError, KeyError):
174
+ new_file = path / f"invalid_{int(time.time())}_{cls._file}"
175
+ file.rename(new_file)
176
+ BridgeManagerData(path=path).store()
177
+
178
+ return super().load(path)
179
+
180
+
181
+ class BridgeManager:
182
+ """BridgeManager"""
183
+
184
+ def __init__(
185
+ self,
186
+ path: Path,
187
+ wallet_manager: MasterWalletManager,
188
+ logger: logging.Logger,
189
+ bundle_validity_period: int = DEFAULT_BUNDLE_VALIDITY_PERIOD,
190
+ ) -> None:
191
+ """Initialize bridge manager."""
192
+ self.path = path
193
+ self.wallet_manager = wallet_manager
194
+ self.logger = logger
195
+ self.bundle_validity_period = bundle_validity_period
196
+ self.path.mkdir(exist_ok=True)
197
+ (self.path / EXECUTED_BUNDLES_PATH).mkdir(exist_ok=True)
198
+ self.data: BridgeManagerData = cast(
199
+ BridgeManagerData, BridgeManagerData.load(path)
200
+ )
201
+ self._native_bridge_providers = {
202
+ provider_id: NativeBridgeProvider(
203
+ config["bridge_contract_adaptor_class"](
204
+ from_chain=config["from_chain"],
205
+ to_chain=config["to_chain"],
206
+ from_bridge=config["from_bridge"],
207
+ to_bridge=config["to_bridge"],
208
+ bridge_eta=config["bridge_eta"],
209
+ ),
210
+ provider_id,
211
+ wallet_manager,
212
+ logger,
213
+ )
214
+ for provider_id, config in NATIVE_BRIDGE_PROVIDER_CONFIGS.items()
215
+ }
216
+
217
+ self._providers: t.Dict[str, Provider] = {}
218
+ self._providers.update(self._native_bridge_providers)
219
+ self._providers[LIFI_PROVIDER_ID] = LiFiProvider(
220
+ provider_id=LIFI_PROVIDER_ID,
221
+ wallet_manager=wallet_manager,
222
+ logger=logger,
223
+ )
224
+ self._providers[RELAY_PROVIDER_ID] = RelayProvider(
225
+ provider_id=RELAY_PROVIDER_ID,
226
+ wallet_manager=wallet_manager,
227
+ logger=logger,
228
+ )
229
+
230
+ def _store_data(self) -> None:
231
+ self.logger.info("[BRIDGE MANAGER] Storing data to file.")
232
+ self.data.store()
233
+
234
+ def _get_updated_bundle(
235
+ self, requests_params: t.List[t.Dict], force_update: bool
236
+ ) -> ProviderRequestBundle:
237
+ """Ensures to return a valid (non expired) bundle for the given inputs."""
238
+
239
+ now = int(time.time())
240
+ bundle = self.data.last_requested_bundle
241
+ create_new_bundle = False
242
+
243
+ if not bundle:
244
+ self.logger.info("[BRIDGE MANAGER] No last bundle.")
245
+ create_new_bundle = True
246
+ elif DeepDiff(requests_params, bundle.requests_params):
247
+ self.logger.info("[BRIDGE MANAGER] Different requests params.")
248
+ create_new_bundle = True
249
+ elif force_update:
250
+ self.logger.info("[BRIDGE MANAGER] Force bundle update.")
251
+ self.quote_bundle(bundle)
252
+ self._store_data()
253
+ elif now > bundle.timestamp + self.bundle_validity_period:
254
+ self.logger.info("[BRIDGE MANAGER] Bundle expired.")
255
+ self.quote_bundle(bundle)
256
+ self._store_data()
257
+
258
+ if not bundle or create_new_bundle:
259
+ self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.")
260
+
261
+ provider_requests = []
262
+ for params in requests_params:
263
+ for provider in self._native_bridge_providers.values():
264
+ if provider.can_handle_request(params):
265
+ provider_requests.append(provider.create_request(params=params))
266
+ break
267
+ else:
268
+ provider_id = ROUTES.get(
269
+ (
270
+ Chain(params["from"]["chain"]),
271
+ params["from"]["token"],
272
+ Chain(params["to"]["chain"]),
273
+ params["to"]["token"],
274
+ ),
275
+ RELAY_PROVIDER_ID,
276
+ )
277
+
278
+ provider_requests.append(
279
+ self._providers[provider_id].create_request(params=params)
280
+ )
281
+
282
+ bundle = ProviderRequestBundle(
283
+ id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}",
284
+ requests_params=requests_params,
285
+ provider_requests=provider_requests,
286
+ timestamp=now,
287
+ )
288
+
289
+ self.data.last_requested_bundle = bundle
290
+ self.quote_bundle(bundle)
291
+ self._store_data()
292
+
293
+ return bundle
294
+
295
+ def _sanitize(self, requests_params: t.List) -> None:
296
+ """Sanitize quote requests."""
297
+ w3 = Web3()
298
+ for params in requests_params:
299
+ params["from"]["address"] = w3.to_checksum_address(
300
+ params["from"]["address"]
301
+ )
302
+ params["from"]["token"] = w3.to_checksum_address(params["from"]["token"])
303
+ params["to"]["address"] = w3.to_checksum_address(params["to"]["address"])
304
+ params["to"]["token"] = w3.to_checksum_address(params["to"]["token"])
305
+ params["to"]["amount"] = int(params["to"]["amount"])
306
+
307
+ def _raise_if_invalid(self, requests_params: t.List) -> None:
308
+ """Preprocess quote requests."""
309
+
310
+ for params in requests_params:
311
+ from_chain = params["from"]["chain"]
312
+ from_address = params["from"]["address"]
313
+
314
+ wallet = self.wallet_manager.load(Chain(from_chain).ledger_type)
315
+ wallet_address = wallet.address
316
+ safe_address = wallet.safes.get(Chain(from_chain))
317
+
318
+ if from_address is None or not (
319
+ from_address == wallet_address or from_address == safe_address
320
+ ):
321
+ raise ValueError(
322
+ f"Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}."
323
+ )
324
+
325
+ def bridge_refill_requirements(
326
+ self, requests_params: t.List[t.Dict], force_update: bool = False
327
+ ) -> t.Dict:
328
+ """Get bridge refill requirements."""
329
+ self._sanitize(requests_params)
330
+ self._raise_if_invalid(requests_params)
331
+ self.logger.info(
332
+ f"[BRIDGE MANAGER] Quote requests count: {len(requests_params)}."
333
+ )
334
+
335
+ bundle = self._get_updated_bundle(requests_params, force_update)
336
+
337
+ balances = ChainAmounts()
338
+ for chain in bundle.get_from_chains():
339
+ ledger_api = self.wallet_manager.load(chain.ledger_type).ledger_api(chain)
340
+ balances[chain.value] = get_assets_balances(
341
+ ledger_api=ledger_api,
342
+ asset_addresses={ZERO_ADDRESS} | bundle.get_from_tokens(chain),
343
+ addresses=bundle.get_from_addresses(chain),
344
+ )
345
+
346
+ bridge_total_requirements = self.bridge_total_requirements(bundle)
347
+
348
+ bridge_refill_requirements = ChainAmounts.shortfalls(
349
+ bridge_total_requirements, balances
350
+ )
351
+
352
+ is_refill_required = any(
353
+ amount > 0
354
+ for from_addresses in bridge_refill_requirements.values()
355
+ for from_tokens in from_addresses.values()
356
+ for amount in from_tokens.values()
357
+ )
358
+
359
+ status_json = self.get_status_json(bundle.id)
360
+ status_json.update(
361
+ {
362
+ "balances": balances,
363
+ "bridge_refill_requirements": bridge_refill_requirements,
364
+ "bridge_total_requirements": bridge_total_requirements,
365
+ "expiration_timestamp": bundle.timestamp + self.bundle_validity_period,
366
+ "is_refill_required": is_refill_required,
367
+ }
368
+ )
369
+ return status_json
370
+
371
+ def execute_bundle(self, bundle_id: str) -> t.Dict:
372
+ """Execute the bundle"""
373
+
374
+ bundle = self.data.last_requested_bundle
375
+
376
+ if not bundle:
377
+ raise RuntimeError("[BRIDGE MANAGER] No bundle.")
378
+
379
+ if bundle.id != bundle_id:
380
+ raise RuntimeError(
381
+ f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}."
382
+ )
383
+
384
+ requirements = self.bridge_refill_requirements(bundle.requests_params)
385
+ self.data.last_requested_bundle = None
386
+ self.data.last_executed_bundle_id = bundle_id
387
+ bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json"
388
+ bundle.path = bundle_path
389
+ self._store_data()
390
+ bundle.store()
391
+
392
+ if requirements["is_refill_required"]:
393
+ self.logger.warning(
394
+ f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}."
395
+ )
396
+
397
+ self.logger.info("[BRIDGE MANAGER] Executing quotes.")
398
+
399
+ for request in bundle.provider_requests:
400
+ provider = self._providers[request.provider_id]
401
+ provider.execute(request)
402
+ self._store_data()
403
+
404
+ self._store_data()
405
+ bundle.store()
406
+ self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.")
407
+ return self.get_status_json(bundle_id)
408
+
409
+ def get_status_json(self, bundle_id: str) -> t.Dict:
410
+ """Get execution status of bundle."""
411
+ bundle = self.data.last_requested_bundle
412
+
413
+ if bundle is not None and bundle.id == bundle_id:
414
+ pass
415
+ else:
416
+ bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json"
417
+ if bundle_path.exists():
418
+ bundle = cast(
419
+ ProviderRequestBundle, ProviderRequestBundle.load(bundle_path)
420
+ )
421
+ bundle.path = bundle_path # TODO backport to resource.py ?
422
+ else:
423
+ raise FileNotFoundError(f"Bundle with ID {bundle_id} does not exist.")
424
+
425
+ initial_status = [request.status for request in bundle.provider_requests]
426
+
427
+ provider_request_status = []
428
+ for request in bundle.provider_requests:
429
+ provider = self._providers[request.provider_id]
430
+ provider_request_status.append(provider.status_json(request))
431
+
432
+ updated_status = [request.status for request in bundle.provider_requests]
433
+
434
+ if initial_status != updated_status and bundle.path is not None:
435
+ bundle.store()
436
+
437
+ return {
438
+ "id": bundle.id,
439
+ "bridge_request_status": provider_request_status,
440
+ }
441
+
442
+ def bridge_total_requirements(self, bundle: ProviderRequestBundle) -> ChainAmounts:
443
+ """Sum bridge requirements."""
444
+ requirements = []
445
+ for provider_request in bundle.provider_requests:
446
+ provider = self._providers[provider_request.provider_id]
447
+ requirements.append(provider.requirements(provider_request))
448
+
449
+ return ChainAmounts.add(*requirements)
450
+
451
+ def quote_bundle(self, bundle: ProviderRequestBundle) -> None:
452
+ """Update the bundle with the quotes."""
453
+ for provider_request in bundle.provider_requests:
454
+ provider = self._providers[provider_request.provider_id]
455
+ provider.quote(provider_request)
456
+ bundle.timestamp = int(time.time())
457
+
458
+ def last_executed_bundle_id(self) -> t.Optional[str]:
459
+ """Get the last executed bundle id."""
460
+ return self.data.last_executed_bundle_id