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.
Files changed (98) hide show
  1. olas_operate_middleware-0.13.2.dist-info/METADATA +75 -0
  2. olas_operate_middleware-0.13.2.dist-info/RECORD +101 -0
  3. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/WHEEL +1 -1
  4. operate/__init__.py +17 -0
  5. operate/account/user.py +35 -9
  6. operate/bridge/bridge_manager.py +470 -0
  7. operate/bridge/providers/lifi_provider.py +377 -0
  8. operate/bridge/providers/native_bridge_provider.py +677 -0
  9. operate/bridge/providers/provider.py +469 -0
  10. operate/bridge/providers/relay_provider.py +457 -0
  11. operate/cli.py +1565 -417
  12. operate/constants.py +60 -12
  13. operate/data/README.md +19 -0
  14. operate/data/contracts/{service_staking_token → dual_staking_token}/__init__.py +2 -2
  15. operate/data/contracts/dual_staking_token/build/DualStakingToken.json +443 -0
  16. operate/data/contracts/dual_staking_token/contract.py +132 -0
  17. operate/data/contracts/dual_staking_token/contract.yaml +23 -0
  18. operate/{ledger/base.py → data/contracts/foreign_omnibridge/__init__.py} +2 -19
  19. operate/data/contracts/foreign_omnibridge/build/ForeignOmnibridge.json +1372 -0
  20. operate/data/contracts/foreign_omnibridge/contract.py +130 -0
  21. operate/data/contracts/foreign_omnibridge/contract.yaml +23 -0
  22. operate/{ledger/solana.py → data/contracts/home_omnibridge/__init__.py} +2 -20
  23. operate/data/contracts/home_omnibridge/build/HomeOmnibridge.json +1421 -0
  24. operate/data/contracts/home_omnibridge/contract.py +80 -0
  25. operate/data/contracts/home_omnibridge/contract.yaml +23 -0
  26. operate/data/contracts/l1_standard_bridge/__init__.py +20 -0
  27. operate/data/contracts/l1_standard_bridge/build/L1StandardBridge.json +831 -0
  28. operate/data/contracts/l1_standard_bridge/contract.py +158 -0
  29. operate/data/contracts/l1_standard_bridge/contract.yaml +23 -0
  30. operate/data/contracts/l2_standard_bridge/__init__.py +20 -0
  31. operate/data/contracts/l2_standard_bridge/build/L2StandardBridge.json +626 -0
  32. operate/data/contracts/l2_standard_bridge/contract.py +130 -0
  33. operate/data/contracts/l2_standard_bridge/contract.yaml +23 -0
  34. operate/data/contracts/mech_activity/__init__.py +20 -0
  35. operate/data/contracts/mech_activity/build/MechActivity.json +111 -0
  36. operate/data/contracts/mech_activity/contract.py +44 -0
  37. operate/data/contracts/mech_activity/contract.yaml +23 -0
  38. operate/data/contracts/optimism_mintable_erc20/__init__.py +20 -0
  39. operate/data/contracts/optimism_mintable_erc20/build/OptimismMintableERC20.json +491 -0
  40. operate/data/contracts/optimism_mintable_erc20/contract.py +45 -0
  41. operate/data/contracts/optimism_mintable_erc20/contract.yaml +23 -0
  42. operate/data/contracts/recovery_module/__init__.py +20 -0
  43. operate/data/contracts/recovery_module/build/RecoveryModule.json +811 -0
  44. operate/data/contracts/recovery_module/contract.py +61 -0
  45. operate/data/contracts/recovery_module/contract.yaml +23 -0
  46. operate/data/contracts/requester_activity_checker/__init__.py +20 -0
  47. operate/data/contracts/requester_activity_checker/build/RequesterActivityChecker.json +111 -0
  48. operate/data/contracts/requester_activity_checker/contract.py +33 -0
  49. operate/data/contracts/requester_activity_checker/contract.yaml +23 -0
  50. operate/data/contracts/staking_token/__init__.py +20 -0
  51. operate/data/contracts/staking_token/build/StakingToken.json +1336 -0
  52. operate/data/contracts/{service_staking_token → staking_token}/contract.py +27 -13
  53. operate/data/contracts/staking_token/contract.yaml +23 -0
  54. operate/data/contracts/uniswap_v2_erc20/contract.yaml +3 -1
  55. operate/data/contracts/uniswap_v2_erc20/tests/__init__.py +20 -0
  56. operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py +363 -0
  57. operate/keys.py +118 -33
  58. operate/ledger/__init__.py +159 -56
  59. operate/ledger/profiles.py +321 -18
  60. operate/migration.py +555 -0
  61. operate/{http → operate_http}/__init__.py +3 -2
  62. operate/{http → operate_http}/exceptions.py +6 -4
  63. operate/operate_types.py +544 -0
  64. operate/pearl.py +13 -1
  65. operate/quickstart/analyse_logs.py +118 -0
  66. operate/quickstart/claim_staking_rewards.py +104 -0
  67. operate/quickstart/reset_configs.py +106 -0
  68. operate/quickstart/reset_password.py +70 -0
  69. operate/quickstart/reset_staking.py +145 -0
  70. operate/quickstart/run_service.py +726 -0
  71. operate/quickstart/stop_service.py +72 -0
  72. operate/quickstart/terminate_on_chain_service.py +83 -0
  73. operate/quickstart/utils.py +298 -0
  74. operate/resource.py +62 -3
  75. operate/services/agent_runner.py +202 -0
  76. operate/services/deployment_runner.py +868 -0
  77. operate/services/funding_manager.py +929 -0
  78. operate/services/health_checker.py +280 -0
  79. operate/services/manage.py +2356 -620
  80. operate/services/protocol.py +1246 -340
  81. operate/services/service.py +756 -391
  82. operate/services/utils/mech.py +103 -0
  83. operate/services/utils/tendermint.py +86 -12
  84. operate/settings.py +70 -0
  85. operate/utils/__init__.py +135 -0
  86. operate/utils/gnosis.py +407 -80
  87. operate/utils/single_instance.py +226 -0
  88. operate/utils/ssl.py +133 -0
  89. operate/wallet/master.py +708 -123
  90. operate/wallet/wallet_recovery_manager.py +507 -0
  91. olas_operate_middleware-0.1.0rc59.dist-info/METADATA +0 -304
  92. olas_operate_middleware-0.1.0rc59.dist-info/RECORD +0 -41
  93. operate/data/contracts/service_staking_token/build/ServiceStakingToken.json +0 -1273
  94. operate/data/contracts/service_staking_token/contract.yaml +0 -23
  95. operate/ledger/ethereum.py +0 -48
  96. operate/types.py +0 -260
  97. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info}/entry_points.txt +0 -0
  98. {olas_operate_middleware-0.1.0rc59.dist-info → olas_operate_middleware-0.13.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,726 @@
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
+ """Agent Quickstart script."""
20
+
21
+ import json
22
+ import os
23
+ import shutil
24
+ import textwrap
25
+ import time
26
+ import typing as t
27
+ import warnings
28
+ from http import HTTPStatus
29
+
30
+ import requests
31
+ from aea.crypto.registries import make_ledger_api
32
+ from aea_ledger_ethereum import LedgerApi
33
+ from halo import Halo # type: ignore[import]
34
+ from web3.exceptions import Web3Exception
35
+
36
+ from operate.account.user import UserAccount
37
+ from operate.constants import IPFS_ADDRESS, NO_STAKING_PROGRAM_ID, USER_JSON
38
+ from operate.data import DATA_DIR
39
+ from operate.data.contracts.staking_token.contract import StakingTokenContract
40
+ from operate.ledger import DEFAULT_RPCS
41
+ from operate.ledger.profiles import STAKING, get_staking_contract
42
+ from operate.operate_types import (
43
+ Chain,
44
+ LedgerType,
45
+ ServiceEnvProvisionType,
46
+ ServiceTemplate,
47
+ )
48
+ from operate.quickstart.utils import (
49
+ CHAIN_TO_METADATA,
50
+ QuickstartConfig,
51
+ ask_or_get_from_env,
52
+ check_rpc,
53
+ print_box,
54
+ print_section,
55
+ print_title,
56
+ wei_to_token,
57
+ )
58
+ from operate.services.manage import ServiceManager
59
+ from operate.services.service import Service
60
+ from operate.utils.gnosis import get_asset_balance
61
+ from operate.wallet.master import MasterWallet
62
+
63
+
64
+ warnings.filterwarnings("ignore", category=UserWarning)
65
+
66
+
67
+ if t.TYPE_CHECKING:
68
+ from operate.cli import OperateApp
69
+
70
+ NO_STAKING_PROGRAM_METADATA = {
71
+ "name": "No staking",
72
+ "description": "Your agent will still work as expected, but it will not be staked within any staking program.",
73
+ }
74
+ CUSTOM_PROGRAM_ID = "custom_staking"
75
+ QS_STAKING_PROGRAMS: t.Dict[Chain, t.Dict[str, str]] = {
76
+ Chain.GNOSIS: {
77
+ "quickstart_beta_hobbyist": "trader",
78
+ "quickstart_beta_hobbyist_2": "trader",
79
+ "quickstart_beta_expert": "trader",
80
+ "quickstart_beta_expert_2": "trader",
81
+ "quickstart_beta_expert_3": "trader",
82
+ "quickstart_beta_expert_4": "trader",
83
+ "quickstart_beta_expert_5": "trader",
84
+ "quickstart_beta_expert_6": "trader",
85
+ "quickstart_beta_expert_7": "trader",
86
+ "quickstart_beta_expert_8": "trader",
87
+ "quickstart_beta_expert_9": "trader",
88
+ "quickstart_beta_expert_10": "trader",
89
+ "quickstart_beta_expert_11": "trader",
90
+ "quickstart_beta_expert_12": "trader",
91
+ "quickstart_beta_expert_15_mech_marketplace": "trader",
92
+ "quickstart_beta_expert_16_mech_marketplace": "trader",
93
+ "quickstart_beta_expert_17_mech_marketplace": "trader",
94
+ "quickstart_beta_expert_18_mech_marketplace": "trader",
95
+ "quickstart_beta_mech_marketplace_expert_1": "trader",
96
+ "quickstart_beta_mech_marketplace_expert_2": "trader",
97
+ "quickstart_beta_mech_marketplace_expert_3": "trader",
98
+ "quickstart_beta_mech_marketplace_expert_4": "trader",
99
+ "quickstart_beta_mech_marketplace_expert_5": "trader",
100
+ "quickstart_beta_mech_marketplace_expert_6": "trader",
101
+ "quickstart_beta_mech_marketplace_expert_7": "trader",
102
+ "quickstart_beta_mech_marketplace_expert_8": "trader",
103
+ "quickstart_beta_mech_marketplace_expert_9": "trader",
104
+ "quickstart_beta_mech_marketplace_expert_10": "trader",
105
+ "mech_marketplace": "mech",
106
+ "marketplace_supply_alpha": "mech",
107
+ },
108
+ Chain.OPTIMISM: {
109
+ "optimus_alpha_2": "optimus",
110
+ "optimus_alpha_3": "optimus",
111
+ "optimus_alpha_4": "optimus",
112
+ },
113
+ Chain.ETHEREUM: {},
114
+ Chain.BASE: {
115
+ "meme_base_alpha_2": "memeooorr",
116
+ "marketplace_supply_alpha": "mech",
117
+ "agents_fun_1": "memeooorr",
118
+ "agents_fun_2": "memeooorr",
119
+ "agents_fun_3": "memeooorr",
120
+ },
121
+ Chain.CELO: {},
122
+ Chain.MODE: {
123
+ "optimus_alpha": "modius",
124
+ },
125
+ }
126
+
127
+
128
+ def ask_confirm_password() -> str:
129
+ """Ask for password confirmation."""
130
+ while True:
131
+ password = ask_or_get_from_env(
132
+ "Please input your password (or press enter): ", True, "OPERATE_PASSWORD"
133
+ )
134
+ confirm_password = ask_or_get_from_env(
135
+ "Please confirm your password: ", True, "OPERATE_PASSWORD"
136
+ )
137
+
138
+ if password == confirm_password:
139
+ return password
140
+ else:
141
+ print("Passwords do not match!")
142
+
143
+
144
+ def load_local_config(operate: "OperateApp", service_name: str) -> QuickstartConfig:
145
+ """Load the local quickstart configuration."""
146
+ operate_home = operate._path
147
+ old_path = operate_home / "local_config.json"
148
+ if old_path.exists(): # Migrate to new naming scheme
149
+ config = t.cast(QuickstartConfig, QuickstartConfig.load(old_path))
150
+ service_manager = operate.service_manager()
151
+ services = service_manager.json
152
+ if config.staking_program_id == NO_STAKING_PROGRAM_ID:
153
+ for service in services:
154
+ if service["name"] == service_name:
155
+ config.path = (
156
+ config.path.parent / f"{service_name}-quickstart-config.json"
157
+ )
158
+ shutil.move(old_path, config.path)
159
+ break
160
+ else:
161
+ for staking_program, _agent_keyword in QS_STAKING_PROGRAMS[
162
+ Chain.from_string(config.principal_chain)
163
+ ].items():
164
+ if staking_program == config.staking_program_id:
165
+ break
166
+ else:
167
+ raise ValueError(
168
+ f"Staking program {config.staking_program_id} not found in {QS_STAKING_PROGRAMS[Chain.from_string(config.principal_chain)].keys()}.\n"
169
+ "Please resolve manually!"
170
+ )
171
+
172
+ for service in services:
173
+ if _agent_keyword in service["name"].lower():
174
+ config.path = (
175
+ config.path.parent / f"{service['name']}-quickstart-config.json"
176
+ )
177
+ shutil.move(old_path, config.path)
178
+ break
179
+
180
+ for qs_config in operate_home.glob("*-quickstart-config.json"):
181
+ if f"{service_name}-quickstart-config.json" == qs_config.name:
182
+ config = t.cast(QuickstartConfig, QuickstartConfig.load(qs_config))
183
+ break
184
+ else:
185
+ config = QuickstartConfig(
186
+ operate_home / f"{service_name}-quickstart-config.json"
187
+ )
188
+
189
+ return config
190
+
191
+
192
+ def configure_local_config(
193
+ template: ServiceTemplate, operate: "OperateApp"
194
+ ) -> QuickstartConfig:
195
+ """Configure local quickstart configuration."""
196
+ config = load_local_config(operate=operate, service_name=template["name"])
197
+
198
+ if config.rpc is None:
199
+ config.rpc = {}
200
+
201
+ for chain in template["configurations"]:
202
+ while not check_rpc(chain, config.rpc.get(chain)):
203
+ config.rpc[chain] = ask_or_get_from_env(
204
+ f"Enter a {CHAIN_TO_METADATA[chain]['name']} RPC that supports eth_newFilter [hidden input]: ",
205
+ True,
206
+ f"{chain.upper()}_LEDGER_RPC",
207
+ )
208
+ os.environ[f"{chain.upper()}_LEDGER_RPC"] = config.rpc[chain]
209
+ DEFAULT_RPCS[Chain.from_string(chain)] = config.rpc[chain]
210
+
211
+ config.principal_chain = template["home_chain"]
212
+
213
+ home_chain = Chain.from_string(config.principal_chain)
214
+ staking_ctr = t.cast(
215
+ StakingTokenContract,
216
+ StakingTokenContract.from_dir(
217
+ directory=str(DATA_DIR / "contracts" / "staking_token")
218
+ ),
219
+ )
220
+ ledger_api = make_ledger_api(
221
+ LedgerType.ETHEREUM.lower(),
222
+ address=config.rpc[config.principal_chain], # type: ignore[index]
223
+ chain_id=home_chain.id,
224
+ )
225
+
226
+ if config.staking_program_id is None:
227
+ print_section("Please, select your staking program preference")
228
+ available_choices = {}
229
+ ids = (
230
+ [NO_STAKING_PROGRAM_ID]
231
+ + [
232
+ id
233
+ for id in STAKING[home_chain]
234
+ if id in QS_STAKING_PROGRAMS[home_chain]
235
+ and QS_STAKING_PROGRAMS[home_chain][id] in template["name"].lower()
236
+ ]
237
+ + [CUSTOM_PROGRAM_ID]
238
+ )
239
+
240
+ for index, program_id in enumerate(ids):
241
+ if program_id == NO_STAKING_PROGRAM_ID:
242
+ metadata = NO_STAKING_PROGRAM_METADATA
243
+ elif program_id == CUSTOM_PROGRAM_ID:
244
+ metadata = {
245
+ "name": "Custom Staking contract",
246
+ "description": "If you choose this option, you will be asked to provide the staking contract address.",
247
+ }
248
+ else:
249
+ instance = staking_ctr.get_instance(
250
+ ledger_api=ledger_api,
251
+ contract_address=STAKING[home_chain][program_id],
252
+ )
253
+ try:
254
+ metadata_hash = instance.functions.metadataHash().call().hex()
255
+ ipfs_address = IPFS_ADDRESS.format(hash=metadata_hash)
256
+ response = requests.get(ipfs_address)
257
+ if response.status_code != HTTPStatus.OK:
258
+ raise requests.RequestException(
259
+ f"Failed to fetch data from {ipfs_address}: {response.status_code}"
260
+ )
261
+ metadata = response.json()
262
+ except (Web3Exception, requests.RequestException):
263
+ metadata = {
264
+ "name": program_id,
265
+ "description": program_id,
266
+ "available_staking_slots": "?",
267
+ }
268
+
269
+ # Add staking slots count to successful response
270
+ try:
271
+ max_services = instance.functions.maxNumServices().call()
272
+ current_services = instance.functions.getServiceIds().call()
273
+ metadata["available_staking_slots"] = max_services - len(
274
+ current_services
275
+ )
276
+ except Web3Exception:
277
+ metadata["available_staking_slots"] = "?"
278
+
279
+ name = metadata["name"]
280
+ description = metadata["description"]
281
+ if "available_staking_slots" in metadata:
282
+ available_slots_str = (
283
+ f"(available slots : {metadata['available_staking_slots']})"
284
+ )
285
+ else:
286
+ available_slots_str = ""
287
+
288
+ wrapped_description = textwrap.fill(
289
+ description, width=80, initial_indent=" ", subsequent_indent=" "
290
+ )
291
+ print(
292
+ f"{index + 1}) {name}\t{available_slots_str}\n{wrapped_description}\n"
293
+ )
294
+ if available_slots_str or program_id in (
295
+ NO_STAKING_PROGRAM_ID,
296
+ CUSTOM_PROGRAM_ID,
297
+ ):
298
+ available_choices[index + 1] = {
299
+ "program_id": program_id,
300
+ "slots": available_slots_str,
301
+ "name": name,
302
+ }
303
+
304
+ while True:
305
+ try:
306
+ input_value = ask_or_get_from_env(
307
+ f"Enter your choice (1 - {len(ids)}): ", False, "STAKING_PROGRAM"
308
+ )
309
+ try:
310
+ choice = int(input_value)
311
+ if choice not in available_choices:
312
+ print("\nPlease select a program with available slots:")
313
+ for idx, prog in available_choices.items():
314
+ print(f"{idx}) {prog['name']} : {prog['slots']}")
315
+ continue
316
+ selected_program = available_choices[choice]
317
+ config.staking_program_id = selected_program["program_id"]
318
+ print(f"Selected staking program: {selected_program['name']}")
319
+ break
320
+ except ValueError:
321
+ if input_value in ids:
322
+ config.staking_program_id = input_value
323
+ break
324
+ else:
325
+ raise ValueError(f"STAKING_PROGRAM must be one of {ids}")
326
+ except Exception as e:
327
+ print(f"Error in getting input: {str(e)}")
328
+ raise
329
+
330
+ if config.staking_program_id == CUSTOM_PROGRAM_ID:
331
+ while True:
332
+ try:
333
+ config.staking_program_id = ask_or_get_from_env(
334
+ "Enter the staking contract address: ",
335
+ False,
336
+ "STAKING_CONTRACT_ADDRESS",
337
+ )
338
+ instance = staking_ctr.get_instance(
339
+ ledger_api=ledger_api,
340
+ contract_address=config.staking_program_id,
341
+ )
342
+ max_services = instance.functions.maxNumServices().call()
343
+ current_services = instance.functions.getServiceIds().call()
344
+ available_slots = max_services - len(current_services)
345
+ if available_slots > 0:
346
+ print(f"Found {available_slots} available staking slots.")
347
+ break
348
+ else:
349
+ print(
350
+ "No available staking slots found. Please enter another address."
351
+ )
352
+ except Exception:
353
+ print("This address is not a valid staking contract address.")
354
+
355
+ # set chain configs in the service template
356
+ for chain in template["configurations"]:
357
+ if chain == config.principal_chain:
358
+ staking_contract_address = get_staking_contract(
359
+ chain, config.staking_program_id
360
+ )
361
+ if staking_contract_address is None:
362
+ min_staking_deposit = 1
363
+ else:
364
+ instance = staking_ctr.get_instance(
365
+ ledger_api=ledger_api,
366
+ contract_address=staking_contract_address,
367
+ )
368
+ min_staking_deposit = int(instance.functions.minStakingDeposit().call())
369
+
370
+ template["configurations"][chain] |= {
371
+ "staking_program_id": config.staking_program_id,
372
+ "rpc": config.rpc[chain],
373
+ "cost_of_bond": min_staking_deposit,
374
+ }
375
+ else:
376
+ template["configurations"][chain] |= {
377
+ "staking_program_id": NO_STAKING_PROGRAM_ID,
378
+ "rpc": config.rpc[chain],
379
+ "cost_of_bond": 1,
380
+ }
381
+
382
+ if config.user_provided_args is None:
383
+ config.user_provided_args = {}
384
+
385
+ if any(
386
+ (
387
+ env_var_data["provision_type"] == ServiceEnvProvisionType.USER
388
+ and env_var_name not in config.user_provided_args
389
+ )
390
+ for env_var_name, env_var_data in template["env_variables"].items()
391
+ ):
392
+ print_section("Please enter the arguments that will be used by the service.")
393
+
394
+ service_manager = operate.service_manager()
395
+ mech_configs = service_manager.get_mech_configs(
396
+ chain=config.principal_chain,
397
+ ledger_api=ledger_api,
398
+ staking_program_id=config.staking_program_id,
399
+ )
400
+
401
+ for env_var_name, env_var_data in template["env_variables"].items():
402
+ if env_var_data["provision_type"] == ServiceEnvProvisionType.USER:
403
+ # PRIORITY_MECH_ADDRESS and PRIORITY_MECH_SERVICE_ID are given dynamic default values
404
+ if env_var_name == "PRIORITY_MECH_ADDRESS":
405
+ env_var_data["value"] = mech_configs.priority_mech_address
406
+ if (
407
+ env_var_name in config.user_provided_args
408
+ and env_var_data["value"] != config.user_provided_args[env_var_name]
409
+ ):
410
+ del config.user_provided_args[env_var_name]
411
+
412
+ if env_var_name == "PRIORITY_MECH_SERVICE_ID":
413
+ env_var_data["value"] = mech_configs.priority_mech_service_id
414
+ if (
415
+ env_var_name in config.user_provided_args
416
+ and env_var_data["value"] != config.user_provided_args[env_var_name]
417
+ ):
418
+ del config.user_provided_args[env_var_name]
419
+
420
+ if env_var_name not in config.user_provided_args:
421
+ print(f"Description: {env_var_data['description']}")
422
+ if env_var_data["value"] is not None and env_var_data["value"] != "":
423
+ print(f"Default: {env_var_data['value']}")
424
+
425
+ user_provided_arg = ask_or_get_from_env(
426
+ f"Please enter {env_var_data['name']}: ", False, env_var_name
427
+ )
428
+ config.user_provided_args[env_var_name] = env_var_data["value"]
429
+ if user_provided_arg:
430
+ config.user_provided_args[env_var_name] = user_provided_arg
431
+
432
+ print()
433
+
434
+ template["env_variables"][env_var_name][
435
+ "value"
436
+ ] = config.user_provided_args[env_var_name]
437
+
438
+ # TODO: Handle it in a more generic way
439
+ if (
440
+ template["env_variables"][env_var_name]["provision_type"]
441
+ == ServiceEnvProvisionType.COMPUTED
442
+ and "SUBGRAPH_API_KEY" in config.user_provided_args
443
+ and "{SUBGRAPH_API_KEY}" in template["env_variables"][env_var_name]["value"]
444
+ ):
445
+ template["env_variables"][env_var_name]["value"] = template[
446
+ "env_variables"
447
+ ][env_var_name]["value"].format(
448
+ SUBGRAPH_API_KEY=config.user_provided_args["SUBGRAPH_API_KEY"],
449
+ )
450
+
451
+ config.store()
452
+ return config
453
+
454
+
455
+ def ask_password_if_needed(operate: "OperateApp") -> None:
456
+ """Ask password if needed."""
457
+ if operate.user_account is None:
458
+ print_section("Set up local user account")
459
+ print("Creating a new local user account...")
460
+ password = ask_confirm_password()
461
+ UserAccount.new(
462
+ password=password,
463
+ path=operate._path / USER_JSON,
464
+ )
465
+ else:
466
+ _password = None
467
+ while _password is None:
468
+ _password = ask_or_get_from_env(
469
+ "\nEnter local user account password [hidden input]: ",
470
+ True,
471
+ "OPERATE_PASSWORD",
472
+ )
473
+ if operate.user_account.is_valid(password=_password):
474
+ break
475
+ _password = None
476
+ print("Invalid password!")
477
+
478
+ password = _password
479
+
480
+ operate.password = password
481
+
482
+
483
+ def get_service(manager: ServiceManager, template: ServiceTemplate) -> Service:
484
+ """Get service."""
485
+ for service in manager.json:
486
+ if service["name"] == template["name"]:
487
+ old_hash = service["hash"]
488
+ old_version = service["agent_release"]["repository"]["version"]
489
+ if (
490
+ old_hash == template["hash"]
491
+ and old_version == template["agent_release"]["repository"]["version"]
492
+ ):
493
+ print(f'Loading service {template["hash"]}')
494
+ service = manager.load(
495
+ service_config_id=service["service_config_id"],
496
+ )
497
+ else:
498
+ print(f"Updating service from {old_hash} to " + template["hash"])
499
+ service = manager.update(
500
+ service_config_id=service["service_config_id"],
501
+ service_template=template,
502
+ )
503
+
504
+ for env_var_name, env_var_data in template["env_variables"].items():
505
+ if env_var_name not in service.env_variables:
506
+ service.env_variables[env_var_name] = env_var_data
507
+
508
+ service.update_user_params_from_template(service_template=template)
509
+ service.store()
510
+ break
511
+ else:
512
+ print(f'Creating service {template["hash"]}')
513
+ service = manager.load_or_create(
514
+ hash=template["hash"],
515
+ service_template=template,
516
+ )
517
+
518
+ return service
519
+
520
+
521
+ def ask_funds_in_address(
522
+ ledger_api: LedgerApi,
523
+ required_balance: int,
524
+ asset_address: str,
525
+ recipient_name: str,
526
+ recipient_address: str,
527
+ chain: str,
528
+ ) -> None:
529
+ """Ask for funds in address."""
530
+ if required_balance == 0:
531
+ return
532
+
533
+ current_balance = get_asset_balance(ledger_api, asset_address, recipient_address)
534
+ print(
535
+ f"[{chain}] Please transfer at least {wei_to_token(required_balance, chain, asset_address)} "
536
+ f"to the {recipient_name} {recipient_address} "
537
+ )
538
+ spinner = Halo(
539
+ text=f"[{chain}] Waiting for at least {wei_to_token(required_balance, chain, asset_address)}...",
540
+ spinner="dots",
541
+ )
542
+ spinner.start()
543
+
544
+ while True:
545
+ time.sleep(1)
546
+ updated_balance = get_asset_balance(
547
+ ledger_api, asset_address, recipient_address
548
+ )
549
+ if updated_balance >= current_balance + required_balance:
550
+ break
551
+
552
+ remaining_requirement = current_balance + required_balance - updated_balance
553
+ spinner.text = f"[{chain}] Waiting for at least {wei_to_token(remaining_requirement, chain, asset_address)}..."
554
+
555
+ spinner.succeed(
556
+ f"[{chain}] {recipient_name} updated balance: {wei_to_token(updated_balance, chain, asset_address)}."
557
+ )
558
+
559
+
560
+ def _ask_funds_from_requirements(
561
+ manager: ServiceManager,
562
+ wallet: MasterWallet,
563
+ service: Service,
564
+ ) -> bool:
565
+ """Ask for funds from requirements."""
566
+ spinner = Halo(text="Calculating funds requirements...", spinner="dots")
567
+ spinner.start()
568
+ requirements = manager.funding_requirements(
569
+ service_config_id=service.service_config_id
570
+ )
571
+ spinner.stop()
572
+
573
+ wallet_names = (
574
+ {
575
+ wallet.crypto.address: "Master EOA",
576
+ "master_safe": "Master Safe",
577
+ "service_safe": "Service Safe",
578
+ }
579
+ | {safe_address: "Master Safe" for safe_address in wallet.safes.values()}
580
+ | {
581
+ chain_config.chain_data.multisig: "Service Safe"
582
+ for chain_config in service.chain_configs.values()
583
+ }
584
+ | {address: "Agent EOA" for address in service.agent_addresses}
585
+ )
586
+
587
+ if not requirements["is_refill_required"] and requirements["allow_start_agent"]:
588
+ for chain_name, balances in requirements["balances"].items():
589
+ ledger_api = wallet.ledger_api(
590
+ chain=Chain(chain_name),
591
+ rpc=service.chain_configs[chain_name].ledger_config.rpc,
592
+ )
593
+ for wallet_address, asset_balances in balances.items():
594
+ for asset_address, balance in asset_balances.items():
595
+ print(
596
+ f"[{chain_name}] {wallet_names[wallet_address]} has {wei_to_token(balance, chain_name, asset_address)}"
597
+ )
598
+
599
+ return True
600
+
601
+ for chain_name, chain_requirements in requirements["refill_requirements"].items():
602
+ chain = Chain(chain_name)
603
+ ledger_api = wallet.ledger_api(
604
+ chain=chain,
605
+ rpc=service.chain_configs[chain_name].ledger_config.rpc,
606
+ )
607
+ for wallet_address, requirements in chain_requirements.items():
608
+ if wallet_address in ("master_safe", "service_safe"):
609
+ continue # we can't ask funds in placeholder addresses
610
+
611
+ for asset_address, requirement in requirements.items():
612
+ ask_funds_in_address(
613
+ ledger_api=ledger_api,
614
+ chain=chain_name,
615
+ asset_address=asset_address,
616
+ required_balance=requirement,
617
+ recipient_address=wallet_address,
618
+ recipient_name=wallet_names[wallet_address],
619
+ )
620
+
621
+ return False
622
+
623
+
624
+ def _maybe_create_master_eoa(operate: "OperateApp") -> None:
625
+ """Maybe create the Master EOA."""
626
+ if not operate.wallet_manager.exists(ledger_type=LedgerType.ETHEREUM):
627
+ print("Creating the Master EOA...")
628
+ wallet, mnemonic = operate.wallet_manager.create(
629
+ ledger_type=LedgerType.ETHEREUM
630
+ )
631
+ wallet.password = operate.password
632
+ print_box(
633
+ f"Please save the mnemonic phrase for the Master EOA:\n{', '.join(mnemonic)}",
634
+ 0,
635
+ "-",
636
+ )
637
+ ask_or_get_from_env(
638
+ "Press enter to continue...", False, "CONTINUE", raise_if_missing=False
639
+ )
640
+
641
+
642
+ def ensure_enough_funds(operate: "OperateApp", service: Service) -> None:
643
+ """Ensure enough funds."""
644
+ _maybe_create_master_eoa(operate)
645
+ wallet = operate.wallet_manager.load(ledger_type=LedgerType.ETHEREUM)
646
+ manager = operate.service_manager()
647
+ manager.funding_manager.is_for_quickstart = True
648
+
649
+ backup_owner = None
650
+ while not _ask_funds_from_requirements(manager, wallet, service):
651
+ for chain_name, chain_config in service.chain_configs.items():
652
+ chain = Chain.from_string(chain_name)
653
+ if wallet.safes.get(chain) is None:
654
+ print(f"[{chain_name}] Creating Master Safe")
655
+ if backup_owner is None:
656
+ backup_owner = ask_or_get_from_env(
657
+ "Please input your backup owner (leave empty to skip): ",
658
+ False,
659
+ "BACKUP_OWNER",
660
+ raise_if_missing=False,
661
+ )
662
+
663
+ wallet.create_safe(
664
+ chain=chain,
665
+ rpc=chain_config.ledger_config.rpc,
666
+ backup_owner=None if backup_owner == "" else backup_owner,
667
+ )
668
+
669
+
670
+ def run_service(
671
+ operate: "OperateApp",
672
+ config_path: str,
673
+ build_only: bool = False,
674
+ skip_dependency_check: bool = False,
675
+ use_binary: bool = False,
676
+ ) -> None:
677
+ """Run service."""
678
+
679
+ with open(config_path, "r") as config_file:
680
+ template = json.load(config_file)
681
+
682
+ print_title(f"{template['name']} quickstart")
683
+
684
+ ask_password_if_needed(operate)
685
+ _maybe_create_master_eoa(operate)
686
+
687
+ config = configure_local_config(template, operate)
688
+ manager = operate.service_manager()
689
+ service = get_service(manager, template)
690
+
691
+ # reload manger and config after setting operate.password
692
+ manager = operate.service_manager(skip_dependency_check=skip_dependency_check)
693
+ config = load_local_config(operate=operate, service_name=t.cast(str, service.name))
694
+ ensure_enough_funds(operate, service)
695
+
696
+ print_box("PLEASE, DO NOT INTERRUPT THIS PROCESS.")
697
+ print_section(f"Deploying on-chain service on {config.principal_chain}...")
698
+ print(
699
+ "Cancelling the on-chain service update prematurely could lead to an inconsistent state of the Safe or the on-chain service state, which may require manual intervention to resolve.\n"
700
+ )
701
+ manager.deploy_service_onchain_from_safe(
702
+ service_config_id=service.service_config_id
703
+ )
704
+
705
+ print_section("Funding the service")
706
+ service = get_service(manager, template)
707
+ manager.funding_manager.topup_service_initial(service=service)
708
+
709
+ print_section("Deploying the service")
710
+ if use_binary:
711
+ use_docker = False
712
+ use_k8s = False
713
+ else:
714
+ use_docker = True
715
+ use_k8s = True
716
+
717
+ manager.deploy_service_locally(
718
+ service_config_id=service.service_config_id,
719
+ use_docker=use_docker,
720
+ use_kubernetes=use_k8s,
721
+ build_only=build_only,
722
+ )
723
+ if build_only:
724
+ print_section(f"Built the {template['name']}")
725
+ else:
726
+ print_section(f"Starting the {template['name']}")