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
operate/utils/gnosis.py CHANGED
@@ -20,25 +20,33 @@
20
20
  """Safe helpers."""
21
21
 
22
22
  import binascii
23
+ import itertools
23
24
  import secrets
25
+ import time
24
26
  import typing as t
25
27
  from enum import Enum
26
28
 
27
29
  from aea.crypto.base import Crypto, LedgerApi
30
+ from aea.helpers.logging import setup_logger
28
31
  from autonomy.chain.base import registry_contracts
29
32
  from autonomy.chain.config import ChainType as ChainProfile
33
+ from autonomy.chain.exceptions import ChainInteractionError
30
34
  from autonomy.chain.tx import TxSettler
35
+ from web3 import Web3
31
36
 
32
37
  from operate.constants import (
33
38
  ON_CHAIN_INTERACT_RETRIES,
34
39
  ON_CHAIN_INTERACT_SLEEP,
35
40
  ON_CHAIN_INTERACT_TIMEOUT,
41
+ ZERO_ADDRESS,
36
42
  )
43
+ from operate.ledger import get_default_ledger_api
44
+ from operate.operate_types import Chain
37
45
 
38
46
 
39
- NULL_ADDRESS: str = "0x" + "0" * 40
47
+ logger = setup_logger(name="operate.utils.gnosis")
40
48
  MAX_UINT256 = 2**256 - 1
41
- ZERO_ETH = 0
49
+ SENTINEL_OWNERS = "0x0000000000000000000000000000000000000001"
42
50
 
43
51
 
44
52
  class SafeOperation(Enum):
@@ -65,8 +73,8 @@ def hash_payload_to_hex( # pylint: disable=too-many-arguments,too-many-locals
65
73
  operation: int = SafeOperation.CALL.value,
66
74
  base_gas: int = 0,
67
75
  safe_gas_price: int = 0,
68
- gas_token: str = NULL_ADDRESS,
69
- refund_receiver: str = NULL_ADDRESS,
76
+ gas_token: str = ZERO_ADDRESS,
77
+ refund_receiver: str = ZERO_ADDRESS,
70
78
  use_flashbots: bool = False,
71
79
  gas_limit: int = 0,
72
80
  raise_on_failed_simulation: bool = False,
@@ -156,9 +164,9 @@ def _get_nonce() -> int:
156
164
  def create_safe(
157
165
  ledger_api: LedgerApi,
158
166
  crypto: Crypto,
159
- owner: t.Optional[str] = None,
167
+ backup_owner: t.Optional[str] = None,
160
168
  salt_nonce: t.Optional[int] = None,
161
- ) -> t.Tuple[str, int]:
169
+ ) -> t.Tuple[str, int, str]:
162
170
  """Create gnosis safe."""
163
171
  salt_nonce = salt_nonce or _get_nonce()
164
172
 
@@ -168,7 +176,7 @@ def create_safe(
168
176
  tx = registry_contracts.gnosis_safe.get_deploy_transaction(
169
177
  ledger_api=ledger_api,
170
178
  deployer_address=crypto.address,
171
- owners=[crypto.address] if owner is None else [crypto.address, owner],
179
+ owners=([crypto.address]),
172
180
  threshold=1,
173
181
  salt_nonce=salt_nonce,
174
182
  )
@@ -193,12 +201,37 @@ def create_safe(
193
201
  contract="",
194
202
  kwargs={},
195
203
  )
204
+ tx_hash = receipt.get("transactionHash", "").hex()
196
205
  instance = registry_contracts.gnosis_safe_proxy_factory.get_instance(
197
206
  ledger_api=ledger_api,
198
207
  contract_address="0xa6b71e26c5e0845f74c812102ca7114b6a896ab2",
199
208
  )
200
209
  (event,) = instance.events.ProxyCreation().process_receipt(receipt)
201
- return event["args"]["proxy"], salt_nonce
210
+ safe_address = event["args"]["proxy"]
211
+
212
+ if backup_owner is not None:
213
+ retry_delays = [0, 60, 120, 180, 240]
214
+ for attempt in range(1, len(retry_delays) + 1):
215
+ try:
216
+ add_owner(
217
+ ledger_api=ledger_api,
218
+ crypto=crypto,
219
+ safe=safe_address,
220
+ owner=backup_owner,
221
+ )
222
+ break # success
223
+ except Exception as e: # pylint: disable=broad-except
224
+ if attempt == len(retry_delays):
225
+ raise RuntimeError(
226
+ f"Failed to add backup owner {backup_owner} after {len(retry_delays)} attempts: {e}"
227
+ ) from e
228
+ next_delay = retry_delays[attempt]
229
+ logger.error(
230
+ f"Retry add owner {attempt}/{len(retry_delays)} in {next_delay} seconds due to error: {e}"
231
+ )
232
+ time.sleep(next_delay)
233
+
234
+ return safe_address, salt_nonce, tx_hash
202
235
 
203
236
 
204
237
  def get_owners(ledger_api: LedgerApi, safe: str) -> t.List[str]:
@@ -214,49 +247,68 @@ def send_safe_txs(
214
247
  safe: str,
215
248
  ledger_api: LedgerApi,
216
249
  crypto: Crypto,
217
- ) -> None:
250
+ to: t.Optional[str] = None,
251
+ ) -> t.Optional[str]:
218
252
  """Send internal safe transaction."""
219
253
  owner = ledger_api.api.to_checksum_address(
220
254
  crypto.address,
221
255
  )
222
- safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
223
- ledger_api=ledger_api,
224
- contract_address=safe,
225
- value=0,
226
- safe_tx_gas=0,
227
- to_address=safe,
228
- data=txd,
229
- operation=SafeOperation.CALL.value,
230
- ).get("tx_hash")
231
- safe_tx_bytes = binascii.unhexlify(
232
- safe_tx_hash[2:],
233
- )
234
- signatures = {
235
- owner: crypto.sign_message(
236
- message=safe_tx_bytes,
237
- is_deprecated_mode=True,
238
- )[2:]
239
- }
240
- transaction = registry_contracts.gnosis_safe.get_raw_safe_transaction(
256
+ to_address = to or safe
257
+
258
+ def _build_tx( # pylint: disable=unused-argument
259
+ *args: t.Any, **kwargs: t.Any
260
+ ) -> t.Optional[str]:
261
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
262
+ ledger_api=ledger_api,
263
+ contract_address=safe,
264
+ value=0,
265
+ safe_tx_gas=0,
266
+ to_address=to_address,
267
+ data=txd,
268
+ operation=SafeOperation.CALL.value,
269
+ ).get("tx_hash")
270
+ safe_tx_bytes = binascii.unhexlify(
271
+ safe_tx_hash[2:],
272
+ )
273
+ signatures = {
274
+ owner: crypto.sign_message(
275
+ message=safe_tx_bytes,
276
+ is_deprecated_mode=True,
277
+ )[2:]
278
+ }
279
+ return registry_contracts.gnosis_safe.get_raw_safe_transaction(
280
+ ledger_api=ledger_api,
281
+ contract_address=safe,
282
+ sender_address=owner,
283
+ owners=(owner,), # type: ignore
284
+ to_address=to_address,
285
+ value=0,
286
+ data=txd,
287
+ safe_tx_gas=0,
288
+ signatures_by_owner=signatures,
289
+ operation=SafeOperation.CALL.value,
290
+ nonce=ledger_api.api.eth.get_transaction_count(owner),
291
+ )
292
+
293
+ tx_settler = TxSettler(
241
294
  ledger_api=ledger_api,
242
- contract_address=safe,
243
- sender_address=owner,
244
- owners=(owner,), # type: ignore
245
- to_address=safe,
246
- value=0,
247
- data=txd,
248
- safe_tx_gas=0,
249
- signatures_by_owner=signatures,
250
- operation=SafeOperation.CALL.value,
251
- nonce=ledger_api.api.eth.get_transaction_count(owner),
295
+ crypto=crypto,
296
+ chain_type=Chain.from_id(
297
+ ledger_api._chain_id # pylint: disable=protected-access
298
+ ),
299
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
300
+ retries=ON_CHAIN_INTERACT_RETRIES,
301
+ sleep=ON_CHAIN_INTERACT_SLEEP,
252
302
  )
253
- ledger_api.get_transaction_receipt(
254
- ledger_api.send_signed_transaction(
255
- crypto.sign_transaction(
256
- transaction,
257
- ),
258
- )
303
+ setattr(tx_settler, "build", _build_tx) # noqa: B010
304
+ tx_receipt = tx_settler.transact(
305
+ method=lambda: {},
306
+ contract="",
307
+ kwargs={},
308
+ dry_run=False,
259
309
  )
310
+ tx_hash = tx_receipt.get("transactionHash", "").hex()
311
+ return tx_hash
260
312
 
261
313
 
262
314
  def add_owner(
@@ -285,14 +337,81 @@ def add_owner(
285
337
  )
286
338
 
287
339
 
288
- def swap_owner( # pylint: disable=unused-argument
340
+ def get_prev_owner(ledger_api: LedgerApi, safe: str, owner: str) -> str:
341
+ """Retrieve the previous owner in the owners list of the Safe."""
342
+
343
+ owners = get_owners(ledger_api=ledger_api, safe=safe)
344
+
345
+ try:
346
+ index = owners.index(owner) - 1
347
+ except ValueError as e:
348
+ raise ValueError(
349
+ f"Owner {owner} not found in the owners' list of the Safe."
350
+ ) from e
351
+
352
+ if index < 0:
353
+ return SENTINEL_OWNERS
354
+ return owners[index]
355
+
356
+
357
+ def swap_owner(
289
358
  ledger_api: LedgerApi,
290
359
  crypto: Crypto,
291
360
  safe: str,
292
361
  old_owner: str,
293
362
  new_owner: str,
294
363
  ) -> None:
295
- """Swap owner on a safe."""
364
+ """Swap owner of a safe."""
365
+
366
+ prev_owner = get_prev_owner(ledger_api=ledger_api, safe=safe, owner=old_owner)
367
+ instance = registry_contracts.gnosis_safe.get_instance(
368
+ ledger_api=ledger_api,
369
+ contract_address=safe,
370
+ )
371
+ txd = instance.encodeABI(
372
+ fn_name="swapOwner",
373
+ args=[
374
+ prev_owner,
375
+ old_owner,
376
+ new_owner,
377
+ ],
378
+ )
379
+ send_safe_txs(
380
+ txd=bytes.fromhex(txd[2:]),
381
+ safe=safe,
382
+ ledger_api=ledger_api,
383
+ crypto=crypto,
384
+ )
385
+
386
+
387
+ def remove_owner(
388
+ ledger_api: LedgerApi,
389
+ crypto: Crypto,
390
+ safe: str,
391
+ owner: str,
392
+ threshold: int,
393
+ ) -> None:
394
+ """Remove owner from a safe."""
395
+
396
+ prev_owner = get_prev_owner(ledger_api=ledger_api, safe=safe, owner=owner)
397
+ instance = registry_contracts.gnosis_safe.get_instance(
398
+ ledger_api=ledger_api,
399
+ contract_address=safe,
400
+ )
401
+ txd = instance.encodeABI(
402
+ fn_name="removeOwner",
403
+ args=[
404
+ prev_owner,
405
+ owner,
406
+ threshold,
407
+ ],
408
+ )
409
+ send_safe_txs(
410
+ txd=bytes.fromhex(txd[2:]),
411
+ safe=safe,
412
+ ledger_api=ledger_api,
413
+ crypto=crypto,
414
+ )
296
415
 
297
416
 
298
417
  def transfer(
@@ -301,47 +420,255 @@ def transfer(
301
420
  safe: str,
302
421
  to: str,
303
422
  amount: t.Union[float, int],
304
- ) -> None:
423
+ ) -> t.Optional[str]:
305
424
  """Transfer assets from safe to given address."""
306
425
  amount = int(amount)
307
426
  owner = ledger_api.api.to_checksum_address(
308
427
  crypto.address,
309
428
  )
310
- safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
429
+
430
+ def _build_tx( # pylint: disable=unused-argument
431
+ *args: t.Any, **kwargs: t.Any
432
+ ) -> t.Optional[str]:
433
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
434
+ ledger_api=ledger_api,
435
+ contract_address=safe,
436
+ value=amount,
437
+ safe_tx_gas=0,
438
+ to_address=to,
439
+ data=b"",
440
+ operation=SafeOperation.CALL.value,
441
+ ).get("tx_hash")
442
+ safe_tx_bytes = binascii.unhexlify(
443
+ safe_tx_hash[2:],
444
+ )
445
+ signatures = {
446
+ owner: crypto.sign_message(
447
+ message=safe_tx_bytes,
448
+ is_deprecated_mode=True,
449
+ )[2:]
450
+ }
451
+ return registry_contracts.gnosis_safe.get_raw_safe_transaction(
452
+ ledger_api=ledger_api,
453
+ contract_address=safe,
454
+ sender_address=owner,
455
+ owners=(owner,), # type: ignore
456
+ to_address=to,
457
+ value=amount,
458
+ data=b"",
459
+ safe_tx_gas=0,
460
+ signatures_by_owner=signatures,
461
+ operation=SafeOperation.CALL.value,
462
+ nonce=ledger_api.api.eth.get_transaction_count(owner),
463
+ )
464
+
465
+ tx_settler = TxSettler(
311
466
  ledger_api=ledger_api,
312
- contract_address=safe,
313
- value=amount,
314
- safe_tx_gas=0,
315
- to_address=to,
316
- data=b"",
317
- operation=SafeOperation.CALL.value,
318
- ).get("tx_hash")
319
- safe_tx_bytes = binascii.unhexlify(
320
- safe_tx_hash[2:],
467
+ crypto=crypto,
468
+ chain_type=Chain.from_id(
469
+ ledger_api._chain_id # pylint: disable=protected-access
470
+ ),
471
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
472
+ retries=ON_CHAIN_INTERACT_RETRIES,
473
+ sleep=ON_CHAIN_INTERACT_SLEEP,
474
+ )
475
+ setattr(tx_settler, "build", _build_tx) # noqa: B010
476
+ tx_receipt = tx_settler.transact(
477
+ method=lambda: {},
478
+ contract="",
479
+ kwargs={},
480
+ dry_run=False,
481
+ )
482
+ tx_hash = tx_receipt.get("transactionHash", "").hex()
483
+ return tx_hash
484
+
485
+
486
+ def transfer_erc20_from_safe(
487
+ ledger_api: LedgerApi,
488
+ crypto: Crypto,
489
+ safe: str,
490
+ token: str,
491
+ to: str,
492
+ amount: t.Union[float, int],
493
+ ) -> t.Optional[str]:
494
+ """Transfer ERC20 assets from safe to given address."""
495
+ amount = int(amount)
496
+ instance = registry_contracts.erc20.get_instance(
497
+ ledger_api=ledger_api,
498
+ contract_address=token,
321
499
  )
322
- signatures = {
323
- owner: crypto.sign_message(
324
- message=safe_tx_bytes,
325
- is_deprecated_mode=True,
326
- )[2:]
327
- }
328
- transaction = registry_contracts.gnosis_safe.get_raw_safe_transaction(
500
+ txd = instance.encodeABI(
501
+ fn_name="transfer",
502
+ args=[
503
+ to,
504
+ amount,
505
+ ],
506
+ )
507
+ return send_safe_txs(
508
+ txd=bytes.fromhex(txd[2:]),
509
+ safe=safe,
329
510
  ledger_api=ledger_api,
330
- contract_address=safe,
331
- sender_address=owner,
332
- owners=(owner,), # type: ignore
333
- to_address=to,
334
- value=amount,
335
- data=b"",
336
- safe_tx_gas=0,
337
- signatures_by_owner=signatures,
338
- operation=SafeOperation.CALL.value,
339
- nonce=ledger_api.api.eth.get_transaction_count(owner),
511
+ crypto=crypto,
512
+ to=token,
340
513
  )
341
- ledger_api.get_transaction_receipt(
342
- ledger_api.send_signed_transaction(
343
- crypto.sign_transaction(
344
- transaction,
345
- ),
346
- )
514
+
515
+
516
+ def estimate_transfer_tx_fee(chain: Chain, sender_address: str, to: str) -> int:
517
+ """Estimate transfer transaction fee."""
518
+ ledger_api = get_default_ledger_api(chain)
519
+ tx = ledger_api.get_transfer_transaction(
520
+ sender_address=sender_address,
521
+ destination_address=to,
522
+ amount=0,
523
+ tx_fee=0,
524
+ tx_nonce="0x",
525
+ chain_id=chain.id,
526
+ raise_on_try=True,
527
+ )
528
+ tx = ledger_api.update_with_gas_estimate(
529
+ transaction=tx,
530
+ raise_on_try=False,
531
+ )
532
+ chain_fee = tx["gas"] * tx["maxFeePerGas"]
533
+ if chain in (
534
+ Chain.ARBITRUM_ONE,
535
+ Chain.BASE,
536
+ Chain.OPTIMISM,
537
+ Chain.MODE,
538
+ ):
539
+ chain_fee += ledger_api.get_l1_data_fee(tx)
540
+ return chain_fee
541
+
542
+
543
+ def drain_eoa(
544
+ ledger_api: LedgerApi,
545
+ crypto: Crypto,
546
+ withdrawal_address: str,
547
+ chain_id: int,
548
+ ) -> t.Optional[str]:
549
+ """Drain all the native tokens from the crypto wallet."""
550
+ tx_helper = TxSettler(
551
+ ledger_api=ledger_api,
552
+ crypto=crypto,
553
+ chain_type=ChainProfile.CUSTOM,
554
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
555
+ retries=ON_CHAIN_INTERACT_RETRIES,
556
+ sleep=ON_CHAIN_INTERACT_SLEEP,
347
557
  )
558
+
559
+ def _build_tx( # pylint: disable=unused-argument
560
+ *args: t.Any, **kwargs: t.Any
561
+ ) -> t.Dict:
562
+ """Build transaction"""
563
+ chain_fee = estimate_transfer_tx_fee(
564
+ chain=Chain.from_id(chain_id),
565
+ sender_address=crypto.address,
566
+ to=withdrawal_address,
567
+ )
568
+
569
+ amount = ledger_api.get_balance(crypto.address) - chain_fee
570
+ if amount <= 0:
571
+ raise ChainInteractionError(
572
+ f"No balance to drain from wallet: {crypto.address}"
573
+ )
574
+
575
+ tx = ledger_api.get_transfer_transaction(
576
+ sender_address=crypto.address,
577
+ destination_address=withdrawal_address,
578
+ amount=amount,
579
+ tx_fee=0,
580
+ tx_nonce="0x",
581
+ chain_id=chain_id,
582
+ raise_on_try=True,
583
+ )
584
+ empty_tx = tx.copy()
585
+ empty_tx["value"] = 0
586
+ empty_tx = ledger_api.update_with_gas_estimate(
587
+ transaction=empty_tx,
588
+ raise_on_try=False,
589
+ )
590
+ tx["gas"] = empty_tx["gas"]
591
+
592
+ logger.info(
593
+ f"Draining {tx['value']} native units from wallet: {crypto.address}"
594
+ )
595
+
596
+ return tx
597
+
598
+ setattr(tx_helper, "build", _build_tx) # noqa: B010
599
+ try:
600
+ tx_receipt = tx_helper.transact(
601
+ method=lambda: {},
602
+ contract="",
603
+ kwargs={},
604
+ dry_run=False,
605
+ )
606
+ except ChainInteractionError as e:
607
+ if "No balance to drain from wallet" in str(e):
608
+ logger.warning(f"Failed to drain wallet {crypto.address} with error: {e}.")
609
+ return None
610
+
611
+ raise e
612
+
613
+ tx_hash = tx_receipt.get("transactionHash", None)
614
+ if tx_hash is not None:
615
+ return tx_hash.hex()
616
+
617
+ return None
618
+
619
+
620
+ def get_asset_balance(
621
+ ledger_api: LedgerApi,
622
+ asset_address: str,
623
+ address: str,
624
+ raise_on_invalid_address: bool = True,
625
+ ) -> int:
626
+ """
627
+ Get the balance of a native asset or ERC20 token.
628
+
629
+ If contract address is a zero address, return the native balance.
630
+ """
631
+ if not Web3.is_address(address):
632
+ if raise_on_invalid_address:
633
+ raise ValueError(f"Invalid address: {address}")
634
+ return 0
635
+
636
+ try:
637
+ if asset_address == ZERO_ADDRESS:
638
+ return ledger_api.get_balance(address, raise_on_try=True)
639
+ return (
640
+ registry_contracts.erc20.get_instance(
641
+ ledger_api=ledger_api,
642
+ contract_address=asset_address,
643
+ )
644
+ .functions.balanceOf(address)
645
+ .call()
646
+ )
647
+ except Exception as e:
648
+ raise RuntimeError(
649
+ f"Cannot get balance of {address=} {asset_address=} rpc={ledger_api._api.provider.endpoint_uri}." # pylint: disable=protected-access
650
+ ) from e
651
+
652
+
653
+ def get_assets_balances(
654
+ ledger_api: LedgerApi,
655
+ asset_addresses: t.Set[str],
656
+ addresses: t.Set[str],
657
+ raise_on_invalid_address: bool = True,
658
+ ) -> t.Dict[str, t.Dict[str, int]]:
659
+ """
660
+ Get the balances of a list of native assets or ERC20 tokens.
661
+
662
+ If asset address is a zero address, return the native balance.
663
+ """
664
+ output: t.Dict[str, t.Dict[str, int]] = {}
665
+
666
+ for asset, address in itertools.product(asset_addresses, addresses):
667
+ output.setdefault(address, {})[asset] = get_asset_balance(
668
+ ledger_api=ledger_api,
669
+ asset_address=asset,
670
+ address=address,
671
+ raise_on_invalid_address=raise_on_invalid_address,
672
+ )
673
+
674
+ return output