olas-operate-middleware 0.1.0rc59__py3-none-any.whl → 0.13.2__py3-none-any.whl
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.2.dist-info/METADATA +75 -0
- olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
- operate/__init__.py +17 -0
- operate/account/user.py +35 -9
- operate/bridge/bridge_manager.py +470 -0
- operate/bridge/providers/lifi_provider.py +377 -0
- operate/bridge/providers/native_bridge_provider.py +677 -0
- operate/bridge/providers/provider.py +469 -0
- operate/bridge/providers/relay_provider.py +457 -0
- operate/cli.py +1565 -417
- operate/constants.py +60 -12
- operate/data/README.md +19 -0
- operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
- operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
- operate/data/contracts/dual_staking_token/contract.py +132 -0
- operate/data/contracts/dual_staking_token/contract.yaml +23 -0
- operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
- operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
- operate/data/contracts/foreign_omnibridge/contract.py +130 -0
- operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
- operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
- operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
- operate/data/contracts/home_omnibridge/contract.py +80 -0
- operate/data/contracts/home_omnibridge/contract.yaml +23 -0
- operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
- operate/data/contracts/l1_standard_bridge/contract.py +158 -0
- operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
- operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
- operate/data/contracts/l2_standard_bridge/contract.py +130 -0
- operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
- operate/data/contracts/mech_activity/__init__.py +20 -0
- operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
- operate/data/contracts/mech_activity/contract.py +44 -0
- operate/data/contracts/mech_activity/contract.yaml +23 -0
- operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
- operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
- operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
- operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
- operate/data/contracts/recovery_module/__init__.py +20 -0
- operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
- operate/data/contracts/recovery_module/contract.py +61 -0
- operate/data/contracts/recovery_module/contract.yaml +23 -0
- operate/data/contracts/requester_activity_checker/__init__.py +20 -0
- operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
- operate/data/contracts/requester_activity_checker/contract.py +33 -0
- operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
- operate/data/contracts/staking_token/__init__.py +20 -0
- operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
- operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
- operate/data/contracts/staking_token/contract.yaml +23 -0
- operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
- operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
- operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
- operate/keys.py +118 -33
- operate/ledger/__init__.py +159 -56
- operate/ledger/profiles.py +321 -18
- operate/migration.py +555 -0
- operate/{http → operate_http}/__init__.py +3 -2
- operate/{http → operate_http}/exceptions.py +6 -4
- operate/operate_types.py +544 -0
- operate/pearl.py +13 -1
- operate/quickstart/analyse_logs.py +118 -0
- operate/quickstart/claim_staking_rewards.py +104 -0
- operate/quickstart/reset_configs.py +106 -0
- operate/quickstart/reset_password.py +70 -0
- operate/quickstart/reset_staking.py +145 -0
- operate/quickstart/run_service.py +726 -0
- operate/quickstart/stop_service.py +72 -0
- operate/quickstart/terminate_on_chain_service.py +83 -0
- operate/quickstart/utils.py +298 -0
- operate/resource.py +62 -3
- operate/services/agent_runner.py +202 -0
- operate/services/deployment_runner.py +868 -0
- operate/services/funding_manager.py +929 -0
- operate/services/health_checker.py +280 -0
- operate/services/manage.py +2356 -620
- operate/services/protocol.py +1246 -340
- operate/services/service.py +756 -391
- operate/services/utils/mech.py +103 -0
- operate/services/utils/tendermint.py +86 -12
- operate/settings.py +70 -0
- operate/utils/__init__.py +135 -0
- operate/utils/gnosis.py +407 -80
- operate/utils/single_instance.py +226 -0
- operate/utils/ssl.py +133 -0
- operate/wallet/master.py +708 -123
- operate/wallet/wallet_recovery_manager.py +507 -0
- olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
- olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
- operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
- operate/data/contracts/service_staking_token/contract.yaml +0 -23
- operate/ledger/ethereum.py +0 -48
- operate/types.py +0 -260
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
- {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ------------------------------------------------------------------------------
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2024 Valory AG
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
# ------------------------------------------------------------------------------
|
|
19
|
+
"""Quickstop script."""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import warnings
|
|
23
|
+
from typing import TYPE_CHECKING, cast
|
|
24
|
+
|
|
25
|
+
from operate.quickstart.run_service import (
|
|
26
|
+
ask_password_if_needed,
|
|
27
|
+
configure_local_config,
|
|
28
|
+
get_service,
|
|
29
|
+
load_local_config,
|
|
30
|
+
)
|
|
31
|
+
from operate.quickstart.utils import print_section, print_title
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from operate.cli import OperateApp
|
|
36
|
+
|
|
37
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def stop_service(
|
|
41
|
+
operate: "OperateApp", config_path: str, use_binary: bool = False
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Stop service."""
|
|
44
|
+
|
|
45
|
+
with open(config_path, "r") as config_file:
|
|
46
|
+
template = json.load(config_file)
|
|
47
|
+
|
|
48
|
+
print_title(f"Stop {template['name']} Quickstart")
|
|
49
|
+
|
|
50
|
+
# check if agent was started before
|
|
51
|
+
config = load_local_config(
|
|
52
|
+
operate=operate, service_name=cast(str, template["name"])
|
|
53
|
+
)
|
|
54
|
+
if not config.path.exists():
|
|
55
|
+
print("No previous agent setup found. Exiting.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
ask_password_if_needed(operate)
|
|
59
|
+
configure_local_config(template, operate)
|
|
60
|
+
manager = operate.service_manager()
|
|
61
|
+
service = get_service(manager, template)
|
|
62
|
+
if use_binary:
|
|
63
|
+
use_docker = False
|
|
64
|
+
else:
|
|
65
|
+
use_docker = True
|
|
66
|
+
|
|
67
|
+
manager.stop_service_locally(
|
|
68
|
+
service_config_id=service.service_config_id, use_docker=use_docker, force=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print()
|
|
72
|
+
print_section(f"{template['name']} service stopped")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ------------------------------------------------------------------------------
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2023-2024 Valory AG
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
#
|
|
17
|
+
# ------------------------------------------------------------------------------
|
|
18
|
+
"""Terminate on-chain service."""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from typing import TYPE_CHECKING, cast
|
|
22
|
+
|
|
23
|
+
from operate.operate_types import OnChainState
|
|
24
|
+
from operate.quickstart.run_service import (
|
|
25
|
+
ask_password_if_needed,
|
|
26
|
+
configure_local_config,
|
|
27
|
+
ensure_enough_funds,
|
|
28
|
+
get_service,
|
|
29
|
+
load_local_config,
|
|
30
|
+
)
|
|
31
|
+
from operate.quickstart.utils import ask_yes_or_no, print_section, print_title
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from operate.cli import OperateApp
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def terminate_service(operate: "OperateApp", config_path: str) -> None:
|
|
39
|
+
"""Terminate service."""
|
|
40
|
+
|
|
41
|
+
with open(config_path, "r") as config_file:
|
|
42
|
+
template = json.load(config_file)
|
|
43
|
+
|
|
44
|
+
print_title(f"Terminate {template['name']} on-chain service")
|
|
45
|
+
|
|
46
|
+
# check if agent was started before
|
|
47
|
+
config = load_local_config(
|
|
48
|
+
operate=operate, service_name=cast(str, template["name"])
|
|
49
|
+
)
|
|
50
|
+
if not config.path.exists():
|
|
51
|
+
print("No previous agent setup found. Exiting.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if not ask_yes_or_no(
|
|
55
|
+
"Please, ensure that your service is stopped (./stop_service.sh) before proceeding. "
|
|
56
|
+
"Do you want to continue?"
|
|
57
|
+
):
|
|
58
|
+
print("Cancelled.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
ask_password_if_needed(operate)
|
|
62
|
+
config = configure_local_config(template, operate)
|
|
63
|
+
manager = operate.service_manager()
|
|
64
|
+
service = get_service(manager, template)
|
|
65
|
+
ensure_enough_funds(operate, service)
|
|
66
|
+
manager.terminate_service_on_chain_from_safe(
|
|
67
|
+
service_config_id=service.service_config_id,
|
|
68
|
+
chain=config.principal_chain,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
manager._get_on_chain_state(service, config.principal_chain)
|
|
73
|
+
== OnChainState.PRE_REGISTRATION
|
|
74
|
+
):
|
|
75
|
+
service_id = service.chain_configs[config.principal_chain].chain_data.token
|
|
76
|
+
print(
|
|
77
|
+
f"\nService {service_id} is now terminated and unbonded (i.e., it is on PRE-REGISTRATION state)."
|
|
78
|
+
f"You can check this on https://registry.olas.network/{config.principal_chain}/services/{service_id}."
|
|
79
|
+
"In order to deploy your on-chain service again, please run the service again'."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print()
|
|
83
|
+
print_section(f"{template['name']} service terminated")
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ------------------------------------------------------------------------------
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2023-2024 Valory AG
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
#
|
|
18
|
+
# ------------------------------------------------------------------------------
|
|
19
|
+
"""Common utilities."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import getpass
|
|
23
|
+
import os
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from decimal import Decimal, ROUND_UP
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Dict, Optional, Union, get_args, get_origin
|
|
28
|
+
|
|
29
|
+
import requests
|
|
30
|
+
from halo import Halo # type: ignore[import] # pylint: disable=import-error
|
|
31
|
+
|
|
32
|
+
from operate.constants import ZERO_ADDRESS
|
|
33
|
+
from operate.ledger.profiles import OLAS, USDC
|
|
34
|
+
from operate.operate_types import Chain
|
|
35
|
+
from operate.resource import LocalResource, deserialize
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def print_box(text: str, margin: int = 1, character: str = "=") -> None:
|
|
39
|
+
"""Print text centered within a box."""
|
|
40
|
+
|
|
41
|
+
lines = text.split("\n")
|
|
42
|
+
text_length = max(len(line) for line in lines)
|
|
43
|
+
length = text_length + 2 * margin
|
|
44
|
+
|
|
45
|
+
border = character * length
|
|
46
|
+
margin_str = " " * margin
|
|
47
|
+
|
|
48
|
+
print()
|
|
49
|
+
print(border)
|
|
50
|
+
print(f"{margin_str}{text}{margin_str}")
|
|
51
|
+
print(border)
|
|
52
|
+
print()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_title(text: str) -> None:
|
|
56
|
+
"""Print title."""
|
|
57
|
+
print_box(text, 4, "=")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def print_section(text: str) -> None:
|
|
61
|
+
"""Print section."""
|
|
62
|
+
print_box(text, 1, "-")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def unit_to_wei(unit: float) -> int:
|
|
66
|
+
"""Convert unit to Wei."""
|
|
67
|
+
return int(unit * 1e18)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
CHAIN_TO_METADATA = {
|
|
71
|
+
"gnosis": {
|
|
72
|
+
"name": "Gnosis",
|
|
73
|
+
"gasFundReq": unit_to_wei(0.5), # fund for master EOA
|
|
74
|
+
"staking_bonding_token": OLAS[Chain.GNOSIS],
|
|
75
|
+
"token_data": {
|
|
76
|
+
ZERO_ADDRESS: {
|
|
77
|
+
"symbol": "xDAI",
|
|
78
|
+
"decimals": 18,
|
|
79
|
+
},
|
|
80
|
+
USDC[Chain.GNOSIS]: {
|
|
81
|
+
"symbol": "USDC",
|
|
82
|
+
"decimals": 6,
|
|
83
|
+
},
|
|
84
|
+
OLAS[Chain.GNOSIS]: {
|
|
85
|
+
"symbol": "OLAS",
|
|
86
|
+
"decimals": 18,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"gasParams": {
|
|
90
|
+
# this means default values will be used
|
|
91
|
+
"MAX_PRIORITY_FEE_PER_GAS": "",
|
|
92
|
+
"MAX_FEE_PER_GAS": "",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
"mode": {
|
|
96
|
+
"name": "Mode",
|
|
97
|
+
"gasFundReq": unit_to_wei(0.005), # fund for master EOA
|
|
98
|
+
"staking_bonding_token": OLAS[Chain.MODE],
|
|
99
|
+
"token_data": {
|
|
100
|
+
ZERO_ADDRESS: {
|
|
101
|
+
"symbol": "ETH",
|
|
102
|
+
"decimals": 18,
|
|
103
|
+
},
|
|
104
|
+
USDC[Chain.MODE]: {
|
|
105
|
+
"symbol": "USDC",
|
|
106
|
+
"decimals": 6,
|
|
107
|
+
},
|
|
108
|
+
OLAS[Chain.MODE]: {
|
|
109
|
+
"symbol": "OLAS",
|
|
110
|
+
"decimals": 18,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
"gasParams": {
|
|
114
|
+
# this means default values will be used
|
|
115
|
+
"MAX_PRIORITY_FEE_PER_GAS": "",
|
|
116
|
+
"MAX_FEE_PER_GAS": "",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
"optimism": {
|
|
120
|
+
"name": "Optimism",
|
|
121
|
+
"gasFundReq": unit_to_wei(0.005), # fund for master EOA
|
|
122
|
+
"staking_bonding_token": OLAS[Chain.OPTIMISM],
|
|
123
|
+
"token_data": {
|
|
124
|
+
ZERO_ADDRESS: {
|
|
125
|
+
"symbol": "ETH",
|
|
126
|
+
"decimals": 18,
|
|
127
|
+
},
|
|
128
|
+
USDC[Chain.OPTIMISM]: {
|
|
129
|
+
"symbol": "USDC",
|
|
130
|
+
"decimals": 6,
|
|
131
|
+
},
|
|
132
|
+
OLAS[Chain.OPTIMISM]: {
|
|
133
|
+
"symbol": "OLAS",
|
|
134
|
+
"decimals": 18,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"gasParams": {
|
|
138
|
+
# this means default values will be used
|
|
139
|
+
"MAX_PRIORITY_FEE_PER_GAS": "",
|
|
140
|
+
"MAX_FEE_PER_GAS": "",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
"base": {
|
|
144
|
+
"name": "Base",
|
|
145
|
+
"gasFundReq": unit_to_wei(0.005), # fund for master EOA
|
|
146
|
+
"staking_bonding_token": OLAS[Chain.BASE],
|
|
147
|
+
"token_data": {
|
|
148
|
+
ZERO_ADDRESS: {
|
|
149
|
+
"symbol": "ETH",
|
|
150
|
+
"decimals": 18,
|
|
151
|
+
},
|
|
152
|
+
USDC[Chain.BASE]: {
|
|
153
|
+
"symbol": "USDC",
|
|
154
|
+
"decimals": 6,
|
|
155
|
+
},
|
|
156
|
+
OLAS[Chain.BASE]: {
|
|
157
|
+
"symbol": "OLAS",
|
|
158
|
+
"decimals": 18,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
"gasParams": {
|
|
162
|
+
# this means default values will be used
|
|
163
|
+
"MAX_PRIORITY_FEE_PER_GAS": "",
|
|
164
|
+
"MAX_FEE_PER_GAS": "",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def wei_to_unit(wei: int, chain: str, token_address: str = ZERO_ADDRESS) -> Decimal:
|
|
171
|
+
"""Convert Wei to unit."""
|
|
172
|
+
unit: Decimal = (
|
|
173
|
+
Decimal(str(wei))
|
|
174
|
+
/ 10 ** CHAIN_TO_METADATA[chain]["token_data"][token_address]["decimals"]
|
|
175
|
+
)
|
|
176
|
+
return unit.quantize(Decimal("0.000001"), rounding=ROUND_UP)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def wei_to_token(wei: int, chain: str, token_address: str = ZERO_ADDRESS) -> str:
|
|
180
|
+
"""Convert Wei to token."""
|
|
181
|
+
return f"{wei_to_unit(wei, chain, token_address)} {CHAIN_TO_METADATA[chain]['token_data'][token_address]['symbol']}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def ask_yes_or_no(question: str) -> bool:
|
|
185
|
+
"""Ask a yes/no question."""
|
|
186
|
+
if os.environ.get("ATTENDED", "true").lower() != "true":
|
|
187
|
+
return True
|
|
188
|
+
while True:
|
|
189
|
+
response = input(f"{question} (yes/no): ").strip().lower()
|
|
190
|
+
if response.lower() in ("yes", "y"):
|
|
191
|
+
return True
|
|
192
|
+
if response.lower() in ("no", "n"):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def ask_or_get_from_env(
|
|
197
|
+
prompt: str, is_pass: bool, env_var_name: str, raise_if_missing: bool = True
|
|
198
|
+
) -> str:
|
|
199
|
+
"""Get user input either interactively or from environment variables."""
|
|
200
|
+
if os.getenv("ATTENDED", "true").lower() == "true":
|
|
201
|
+
if is_pass:
|
|
202
|
+
return getpass.getpass(prompt).strip()
|
|
203
|
+
return input(prompt).strip()
|
|
204
|
+
if env_var_name in os.environ:
|
|
205
|
+
return os.environ[env_var_name].strip()
|
|
206
|
+
if raise_if_missing:
|
|
207
|
+
raise ValueError(f"{env_var_name} env var required in unattended mode")
|
|
208
|
+
return ""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def check_rpc(chain: str, rpc_url: Optional[str] = None) -> bool:
|
|
212
|
+
"""Check RPC."""
|
|
213
|
+
if rpc_url is None:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
spinner = Halo(text=f"[{chain}] Checking RPC...", spinner="dots")
|
|
217
|
+
spinner.start()
|
|
218
|
+
|
|
219
|
+
rpc_data = {
|
|
220
|
+
"jsonrpc": "2.0",
|
|
221
|
+
"method": "eth_newFilter",
|
|
222
|
+
"params": ["invalid"],
|
|
223
|
+
"id": 1,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
response = requests.post(
|
|
228
|
+
rpc_url, json=rpc_data, headers={"Content-Type": "application/json"}
|
|
229
|
+
)
|
|
230
|
+
response.raise_for_status()
|
|
231
|
+
rpc_response = response.json()
|
|
232
|
+
except (requests.exceptions.RequestException, ValueError, TypeError) as e:
|
|
233
|
+
spinner.fail(f"Error: Failed to send {chain} RPC request: {e}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
rpc_error_message = (
|
|
237
|
+
rpc_response.get("error", {})
|
|
238
|
+
.get("message", "exception processing rpc response")
|
|
239
|
+
.lower()
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if rpc_error_message == "exception processing rpc response":
|
|
243
|
+
print(
|
|
244
|
+
"Error: The received rpc response is malformed. Please verify the RPC address and/or rpc behavior."
|
|
245
|
+
)
|
|
246
|
+
print(" Received response:")
|
|
247
|
+
print(" ", rpc_response)
|
|
248
|
+
print("")
|
|
249
|
+
spinner.fail(f"[{chain}] Terminating script.")
|
|
250
|
+
elif rpc_error_message == "out of requests":
|
|
251
|
+
print("Error: The provided rpc is out of requests.")
|
|
252
|
+
spinner.fail(f"[{chain}] Terminating script.")
|
|
253
|
+
elif (
|
|
254
|
+
rpc_error_message == "the method eth_newfilter does not exist/is not available"
|
|
255
|
+
):
|
|
256
|
+
print("Error: The provided RPC does not support 'eth_newFilter'.")
|
|
257
|
+
spinner.fail(f"[{chain}] Terminating script.")
|
|
258
|
+
elif "invalid" in rpc_error_message or "params" in rpc_error_message:
|
|
259
|
+
spinner.succeed(f"[{chain}] RPC checks passed.")
|
|
260
|
+
return True
|
|
261
|
+
else:
|
|
262
|
+
print("Error: Unknown rpc error.")
|
|
263
|
+
print(" Received response:")
|
|
264
|
+
print(" ", rpc_response)
|
|
265
|
+
print("")
|
|
266
|
+
spinner.fail(f"[{chain}] Terminating script.")
|
|
267
|
+
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class QuickstartConfig(LocalResource):
|
|
273
|
+
"""Local configuration."""
|
|
274
|
+
|
|
275
|
+
path: Path
|
|
276
|
+
rpc: Optional[Dict[str, str]] = None
|
|
277
|
+
staking_program_id: Optional[str] = None
|
|
278
|
+
principal_chain: Optional[str] = None
|
|
279
|
+
user_provided_args: Optional[Dict[str, str]] = None
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def from_json(cls, obj: Dict) -> "LocalResource":
|
|
283
|
+
"""Load LocalResource from json."""
|
|
284
|
+
kwargs = {}
|
|
285
|
+
for pname, ptype in cls.__annotations__.items():
|
|
286
|
+
if pname.startswith("_"):
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# allow for optional types
|
|
290
|
+
is_optional_type = get_origin(ptype) is Union and type(None) in get_args(
|
|
291
|
+
ptype
|
|
292
|
+
)
|
|
293
|
+
value = obj.get(pname, None)
|
|
294
|
+
if is_optional_type and value is None:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
kwargs[pname] = deserialize(obj=obj[pname], otype=ptype)
|
|
298
|
+
return cls(**kwargs)
|
operate/resource.py
CHANGED
|
@@ -21,31 +21,56 @@
|
|
|
21
21
|
|
|
22
22
|
import enum
|
|
23
23
|
import json
|
|
24
|
+
import os
|
|
25
|
+
import platform
|
|
26
|
+
import shutil
|
|
27
|
+
import types
|
|
24
28
|
import typing as t
|
|
25
29
|
from dataclasses import asdict, is_dataclass
|
|
26
30
|
from pathlib import Path
|
|
27
31
|
|
|
32
|
+
from operate.utils import safe_file_operation
|
|
33
|
+
|
|
28
34
|
|
|
29
35
|
# pylint: disable=too-many-return-statements,no-member
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
N_BACKUPS = 5
|
|
39
|
+
|
|
40
|
+
|
|
32
41
|
def serialize(obj: t.Any) -> t.Any:
|
|
33
42
|
"""Serialize object."""
|
|
34
43
|
if is_dataclass(obj):
|
|
35
|
-
return asdict(obj)
|
|
44
|
+
return serialize(asdict(obj))
|
|
36
45
|
if isinstance(obj, Path):
|
|
37
46
|
return str(obj)
|
|
38
47
|
if isinstance(obj, dict):
|
|
39
|
-
return {key: serialize(obj=value) for key, value in obj.items()}
|
|
48
|
+
return {serialize(key): serialize(obj=value) for key, value in obj.items()}
|
|
40
49
|
if isinstance(obj, list):
|
|
41
50
|
return [serialize(obj=value) for value in obj]
|
|
42
51
|
if isinstance(obj, enum.Enum):
|
|
43
52
|
return obj.value
|
|
53
|
+
if isinstance(obj, bytes):
|
|
54
|
+
return obj.hex()
|
|
44
55
|
return obj
|
|
45
56
|
|
|
46
57
|
|
|
47
58
|
def deserialize(obj: t.Any, otype: t.Any) -> t.Any:
|
|
48
59
|
"""Desrialize a json object."""
|
|
60
|
+
|
|
61
|
+
origin = getattr(otype, "__origin__", None)
|
|
62
|
+
|
|
63
|
+
# Handle Union and Optional
|
|
64
|
+
if origin is t.Union or isinstance(otype, types.UnionType):
|
|
65
|
+
for arg in t.get_args(otype):
|
|
66
|
+
if arg is type(None): # noqa: E721
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
return deserialize(obj, arg)
|
|
70
|
+
except Exception: # pylint: disable=broad-except # nosec
|
|
71
|
+
continue
|
|
72
|
+
return None
|
|
73
|
+
|
|
49
74
|
base = getattr(otype, "__class__") # noqa: B009
|
|
50
75
|
if base.__name__ == "_GenericAlias": # type: ignore
|
|
51
76
|
args = otype.__args__ # type: ignore
|
|
@@ -65,6 +90,8 @@ def deserialize(obj: t.Any, otype: t.Any) -> t.Any:
|
|
|
65
90
|
return Path(obj)
|
|
66
91
|
if is_dataclass(otype):
|
|
67
92
|
return otype.from_json(obj)
|
|
93
|
+
if otype is bytes:
|
|
94
|
+
return bytes.fromhex(obj)
|
|
68
95
|
return obj
|
|
69
96
|
|
|
70
97
|
|
|
@@ -117,10 +144,42 @@ class LocalResource:
|
|
|
117
144
|
if self._file is not None:
|
|
118
145
|
path = path / self._file
|
|
119
146
|
|
|
120
|
-
path.
|
|
147
|
+
bak0 = path.with_name(f"{path.name}.0.bak")
|
|
148
|
+
|
|
149
|
+
if path.exists() and not bak0.exists():
|
|
150
|
+
safe_file_operation(shutil.copy2, path, bak0)
|
|
151
|
+
|
|
152
|
+
tmp_path = path.parent / f".{path.name}.tmp"
|
|
153
|
+
|
|
154
|
+
# Clean up any existing tmp file
|
|
155
|
+
if tmp_path.exists():
|
|
156
|
+
safe_file_operation(tmp_path.unlink)
|
|
157
|
+
|
|
158
|
+
tmp_path.write_text(
|
|
121
159
|
json.dumps(
|
|
122
160
|
self.json,
|
|
123
161
|
indent=2,
|
|
124
162
|
),
|
|
125
163
|
encoding="utf-8",
|
|
126
164
|
)
|
|
165
|
+
|
|
166
|
+
# Atomic replace to avoid corruption
|
|
167
|
+
try:
|
|
168
|
+
safe_file_operation(os.replace, tmp_path, path)
|
|
169
|
+
except (PermissionError, FileNotFoundError):
|
|
170
|
+
# On Windows, if the replace fails, clean up and skip
|
|
171
|
+
if platform.system() == "Windows":
|
|
172
|
+
safe_file_operation(tmp_path.unlink)
|
|
173
|
+
|
|
174
|
+
self.load(self.path) # Validate before making backup
|
|
175
|
+
|
|
176
|
+
# Rotate backup files
|
|
177
|
+
for i in reversed(range(N_BACKUPS - 1)):
|
|
178
|
+
newer = path.with_name(f"{path.name}.{i}.bak")
|
|
179
|
+
older = path.with_name(f"{path.name}.{i + 1}.bak")
|
|
180
|
+
if newer.exists():
|
|
181
|
+
if older.exists():
|
|
182
|
+
safe_file_operation(older.unlink)
|
|
183
|
+
safe_file_operation(newer.rename, older)
|
|
184
|
+
|
|
185
|
+
safe_file_operation(shutil.copy2, path, bak0)
|