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,470 @@
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
+ """Bridge manager."""
21
+
22
+
23
+ import json
24
+ import logging
25
+ import time
26
+ import typing as t
27
+ import uuid
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import cast
31
+
32
+ from deepdiff import DeepDiff
33
+ from web3 import Web3
34
+
35
+ from operate.bridge.providers.lifi_provider import LiFiProvider
36
+ from operate.bridge.providers.native_bridge_provider import (
37
+ NativeBridgeProvider,
38
+ OmnibridgeContractAdaptor,
39
+ OptimismContractAdaptor,
40
+ )
41
+ from operate.bridge.providers.provider import Provider, ProviderRequest
42
+ from operate.bridge.providers.relay_provider import RelayProvider
43
+ from operate.constants import ZERO_ADDRESS
44
+ from operate.ledger.profiles import USDC
45
+ from operate.operate_types import Chain, ChainAmounts
46
+ from operate.resource import LocalResource
47
+ from operate.services.manage import get_assets_balances
48
+ from operate.wallet.master import MasterWalletManager
49
+
50
+
51
+ DEFAULT_BUNDLE_VALIDITY_PERIOD = 3 * 60
52
+ EXECUTED_BUNDLES_PATH = "executed"
53
+ BRIDGE_REQUEST_BUNDLE_PREFIX = "rb-"
54
+
55
+ LIFI_PROVIDER_ID = "lifi-provider"
56
+ RELAY_PROVIDER_ID = "relay-provider"
57
+
58
+ NATIVE_BRIDGE_PROVIDER_CONFIGS: t.Dict[str, t.Any] = {
59
+ "native-ethereum-to-base": {
60
+ "from_chain": Chain.ETHEREUM.value,
61
+ "from_bridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35",
62
+ "to_chain": Chain.BASE.value,
63
+ "to_bridge": "0x4200000000000000000000000000000000000010",
64
+ "bridge_eta": 300,
65
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
66
+ },
67
+ "native-ethereum-to-celo": {
68
+ "from_chain": Chain.ETHEREUM.value,
69
+ "from_bridge": "0x9C4955b92F34148dbcfDCD82e9c9eCe5CF2badfe",
70
+ "to_chain": Chain.CELO.value,
71
+ "to_bridge": "0x4200000000000000000000000000000000000010",
72
+ "bridge_eta": 300,
73
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
74
+ },
75
+ "native-ethereum-to-gnosis": {
76
+ "from_chain": Chain.ETHEREUM.value,
77
+ "from_bridge": "0x88ad09518695c6c3712AC10a214bE5109a655671",
78
+ "to_chain": Chain.GNOSIS.value,
79
+ "to_bridge": "0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d",
80
+ "bridge_eta": 1800,
81
+ "bridge_contract_adaptor_class": OmnibridgeContractAdaptor,
82
+ },
83
+ "native-ethereum-to-mode": {
84
+ "from_chain": Chain.ETHEREUM.value,
85
+ "from_bridge": "0x735aDBbE72226BD52e818E7181953f42E3b0FF21",
86
+ "to_chain": Chain.MODE.value,
87
+ "to_bridge": "0x4200000000000000000000000000000000000010",
88
+ "bridge_eta": 300,
89
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
90
+ },
91
+ "native-ethereum-to-optimism": {
92
+ "from_chain": Chain.ETHEREUM.value,
93
+ "from_bridge": "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1",
94
+ "to_chain": Chain.OPTIMISM.value,
95
+ "to_bridge": "0x4200000000000000000000000000000000000010",
96
+ "bridge_eta": 300,
97
+ "bridge_contract_adaptor_class": OptimismContractAdaptor,
98
+ },
99
+ }
100
+
101
+ # Routes are defined as the tuples (from_chain, from_token, to_chain, to_token)
102
+ PREFERRED_ROUTES = {
103
+ (
104
+ Chain.ETHEREUM,
105
+ USDC[Chain.ETHEREUM],
106
+ Chain.OPTIMISM,
107
+ USDC[Chain.OPTIMISM],
108
+ ): LIFI_PROVIDER_ID,
109
+ (
110
+ Chain.ETHEREUM,
111
+ USDC[Chain.ETHEREUM],
112
+ Chain.BASE,
113
+ USDC[Chain.BASE],
114
+ ): LIFI_PROVIDER_ID,
115
+ (Chain.ETHEREUM, ZERO_ADDRESS, Chain.GNOSIS, ZERO_ADDRESS): RELAY_PROVIDER_ID,
116
+ }
117
+
118
+
119
+ @dataclass
120
+ class ProviderRequestBundle(LocalResource):
121
+ """ProviderRequestBundle"""
122
+
123
+ requests_params: t.List[t.Dict]
124
+ provider_requests: t.List[ProviderRequest]
125
+ timestamp: int
126
+ id: str
127
+
128
+ def get_from_chains(self) -> set[Chain]:
129
+ """Get 'from' chains."""
130
+ return {
131
+ Chain(request.params["from"]["chain"]) for request in self.provider_requests
132
+ }
133
+
134
+ def get_from_addresses(self, chain: Chain) -> set[str]:
135
+ """Get 'from' addresses."""
136
+ chain_str = chain.value
137
+ return {
138
+ request.params["from"]["address"]
139
+ for request in self.provider_requests
140
+ if request.params["from"]["chain"] == chain_str
141
+ }
142
+
143
+ def get_from_tokens(self, chain: Chain) -> set[str]:
144
+ """Get 'from' tokens."""
145
+ chain_str = chain.value
146
+ return {
147
+ request.params["from"]["token"]
148
+ for request in self.provider_requests
149
+ if request.params["from"]["chain"] == chain_str
150
+ }
151
+
152
+
153
+ @dataclass
154
+ class BridgeManagerData(LocalResource):
155
+ """BridgeManagerData"""
156
+
157
+ path: Path
158
+ version: int = 1
159
+ last_requested_bundle: t.Optional[ProviderRequestBundle] = None
160
+ last_executed_bundle_id: t.Optional[str] = None
161
+
162
+ _file = "bridge.json"
163
+
164
+ # TODO Migrate to LocalResource?
165
+ # It can be inconvenient that all local resources create an empty resource
166
+ # if the file is corrupted. For example, if a service configuration is
167
+ # corrupted, we might want to halt execution, because otherwise, the application
168
+ # could continue as if the user is creating a service from scratch.
169
+ # For the bridge manager data, it's harmless, because its memory
170
+ # is limited to the process of getting and executing a quote.
171
+ @classmethod # Overrides from LocalResource
172
+ def load(cls, path: Path) -> "LocalResource":
173
+ """Load local resource."""
174
+
175
+ file = path / cls._file
176
+ if not file.exists():
177
+ BridgeManagerData(path=path).store()
178
+
179
+ try:
180
+ super().load(path=file)
181
+ except (json.JSONDecodeError, KeyError):
182
+ new_file = path / f"invalid_{int(time.time())}_{cls._file}"
183
+ file.rename(new_file)
184
+ BridgeManagerData(path=path).store()
185
+
186
+ return super().load(path)
187
+
188
+
189
+ class BridgeManager:
190
+ """BridgeManager"""
191
+
192
+ def __init__(
193
+ self,
194
+ path: Path,
195
+ wallet_manager: MasterWalletManager,
196
+ logger: logging.Logger,
197
+ bundle_validity_period: int = DEFAULT_BUNDLE_VALIDITY_PERIOD,
198
+ ) -> None:
199
+ """Initialize bridge manager."""
200
+ self.path = path
201
+ self.wallet_manager = wallet_manager
202
+ self.logger = logger
203
+ self.bundle_validity_period = bundle_validity_period
204
+ self.path.mkdir(exist_ok=True)
205
+ (self.path / EXECUTED_BUNDLES_PATH).mkdir(exist_ok=True)
206
+ self.data: BridgeManagerData = cast(
207
+ BridgeManagerData, BridgeManagerData.load(path)
208
+ )
209
+ self._native_bridge_providers = {
210
+ provider_id: NativeBridgeProvider(
211
+ config["bridge_contract_adaptor_class"](
212
+ from_chain=config["from_chain"],
213
+ to_chain=config["to_chain"],
214
+ from_bridge=config["from_bridge"],
215
+ to_bridge=config["to_bridge"],
216
+ bridge_eta=config["bridge_eta"],
217
+ ),
218
+ provider_id,
219
+ wallet_manager,
220
+ logger,
221
+ )
222
+ for provider_id, config in NATIVE_BRIDGE_PROVIDER_CONFIGS.items()
223
+ }
224
+
225
+ self._providers: t.Dict[str, Provider] = {}
226
+ self._providers.update(self._native_bridge_providers)
227
+ self._providers[LIFI_PROVIDER_ID] = LiFiProvider(
228
+ provider_id=LIFI_PROVIDER_ID,
229
+ wallet_manager=wallet_manager,
230
+ logger=logger,
231
+ )
232
+ self._providers[RELAY_PROVIDER_ID] = RelayProvider(
233
+ provider_id=RELAY_PROVIDER_ID,
234
+ wallet_manager=wallet_manager,
235
+ logger=logger,
236
+ )
237
+
238
+ def _store_data(self) -> None:
239
+ self.logger.info("[BRIDGE MANAGER] Storing data to file.")
240
+ self.data.store()
241
+
242
+ def _get_updated_bundle(
243
+ self, requests_params: t.List[t.Dict], force_update: bool
244
+ ) -> ProviderRequestBundle:
245
+ """Ensures to return a valid (non expired) bundle for the given inputs."""
246
+
247
+ now = int(time.time())
248
+ bundle = self.data.last_requested_bundle
249
+ create_new_bundle = False
250
+
251
+ if not bundle:
252
+ self.logger.info("[BRIDGE MANAGER] No last bundle.")
253
+ create_new_bundle = True
254
+ elif DeepDiff(requests_params, bundle.requests_params):
255
+ self.logger.info("[BRIDGE MANAGER] Different requests params.")
256
+ create_new_bundle = True
257
+ elif force_update:
258
+ self.logger.info("[BRIDGE MANAGER] Force bundle update.")
259
+ self.quote_bundle(bundle)
260
+ self._store_data()
261
+ elif now > bundle.timestamp + self.bundle_validity_period:
262
+ self.logger.info("[BRIDGE MANAGER] Bundle expired.")
263
+ self.quote_bundle(bundle)
264
+ self._store_data()
265
+
266
+ if not bundle or create_new_bundle:
267
+ self.logger.info("[BRIDGE MANAGER] Creating new bridge request bundle.")
268
+
269
+ provider_requests = []
270
+ for params in requests_params:
271
+ route = (
272
+ Chain(params["from"]["chain"]),
273
+ params["from"]["token"],
274
+ Chain(params["to"]["chain"]),
275
+ params["to"]["token"],
276
+ )
277
+ provider_id = PREFERRED_ROUTES.get(route)
278
+
279
+ if not provider_id:
280
+ for provider in self._native_bridge_providers.values():
281
+ if provider.can_handle_request(params):
282
+ provider_id = provider.provider_id
283
+ break
284
+
285
+ if not provider_id:
286
+ provider_id = RELAY_PROVIDER_ID
287
+
288
+ provider_requests.append(
289
+ self._providers[provider_id].create_request(params=params)
290
+ )
291
+
292
+ bundle = ProviderRequestBundle(
293
+ id=f"{BRIDGE_REQUEST_BUNDLE_PREFIX}{uuid.uuid4()}",
294
+ requests_params=requests_params,
295
+ provider_requests=provider_requests,
296
+ timestamp=now,
297
+ )
298
+
299
+ self.data.last_requested_bundle = bundle
300
+ self.quote_bundle(bundle)
301
+ self._store_data()
302
+
303
+ return bundle
304
+
305
+ def _sanitize(self, requests_params: t.List) -> None:
306
+ """Sanitize quote requests."""
307
+ w3 = Web3()
308
+ for params in requests_params:
309
+ params["from"]["address"] = w3.to_checksum_address(
310
+ params["from"]["address"]
311
+ )
312
+ params["from"]["token"] = w3.to_checksum_address(params["from"]["token"])
313
+ params["to"]["address"] = w3.to_checksum_address(params["to"]["address"])
314
+ params["to"]["token"] = w3.to_checksum_address(params["to"]["token"])
315
+ params["to"]["amount"] = int(params["to"]["amount"])
316
+
317
+ def _raise_if_invalid(self, requests_params: t.List) -> None:
318
+ """Preprocess quote requests."""
319
+
320
+ for params in requests_params:
321
+ from_chain = params["from"]["chain"]
322
+ from_address = params["from"]["address"]
323
+
324
+ wallet = self.wallet_manager.load(Chain(from_chain).ledger_type)
325
+ wallet_address = wallet.address
326
+ safe_address = wallet.safes.get(Chain(from_chain))
327
+
328
+ if from_address is None or not (
329
+ from_address == wallet_address or from_address == safe_address
330
+ ):
331
+ raise ValueError(
332
+ f"Invalid input: 'from' address {from_address} does not match Master EOA nor Master Safe on chain {Chain(from_chain).name}."
333
+ )
334
+
335
+ def bridge_refill_requirements(
336
+ self, requests_params: t.List[t.Dict], force_update: bool = False
337
+ ) -> t.Dict:
338
+ """Get bridge refill requirements."""
339
+ self._sanitize(requests_params)
340
+ self._raise_if_invalid(requests_params)
341
+ self.logger.info(
342
+ f"[BRIDGE MANAGER] Quote requests count: {len(requests_params)}."
343
+ )
344
+
345
+ bundle = self._get_updated_bundle(requests_params, force_update)
346
+
347
+ balances = ChainAmounts()
348
+ for chain in bundle.get_from_chains():
349
+ ledger_api = self.wallet_manager.load(chain.ledger_type).ledger_api(chain)
350
+ balances[chain.value] = get_assets_balances(
351
+ ledger_api=ledger_api,
352
+ asset_addresses={ZERO_ADDRESS} | bundle.get_from_tokens(chain),
353
+ addresses=bundle.get_from_addresses(chain),
354
+ )
355
+
356
+ bridge_total_requirements = self.bridge_total_requirements(bundle)
357
+
358
+ bridge_refill_requirements = ChainAmounts.shortfalls(
359
+ bridge_total_requirements, balances
360
+ )
361
+
362
+ is_refill_required = any(
363
+ amount > 0
364
+ for from_addresses in bridge_refill_requirements.values()
365
+ for from_tokens in from_addresses.values()
366
+ for amount in from_tokens.values()
367
+ )
368
+
369
+ status_json = self.get_status_json(bundle.id)
370
+ status_json.update(
371
+ {
372
+ "balances": balances,
373
+ "bridge_refill_requirements": bridge_refill_requirements,
374
+ "bridge_total_requirements": bridge_total_requirements,
375
+ "expiration_timestamp": bundle.timestamp + self.bundle_validity_period,
376
+ "is_refill_required": is_refill_required,
377
+ }
378
+ )
379
+ return status_json
380
+
381
+ def execute_bundle(self, bundle_id: str) -> t.Dict:
382
+ """Execute the bundle"""
383
+
384
+ bundle = self.data.last_requested_bundle
385
+
386
+ if not bundle:
387
+ raise RuntimeError("[BRIDGE MANAGER] No bundle.")
388
+
389
+ if bundle.id != bundle_id:
390
+ raise RuntimeError(
391
+ f"Quote bundle id {bundle_id} does not match last requested bundle id {bundle.id}."
392
+ )
393
+
394
+ requirements = self.bridge_refill_requirements(bundle.requests_params)
395
+ self.data.last_requested_bundle = None
396
+ self.data.last_executed_bundle_id = bundle_id
397
+ bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle.id}.json"
398
+ bundle.path = bundle_path
399
+ self._store_data()
400
+ bundle.store()
401
+
402
+ if requirements["is_refill_required"]:
403
+ self.logger.warning(
404
+ f"[BRIDGE MANAGER] Refill requirements not satisfied for bundle id {bundle_id}."
405
+ )
406
+
407
+ self.logger.info("[BRIDGE MANAGER] Executing quotes.")
408
+
409
+ for request in bundle.provider_requests:
410
+ provider = self._providers[request.provider_id]
411
+ provider.execute(request)
412
+ self._store_data()
413
+
414
+ self._store_data()
415
+ bundle.store()
416
+ self.logger.info(f"[BRIDGE MANAGER] Bundle id {bundle_id} executed.")
417
+ return self.get_status_json(bundle_id)
418
+
419
+ def get_status_json(self, bundle_id: str) -> t.Dict:
420
+ """Get execution status of bundle."""
421
+ bundle = self.data.last_requested_bundle
422
+
423
+ if bundle is not None and bundle.id == bundle_id:
424
+ pass
425
+ else:
426
+ bundle_path = self.path / EXECUTED_BUNDLES_PATH / f"{bundle_id}.json"
427
+ if bundle_path.exists():
428
+ bundle = cast(
429
+ ProviderRequestBundle, ProviderRequestBundle.load(bundle_path)
430
+ )
431
+ bundle.path = bundle_path # TODO backport to resource.py ?
432
+ else:
433
+ raise FileNotFoundError(f"Bundle with ID {bundle_id} does not exist.")
434
+
435
+ initial_status = [request.status for request in bundle.provider_requests]
436
+
437
+ provider_request_status = []
438
+ for request in bundle.provider_requests:
439
+ provider = self._providers[request.provider_id]
440
+ provider_request_status.append(provider.status_json(request))
441
+
442
+ updated_status = [request.status for request in bundle.provider_requests]
443
+
444
+ if initial_status != updated_status and bundle.path is not None:
445
+ bundle.store()
446
+
447
+ return {
448
+ "id": bundle.id,
449
+ "bridge_request_status": provider_request_status,
450
+ }
451
+
452
+ def bridge_total_requirements(self, bundle: ProviderRequestBundle) -> ChainAmounts:
453
+ """Sum bridge requirements."""
454
+ requirements = []
455
+ for provider_request in bundle.provider_requests:
456
+ provider = self._providers[provider_request.provider_id]
457
+ requirements.append(provider.requirements(provider_request))
458
+
459
+ return ChainAmounts.add(*requirements)
460
+
461
+ def quote_bundle(self, bundle: ProviderRequestBundle) -> None:
462
+ """Update the bundle with the quotes."""
463
+ for provider_request in bundle.provider_requests:
464
+ provider = self._providers[provider_request.provider_id]
465
+ provider.quote(provider_request)
466
+ bundle.timestamp = int(time.time())
467
+
468
+ def last_executed_bundle_id(self) -> t.Optional[str]:
469
+ """Get the last executed bundle id."""
470
+ return self.data.last_executed_bundle_id