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,103 @@
|
|
|
1
|
+
# ------------------------------------------------------------------------------
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2023-2025 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
|
+
"""Utilities for the Mech service."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from logging import getLogger
|
|
22
|
+
from typing import Tuple
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
from aea_ledger_ethereum import Web3
|
|
26
|
+
|
|
27
|
+
from operate.constants import MECH_MARKETPLACE_JSON_URL
|
|
28
|
+
from operate.operate_types import Chain
|
|
29
|
+
from operate.quickstart.utils import print_section
|
|
30
|
+
from operate.services.protocol import EthSafeTxBuilder
|
|
31
|
+
from operate.services.service import Service
|
|
32
|
+
from operate.utils.gnosis import SafeOperation
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
MECH_FACTORY_ADDRESS = {
|
|
36
|
+
Chain.GNOSIS: {
|
|
37
|
+
"0xad380C51cd5297FbAE43494dD5D407A2a3260b58": {
|
|
38
|
+
"Native": "0x42f43be9E5E50df51b86C5c6427223ff565f40C6",
|
|
39
|
+
"Token": "0x161b862568E900Dd9d8c64364F3B83a43792e50f",
|
|
40
|
+
"Nevermined": "0xCB26B91B0E21ADb04FFB6e5f428f41858c64936A",
|
|
41
|
+
},
|
|
42
|
+
"0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB": {
|
|
43
|
+
"Native": "0x8b299c20F87e3fcBfF0e1B86dC0acC06AB6993EF",
|
|
44
|
+
"Token": "0x31ffDC795FDF36696B8eDF7583A3D115995a45FA",
|
|
45
|
+
"Nevermined": "0x65fd74C29463afe08c879a3020323DD7DF02DA57",
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger = getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def deploy_mech(sftxb: EthSafeTxBuilder, service: Service) -> Tuple[str, str]:
|
|
54
|
+
"""Deploy the Mech service."""
|
|
55
|
+
print_section("Creating a new Mech On Chain")
|
|
56
|
+
|
|
57
|
+
# Get the mech type from service config
|
|
58
|
+
mech_type = service.env_variables.get("MECH_TYPE", {}).get("value", "Native")
|
|
59
|
+
|
|
60
|
+
abi = requests.get(MECH_MARKETPLACE_JSON_URL).json()["abi"]
|
|
61
|
+
chain = Chain.from_string(service.home_chain)
|
|
62
|
+
mech_marketplace_address = service.env_variables["MECH_MARKETPLACE_ADDRESS"][
|
|
63
|
+
"value"
|
|
64
|
+
]
|
|
65
|
+
# Get factory address based on mech type
|
|
66
|
+
if mech_marketplace_address not in MECH_FACTORY_ADDRESS[chain]:
|
|
67
|
+
logger.warning(
|
|
68
|
+
f"The given {mech_marketplace_address=} is not supported. "
|
|
69
|
+
f"Defaulting back to 0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB."
|
|
70
|
+
)
|
|
71
|
+
mech_marketplace_address = "0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB"
|
|
72
|
+
|
|
73
|
+
mech_factory_address = MECH_FACTORY_ADDRESS[chain][mech_marketplace_address][
|
|
74
|
+
mech_type
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
mech_request_price = int(
|
|
78
|
+
service.env_variables.get("MECH_REQUEST_PRICE", {}).get(
|
|
79
|
+
"value", 10000000000000000
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
contract = sftxb.ledger_api.api.eth.contract(
|
|
83
|
+
address=Web3.to_checksum_address(mech_marketplace_address), abi=abi
|
|
84
|
+
)
|
|
85
|
+
data = contract.encodeABI(
|
|
86
|
+
"create",
|
|
87
|
+
args=[
|
|
88
|
+
service.chain_configs[service.home_chain].chain_data.token,
|
|
89
|
+
Web3.to_checksum_address(mech_factory_address),
|
|
90
|
+
mech_request_price.to_bytes(32, byteorder="big"),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
tx_dict = {
|
|
94
|
+
"to": mech_marketplace_address,
|
|
95
|
+
"data": data,
|
|
96
|
+
"value": 0,
|
|
97
|
+
"operation": SafeOperation.CALL,
|
|
98
|
+
}
|
|
99
|
+
receipt = sftxb.new_tx().add(tx_dict).settle()
|
|
100
|
+
event = contract.events.CreateMech().process_receipt(receipt)[0]
|
|
101
|
+
mech_address = event["args"]["mech"]
|
|
102
|
+
agent_id = sftxb.info(token_id=event["args"]["serviceId"])["canonical_agents"][0]
|
|
103
|
+
return mech_address, agent_id
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
# ------------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
"""Tendermint manager."""
|
|
21
|
+
import contextlib
|
|
21
22
|
import json
|
|
22
23
|
import logging
|
|
24
|
+
import multiprocessing
|
|
23
25
|
import os
|
|
24
26
|
import platform
|
|
25
27
|
import re
|
|
@@ -29,9 +31,11 @@ import stat
|
|
|
29
31
|
import subprocess # nosec:
|
|
30
32
|
import sys
|
|
31
33
|
import traceback
|
|
34
|
+
from http import HTTPStatus
|
|
32
35
|
from logging import Logger
|
|
33
36
|
from pathlib import Path
|
|
34
37
|
from threading import Event, Thread
|
|
38
|
+
from time import sleep
|
|
35
39
|
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
|
36
40
|
|
|
37
41
|
import requests
|
|
@@ -306,12 +310,12 @@ class TendermintNode:
|
|
|
306
310
|
"""Stop a monitoring process."""
|
|
307
311
|
if self._monitoring is not None:
|
|
308
312
|
self._monitoring.stop() # set stop event
|
|
309
|
-
self._monitoring.join()
|
|
313
|
+
self._monitoring.join(timeout=20)
|
|
310
314
|
|
|
311
315
|
def stop(self) -> None:
|
|
312
316
|
"""Stop a Tendermint node process."""
|
|
313
|
-
self._stop_tm_process()
|
|
314
317
|
self._stop_monitoring_thread()
|
|
318
|
+
self._stop_tm_process()
|
|
315
319
|
|
|
316
320
|
@staticmethod
|
|
317
321
|
def _write_to_console(line: str) -> None:
|
|
@@ -503,6 +507,9 @@ def create_app( # pylint: disable=too-many-statements
|
|
|
503
507
|
)
|
|
504
508
|
|
|
505
509
|
app = Flask(__name__) # pylint: disable=redefined-outer-name
|
|
510
|
+
app._is_on_exit = ( # pylint: disable=protected-access
|
|
511
|
+
False # ugly but better than global ver
|
|
512
|
+
)
|
|
506
513
|
period_dumper = PeriodDumper(
|
|
507
514
|
logger=app.logger,
|
|
508
515
|
dump_dir=Path(os.environ["TMSTATE"]),
|
|
@@ -570,12 +577,20 @@ def create_app( # pylint: disable=too-many-statements
|
|
|
570
577
|
@app.route("/gentle_reset")
|
|
571
578
|
def gentle_reset() -> Tuple[Any, int]:
|
|
572
579
|
"""Reset the tendermint node gently."""
|
|
580
|
+
if app._is_on_exit: # pylint: disable=protected-access
|
|
581
|
+
raise RuntimeError("server exit now")
|
|
573
582
|
try:
|
|
574
583
|
tendermint_node.stop()
|
|
575
584
|
tendermint_node.start()
|
|
576
|
-
return
|
|
585
|
+
return (
|
|
586
|
+
jsonify({"message": "Reset successful.", "status": True}),
|
|
587
|
+
HTTPStatus.OK,
|
|
588
|
+
)
|
|
577
589
|
except Exception as e: # pylint: disable=W0703
|
|
578
|
-
return
|
|
590
|
+
return (
|
|
591
|
+
jsonify({"message": f"Reset failed: {e}", "status": False}),
|
|
592
|
+
HTTPStatus.OK,
|
|
593
|
+
)
|
|
579
594
|
|
|
580
595
|
@app.route("/app_hash")
|
|
581
596
|
def app_hash() -> Tuple[Any, int]:
|
|
@@ -597,6 +612,8 @@ def create_app( # pylint: disable=too-many-statements
|
|
|
597
612
|
@app.route("/hard_reset")
|
|
598
613
|
def hard_reset() -> Tuple[Any, int]:
|
|
599
614
|
"""Reset the node forcefully, and prune the blocks"""
|
|
615
|
+
if app._is_on_exit: # pylint: disable=protected-access
|
|
616
|
+
raise RuntimeError("server exit now")
|
|
600
617
|
try:
|
|
601
618
|
tendermint_node.stop()
|
|
602
619
|
if IS_DEV_MODE:
|
|
@@ -614,21 +631,33 @@ def create_app( # pylint: disable=too-many-statements
|
|
|
614
631
|
request.args.get("period_count", "0"),
|
|
615
632
|
)
|
|
616
633
|
tendermint_node.start()
|
|
617
|
-
return
|
|
634
|
+
return (
|
|
635
|
+
jsonify({"message": "Reset successful.", "status": True}),
|
|
636
|
+
HTTPStatus.OK,
|
|
637
|
+
)
|
|
618
638
|
except Exception as e: # pylint: disable=W0703
|
|
619
|
-
return
|
|
639
|
+
return (
|
|
640
|
+
jsonify({"message": f"Reset failed: {e}", "status": False}),
|
|
641
|
+
HTTPStatus.OK,
|
|
642
|
+
)
|
|
620
643
|
|
|
621
|
-
@app.errorhandler(
|
|
644
|
+
@app.errorhandler(HTTPStatus.NOT_FOUND) # type: ignore
|
|
622
645
|
def handle_notfound(e: NotFound) -> Response:
|
|
623
646
|
"""Handle server error."""
|
|
624
647
|
cast(logging.Logger, app.logger).info(e) # pylint: disable=E
|
|
625
|
-
return Response(
|
|
648
|
+
return Response(
|
|
649
|
+
"Not Found", status=HTTPStatus.NOT_FOUND, mimetype="application/json"
|
|
650
|
+
)
|
|
626
651
|
|
|
627
|
-
@app.errorhandler(
|
|
652
|
+
@app.errorhandler(HTTPStatus.INTERNAL_SERVER_ERROR) # type: ignore
|
|
628
653
|
def handle_server_error(e: InternalServerError) -> Response:
|
|
629
654
|
"""Handle server error."""
|
|
630
655
|
cast(logging.Logger, app.logger).info(e) # pylint: disable=E
|
|
631
|
-
return Response(
|
|
656
|
+
return Response(
|
|
657
|
+
"Error Closing Node",
|
|
658
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
659
|
+
mimetype="application/json",
|
|
660
|
+
)
|
|
632
661
|
|
|
633
662
|
return app, tendermint_node
|
|
634
663
|
|
|
@@ -639,7 +668,52 @@ def create_server() -> Any:
|
|
|
639
668
|
return flask_app
|
|
640
669
|
|
|
641
670
|
|
|
642
|
-
|
|
643
|
-
|
|
671
|
+
def run_app_in_subprocess(q: multiprocessing.Queue) -> None:
|
|
672
|
+
"""Run flask app in a subprocess to kill it when needed."""
|
|
673
|
+
print("app in subprocess")
|
|
674
|
+
app, tendermint_node = create_app()
|
|
675
|
+
|
|
676
|
+
@app.route("/exit")
|
|
677
|
+
def handle_server_exit() -> Response:
|
|
678
|
+
"""Handle server exit."""
|
|
679
|
+
app._is_on_exit = True # pylint: disable=protected-access
|
|
680
|
+
try:
|
|
681
|
+
tendermint_node.stop()
|
|
682
|
+
finally:
|
|
683
|
+
q.put(True)
|
|
684
|
+
return {"node": "stopped"}
|
|
685
|
+
|
|
686
|
+
app.run(host="localhost", port=8080)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def run_stoppable_main() -> None:
|
|
690
|
+
"""Main to spawn flask in a subprocess."""
|
|
691
|
+
print("run stoppable main!")
|
|
692
|
+
q: multiprocessing.Queue = multiprocessing.Queue()
|
|
693
|
+
p = multiprocessing.Process(target=run_app_in_subprocess, args=(q,))
|
|
694
|
+
p.start()
|
|
695
|
+
# wait for stop marker
|
|
696
|
+
try:
|
|
697
|
+
q.get(block=True)
|
|
698
|
+
sleep(1)
|
|
699
|
+
finally:
|
|
700
|
+
p.terminate()
|
|
701
|
+
with contextlib.suppress(Exception):
|
|
702
|
+
p.join(timeout=10)
|
|
703
|
+
p.terminate()
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def main() -> None:
|
|
707
|
+
"""Main entrance."""
|
|
644
708
|
app = create_server()
|
|
645
709
|
app.run(host="localhost", port=8080)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
if __name__ == "__main__":
|
|
713
|
+
# Start the Flask server programmatically
|
|
714
|
+
|
|
715
|
+
with contextlib.suppress(Exception):
|
|
716
|
+
# support for pyinstaller multiprocessing
|
|
717
|
+
multiprocessing.freeze_support()
|
|
718
|
+
|
|
719
|
+
run_stoppable_main()
|
operate/settings.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
"""Settings for operate."""
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
|
|
24
|
+
from operate.constants import SETTINGS_JSON
|
|
25
|
+
from operate.ledger.profiles import DEFAULT_EOA_TOPUPS
|
|
26
|
+
from operate.operate_types import ChainAmounts
|
|
27
|
+
from operate.resource import LocalResource
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SETTINGS_JSON_VERSION = 1
|
|
31
|
+
DEFAULT_SETTINGS = {
|
|
32
|
+
"version": SETTINGS_JSON_VERSION,
|
|
33
|
+
"eoa_topups": DEFAULT_EOA_TOPUPS,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Settings(LocalResource):
|
|
38
|
+
"""Settings for operate."""
|
|
39
|
+
|
|
40
|
+
_file = SETTINGS_JSON
|
|
41
|
+
|
|
42
|
+
version: int
|
|
43
|
+
eoa_topups: Dict[str, Dict[str, int]]
|
|
44
|
+
|
|
45
|
+
def __init__(self, path: Optional[Path] = None, **kwargs: Any) -> None:
|
|
46
|
+
"""Initialize settings."""
|
|
47
|
+
super().__init__(path=path)
|
|
48
|
+
if path is not None and (path / self._file).exists():
|
|
49
|
+
self.load(path)
|
|
50
|
+
|
|
51
|
+
for key, default_value in DEFAULT_SETTINGS.items():
|
|
52
|
+
value = kwargs.get(key, default_value)
|
|
53
|
+
if not hasattr(self, key):
|
|
54
|
+
setattr(self, key, value)
|
|
55
|
+
|
|
56
|
+
if self.version != SETTINGS_JSON_VERSION:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Settings version {self.version} is not supported. Expected version {SETTINGS_JSON_VERSION}."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def get_eoa_topups(self, with_safe: bool = False) -> ChainAmounts:
|
|
62
|
+
"""Get the EOA topups."""
|
|
63
|
+
return (
|
|
64
|
+
self.eoa_topups
|
|
65
|
+
if with_safe
|
|
66
|
+
else {
|
|
67
|
+
chain: {asset: amount * 2 for asset, amount in asset_amount.items()}
|
|
68
|
+
for chain, asset_amount in self.eoa_topups.items()
|
|
69
|
+
}
|
|
70
|
+
)
|
operate/utils/__init__.py
CHANGED
|
@@ -18,3 +18,138 @@
|
|
|
18
18
|
# ------------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
"""Helper utilities."""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import shutil
|
|
25
|
+
import time
|
|
26
|
+
import typing as t
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from threading import Lock
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SingletonMeta(type):
|
|
32
|
+
"""A metaclass for creating thread-safe singleton classes."""
|
|
33
|
+
|
|
34
|
+
_instances: t.Dict[t.Type, t.Any] = {}
|
|
35
|
+
_lock: Lock = Lock()
|
|
36
|
+
|
|
37
|
+
def __call__(cls, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
38
|
+
"""Override the __call__ method to control instance creation."""
|
|
39
|
+
with cls._lock:
|
|
40
|
+
if cls not in cls._instances:
|
|
41
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
|
42
|
+
return cls._instances[cls]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_backup(path: Path) -> Path:
|
|
46
|
+
"""Creates a backup of the specified path.
|
|
47
|
+
|
|
48
|
+
This function creates a backup of a file or directory by copying it and appending
|
|
49
|
+
the current UNIX timestamp followed by the '.bak' suffix.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
path = path.resolve()
|
|
53
|
+
|
|
54
|
+
if not path.exists():
|
|
55
|
+
raise FileNotFoundError(f"{path} does not exist")
|
|
56
|
+
|
|
57
|
+
timestamp = int(time.time())
|
|
58
|
+
backup_path = path.with_name(f"{path.name}.{timestamp}.bak")
|
|
59
|
+
|
|
60
|
+
if path.is_dir():
|
|
61
|
+
shutil.copytree(path, backup_path)
|
|
62
|
+
else:
|
|
63
|
+
shutil.copy2(path, backup_path)
|
|
64
|
+
|
|
65
|
+
return backup_path
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
NestedDict = t.Union[int, t.Dict[str, "NestedDict"]]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def merge_sum_dicts(*dicts: t.Dict[str, NestedDict]) -> t.Dict[str, NestedDict]:
|
|
72
|
+
"""
|
|
73
|
+
Merge a list of nested dicts by summing all innermost `int` values.
|
|
74
|
+
|
|
75
|
+
Supports arbitrary depth; keys not in all dicts are still included.
|
|
76
|
+
Missing values are treated as 0.
|
|
77
|
+
All `dicts` must follow the same nesting structure.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
result: t.Dict[str, NestedDict] = {}
|
|
81
|
+
for d in dicts:
|
|
82
|
+
for k, v in d.items(): # type: ignore
|
|
83
|
+
if isinstance(v, dict):
|
|
84
|
+
result[k] = merge_sum_dicts(result.get(k, {}), v) # type: ignore
|
|
85
|
+
elif isinstance(v, int):
|
|
86
|
+
result[k] = result.get(k, 0) + v # type: ignore
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def subtract_dicts(
|
|
91
|
+
a: t.Dict[str, NestedDict], b: t.Dict[str, NestedDict]
|
|
92
|
+
) -> t.Dict[str, NestedDict]:
|
|
93
|
+
"""
|
|
94
|
+
Recursively subtract values in `b` from `a`. Negative results are upper bounded at 0.
|
|
95
|
+
|
|
96
|
+
Supports arbitrary depth; keys not in all dicts are still included.
|
|
97
|
+
Missing values are treated as 0.
|
|
98
|
+
All `dicts` must follow the same nesting structure.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
result: t.Dict[str, NestedDict] = {}
|
|
102
|
+
for key in a.keys() | b.keys(): # type: ignore
|
|
103
|
+
va = a.get(key) # type: ignore
|
|
104
|
+
vb = b.get(key) # type: ignore
|
|
105
|
+
if isinstance(va, dict) or isinstance(vb, dict):
|
|
106
|
+
result[key] = subtract_dicts(
|
|
107
|
+
va if isinstance(va, dict) else {}, vb if isinstance(vb, dict) else {}
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
result[key] = max((va or 0) - (vb or 0), 0) # type: ignore
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def safe_file_operation(operation: t.Callable, *args: t.Any, **kwargs: t.Any) -> None:
|
|
115
|
+
"""Safely perform file operation with retries on Windows."""
|
|
116
|
+
max_retries = 3 if platform.system() == "Windows" else 1
|
|
117
|
+
|
|
118
|
+
for attempt in range(max_retries):
|
|
119
|
+
try:
|
|
120
|
+
operation(*args, **kwargs)
|
|
121
|
+
return
|
|
122
|
+
except (PermissionError, FileNotFoundError, OSError) as e:
|
|
123
|
+
if attempt == max_retries - 1:
|
|
124
|
+
raise e
|
|
125
|
+
|
|
126
|
+
if platform.system() == "Windows":
|
|
127
|
+
# On Windows, wait a bit and retry
|
|
128
|
+
time.sleep(0.1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def unrecoverable_delete(file_path: Path, passes: int = 3) -> None:
|
|
132
|
+
"""Delete a file unrecoverably."""
|
|
133
|
+
if not file_path.exists():
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if not file_path.is_file():
|
|
137
|
+
raise ValueError(f"{file_path} is not a file")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
file_size = os.path.getsize(file_path)
|
|
141
|
+
|
|
142
|
+
with open(file_path, "r+b") as f:
|
|
143
|
+
for _ in range(passes):
|
|
144
|
+
# Overwrite with random bytes
|
|
145
|
+
f.seek(0)
|
|
146
|
+
random_data = os.urandom(file_size)
|
|
147
|
+
f.write(random_data)
|
|
148
|
+
f.flush() # Ensure data is written to disk
|
|
149
|
+
|
|
150
|
+
# Finally, delete the file
|
|
151
|
+
safe_file_operation(os.remove, file_path)
|
|
152
|
+
except PermissionError:
|
|
153
|
+
print(f"Permission denied to securely delete file '{file_path}'.")
|
|
154
|
+
except Exception as e: # pylint: disable=broad-except
|
|
155
|
+
print(f"Error during secure deletion of '{file_path}': {e}")
|