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.
- olas_operate_middleware-0.13.5/PKG-INFO +56 -0
- olas_operate_middleware-0.13.5/README.md +25 -0
- olas_operate_middleware-0.1.0rc36/operate/ledger/base.py → olas_operate_middleware-0.13.5/operate/__init__.py +14 -14
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/account/user.py +35 -9
- olas_operate_middleware-0.13.5/operate/bridge/bridge_manager.py +470 -0
- olas_operate_middleware-0.13.5/operate/bridge/providers/lifi_provider.py +377 -0
- olas_operate_middleware-0.13.5/operate/bridge/providers/native_bridge_provider.py +677 -0
- olas_operate_middleware-0.13.5/operate/bridge/providers/provider.py +461 -0
- olas_operate_middleware-0.13.5/operate/bridge/providers/relay_provider.py +457 -0
- olas_operate_middleware-0.13.5/operate/cli.py +1885 -0
- olas_operate_middleware-0.13.5/operate/constants.py +86 -0
- olas_operate_middleware-0.13.5/operate/data/README.md +19 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/contract.py +132 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/dual_staking_token/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/contract.py +130 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/contract.py +80 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/home_omnibridge/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/contract.py +158 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/contract.py +130 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/contract.py +44 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/mech_activity/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/contract.py +61 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/recovery_module/contract.yaml +23 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/__init__.py +20 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/contract.py +33 -0
- olas_operate_middleware-0.13.5/operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
- {olas_operate_middleware-0.1.0rc36/operate/utils → olas_operate_middleware-0.13.5/operate/data/contracts/staking_token}/__init__.py +1 -1
- olas_operate_middleware-0.13.5/operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
- {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
- olas_operate_middleware-0.13.5/operate/data/contracts/staking_token/contract.yaml +23 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
- {olas_operate_middleware-0.1.0rc36/operate → olas_operate_middleware-0.13.5/operate/data/contracts/uniswap_v2_erc20/tests}/__init__.py +2 -2
- olas_operate_middleware-0.13.5/operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
- olas_operate_middleware-0.13.5/operate/keys.py +193 -0
- olas_operate_middleware-0.13.5/operate/ledger/__init__.py +196 -0
- olas_operate_middleware-0.13.5/operate/ledger/profiles.py +348 -0
- olas_operate_middleware-0.13.5/operate/migration.py +555 -0
- {olas_operate_middleware-0.1.0rc36/operate/http → olas_operate_middleware-0.13.5/operate/operate_http}/__init__.py +3 -4
- {olas_operate_middleware-0.1.0rc36/operate/http → olas_operate_middleware-0.13.5/operate/operate_http}/exceptions.py +6 -4
- olas_operate_middleware-0.13.5/operate/operate_types.py +454 -0
- olas_operate_middleware-0.13.5/operate/pearl.py +50 -0
- olas_operate_middleware-0.13.5/operate/quickstart/analyse_logs.py +118 -0
- olas_operate_middleware-0.13.5/operate/quickstart/claim_staking_rewards.py +104 -0
- olas_operate_middleware-0.13.5/operate/quickstart/reset_configs.py +106 -0
- olas_operate_middleware-0.13.5/operate/quickstart/reset_password.py +70 -0
- olas_operate_middleware-0.13.5/operate/quickstart/reset_staking.py +145 -0
- olas_operate_middleware-0.13.5/operate/quickstart/run_service.py +784 -0
- olas_operate_middleware-0.13.5/operate/quickstart/stop_service.py +72 -0
- olas_operate_middleware-0.13.5/operate/quickstart/terminate_on_chain_service.py +83 -0
- olas_operate_middleware-0.13.5/operate/quickstart/utils.py +302 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/resource.py +64 -5
- olas_operate_middleware-0.13.5/operate/services/agent_runner.py +202 -0
- olas_operate_middleware-0.13.5/operate/services/deployment_runner.py +868 -0
- olas_operate_middleware-0.13.5/operate/services/funding_manager.py +929 -0
- olas_operate_middleware-0.13.5/operate/services/health_checker.py +280 -0
- olas_operate_middleware-0.13.5/operate/services/manage.py +2774 -0
- olas_operate_middleware-0.13.5/operate/services/protocol.py +2027 -0
- olas_operate_middleware-0.13.5/operate/services/service.py +1270 -0
- olas_operate_middleware-0.13.5/operate/services/utils/mech.py +103 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/services/utils/tendermint.py +96 -14
- olas_operate_middleware-0.13.5/operate/settings.py +70 -0
- olas_operate_middleware-0.13.5/operate/utils/__init__.py +155 -0
- olas_operate_middleware-0.13.5/operate/utils/gnosis.py +649 -0
- olas_operate_middleware-0.13.5/operate/utils/single_instance.py +226 -0
- olas_operate_middleware-0.13.5/operate/utils/ssl.py +133 -0
- olas_operate_middleware-0.13.5/operate/wallet/master.py +994 -0
- olas_operate_middleware-0.13.5/operate/wallet/wallet_recovery_manager.py +509 -0
- olas_operate_middleware-0.13.5/pyproject.toml +46 -0
- olas_operate_middleware-0.1.0rc36/PKG-INFO +0 -302
- olas_operate_middleware-0.1.0rc36/README.md +0 -262
- olas_operate_middleware-0.1.0rc36/operate/cli.py +0 -703
- olas_operate_middleware-0.1.0rc36/operate/constants.py +0 -37
- olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token/__init__.py +0 -20
- olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
- olas_operate_middleware-0.1.0rc36/operate/data/contracts/service_staking_token/contract.yaml +0 -23
- olas_operate_middleware-0.1.0rc36/operate/keys.py +0 -106
- olas_operate_middleware-0.1.0rc36/operate/ledger/__init__.py +0 -99
- olas_operate_middleware-0.1.0rc36/operate/ledger/ethereum.py +0 -48
- olas_operate_middleware-0.1.0rc36/operate/ledger/profiles.py +0 -44
- olas_operate_middleware-0.1.0rc36/operate/ledger/solana.py +0 -38
- olas_operate_middleware-0.1.0rc36/operate/services/manage.py +0 -945
- olas_operate_middleware-0.1.0rc36/operate/services/protocol.py +0 -1167
- olas_operate_middleware-0.1.0rc36/operate/services/service.py +0 -934
- olas_operate_middleware-0.1.0rc36/operate/types.py +0 -260
- olas_operate_middleware-0.1.0rc36/operate/utils/gnosis.py +0 -347
- olas_operate_middleware-0.1.0rc36/operate/wallet/master.py +0 -411
- olas_operate_middleware-0.1.0rc36/pyproject.toml +0 -50
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/LICENSE +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/account/__init__.py +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/__init__.py +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/__init__.py +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/__init__.py +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/build/IUniswapV2ERC20.json +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/data/contracts/uniswap_v2_erc20/contract.py +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/services/__init__.py +0 -0
- {olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/services/utils/__init__.py +0 -0
- {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
|
-
"""
|
|
20
|
+
"""Operate app."""
|
|
21
21
|
|
|
22
|
-
import
|
|
23
|
-
from
|
|
22
|
+
import logging
|
|
23
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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)
|
{olas_operate_middleware-0.1.0rc36 → olas_operate_middleware-0.13.5}/operate/account/user.py
RENAMED
|
@@ -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
|
|
30
|
-
"""Get
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|