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,469 @@
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
+ """Provider."""
21
+
22
+
23
+ import copy
24
+ import enum
25
+ import logging
26
+ import time
27
+ import typing as t
28
+ import uuid
29
+ from abc import ABC, abstractmethod
30
+ from dataclasses import dataclass
31
+
32
+ from aea.crypto.base import LedgerApi
33
+ from autonomy.chain.tx import TxSettler
34
+ from web3 import Web3
35
+
36
+ from operate.constants import (
37
+ ON_CHAIN_INTERACT_RETRIES,
38
+ ON_CHAIN_INTERACT_SLEEP,
39
+ ON_CHAIN_INTERACT_TIMEOUT,
40
+ ZERO_ADDRESS,
41
+ )
42
+ from operate.ledger import get_default_ledger_api, update_tx_with_gas_pricing
43
+ from operate.operate_types import Chain, ChainAmounts
44
+ from operate.resource import LocalResource
45
+ from operate.wallet.master import MasterWalletManager
46
+
47
+
48
+ DEFAULT_MAX_QUOTE_RETRIES = 3
49
+ PROVIDER_REQUEST_PREFIX = "r-"
50
+ MESSAGE_QUOTE_ZERO = "Zero-amount quote requested."
51
+ MESSAGE_EXECUTION_SKIPPED = "Execution skipped."
52
+ MESSAGE_EXECUTION_FAILED = "Execution failed:"
53
+ MESSAGE_EXECUTION_FAILED_ETA = f"{MESSAGE_EXECUTION_FAILED} ETA exceeded."
54
+ MESSAGE_EXECUTION_FAILED_QUOTE_FAILED = f"{MESSAGE_EXECUTION_FAILED} quote failed."
55
+ MESSAGE_EXECUTION_FAILED_REVERTED = f"{MESSAGE_EXECUTION_FAILED} transaction reverted."
56
+ MESSAGE_EXECUTION_FAILED_SETTLEMENT = (
57
+ f"{MESSAGE_EXECUTION_FAILED} transaction settlement failed."
58
+ )
59
+ MESSAGE_REQUIREMENTS_QUOTE_FAILED = "Cannot compute requirements for failed quote."
60
+
61
+ ERC20_APPROVE_SELECTOR = "0x095ea7b3" # First 4 bytes of Web3.keccak(text='approve(address,uint256)').hex()[:10]
62
+
63
+
64
+ @dataclass
65
+ class QuoteData(LocalResource):
66
+ """QuoteData"""
67
+
68
+ eta: t.Optional[int]
69
+ elapsed_time: float
70
+ message: t.Optional[str]
71
+ timestamp: int
72
+ provider_data: t.Optional[t.Dict] # Provider-specific data
73
+
74
+
75
+ @dataclass
76
+ class ExecutionData(LocalResource):
77
+ """ExecutionData"""
78
+
79
+ elapsed_time: float
80
+ message: t.Optional[str]
81
+ timestamp: int
82
+ from_tx_hash: t.Optional[str]
83
+ to_tx_hash: t.Optional[str]
84
+ provider_data: t.Optional[t.Dict] # Provider-specific data
85
+
86
+
87
+ class ProviderRequestStatus(str, enum.Enum):
88
+ """ProviderRequestStatus"""
89
+
90
+ CREATED = "CREATED"
91
+ QUOTE_DONE = "QUOTE_DONE"
92
+ QUOTE_FAILED = "QUOTE_FAILED"
93
+ EXECUTION_PENDING = "EXECUTION_PENDING"
94
+ EXECUTION_DONE = "EXECUTION_DONE"
95
+ EXECUTION_FAILED = "EXECUTION_FAILED"
96
+ EXECUTION_UNKNOWN = "EXECUTION_UNKNOWN"
97
+
98
+ def __str__(self) -> str:
99
+ """__str__"""
100
+ return self.value
101
+
102
+
103
+ @dataclass
104
+ class ProviderRequest(LocalResource):
105
+ """ProviderRequest"""
106
+
107
+ params: t.Dict
108
+ provider_id: str
109
+ id: str
110
+ status: ProviderRequestStatus
111
+ quote_data: t.Optional[QuoteData]
112
+ execution_data: t.Optional[ExecutionData]
113
+
114
+
115
+ class Provider(ABC):
116
+ """(Abstract) Provider.
117
+
118
+ Expected usage:
119
+ params = {...}
120
+
121
+ 1. request = provider.create_request(params)
122
+ 2. provider.quote(request)
123
+ 3. provider.requirements(request)
124
+ 4. provider.execute(request)
125
+ 5. provider.status_json(request)
126
+
127
+ Derived classes must implement the following methods:
128
+ - description
129
+ - quote
130
+ - _update_execution_status
131
+ - _get_txs
132
+ - _get_explorer_link
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ wallet_manager: MasterWalletManager,
138
+ provider_id: str,
139
+ logger: logging.Logger,
140
+ ) -> None:
141
+ """Initialize the provider."""
142
+ self.wallet_manager = wallet_manager
143
+ self.provider_id = provider_id
144
+ self.logger = logger
145
+
146
+ def description(self) -> str:
147
+ """Get a human-readable description of the provider."""
148
+ return self.__class__.__name__
149
+
150
+ def _validate(self, provider_request: ProviderRequest) -> None:
151
+ """Validate that the request was created by this provider."""
152
+ if provider_request.provider_id != self.provider_id:
153
+ raise ValueError(
154
+ f"Request provider id {provider_request.provider_id} does not match the provider id {self.provider_id}"
155
+ )
156
+
157
+ def can_handle_request(self, params: t.Dict) -> bool:
158
+ """Returns 'true' if the provider can handle a request for 'params'."""
159
+
160
+ if "from" not in params or "to" not in params:
161
+ self.logger.error(
162
+ "[PROVIDER] Invalid input: All requests must contain exactly one 'from' and one 'to' sender."
163
+ )
164
+ return False
165
+
166
+ from_ = params["from"]
167
+ to = params["to"]
168
+
169
+ if (
170
+ not isinstance(from_, t.Dict)
171
+ or "chain" not in from_
172
+ or "address" not in from_
173
+ or "token" not in from_
174
+ ):
175
+ self.logger.error(
176
+ "[PROVIDER] Invalid input: 'from' must contain 'chain', 'address', and 'token'."
177
+ )
178
+ return False
179
+
180
+ if (
181
+ not isinstance(to, t.Dict)
182
+ or "chain" not in to
183
+ or "address" not in to
184
+ or "token" not in to
185
+ or "amount" not in to
186
+ ):
187
+ self.logger.error(
188
+ "[PROVIDER] Invalid input: 'to' must contain 'chain', 'address', 'token', and 'amount'."
189
+ )
190
+ return False
191
+
192
+ return True
193
+
194
+ def create_request(self, params: t.Dict) -> ProviderRequest:
195
+ """Create a request."""
196
+
197
+ if not self.can_handle_request(params):
198
+ raise ValueError("Invalid input: Cannot process request.")
199
+
200
+ w3 = Web3()
201
+ params = copy.deepcopy(params)
202
+ params["from"]["address"] = w3.to_checksum_address(params["from"]["address"])
203
+ params["from"]["token"] = w3.to_checksum_address(params["from"]["token"])
204
+ params["to"]["address"] = w3.to_checksum_address(params["to"]["address"])
205
+ params["to"]["token"] = w3.to_checksum_address(params["to"]["token"])
206
+ params["to"]["amount"] = int(params["to"]["amount"])
207
+
208
+ return ProviderRequest(
209
+ params=params,
210
+ provider_id=self.provider_id,
211
+ id=f"{PROVIDER_REQUEST_PREFIX}{uuid.uuid4()}",
212
+ quote_data=None,
213
+ execution_data=None,
214
+ status=ProviderRequestStatus.CREATED,
215
+ )
216
+
217
+ def _from_ledger_api(self, provider_request: ProviderRequest) -> LedgerApi:
218
+ """Get the from ledger api."""
219
+ from_chain = provider_request.params["from"]["chain"]
220
+ return get_default_ledger_api(Chain(from_chain))
221
+
222
+ def _to_ledger_api(self, provider_request: ProviderRequest) -> LedgerApi:
223
+ """Get the from ledger api."""
224
+ to_chain = provider_request.params["to"]["chain"]
225
+ return get_default_ledger_api(Chain(to_chain))
226
+
227
+ @abstractmethod
228
+ def quote(self, provider_request: ProviderRequest) -> None:
229
+ """Update the request with the quote."""
230
+ raise NotImplementedError()
231
+
232
+ @abstractmethod
233
+ def _get_txs(
234
+ self, provider_request: ProviderRequest, *args: t.Any, **kwargs: t.Any
235
+ ) -> t.List[t.Tuple[str, t.Dict]]:
236
+ """Get the sorted list of transactions to execute the quote."""
237
+ raise NotImplementedError()
238
+
239
+ def requirements(self, provider_request: ProviderRequest) -> ChainAmounts:
240
+ """Gets the requirements to execute the quote, with updated gas estimation."""
241
+ self.logger.info(f"[PROVIDER] Requirements for request {provider_request.id}.")
242
+
243
+ self._validate(provider_request)
244
+
245
+ from_chain = provider_request.params["from"]["chain"]
246
+ from_address = provider_request.params["from"]["address"]
247
+ from_token = provider_request.params["from"]["token"]
248
+ from_ledger_api = self._from_ledger_api(provider_request)
249
+
250
+ txs = self._get_txs(provider_request)
251
+
252
+ if not txs:
253
+ return ChainAmounts(
254
+ {
255
+ from_chain: {
256
+ from_address: {
257
+ ZERO_ADDRESS: 0,
258
+ from_token: 0,
259
+ }
260
+ }
261
+ }
262
+ )
263
+
264
+ total_native = 0
265
+ total_gas_fees = 0
266
+ total_token = 0
267
+
268
+ for tx_label, tx in txs:
269
+ self.logger.debug(
270
+ f"[PROVIDER] Processing transaction {tx_label} for request {provider_request.id}."
271
+ )
272
+ update_tx_with_gas_pricing(tx, from_ledger_api)
273
+ gas_key = "gasPrice" if "gasPrice" in tx else "maxFeePerGas"
274
+ gas_fees = tx.get(gas_key, 0) * tx["gas"]
275
+ tx_value = int(tx.get("value", 0))
276
+ total_gas_fees += gas_fees
277
+ total_native += tx_value + gas_fees
278
+
279
+ self.logger.debug(
280
+ f"[PROVIDER] Transaction {gas_key}={tx.get(gas_key, 0)} maxPriorityFeePerGas={tx.get('maxPriorityFeePerGas', -1)} gas={tx['gas']} {gas_fees=} {tx_value=}"
281
+ )
282
+ self.logger.debug(f"[PROVIDER] {from_ledger_api.api.eth.gas_price=}")
283
+ self.logger.debug(
284
+ f"[PROVIDER] {from_ledger_api.api.eth.get_block('latest').baseFeePerGas=}"
285
+ )
286
+
287
+ if tx.get("to", "").lower() == from_token.lower() and tx.get(
288
+ "data", ""
289
+ ).startswith(ERC20_APPROVE_SELECTOR):
290
+ try:
291
+ amount = int(tx["data"][-64:], 16)
292
+ total_token += amount
293
+ except Exception as e:
294
+ raise RuntimeError("Malformed ERC20 approve transaction.") from e
295
+
296
+ self.logger.info(
297
+ f"[PROVIDER] Total gas fees for request {provider_request.id}: {total_gas_fees} native units."
298
+ )
299
+
300
+ result = ChainAmounts(
301
+ {
302
+ from_chain: {
303
+ from_address: {
304
+ ZERO_ADDRESS: total_native,
305
+ }
306
+ }
307
+ }
308
+ )
309
+
310
+ if from_token != ZERO_ADDRESS:
311
+ result[from_chain][from_address][from_token] = total_token
312
+
313
+ return result
314
+
315
+ def execute(self, provider_request: ProviderRequest) -> None:
316
+ """Execute the request."""
317
+ self.logger.info(f"[PROVIDER] Executing request {provider_request.id}.")
318
+
319
+ self._validate(provider_request)
320
+
321
+ if provider_request.status in (ProviderRequestStatus.QUOTE_FAILED):
322
+ self.logger.info(f"[PROVIDER] {MESSAGE_EXECUTION_FAILED_QUOTE_FAILED}.")
323
+ execution_data = ExecutionData(
324
+ elapsed_time=0,
325
+ message=f"{MESSAGE_EXECUTION_FAILED_QUOTE_FAILED}",
326
+ timestamp=int(time.time()),
327
+ from_tx_hash=None,
328
+ to_tx_hash=None,
329
+ provider_data=None,
330
+ )
331
+ provider_request.execution_data = execution_data
332
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
333
+ return
334
+
335
+ if provider_request.status not in (ProviderRequestStatus.QUOTE_DONE,):
336
+ raise RuntimeError(
337
+ f"Cannot execute request {provider_request.id} with status {provider_request.status}."
338
+ )
339
+ if not provider_request.quote_data:
340
+ raise RuntimeError(
341
+ f"Cannot execute request {provider_request.id}: quote data not present."
342
+ )
343
+ if provider_request.execution_data:
344
+ raise RuntimeError(
345
+ f"Cannot execute request {provider_request.id}: execution data already present."
346
+ )
347
+
348
+ txs = self._get_txs(provider_request)
349
+
350
+ if not txs:
351
+ self.logger.info(
352
+ f"[PROVIDER] {MESSAGE_EXECUTION_SKIPPED} ({provider_request.status=})"
353
+ )
354
+ execution_data = ExecutionData(
355
+ elapsed_time=0,
356
+ message=f"{MESSAGE_EXECUTION_SKIPPED} ({provider_request.status=})",
357
+ timestamp=int(time.time()),
358
+ from_tx_hash=None,
359
+ to_tx_hash=None,
360
+ provider_data=None,
361
+ )
362
+ provider_request.execution_data = execution_data
363
+ provider_request.status = ProviderRequestStatus.EXECUTION_DONE
364
+ return
365
+
366
+ try:
367
+ self.logger.info(f"[PROVIDER] Executing request {provider_request.id}.")
368
+ timestamp = time.time()
369
+ chain = Chain(provider_request.params["from"]["chain"])
370
+ from_address = provider_request.params["from"]["address"]
371
+ wallet = self.wallet_manager.load(chain.ledger_type)
372
+ from_ledger_api = self._from_ledger_api(provider_request)
373
+ tx_settler = TxSettler(
374
+ ledger_api=from_ledger_api,
375
+ crypto=wallet.crypto,
376
+ chain_type=Chain(provider_request.params["from"]["chain"]),
377
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
378
+ retries=ON_CHAIN_INTERACT_RETRIES,
379
+ sleep=ON_CHAIN_INTERACT_SLEEP,
380
+ )
381
+ tx_hashes = []
382
+
383
+ for tx_label, tx in txs:
384
+ self.logger.info(f"[PROVIDER] Executing transaction {tx_label}.")
385
+ nonce = from_ledger_api.api.eth.get_transaction_count(from_address)
386
+ tx["nonce"] = nonce # TODO: backport to TxSettler
387
+ setattr( # noqa: B010
388
+ tx_settler, "build", lambda *args, **kwargs: tx # noqa: B023
389
+ )
390
+ tx_receipt = tx_settler.transact(
391
+ method=lambda: {},
392
+ contract="",
393
+ kwargs={},
394
+ dry_run=False,
395
+ )
396
+ self.logger.info(f"[PROVIDER] Transaction {tx_label} settled.")
397
+ tx_hashes.append(tx_receipt.get("transactionHash", "").hex())
398
+
399
+ execution_data = ExecutionData(
400
+ elapsed_time=time.time() - timestamp,
401
+ message=None,
402
+ timestamp=int(timestamp),
403
+ from_tx_hash=tx_hashes[-1],
404
+ to_tx_hash=None,
405
+ provider_data=None,
406
+ )
407
+ provider_request.execution_data = execution_data
408
+ if len(tx_hashes) == len(txs):
409
+ provider_request.status = ProviderRequestStatus.EXECUTION_PENDING
410
+ else:
411
+ provider_request.execution_data.message = (
412
+ MESSAGE_EXECUTION_FAILED_SETTLEMENT
413
+ )
414
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
415
+
416
+ except Exception as e: # pylint: disable=broad-except
417
+ self.logger.error(f"[PROVIDER] Error executing request: {e}")
418
+ execution_data = ExecutionData(
419
+ elapsed_time=time.time() - timestamp,
420
+ message=f"{MESSAGE_EXECUTION_FAILED} {str(e)}",
421
+ timestamp=int(time.time()),
422
+ from_tx_hash=None,
423
+ to_tx_hash=None,
424
+ provider_data=None,
425
+ )
426
+ provider_request.execution_data = execution_data
427
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
428
+
429
+ @abstractmethod
430
+ def _update_execution_status(self, provider_request: ProviderRequest) -> None:
431
+ """Update the execution status."""
432
+ raise NotImplementedError()
433
+
434
+ @abstractmethod
435
+ def _get_explorer_link(self, provider_request: ProviderRequest) -> t.Optional[str]:
436
+ """Get the explorer link for a transaction."""
437
+ raise NotImplementedError()
438
+
439
+ def status_json(self, provider_request: ProviderRequest) -> t.Dict:
440
+ """JSON representation of the status."""
441
+ self._validate(provider_request)
442
+
443
+ if provider_request.execution_data and provider_request.quote_data:
444
+ self._update_execution_status(provider_request)
445
+ tx_hash = None
446
+ if provider_request.execution_data.from_tx_hash:
447
+ tx_hash = provider_request.execution_data.from_tx_hash
448
+
449
+ return {
450
+ "eta": provider_request.quote_data.eta,
451
+ "explorer_link": self._get_explorer_link(provider_request),
452
+ "message": provider_request.execution_data.message,
453
+ "status": provider_request.status.value,
454
+ "tx_hash": tx_hash,
455
+ }
456
+ if provider_request.quote_data:
457
+ return {
458
+ "eta": provider_request.quote_data.eta,
459
+ "message": provider_request.quote_data.message,
460
+ "status": provider_request.status.value,
461
+ }
462
+
463
+ return {"message": None, "status": provider_request.status.value}
464
+
465
+ @staticmethod
466
+ def _tx_timestamp(tx_hash: str, ledger_api: LedgerApi) -> int:
467
+ receipt = ledger_api.api.eth.get_transaction_receipt(tx_hash)
468
+ block = ledger_api.api.eth.get_block(receipt.blockNumber)
469
+ return block.timestamp