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,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
|