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,677 @@
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
+ """Native bridge provider."""
21
+
22
+
23
+ import logging
24
+ import time
25
+ import typing as t
26
+ from abc import ABC, abstractmethod
27
+
28
+ from aea.common import JSONLike
29
+ from aea.crypto.base import LedgerApi
30
+ from autonomy.chain.base import registry_contracts
31
+ from eth_typing import BlockIdentifier
32
+ from web3 import Web3
33
+
34
+ from operate.bridge.providers.provider import (
35
+ MESSAGE_EXECUTION_FAILED,
36
+ MESSAGE_EXECUTION_FAILED_ETA,
37
+ MESSAGE_EXECUTION_FAILED_REVERTED,
38
+ MESSAGE_QUOTE_ZERO,
39
+ Provider,
40
+ ProviderRequest,
41
+ ProviderRequestStatus,
42
+ QuoteData,
43
+ )
44
+ from operate.constants import ZERO_ADDRESS
45
+ from operate.data import DATA_DIR
46
+ from operate.data.contracts.foreign_omnibridge.contract import ForeignOmnibridge
47
+ from operate.data.contracts.home_omnibridge.contract import HomeOmnibridge
48
+ from operate.data.contracts.l1_standard_bridge.contract import (
49
+ DEFAULT_BRIDGE_MIN_GAS_LIMIT,
50
+ L1StandardBridge,
51
+ )
52
+ from operate.data.contracts.l2_standard_bridge.contract import L2StandardBridge
53
+ from operate.data.contracts.optimism_mintable_erc20.contract import (
54
+ OptimismMintableERC20,
55
+ )
56
+ from operate.ledger import (
57
+ get_default_ledger_api,
58
+ update_tx_with_gas_estimate,
59
+ update_tx_with_gas_pricing,
60
+ )
61
+ from operate.ledger.profiles import ERC20_TOKENS, EXPLORER_URL
62
+ from operate.operate_types import Chain
63
+ from operate.wallet.master import MasterWalletManager
64
+
65
+
66
+ BLOCK_CHUNK_SIZE = 5000
67
+
68
+
69
+ class BridgeContractAdaptor(ABC):
70
+ """Adaptor class for bridge contract packages."""
71
+
72
+ def __init__(
73
+ self,
74
+ from_chain: str,
75
+ from_bridge: str,
76
+ to_chain: str,
77
+ to_bridge: str,
78
+ bridge_eta: int,
79
+ ) -> None:
80
+ """Initialize the bridge contract adaptor."""
81
+ super().__init__()
82
+ if from_chain == to_chain:
83
+ raise ValueError("from_chain and to_chain cannot be the same.")
84
+ self.from_chain = from_chain
85
+ self.from_bridge = from_bridge
86
+ self.to_chain = to_chain
87
+ self.to_bridge = to_bridge
88
+ self.bridge_eta = bridge_eta
89
+
90
+ def can_handle_request(self, params: t.Dict) -> bool:
91
+ """Returns 'true' if the contract adaptor can handle a request for 'params'."""
92
+ from_chain = params["from"]["chain"]
93
+ from_token = Web3.to_checksum_address(params["from"]["token"])
94
+ to_chain = params["to"]["chain"]
95
+ to_token = Web3.to_checksum_address(params["to"]["token"])
96
+
97
+ if from_chain != self.from_chain:
98
+ return False
99
+
100
+ if to_chain != self.to_chain:
101
+ return False
102
+
103
+ if from_token == ZERO_ADDRESS and to_token == ZERO_ADDRESS:
104
+ return True
105
+
106
+ for token_map in ERC20_TOKENS.values():
107
+ if (
108
+ Chain(from_chain) in token_map
109
+ and Chain(to_chain) in token_map
110
+ and token_map[Chain(from_chain)].lower() == from_token.lower()
111
+ and token_map[Chain(to_chain)].lower() == to_token.lower()
112
+ ):
113
+ return True
114
+
115
+ return False
116
+
117
+ @abstractmethod
118
+ def build_bridge_tx(
119
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
120
+ ) -> JSONLike:
121
+ """Build bridge transaction."""
122
+ raise NotImplementedError()
123
+
124
+ @abstractmethod
125
+ def find_bridge_finalized_tx(
126
+ self,
127
+ from_ledger_api: LedgerApi,
128
+ to_ledger_api: LedgerApi,
129
+ provider_request: ProviderRequest,
130
+ from_block: BlockIdentifier,
131
+ to_block: BlockIdentifier,
132
+ ) -> t.Optional[str]:
133
+ """Return the transaction hash of the event indicating bridge completion."""
134
+ raise NotImplementedError()
135
+
136
+ @abstractmethod
137
+ def get_explorer_link(
138
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
139
+ ) -> t.Optional[str]:
140
+ """Get the explorer link for a transaction."""
141
+ raise NotImplementedError()
142
+
143
+
144
+ class OptimismContractAdaptor(BridgeContractAdaptor):
145
+ """Adaptor class for Optimism contract packages."""
146
+
147
+ _l1_standard_bridge_contract = t.cast(
148
+ L1StandardBridge,
149
+ L1StandardBridge.from_dir(
150
+ directory=str(DATA_DIR / "contracts" / "l1_standard_bridge"),
151
+ ),
152
+ )
153
+ _l2_standard_bridge_contract = t.cast(
154
+ L2StandardBridge,
155
+ L2StandardBridge.from_dir(
156
+ directory=str(DATA_DIR / "contracts" / "l2_standard_bridge"),
157
+ ),
158
+ )
159
+
160
+ _optimism_mintable_erc20_contract = t.cast(
161
+ OptimismMintableERC20,
162
+ OptimismMintableERC20.from_dir(
163
+ directory=str(DATA_DIR / "contracts" / "optimism_mintable_erc20"),
164
+ ),
165
+ )
166
+
167
+ def can_handle_request(self, params: t.Dict) -> bool:
168
+ """Returns 'true' if the contract adaptor can handle a request for 'params'."""
169
+
170
+ if not super().can_handle_request(params):
171
+ return False
172
+
173
+ from_chain = params["from"]["chain"]
174
+ from_token = Web3.to_checksum_address(params["from"]["token"])
175
+ from_ledger_api = get_default_ledger_api(Chain(from_chain))
176
+ to_chain = params["to"]["chain"]
177
+ to_token = Web3.to_checksum_address(params["to"]["token"])
178
+ to_ledger_api = get_default_ledger_api(Chain(to_chain))
179
+
180
+ if from_token == ZERO_ADDRESS and to_token == ZERO_ADDRESS:
181
+ if not self._l1_standard_bridge_contract.supports_bridge_eth_to(
182
+ ledger_api=from_ledger_api, contract_address=self.from_bridge
183
+ ):
184
+ return False
185
+
186
+ if to_token != ZERO_ADDRESS:
187
+ try:
188
+ l1_token = self._optimism_mintable_erc20_contract.l1_token(
189
+ ledger_api=to_ledger_api,
190
+ contract_address=to_token,
191
+ )["data"]
192
+
193
+ if l1_token != from_token:
194
+ return False
195
+ except Exception: # pylint: disable=broad-except
196
+ return False
197
+
198
+ return True
199
+
200
+ def build_bridge_tx(
201
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
202
+ ) -> JSONLike:
203
+ """Build bridge transaction."""
204
+ from_address = provider_request.params["from"]["address"]
205
+ from_token = provider_request.params["from"]["token"]
206
+ to_address = provider_request.params["to"]["address"]
207
+ to_token = provider_request.params["to"]["token"]
208
+ to_amount = provider_request.params["to"]["amount"]
209
+ from_bridge = self.from_bridge
210
+ extra_data = Web3.keccak(text=provider_request.id)
211
+
212
+ if from_token == ZERO_ADDRESS:
213
+ return self._l1_standard_bridge_contract.build_bridge_eth_to_tx(
214
+ ledger_api=from_ledger_api,
215
+ contract_address=from_bridge,
216
+ sender=from_address,
217
+ to=to_address,
218
+ amount=int(to_amount),
219
+ min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT,
220
+ extra_data=extra_data,
221
+ )
222
+
223
+ return self._l1_standard_bridge_contract.build_bridge_erc20_to_tx(
224
+ ledger_api=from_ledger_api,
225
+ contract_address=from_bridge,
226
+ sender=from_address,
227
+ local_token=from_token,
228
+ remote_token=to_token,
229
+ to=to_address,
230
+ amount=int(to_amount),
231
+ min_gas_limit=DEFAULT_BRIDGE_MIN_GAS_LIMIT,
232
+ extra_data=extra_data,
233
+ )
234
+
235
+ def find_bridge_finalized_tx(
236
+ self,
237
+ from_ledger_api: LedgerApi,
238
+ to_ledger_api: LedgerApi,
239
+ provider_request: ProviderRequest,
240
+ from_block: BlockIdentifier,
241
+ to_block: BlockIdentifier,
242
+ ) -> t.Optional[str]:
243
+ """Return the transaction hash of the event indicating bridge completion."""
244
+ from_address = provider_request.params["from"]["address"]
245
+ from_token = provider_request.params["from"]["token"]
246
+ to_address = provider_request.params["to"]["address"]
247
+ to_token = provider_request.params["to"]["token"]
248
+ to_amount = provider_request.params["to"]["amount"]
249
+ to_bridge = self.to_bridge
250
+ extra_data = Web3.keccak(text=provider_request.id)
251
+
252
+ if from_token == ZERO_ADDRESS:
253
+ return self._l2_standard_bridge_contract.find_eth_bridge_finalized_tx(
254
+ ledger_api=to_ledger_api,
255
+ contract_address=to_bridge,
256
+ from_=from_address,
257
+ to=to_address,
258
+ amount=to_amount,
259
+ extra_data=extra_data,
260
+ from_block=from_block,
261
+ to_block=to_block,
262
+ )
263
+
264
+ return self._l2_standard_bridge_contract.find_erc20_bridge_finalized_tx(
265
+ ledger_api=to_ledger_api,
266
+ contract_address=to_bridge,
267
+ local_token=to_token,
268
+ remote_token=from_token,
269
+ from_=from_address,
270
+ to=to_address,
271
+ amount=to_amount,
272
+ extra_data=extra_data,
273
+ from_block=from_block,
274
+ to_block=to_block,
275
+ )
276
+
277
+ def get_explorer_link(
278
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
279
+ ) -> t.Optional[str]:
280
+ """Get the explorer link for a transaction."""
281
+ if not provider_request.execution_data:
282
+ return None
283
+
284
+ tx_hash = provider_request.execution_data.from_tx_hash
285
+ if not tx_hash:
286
+ return None
287
+
288
+ chain = Chain(provider_request.params["from"]["chain"])
289
+ url = EXPLORER_URL[chain]["tx"]
290
+ return url.format(tx_hash=tx_hash)
291
+
292
+
293
+ class OmnibridgeContractAdaptor(BridgeContractAdaptor):
294
+ """Adaptor class for Omnibridge contract packages."""
295
+
296
+ _foreign_omnibridge = t.cast(
297
+ ForeignOmnibridge,
298
+ ForeignOmnibridge.from_dir(
299
+ directory=str(DATA_DIR / "contracts" / "foreign_omnibridge"),
300
+ ),
301
+ )
302
+
303
+ _home_omnibridge = t.cast(
304
+ HomeOmnibridge,
305
+ HomeOmnibridge.from_dir(
306
+ directory=str(DATA_DIR / "contracts" / "home_omnibridge"),
307
+ ),
308
+ )
309
+
310
+ def can_handle_request(self, params: t.Dict) -> bool:
311
+ """Returns 'true' if the contract adaptor can handle a request for 'params'."""
312
+ from_token = Web3.to_checksum_address(params["from"]["token"])
313
+ if from_token == ZERO_ADDRESS:
314
+ return False
315
+
316
+ return super().can_handle_request(params)
317
+
318
+ def build_bridge_tx(
319
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
320
+ ) -> JSONLike:
321
+ """Build bridge transaction."""
322
+ from_address = provider_request.params["from"]["address"]
323
+ from_token = provider_request.params["from"]["token"]
324
+ to_address = provider_request.params["to"]["address"]
325
+ to_amount = provider_request.params["to"]["amount"]
326
+ from_bridge = self.from_bridge
327
+
328
+ if from_token == ZERO_ADDRESS:
329
+ raise NotImplementedError(
330
+ f"{self.__class__.__name__} does not support bridge native tokens."
331
+ )
332
+
333
+ return self._foreign_omnibridge.build_relay_tokens_tx(
334
+ ledger_api=from_ledger_api,
335
+ contract_address=from_bridge,
336
+ sender=from_address,
337
+ token=from_token,
338
+ receiver=to_address,
339
+ amount=to_amount,
340
+ )
341
+
342
+ def find_bridge_finalized_tx(
343
+ self,
344
+ from_ledger_api: LedgerApi,
345
+ to_ledger_api: LedgerApi,
346
+ provider_request: ProviderRequest,
347
+ from_block: BlockIdentifier,
348
+ to_block: BlockIdentifier,
349
+ ) -> t.Optional[str]:
350
+ """Return the transaction hash of the event indicating bridge completion."""
351
+ from_token = provider_request.params["from"]["token"]
352
+ to_address = provider_request.params["to"]["address"]
353
+ to_token = provider_request.params["to"]["token"]
354
+ to_amount = provider_request.params["to"]["amount"]
355
+ to_bridge = self.to_bridge
356
+
357
+ if from_token == ZERO_ADDRESS:
358
+ raise NotImplementedError(
359
+ f"{self.__class__.__name__} does not support bridge native tokens."
360
+ )
361
+
362
+ message_id = self.get_message_id(
363
+ from_ledger_api=from_ledger_api,
364
+ provider_request=provider_request,
365
+ )
366
+
367
+ if not message_id:
368
+ raise RuntimeError(
369
+ f"Cannot find 'messageId' for request {provider_request.id}."
370
+ )
371
+
372
+ return self._home_omnibridge.find_tokens_bridged_tx(
373
+ ledger_api=to_ledger_api,
374
+ contract_address=to_bridge,
375
+ token=to_token,
376
+ recipient=to_address,
377
+ value=to_amount,
378
+ message_id=message_id,
379
+ from_block=from_block,
380
+ to_block=to_block,
381
+ )
382
+
383
+ def get_message_id(
384
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
385
+ ) -> t.Optional[str]:
386
+ """Get the bridge message id."""
387
+ if not provider_request.execution_data:
388
+ return None
389
+
390
+ if not provider_request.execution_data.from_tx_hash:
391
+ return None
392
+
393
+ if (
394
+ provider_request.execution_data.provider_data
395
+ and "message_id" in provider_request.execution_data.provider_data
396
+ ):
397
+ return provider_request.execution_data.provider_data.get("message_id", None)
398
+
399
+ from_address = provider_request.params["from"]["address"]
400
+ from_token = provider_request.params["from"]["token"]
401
+ from_tx_hash = provider_request.execution_data.from_tx_hash
402
+ to_amount = provider_request.params["to"]["amount"]
403
+ from_bridge = self.from_bridge
404
+
405
+ message_id = self._foreign_omnibridge.get_tokens_bridging_initiated_message_id(
406
+ ledger_api=from_ledger_api,
407
+ contract_address=from_bridge,
408
+ tx_hash=from_tx_hash,
409
+ token=from_token,
410
+ sender=from_address,
411
+ value=to_amount,
412
+ )
413
+
414
+ if not provider_request.execution_data.provider_data:
415
+ provider_request.execution_data.provider_data = {}
416
+
417
+ provider_request.execution_data.provider_data["message_id"] = message_id
418
+ return message_id
419
+
420
+ def get_explorer_link(
421
+ self, from_ledger_api: LedgerApi, provider_request: ProviderRequest
422
+ ) -> t.Optional[str]:
423
+ """Get the explorer link for a transaction."""
424
+ message_id = self.get_message_id(from_ledger_api, provider_request)
425
+ if not message_id:
426
+ return None
427
+ return (
428
+ f"https://bridge.gnosischain.com/bridge-explorer/transaction/{message_id}"
429
+ )
430
+
431
+
432
+ class NativeBridgeProvider(Provider):
433
+ """Native bridge provider"""
434
+
435
+ def __init__(
436
+ self,
437
+ bridge_contract_adaptor: BridgeContractAdaptor,
438
+ provider_id: str,
439
+ wallet_manager: MasterWalletManager,
440
+ logger: logging.Logger,
441
+ ) -> None:
442
+ """Initialize the provider."""
443
+ self.bridge_contract_adaptor = bridge_contract_adaptor
444
+ super().__init__(
445
+ wallet_manager=wallet_manager, provider_id=provider_id, logger=logger
446
+ )
447
+
448
+ def can_handle_request(self, params: t.Dict) -> bool:
449
+ """Returns 'true' if the provider can handle a request for 'params'."""
450
+ if not super().can_handle_request(params):
451
+ return False
452
+
453
+ if not self.bridge_contract_adaptor.can_handle_request(params):
454
+ return False
455
+
456
+ return True
457
+
458
+ def description(self) -> str:
459
+ """Get a human-readable description of the provider."""
460
+ return f"Native bridge provider ({self.bridge_contract_adaptor.__class__.__name__})."
461
+
462
+ def quote(self, provider_request: ProviderRequest) -> None:
463
+ """Update the request with the quote."""
464
+ self._validate(provider_request)
465
+
466
+ if provider_request.status not in (
467
+ ProviderRequestStatus.CREATED,
468
+ ProviderRequestStatus.QUOTE_DONE,
469
+ ProviderRequestStatus.QUOTE_FAILED,
470
+ ):
471
+ raise RuntimeError(
472
+ f"Cannot quote request {provider_request.id} with status {provider_request.status}."
473
+ )
474
+
475
+ if provider_request.execution_data:
476
+ raise RuntimeError(
477
+ f"Cannot quote request {provider_request.id}: execution already present."
478
+ )
479
+
480
+ to_amount = provider_request.params["to"]["amount"]
481
+ bridge_eta = self.bridge_contract_adaptor.bridge_eta
482
+
483
+ message = None
484
+ if to_amount == 0:
485
+ self.logger.info(f"[NATIVE BRIDGE PROVIDER] {MESSAGE_QUOTE_ZERO}")
486
+ bridge_eta = 0
487
+ message = MESSAGE_QUOTE_ZERO
488
+
489
+ quote_data = QuoteData(
490
+ eta=bridge_eta,
491
+ elapsed_time=0,
492
+ message=message,
493
+ provider_data=None,
494
+ timestamp=int(time.time()),
495
+ )
496
+ provider_request.quote_data = quote_data
497
+ provider_request.status = ProviderRequestStatus.QUOTE_DONE
498
+
499
+ def _get_approve_tx(self, provider_request: ProviderRequest) -> t.Optional[t.Dict]:
500
+ """Get the approve transaction."""
501
+ self.logger.info(
502
+ f"[NATIVE BRIDGE PROVIDER] Get appprove transaction for request {provider_request.id}."
503
+ )
504
+
505
+ if provider_request.params["to"]["amount"] == 0:
506
+ return None
507
+
508
+ quote_data = provider_request.quote_data
509
+ if not quote_data:
510
+ return None
511
+
512
+ from_address = provider_request.params["from"]["address"]
513
+ from_token = provider_request.params["from"]["token"]
514
+ to_amount = provider_request.params["to"]["amount"]
515
+ from_bridge = self.bridge_contract_adaptor.from_bridge
516
+ from_ledger_api = self._from_ledger_api(provider_request)
517
+
518
+ if from_token == ZERO_ADDRESS:
519
+ return None
520
+
521
+ approve_tx = registry_contracts.erc20.get_approve_tx(
522
+ ledger_api=from_ledger_api,
523
+ contract_address=from_token,
524
+ spender=from_bridge,
525
+ sender=from_address,
526
+ amount=to_amount,
527
+ )
528
+ approve_tx["gas"] = 200_000 # TODO backport to ERC20 contract as default
529
+ update_tx_with_gas_pricing(approve_tx, from_ledger_api)
530
+ update_tx_with_gas_estimate(approve_tx, from_ledger_api)
531
+ return approve_tx
532
+
533
+ def _get_bridge_tx(self, provider_request: ProviderRequest) -> t.Optional[t.Dict]:
534
+ """Get the bridge transaction."""
535
+ self.logger.info(
536
+ f"[NATIVE BRIDGE PROVIDER] Get bridge transaction for request {provider_request.id}."
537
+ )
538
+
539
+ if provider_request.params["to"]["amount"] == 0:
540
+ return None
541
+
542
+ quote_data = provider_request.quote_data
543
+ if not quote_data:
544
+ return None
545
+
546
+ from_ledger_api = self._from_ledger_api(provider_request)
547
+ bridge_tx = self.bridge_contract_adaptor.build_bridge_tx(
548
+ from_ledger_api=from_ledger_api, provider_request=provider_request
549
+ )
550
+
551
+ update_tx_with_gas_pricing(bridge_tx, from_ledger_api)
552
+ update_tx_with_gas_estimate(bridge_tx, from_ledger_api)
553
+ return bridge_tx
554
+
555
+ def _get_txs(
556
+ self, provider_request: ProviderRequest, *args: t.Any, **kwargs: t.Any
557
+ ) -> t.List[t.Tuple[str, t.Dict]]:
558
+ """Get the sorted list of transactions to execute the quote."""
559
+ txs = []
560
+ approve_tx = self._get_approve_tx(provider_request)
561
+ if approve_tx:
562
+ txs.append(("approve_tx", approve_tx))
563
+ bridge_tx = self._get_bridge_tx(provider_request)
564
+ if bridge_tx:
565
+ txs.append(("bridge_tx", bridge_tx))
566
+ return txs
567
+
568
+ def _update_execution_status(self, provider_request: ProviderRequest) -> None:
569
+ """Update the execution status."""
570
+
571
+ if provider_request.status != ProviderRequestStatus.EXECUTION_PENDING:
572
+ return
573
+
574
+ self.logger.info(
575
+ f"[NATIVE BRIDGE PROVIDER] Updating execution status for request {provider_request.id}."
576
+ )
577
+
578
+ execution_data = provider_request.execution_data
579
+ if not execution_data:
580
+ raise RuntimeError(
581
+ f"Cannot update {provider_request.id}: execution data not present."
582
+ )
583
+
584
+ from_tx_hash = execution_data.from_tx_hash
585
+ if not from_tx_hash:
586
+ execution_data.message = (
587
+ f"{MESSAGE_EXECUTION_FAILED} missing transaction hash."
588
+ )
589
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
590
+ return
591
+
592
+ bridge_eta = self.bridge_contract_adaptor.bridge_eta
593
+
594
+ try:
595
+ from_ledger_api = self._from_ledger_api(provider_request)
596
+ from_w3 = from_ledger_api.api
597
+
598
+ receipt = from_w3.eth.get_transaction_receipt(from_tx_hash)
599
+ if receipt.status == 0:
600
+ execution_data.message = MESSAGE_EXECUTION_FAILED_REVERTED
601
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
602
+ return
603
+
604
+ # Get the timestamp of the bridge_tx on the 'from' chain
605
+ bridge_tx_receipt = from_w3.eth.get_transaction_receipt(from_tx_hash)
606
+ bridge_tx_block = from_w3.eth.get_block(bridge_tx_receipt.blockNumber)
607
+ bridge_tx_ts = bridge_tx_block.timestamp
608
+
609
+ # Find the event on the 'to' chain
610
+ to_ledger_api = self._to_ledger_api(provider_request)
611
+ to_w3 = to_ledger_api.api
612
+ starting_block = self._find_block_before_timestamp(to_w3, bridge_tx_ts)
613
+ starting_block_ts = to_w3.eth.get_block(starting_block).timestamp
614
+ latest_block = to_w3.eth.block_number
615
+
616
+ for from_block in range(starting_block, latest_block + 1, BLOCK_CHUNK_SIZE):
617
+ to_block = min(from_block + BLOCK_CHUNK_SIZE - 1, latest_block)
618
+
619
+ to_tx_hash = self.bridge_contract_adaptor.find_bridge_finalized_tx(
620
+ from_ledger_api=from_ledger_api,
621
+ to_ledger_api=to_ledger_api,
622
+ provider_request=provider_request,
623
+ from_block=from_block,
624
+ to_block=to_block,
625
+ )
626
+
627
+ if to_tx_hash:
628
+ self.logger.info(
629
+ f"[NATIVE BRIDGE PROVIDER] Execution done for request {provider_request.id}."
630
+ )
631
+ execution_data.message = None
632
+ execution_data.to_tx_hash = to_tx_hash
633
+ execution_data.elapsed_time = Provider._tx_timestamp(
634
+ to_tx_hash, to_ledger_api
635
+ ) - Provider._tx_timestamp(from_tx_hash, from_ledger_api)
636
+ provider_request.status = ProviderRequestStatus.EXECUTION_DONE
637
+ return
638
+
639
+ last_block_ts = to_w3.eth.get_block(to_block).timestamp
640
+ if last_block_ts > starting_block_ts + bridge_eta * 2:
641
+ self.logger.info(
642
+ f"[NATIVE BRIDGE PROVIDER] Execution failed for request {provider_request.id}: bridge exceeds 2*ETA."
643
+ )
644
+ execution_data.message = MESSAGE_EXECUTION_FAILED_ETA
645
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
646
+ return
647
+
648
+ except Exception as e:
649
+ self.logger.error(f"Error updating execution status: {e}")
650
+ import traceback
651
+
652
+ traceback.print_exc()
653
+ execution_data.message = f"{MESSAGE_EXECUTION_FAILED} {str(e)}"
654
+ provider_request.status = ProviderRequestStatus.EXECUTION_FAILED
655
+
656
+ @staticmethod
657
+ def _find_block_before_timestamp(w3: Web3, timestamp: int) -> int:
658
+ """Returns the largest block number of the block before `timestamp`."""
659
+ latest = w3.eth.block_number
660
+ low, high = 0, latest
661
+ best = 0
662
+ while low <= high:
663
+ mid = (low + high) // 2
664
+ block = w3.eth.get_block(mid)
665
+ if block["timestamp"] < timestamp:
666
+ best = mid
667
+ low = mid + 1
668
+ else:
669
+ high = mid - 1
670
+ return best
671
+
672
+ def _get_explorer_link(self, provider_request: ProviderRequest) -> t.Optional[str]:
673
+ """Get the explorer link for a transaction."""
674
+ from_ledger_api = self._from_ledger_api(provider_request)
675
+ return self.bridge_contract_adaptor.get_explorer_link(
676
+ from_ledger_api=from_ledger_api, provider_request=provider_request
677
+ )