olas-operate-middleware 0.6.2__py3-none-any.whl → 0.7.0__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.
@@ -0,0 +1,442 @@
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
+ """Relay provider."""
21
+
22
+
23
+ import enum
24
+ import json
25
+ import math
26
+ import time
27
+ import typing as t
28
+ from http import HTTPStatus
29
+ from urllib.parse import urlencode
30
+
31
+ import requests
32
+
33
+ from operate.bridge.providers.provider import (
34
+ DEFAULT_MAX_QUOTE_RETRIES,
35
+ MESSAGE_EXECUTION_FAILED,
36
+ MESSAGE_QUOTE_ZERO,
37
+ Provider,
38
+ ProviderRequest,
39
+ ProviderRequestStatus,
40
+ QuoteData,
41
+ )
42
+ from operate.operate_types import Chain
43
+
44
+
45
+ GAS_ESTIMATE_FALLBACK_ADDRESS = "0x000000000000000000000000000000000000dEaD"
46
+
47
+ # The following constants were determined empirically (+ margin) from the Relay API/Dapp.
48
+ RELAY_DEFAULT_GAS = {
49
+ Chain.ETHEREUM: {
50
+ "deposit": 50_000,
51
+ "approve": 200_000,
52
+ "authorize": 1,
53
+ "authorize1": 1,
54
+ "authorize2": 1,
55
+ "swap": 400_000,
56
+ "send": 1,
57
+ },
58
+ Chain.BASE: {
59
+ "deposit": 50_000,
60
+ "approve": 200_000,
61
+ "authorize": 1,
62
+ "authorize1": 1,
63
+ "authorize2": 1,
64
+ "swap": 400_000,
65
+ "send": 1,
66
+ },
67
+ Chain.CELO: {
68
+ "deposit": 50_000,
69
+ "approve": 200_000,
70
+ "authorize": 1,
71
+ "authorize1": 1,
72
+ "authorize2": 1,
73
+ "swap": 400_000,
74
+ "send": 1,
75
+ },
76
+ Chain.GNOSIS: {
77
+ "deposit": 350_000,
78
+ "approve": 200_000,
79
+ "authorize": 1,
80
+ "authorize1": 1,
81
+ "authorize2": 1,
82
+ "swap": 500_000,
83
+ "send": 1,
84
+ },
85
+ Chain.MODE: {
86
+ "deposit": 50_000,
87
+ "approve": 200_000,
88
+ "authorize": 1,
89
+ "authorize1": 1,
90
+ "authorize2": 1,
91
+ "swap": 1_500_000,
92
+ "send": 1,
93
+ },
94
+ Chain.OPTIMISTIC: {
95
+ "deposit": 50_000,
96
+ "approve": 200_000,
97
+ "authorize": 1,
98
+ "authorize1": 1,
99
+ "authorize2": 1,
100
+ "swap": 400_000,
101
+ "send": 1,
102
+ },
103
+ }
104
+
105
+
106
+ class RelayExecutionStatus(str, enum.Enum):
107
+ """Relay execution status."""
108
+
109
+ REFUND = "refund"
110
+ DELAYED = "delayed"
111
+ WAITING = "waiting"
112
+ FAILURE = "failure"
113
+ PENDING = "pending"
114
+ SUCCESS = "success"
115
+
116
+ def __str__(self) -> str:
117
+ """__str__"""
118
+ return self.value
119
+
120
+
121
+ class RelayProvider(Provider):
122
+ """Relay provider."""
123
+
124
+ def description(self) -> str:
125
+ """Get a human-readable description of the provider."""
126
+ return "Relay Protocol https://www.relay.link/"
127
+
128
+ def quote(self, provider_request: ProviderRequest) -> None:
129
+ """Update the request with the quote."""
130
+ self._validate(provider_request)
131
+
132
+ if provider_request.status not in (
133
+ ProviderRequestStatus.CREATED,
134
+ ProviderRequestStatus.QUOTE_DONE,
135
+ ProviderRequestStatus.QUOTE_FAILED,
136
+ ):
137
+ raise RuntimeError(
138
+ f"Cannot quote request {provider_request.id} with status {provider_request.status}."
139
+ )
140
+
141
+ if provider_request.execution_data:
142
+ raise RuntimeError(
143
+ f"Cannot quote request {provider_request.id}: execution already present."
144
+ )
145
+
146
+ from_chain = provider_request.params["from"]["chain"]
147
+ from_address = provider_request.params["from"]["address"]
148
+ from_token = provider_request.params["from"]["token"]
149
+ to_chain = provider_request.params["to"]["chain"]
150
+ to_address = provider_request.params["to"]["address"]
151
+ to_token = provider_request.params["to"]["token"]
152
+ to_amount = provider_request.params["to"]["amount"]
153
+
154
+ if to_amount == 0:
155
+ self.logger.info(f"[RELAY PROVIDER] {MESSAGE_QUOTE_ZERO}")
156
+ quote_data = QuoteData(
157
+ eta=0,
158
+ elapsed_time=0,
159
+ message=MESSAGE_QUOTE_ZERO,
160
+ provider_data=None,
161
+ timestamp=int(time.time()),
162
+ )
163
+ provider_request.quote_data = quote_data
164
+ provider_request.status = ProviderRequestStatus.QUOTE_DONE
165
+ return
166
+
167
+ url = "https://api.relay.link/quote"
168
+ headers = {"Content-Type": "application/json"}
169
+ payload = {
170
+ "originChainId": Chain(from_chain).id,
171
+ "user": from_address,
172
+ "originCurrency": from_token,
173
+ "destinationChainId": Chain(to_chain).id,
174
+ "recipient": to_address,
175
+ "destinationCurrency": to_token,
176
+ "amount": to_amount,
177
+ "tradeType": "EXACT_OUTPUT",
178
+ "enableTrueExactOutput": False,
179
+ }
180
+ for attempt in range(1, DEFAULT_MAX_QUOTE_RETRIES + 1):
181
+ start = time.time()
182
+ try:
183
+ self.logger.info(f"[RELAY PROVIDER] POST {url}")
184
+ self.logger.info(
185
+ f"[RELAY PROVIDER] BODY {json.dumps(payload, indent=2, sort_keys=True)}"
186
+ )
187
+ response = requests.post(
188
+ url=url, headers=headers, json=payload, timeout=30
189
+ )
190
+ response.raise_for_status()
191
+ response_json = response.json()
192
+
193
+ # Gas will be returned as 0 (unable to estimate) by the API endpoint when simulation fails.
194
+ # This happens when 'from_address'
195
+ # * does not have enough funds/ERC20,
196
+ # * requires to approve an ERC20 before another transaction.
197
+ # Call the API again using the default 'from_address' placeholder used by Relay DApp.
198
+ gas_missing = any(
199
+ "gas" not in item.get("data", {})
200
+ for step in response_json.get("steps", [])
201
+ for item in step.get("items", [])
202
+ )
203
+
204
+ if gas_missing:
205
+ placeholder_payload = payload.copy()
206
+ placeholder_payload["user"] = GAS_ESTIMATE_FALLBACK_ADDRESS
207
+ self.logger.info(f"[RELAY PROVIDER] POST {url}")
208
+ self.logger.info(
209
+ f"[RELAY PROVIDER] BODY {json.dumps(placeholder_payload, indent=2, sort_keys=True)}"
210
+ )
211
+ placeholder_response = requests.post(
212
+ url=url, headers=headers, json=placeholder_payload, timeout=30
213
+ )
214
+ response_json_placeholder = placeholder_response.json()
215
+
216
+ for i, step in enumerate(response_json.get("steps", [])):
217
+ for j, item in enumerate(step.get("items", [])):
218
+ if "gas" not in item.get("data", {}):
219
+ placeholder_gas = (
220
+ response_json_placeholder.get("steps", {i: {}})[i]
221
+ .get("items", {j: {}})[j]
222
+ .get("data", {})
223
+ .get("gas")
224
+ )
225
+ item["data"]["gas"] = (
226
+ placeholder_gas
227
+ or RELAY_DEFAULT_GAS[Chain(from_chain)][step["id"]]
228
+ )
229
+
230
+ quote_data = QuoteData(
231
+ eta=math.ceil(response_json["details"]["timeEstimate"]),
232
+ elapsed_time=time.time() - start,
233
+ message=None,
234
+ provider_data={
235
+ "attempts": attempt,
236
+ "response": response_json,
237
+ "response_status": response.status_code,
238
+ },
239
+ timestamp=int(time.time()),
240
+ )
241
+ provider_request.quote_data = quote_data
242
+ provider_request.status = ProviderRequestStatus.QUOTE_DONE
243
+ return
244
+ except requests.Timeout as e:
245
+ self.logger.warning(
246
+ f"[RELAY PROVIDER] Timeout request on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}."
247
+ )
248
+ quote_data = QuoteData(
249
+ eta=None,
250
+ elapsed_time=time.time() - start,
251
+ message=str(e),
252
+ provider_data={
253
+ "attempts": attempt,
254
+ "response": None,
255
+ "response_status": HTTPStatus.GATEWAY_TIMEOUT,
256
+ },
257
+ timestamp=int(time.time()),
258
+ )
259
+ except requests.RequestException as e:
260
+ self.logger.warning(
261
+ f"[RELAY PROVIDER] Request failed on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}."
262
+ )
263
+ response_json = response.json()
264
+ quote_data = QuoteData(
265
+ eta=None,
266
+ elapsed_time=time.time() - start,
267
+ message=response_json.get("message") or str(e),
268
+ provider_data={
269
+ "attempts": attempt,
270
+ "response": response_json,
271
+ "response_status": getattr(
272
+ response, "status_code", HTTPStatus.BAD_GATEWAY
273
+ ),
274
+ },
275
+ timestamp=int(time.time()),
276
+ )
277
+ except Exception as e: # pylint:disable=broad-except
278
+ self.logger.warning(
279
+ f"[RELAY PROVIDER] Request failed on attempt {attempt}/{DEFAULT_MAX_QUOTE_RETRIES}: {e}."
280
+ )
281
+ quote_data = QuoteData(
282
+ eta=None,
283
+ elapsed_time=time.time() - start,
284
+ message=str(e),
285
+ provider_data={
286
+ "attempts": attempt,
287
+ "response": None,
288
+ "response_status": HTTPStatus.INTERNAL_SERVER_ERROR,
289
+ },
290
+ timestamp=int(time.time()),
291
+ )
292
+ if attempt >= DEFAULT_MAX_QUOTE_RETRIES:
293
+ self.logger.error(
294
+ f"[RELAY PROVIDER] Request failed after {DEFAULT_MAX_QUOTE_RETRIES} attempts."
295
+ )
296
+ provider_request.quote_data = quote_data
297
+ provider_request.status = ProviderRequestStatus.QUOTE_FAILED
298
+ return
299
+
300
+ time.sleep(2)
301
+
302
+ def _get_txs(
303
+ self, provider_request: ProviderRequest, *args: t.Any, **kwargs: t.Any
304
+ ) -> t.List[t.Tuple[str, t.Dict]]:
305
+ """Get the sorted list of transactions to execute the quote."""
306
+
307
+ if provider_request.params["to"]["amount"] == 0:
308
+ return []
309
+
310
+ quote_data = provider_request.quote_data
311
+ if not quote_data:
312
+ raise RuntimeError(
313
+ f"Cannot get transaction builders {provider_request.id}: quote data not present."
314
+ )
315
+
316
+ provider_data = quote_data.provider_data
317
+ if not provider_data:
318
+ raise RuntimeError(
319
+ f"Cannot get transaction builders {provider_request.id}: provider data not present."
320
+ )
321
+
322
+ txs: t.List[t.Tuple[str, t.Dict]] = []
323
+
324
+ response = provider_data.get("response")
325
+ if not response:
326
+ return txs
327
+
328
+ steps = response.get("steps", [])
329
+ from_ledger_api = self._from_ledger_api(provider_request)
330
+
331
+ for step in steps:
332
+ for i, item in enumerate(step["items"]):
333
+ tx = item["data"].copy()
334
+ tx["to"] = from_ledger_api.api.to_checksum_address(tx["to"])
335
+ tx["value"] = int(tx.get("value", 0))
336
+ tx["gas"] = int(tx.get("gas", 1))
337
+ tx["maxFeePerGas"] = int(tx.get("maxFeePerGas", 0))
338
+ tx["maxPriorityFeePerGas"] = int(tx.get("maxPriorityFeePerGas", 0))
339
+ tx["nonce"] = from_ledger_api.api.eth.get_transaction_count(tx["from"])
340
+ Provider._update_with_gas_pricing(tx, from_ledger_api)
341
+ Provider._update_with_gas_estimate(tx, from_ledger_api)
342
+ txs.append((f"{step['id']}-{i}", tx))
343
+
344
+ return txs
345
+
346
+ def _update_execution_status(self, provider_request: ProviderRequest) -> None:
347
+ """Update the execution status."""
348
+
349
+ if provider_request.status not in (
350
+ ProviderRequestStatus.EXECUTION_PENDING,
351
+ ProviderRequestStatus.EXECUTION_UNKNOWN,
352
+ ):
353
+ return
354
+
355
+ execution_data = provider_request.execution_data
356
+ if not execution_data:
357
+ raise RuntimeError(
358
+ f"Cannot update request {provider_request.id}: execution data not present."
359
+ )
360
+
361
+ from_tx_hash = execution_data.from_tx_hash
362
+ if not from_tx_hash:
363
+ execution_data.message = (
364
+ f"{MESSAGE_EXECUTION_FAILED} missing transaction hash."
365
+ )
366
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
367
+ return
368
+
369
+ relay_status = RelayExecutionStatus.WAITING
370
+ url = "https://api.relay.link/requests/v2"
371
+ headers = {"accept": "application/json"}
372
+ params = {
373
+ "hash": from_tx_hash,
374
+ "sortBy": "createdAt",
375
+ }
376
+
377
+ try:
378
+ self.logger.info(f"[RELAY PROVIDER] GET {url}?{urlencode(params)}")
379
+ response = requests.get(url=url, headers=headers, params=params, timeout=30)
380
+ response_json = response.json()
381
+ relay_requests = response_json.get("requests")
382
+ if relay_requests:
383
+ relay_status = relay_requests[0].get(
384
+ "status", str(RelayExecutionStatus.WAITING)
385
+ )
386
+ execution_data.message = str(relay_status)
387
+ response.raise_for_status()
388
+ except Exception as e:
389
+ self.logger.error(
390
+ f"[RELAY PROVIDER] Failed to update status for request {provider_request.id}: {e}"
391
+ )
392
+
393
+ if relay_status == RelayExecutionStatus.SUCCESS:
394
+ self.logger.info(
395
+ f"[RELAY PROVIDER] Execution done for {provider_request.id}."
396
+ )
397
+ from_ledger_api = self._from_ledger_api(provider_request)
398
+ to_ledger_api = self._to_ledger_api(provider_request)
399
+ to_tx_hash = response_json["requests"][0]["data"]["outTxs"][0]["hash"]
400
+ execution_data.message = response_json.get("details", None)
401
+ execution_data.to_tx_hash = to_tx_hash
402
+ execution_data.elapsed_time = Provider._tx_timestamp(
403
+ to_tx_hash, to_ledger_api
404
+ ) - Provider._tx_timestamp(from_tx_hash, from_ledger_api)
405
+ provider_request.status = ProviderRequestStatus.EXECUTION_DONE
406
+ execution_data.provider_data = {
407
+ "response": response_json,
408
+ }
409
+ elif relay_status in (
410
+ RelayExecutionStatus.FAILURE,
411
+ RelayExecutionStatus.REFUND,
412
+ ):
413
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
414
+ elif relay_status in (
415
+ RelayExecutionStatus.PENDING,
416
+ RelayExecutionStatus.DELAYED,
417
+ ):
418
+ provider_request.status = ProviderRequestStatus.EXECUTION_PENDING
419
+ else:
420
+ provider_request.status = ProviderRequestStatus.EXECUTION_UNKNOWN
421
+
422
+ def _get_explorer_link(self, provider_request: ProviderRequest) -> t.Optional[str]:
423
+ """Get the explorer link for a transaction."""
424
+ if not provider_request.execution_data:
425
+ return None
426
+
427
+ quote_data = provider_request.quote_data
428
+ if not quote_data:
429
+ raise RuntimeError(
430
+ f"Cannot get explorer link for request {provider_request.id}: quote data not present."
431
+ )
432
+
433
+ provider_data = quote_data.provider_data
434
+ if not provider_data:
435
+ return None
436
+
437
+ steps = provider_data.get("response", {}).get("steps", [])
438
+ if not steps:
439
+ return None
440
+
441
+ request_id = steps[-1].get("requestId")
442
+ return f"https://relay.link/transaction/{request_id}"
operate/cli.py CHANGED
@@ -45,8 +45,15 @@ from uvicorn.server import Server
45
45
 
46
46
  from operate import services
47
47
  from operate.account.user import UserAccount
48
- from operate.bridge.bridge import BridgeManager
49
- from operate.constants import KEY, KEYS, OPERATE_HOME, SERVICES, ZERO_ADDRESS
48
+ from operate.bridge.bridge_manager import BridgeManager
49
+ from operate.constants import (
50
+ KEY,
51
+ KEYS,
52
+ MIN_PASSWORD_LENGTH,
53
+ OPERATE_HOME,
54
+ SERVICES,
55
+ ZERO_ADDRESS,
56
+ )
50
57
  from operate.ledger.profiles import (
51
58
  DEFAULT_MASTER_EOA_FUNDS,
52
59
  DEFAULT_NEW_SAFE_FUNDS,
@@ -56,6 +63,7 @@ from operate.migration import MigrationManager
56
63
  from operate.operate_types import Chain, DeploymentStatus, LedgerType
57
64
  from operate.quickstart.analyse_logs import analyse_logs
58
65
  from operate.quickstart.claim_staking_rewards import claim_staking_rewards
66
+ from operate.quickstart.reset_configs import reset_configs
59
67
  from operate.quickstart.reset_password import reset_password
60
68
  from operate.quickstart.reset_staking import reset_staking
61
69
  from operate.quickstart.run_service import run_service
@@ -406,13 +414,19 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
406
414
  if operate.user_account is not None:
407
415
  return JSONResponse(
408
416
  content={"error": "Account already exists"},
417
+ status_code=HTTPStatus.CONFLICT,
418
+ )
419
+
420
+ password = (await request.json()).get("password")
421
+ if not password or len(password) < MIN_PASSWORD_LENGTH:
422
+ return JSONResponse(
423
+ content={
424
+ "error": f"Password must be at least {MIN_PASSWORD_LENGTH} characters long."
425
+ },
409
426
  status_code=HTTPStatus.BAD_REQUEST,
410
427
  )
411
428
 
412
- data = await request.json()
413
- operate.create_user_account(
414
- password=data["password"],
415
- )
429
+ operate.create_user_account(password=password)
416
430
  return JSONResponse(content={"error": None})
417
431
 
418
432
  @app.put("/api/account")
@@ -424,7 +438,7 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
424
438
  if operate.user_account is None:
425
439
  return JSONResponse(
426
440
  content={"error": "Account does not exist."},
427
- status_code=HTTPStatus.BAD_REQUEST,
441
+ status_code=HTTPStatus.CONFLICT,
428
442
  )
429
443
 
430
444
  data = await request.json()
@@ -448,6 +462,14 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
448
462
  status_code=HTTPStatus.BAD_REQUEST,
449
463
  )
450
464
 
465
+ if not new_password or len(new_password) < MIN_PASSWORD_LENGTH:
466
+ return JSONResponse(
467
+ content={
468
+ "error": f"Password must be at least {MIN_PASSWORD_LENGTH} characters long."
469
+ },
470
+ status_code=HTTPStatus.BAD_REQUEST,
471
+ )
472
+
451
473
  try:
452
474
  if old_password:
453
475
  operate.update_password(old_password, new_password)
@@ -537,6 +559,35 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
537
559
  wallet, mnemonic = manager.create(ledger_type=ledger_type)
538
560
  return JSONResponse(content={"wallet": wallet.json, "mnemonic": mnemonic})
539
561
 
562
+ @app.post("/api/wallet/private_key")
563
+ @with_retries
564
+ async def _get_private_key(request: Request) -> t.List[t.Dict]:
565
+ """Get Master EOA private key."""
566
+ if operate.user_account is None:
567
+ return JSONResponse(
568
+ content={
569
+ "error": "Cannot retrieve private key; User account does not exist!"
570
+ },
571
+ status_code=HTTPStatus.BAD_REQUEST,
572
+ )
573
+
574
+ data = await request.json()
575
+ password = data.get("password")
576
+ error = None
577
+ if operate.password is None:
578
+ error = {"error": "You need to login before retrieving the private key"}
579
+ if operate.password != password:
580
+ error = {"error": "Password is not valid"}
581
+ if error is not None:
582
+ return JSONResponse(
583
+ content=error,
584
+ status_code=HTTPStatus.UNAUTHORIZED,
585
+ )
586
+
587
+ ledger_type = data.get("ledger_type", LedgerType.ETHEREUM.value)
588
+ wallet = operate.wallet_manager.load(ledger_type=LedgerType(ledger_type))
589
+ return JSONResponse(content={"private_key": wallet.crypto.private_key})
590
+
540
591
  @app.get("/api/extended/wallet")
541
592
  @with_retries
542
593
  async def _get_wallet_safe(request: Request) -> t.List[t.Dict]:
@@ -1145,6 +1196,20 @@ def qs_claim(
1145
1196
  claim_staking_rewards(operate=operate, config_path=config)
1146
1197
 
1147
1198
 
1199
+ @_operate.command(name="reset-configs")
1200
+ def qs_reset_configs(
1201
+ config: Annotated[str, params.String(help="Quickstart config file path")],
1202
+ attended: Annotated[
1203
+ str, params.String(help="Run in attended/unattended mode (default: true")
1204
+ ] = "true",
1205
+ ) -> None:
1206
+ """Reset configs."""
1207
+ os.environ["ATTENDED"] = attended.lower()
1208
+ operate = OperateApp()
1209
+ operate.setup()
1210
+ reset_configs(operate=operate, config_path=config)
1211
+
1212
+
1148
1213
  @_operate.command(name="reset-staking")
1149
1214
  def qs_reset_staking(
1150
1215
  config: Annotated[str, params.String(help="Quickstart config file path")],
operate/constants.py CHANGED
@@ -39,6 +39,7 @@ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
39
39
  ON_CHAIN_INTERACT_TIMEOUT = 120.0
40
40
  ON_CHAIN_INTERACT_RETRIES = 10
41
41
  ON_CHAIN_INTERACT_SLEEP = 3.0
42
+ MIN_PASSWORD_LENGTH = 8
42
43
 
43
44
  HEALTH_CHECK_URL = "http://127.0.0.1:8716/healthcheck" # possible DNS issues on windows so use IP address
44
45
  SAFE_WEBAPP_URL = "https://app.safe.global/home?safe=gno:"
@@ -19,6 +19,7 @@
19
19
 
20
20
  """Chain profiles."""
21
21
 
22
+ import enum
22
23
  import typing as t
23
24
 
24
25
  from operate.constants import ZERO_ADDRESS
@@ -154,7 +155,28 @@ STAKING: t.Dict[Chain, t.Dict[str, str]] = {
154
155
  },
155
156
  }
156
157
 
157
- DEFAULT_MECH_MARKETPLACE_PRIORITY_MECH = "0x552cEA7Bc33CbBEb9f1D90c1D11D2C6daefFd053"
158
+
159
+ class StakingProgramMechType(enum.Enum):
160
+ """Staking program mech type."""
161
+
162
+ LEGACY_MECH = "legacy_mech"
163
+ LEGACY_MECH_MARKETPLACE = "legacy_mech_marketplace"
164
+ MECH_MARKETPLACE = "mech_marketplace"
165
+
166
+ def __str__(self) -> str:
167
+ """Get the string representation of the enum."""
168
+ return self.value
169
+
170
+
171
+ DEFAULT_PRIORITY_MECH_ADDRESS = {
172
+ StakingProgramMechType.LEGACY_MECH: "0x77af31De935740567Cf4fF1986D04B2c964A786a",
173
+ StakingProgramMechType.LEGACY_MECH_MARKETPLACE: "0x552cEA7Bc33CbBEb9f1D90c1D11D2C6daefFd053",
174
+ StakingProgramMechType.MECH_MARKETPLACE: "0xC05e7412439bD7e91730a6880E18d5D5873F632C",
175
+ }
176
+ DEFAULT_PRIORITY_MECH_SERVICE_ID = {
177
+ StakingProgramMechType.LEGACY_MECH_MARKETPLACE: 975,
178
+ StakingProgramMechType.MECH_MARKETPLACE: 2182,
179
+ }
158
180
 
159
181
 
160
182
  # ERC20 token addresses
@@ -247,3 +269,17 @@ def get_staking_contract(
247
269
  staking_program_id,
248
270
  staking_program_id,
249
271
  )
272
+
273
+
274
+ def get_staking_program_mech_type(
275
+ staking_program_id: t.Optional[str],
276
+ ) -> StakingProgramMechType:
277
+ """Get the staking program mech type based on the staking program ID."""
278
+ if staking_program_id is None:
279
+ return StakingProgramMechType.LEGACY_MECH
280
+
281
+ if staking_program_id.startswith("quickstart_beta_mech_marketplace_expert"):
282
+ return StakingProgramMechType.MECH_MARKETPLACE
283
+ if "mech_marketplace" in staking_program_id:
284
+ return StakingProgramMechType.LEGACY_MECH_MARKETPLACE
285
+ return StakingProgramMechType.LEGACY_MECH