olas-operate-middleware 0.10.20__py3-none-any.whl → 0.11.1__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.
@@ -28,6 +28,7 @@ import os
28
28
  import tempfile
29
29
  import typing as t
30
30
  from enum import Enum
31
+ from functools import cache
31
32
  from pathlib import Path
32
33
  from typing import Optional, Union, cast
33
34
 
@@ -59,6 +60,7 @@ from hexbytes import HexBytes
59
60
  from web3.contract import Contract
60
61
 
61
62
  from operate.constants import (
63
+ NO_STAKING_PROGRAM_ID,
62
64
  ON_CHAIN_INTERACT_RETRIES,
63
65
  ON_CHAIN_INTERACT_SLEEP,
64
66
  ON_CHAIN_INTERACT_TIMEOUT,
@@ -68,8 +70,15 @@ from operate.data import DATA_DIR
68
70
  from operate.data.contracts.dual_staking_token.contract import DualStakingTokenContract
69
71
  from operate.data.contracts.recovery_module.contract import RecoveryModule
70
72
  from operate.data.contracts.staking_token.contract import StakingTokenContract
73
+ from operate.ledger import (
74
+ get_default_ledger_api,
75
+ update_tx_with_gas_estimate,
76
+ update_tx_with_gas_pricing,
77
+ )
78
+ from operate.ledger.profiles import CONTRACTS, STAKING
71
79
  from operate.operate_types import Chain as OperateChain
72
80
  from operate.operate_types import ContractAddresses
81
+ from operate.services.service import NON_EXISTENT_TOKEN
73
82
  from operate.utils.gnosis import (
74
83
  MultiSendOperation,
75
84
  SafeOperation,
@@ -162,6 +171,8 @@ class GnosisSafeTransaction:
162
171
  operation=SafeOperation.DELEGATE_CALL.value,
163
172
  nonce=self.ledger_api.api.eth.get_transaction_count(owner),
164
173
  )
174
+ update_tx_with_gas_pricing(tx, self.ledger_api)
175
+ update_tx_with_gas_estimate(tx, self.ledger_api)
165
176
  return t.cast(t.Dict, tx)
166
177
 
167
178
  def settle(self) -> t.Dict:
@@ -170,6 +181,9 @@ class GnosisSafeTransaction:
170
181
  ledger_api=self.ledger_api,
171
182
  crypto=self.crypto,
172
183
  chain_type=self.chain_type,
184
+ timeout=ON_CHAIN_INTERACT_TIMEOUT,
185
+ retries=ON_CHAIN_INTERACT_RETRIES,
186
+ sleep=ON_CHAIN_INTERACT_SLEEP,
173
187
  )
174
188
  setattr(tx_settler, "build", self.build) # noqa: B010
175
189
  return tx_settler.transact(
@@ -180,31 +194,93 @@ class GnosisSafeTransaction:
180
194
  )
181
195
 
182
196
 
183
- class StakingManager(OnChainHelper):
197
+ class StakingManager:
184
198
  """Helper class for staking a service."""
185
199
 
200
+ staking_ctr = t.cast(
201
+ StakingTokenContract,
202
+ StakingTokenContract.from_dir(
203
+ directory=str(DATA_DIR / "contracts" / "staking_token")
204
+ ),
205
+ )
206
+
207
+ dual_staking_ctr = t.cast(
208
+ DualStakingTokenContract,
209
+ DualStakingTokenContract.from_dir(
210
+ directory=str(DATA_DIR / "contracts" / "dual_staking_token")
211
+ ),
212
+ )
213
+
186
214
  def __init__(
187
215
  self,
188
- key: Path,
189
- chain_type: ChainType = ChainType.CUSTOM,
190
- password: Optional[str] = None,
216
+ chain: OperateChain,
191
217
  ) -> None:
192
218
  """Initialize object."""
193
- super().__init__(key=key, chain_type=chain_type, password=password)
194
- self.staking_ctr = t.cast(
195
- StakingTokenContract,
196
- StakingTokenContract.from_dir(
197
- directory=str(DATA_DIR / "contracts" / "staking_token")
198
- ),
219
+ self.chain = chain
220
+
221
+ @property
222
+ def ledger_api(self) -> LedgerApi:
223
+ """Get ledger api."""
224
+ return get_default_ledger_api(OperateChain(self.chain.value))
225
+
226
+ @staticmethod
227
+ @cache
228
+ def _get_staking_params(chain: OperateChain, staking_contract: str) -> t.Dict:
229
+ """Get staking params"""
230
+ ledger_api = get_default_ledger_api(chain=chain)
231
+ instance = StakingManager.staking_ctr.get_instance(
232
+ ledger_api=ledger_api,
233
+ contract_address=staking_contract,
199
234
  )
200
- self.dual_staking_ctr = t.cast(
201
- DualStakingTokenContract,
202
- DualStakingTokenContract.from_dir(
203
- directory=str(DATA_DIR / "contracts" / "dual_staking_token")
204
- ),
235
+ agent_ids = instance.functions.getAgentIds().call()
236
+ service_registry = instance.functions.serviceRegistry().call()
237
+ staking_token = instance.functions.stakingToken().call()
238
+ service_registry_token_utility = (
239
+ instance.functions.serviceRegistryTokenUtility().call()
240
+ )
241
+ min_staking_deposit = instance.functions.minStakingDeposit().call()
242
+ activity_checker = instance.functions.activityChecker().call()
243
+
244
+ output = {
245
+ "staking_contract": staking_contract,
246
+ "agent_ids": agent_ids,
247
+ "service_registry": service_registry,
248
+ "staking_token": staking_token,
249
+ "service_registry_token_utility": service_registry_token_utility,
250
+ "min_staking_deposit": min_staking_deposit,
251
+ "activity_checker": activity_checker,
252
+ "additional_staking_tokens": {},
253
+ }
254
+ try:
255
+ instance = StakingManager.dual_staking_ctr.get_instance(
256
+ ledger_api=ledger_api,
257
+ contract_address=staking_contract,
258
+ )
259
+ output["additional_staking_tokens"][
260
+ instance.functions.secondToken().call()
261
+ ] = instance.functions.secondTokenAmount().call()
262
+ except Exception: # pylint: disable=broad-except # nosec
263
+ # Contract is not a dual staking contract
264
+
265
+ # TODO The exception caught here should be ContractLogicError.
266
+ # This exception is typically raised when the contract reverts with
267
+ # a reason string. However, in some cases, the error message
268
+ # does not contain a reason string, which means web3.py raises
269
+ # a generic ValueError instead. It should be properly analyzed
270
+ # what exceptions might be raised by web3.py in this case. To
271
+ # avoid any issues we are simply catching all exceptions.
272
+ pass
273
+
274
+ return output
275
+
276
+ def get_staking_params(self, staking_contract: str) -> t.Dict:
277
+ """Get staking params"""
278
+ return StakingManager._get_staking_params(
279
+ chain=self.chain,
280
+ staking_contract=staking_contract,
205
281
  )
206
282
 
207
- def status(self, service_id: int, staking_contract: str) -> StakingState:
283
+ def staking_state(self, service_id: int, staking_contract: str) -> StakingState:
208
284
  """Is the service staked?"""
209
285
  return StakingState(
210
286
  self.staking_ctr.get_instance(
@@ -246,11 +322,12 @@ class StakingManager(OnChainHelper):
246
322
 
247
323
  def service_info(self, staking_contract: str, service_id: int) -> dict:
248
324
  """Get the service onchain info"""
249
- return self.staking_ctr.get_service_info(
250
- self.ledger_api,
251
- staking_contract,
252
- service_id,
253
- ).get("data")
325
+ instance = self.staking_ctr.get_instance(
326
+ ledger_api=self.ledger_api,
327
+ contract_address=staking_contract,
328
+ )
329
+ service_info = instance.functions.getServiceInfo(service_id).call()
330
+ return service_info
254
331
 
255
332
  def agent_ids(self, staking_contract: str) -> t.List[int]:
256
333
  """Get a list of agent IDs for the given staking contract."""
@@ -306,7 +383,7 @@ class StakingManager(OnChainHelper):
306
383
  staking_contract: str,
307
384
  ) -> None:
308
385
  """Check if service can be staked."""
309
- status = self.status(service_id, staking_contract)
386
+ status = self.staking_state(service_id, staking_contract)
310
387
  if status == StakingState.STAKED:
311
388
  raise ValueError("Service already staked")
312
389
 
@@ -316,21 +393,28 @@ class StakingManager(OnChainHelper):
316
393
  if not self.slots_available(staking_contract):
317
394
  raise ValueError("No sataking slots available.")
318
395
 
396
+ # TODO To be deprecated, only used in on-chain manager
319
397
  def stake(
320
398
  self,
321
399
  service_id: int,
322
400
  service_registry: str,
323
401
  staking_contract: str,
402
+ key: Path,
403
+ password: str,
324
404
  ) -> None:
325
405
  """Stake the service"""
406
+ och = OnChainHelper(
407
+ key=key, chain_type=ChainType(self.chain.value), password=password
408
+ )
409
+
326
410
  self.check_staking_compatibility(
327
411
  service_id=service_id, staking_contract=staking_contract
328
412
  )
329
413
 
330
414
  tx_settler = TxSettler(
331
- ledger_api=self.ledger_api,
332
- crypto=self.crypto,
333
- chain_type=self.chain_type,
415
+ ledger_api=och.ledger_api,
416
+ crypto=och.crypto,
417
+ chain_type=och.chain_type,
334
418
  timeout=ON_CHAIN_INTERACT_TIMEOUT,
335
419
  retries=ON_CHAIN_INTERACT_RETRIES,
336
420
  sleep=ON_CHAIN_INTERACT_SLEEP,
@@ -346,10 +430,10 @@ class StakingManager(OnChainHelper):
346
430
  *args: t.Any, **kargs: t.Any
347
431
  ) -> t.Dict:
348
432
  return registry_contracts.erc20.get_approve_tx(
349
- ledger_api=self.ledger_api,
433
+ ledger_api=och.ledger_api,
350
434
  contract_address=service_registry,
351
435
  spender=staking_contract,
352
- sender=self.crypto.address,
436
+ sender=och.crypto.address,
353
437
  amount=service_id, # TODO: This is a workaround and it should be fixed
354
438
  )
355
439
 
@@ -364,15 +448,15 @@ class StakingManager(OnChainHelper):
364
448
  def _build_staking_tx( # pylint: disable=unused-argument
365
449
  *args: t.Any, **kargs: t.Any
366
450
  ) -> t.Dict:
367
- return self.ledger_api.build_transaction(
451
+ return och.ledger_api.build_transaction(
368
452
  contract_instance=self.staking_ctr.get_instance(
369
- ledger_api=self.ledger_api,
453
+ ledger_api=och.ledger_api,
370
454
  contract_address=staking_contract,
371
455
  ),
372
456
  method_name="stake",
373
457
  method_args={"serviceId": service_id},
374
458
  tx_args={
375
- "sender_address": self.crypto.address,
459
+ "sender_address": och.crypto.address,
376
460
  },
377
461
  raise_on_try=True,
378
462
  )
@@ -391,7 +475,7 @@ class StakingManager(OnChainHelper):
391
475
  staking_contract: str,
392
476
  ) -> None:
393
477
  """Check unstaking availability"""
394
- if self.status(
478
+ if self.staking_state(
395
479
  service_id=service_id, staking_contract=staking_contract
396
480
  ) not in {StakingState.STAKED, StakingState.EVICTED}:
397
481
  raise ValueError("Service not staked.")
@@ -415,13 +499,23 @@ class StakingManager(OnChainHelper):
415
499
  if staked_duration < minimum_staking_duration and available_rewards > 0:
416
500
  raise ValueError("Service cannot be unstaked yet.")
417
501
 
418
- def unstake(self, service_id: int, staking_contract: str) -> None:
502
+ # TODO To be deprecated, only used in on-chain manager
503
+ def unstake(
504
+ self,
505
+ service_id: int,
506
+ staking_contract: str,
507
+ key: Path,
508
+ password: str,
509
+ ) -> None:
419
510
  """Unstake the service"""
511
+ och = OnChainHelper(
512
+ key=key, chain_type=ChainType(self.chain.value), password=password
513
+ )
420
514
 
421
515
  tx_settler = TxSettler(
422
- ledger_api=self.ledger_api,
423
- crypto=self.crypto,
424
- chain_type=self.chain_type,
516
+ ledger_api=och.ledger_api,
517
+ crypto=och.crypto,
518
+ chain_type=och.chain_type,
425
519
  timeout=ON_CHAIN_INTERACT_TIMEOUT,
426
520
  retries=ON_CHAIN_INTERACT_RETRIES,
427
521
  sleep=ON_CHAIN_INTERACT_SLEEP,
@@ -430,15 +524,15 @@ class StakingManager(OnChainHelper):
430
524
  def _build_unstaking_tx( # pylint: disable=unused-argument
431
525
  *args: t.Any, **kargs: t.Any
432
526
  ) -> t.Dict:
433
- return self.ledger_api.build_transaction(
527
+ return och.ledger_api.build_transaction(
434
528
  contract_instance=self.staking_ctr.get_instance(
435
- ledger_api=self.ledger_api,
529
+ ledger_api=och.ledger_api,
436
530
  contract_address=staking_contract,
437
531
  ),
438
532
  method_name="unstake",
439
533
  method_args={"serviceId": service_id},
440
534
  tx_args={
441
- "sender_address": self.crypto.address,
535
+ "sender_address": och.crypto.address,
442
536
  },
443
537
  raise_on_try=True,
444
538
  )
@@ -523,6 +617,69 @@ class StakingManager(OnChainHelper):
523
617
  args=[service_id],
524
618
  )
525
619
 
620
+ def get_staking_contract(
621
+ self, staking_program_id: t.Optional[str]
622
+ ) -> t.Optional[str]:
623
+ """Get staking contract based on the config and the staking program."""
624
+ if staking_program_id == NO_STAKING_PROGRAM_ID or staking_program_id is None:
625
+ return None
626
+
627
+ return STAKING[self.chain].get(
628
+ staking_program_id,
629
+ staking_program_id,
630
+ )
631
+
632
+ def get_current_staking_program(self, service_id: int) -> t.Optional[str]:
633
+ """Get the current staking program of a service"""
634
+ ledger_api = self.ledger_api
635
+
636
+ if service_id == NON_EXISTENT_TOKEN:
637
+ return None
638
+
639
+ service_registry = registry_contracts.service_registry.get_instance(
640
+ ledger_api=ledger_api,
641
+ contract_address=CONTRACTS[self.chain]["service_registry"],
642
+ )
643
+
644
+ service_owner = service_registry.functions.ownerOf(service_id).call()
645
+
646
+ try:
647
+ state = self.staking_state(
648
+ service_id=service_id, staking_contract=service_owner
649
+ )
650
+
651
+ except Exception: # pylint: disable=broad-except
652
+ # Service owner is not a staking contract
653
+
654
+ # TODO The exception caught here should be ContractLogicError.
655
+ # This exception is typically raised when the contract reverts with
656
+ # a reason string. However, in some cases, the error message
657
+ # does not contain a reason string, which means web3.py raises
658
+ # a generic ValueError instead. It should be properly analyzed
659
+ # what exceptions might be raised by web3.py in this case. To
660
+ # avoid any issues we are simply catching all exceptions.
661
+ return None
662
+
663
+ if state == StakingState.UNSTAKED:
664
+ return None
665
+
666
+ for staking_program_id, val in STAKING[self.chain].items():
667
+ if val == service_owner:
668
+ return staking_program_id
669
+
670
+ # Fallback, if not possible to determine staking_program_id it means it's an "inner" staking contract
671
+ # (e.g., in the case of DualStakingToken). Loop trough all the known contracts.
672
+ for staking_program_id, staking_program_address in STAKING[self.chain].items():
673
+ state = self.staking_state(
674
+ service_id=service_id, staking_contract=staking_program_address
675
+ )
676
+ if state in (StakingState.STAKED, StakingState.EVICTED):
677
+ return staking_program_id
678
+
679
+ # it's staked, but we don't know which staking program
680
+ # so the staking_program_id should be an arbitrary staking contract
681
+ return service_owner
682
+
526
683
 
527
684
  # TODO Backport this to Open Autonomy MintHelper class
528
685
  # MintHelper should support passing custom 'description', 'name' and 'attributes'.
@@ -572,8 +729,6 @@ class MintManager(MintHelper):
572
729
  class _ChainUtil:
573
730
  """On chain service management."""
574
731
 
575
- _cache = {}
576
-
577
732
  def __init__(
578
733
  self,
579
734
  rpc: str,
@@ -814,9 +969,7 @@ class _ChainUtil:
814
969
  """Check if there are available slots on the staking contract"""
815
970
  self._patch()
816
971
  return StakingManager(
817
- key=self.wallet.key_path,
818
- password=self.wallet.password,
819
- chain_type=self.chain_type,
972
+ chain=OperateChain(self.chain_type.value),
820
973
  ).slots_available(
821
974
  staking_contract=staking_contract,
822
975
  )
@@ -825,9 +978,7 @@ class _ChainUtil:
825
978
  """Check if there are available staking rewards on the staking contract"""
826
979
  self._patch()
827
980
  available_rewards = StakingManager(
828
- key=self.wallet.key_path,
829
- password=self.wallet.password,
830
- chain_type=self.chain_type,
981
+ chain=OperateChain(self.chain_type.value),
831
982
  ).available_rewards(
832
983
  staking_contract=staking_contract,
833
984
  )
@@ -837,9 +988,7 @@ class _ChainUtil:
837
988
  """Check if there are claimable staking rewards on the staking contract"""
838
989
  self._patch()
839
990
  claimable_rewards = StakingManager(
840
- key=self.wallet.key_path,
841
- password=self.wallet.password,
842
- chain_type=self.chain_type,
991
+ chain=OperateChain(self.chain_type.value),
843
992
  ).claimable_rewards(
844
993
  staking_contract=staking_contract,
845
994
  service_id=service_id,
@@ -850,10 +999,8 @@ class _ChainUtil:
850
999
  """Stake the service"""
851
1000
  self._patch()
852
1001
  return StakingManager(
853
- key=self.wallet.key_path,
854
- password=self.wallet.password,
855
- chain_type=self.chain_type,
856
- ).status(
1002
+ chain=OperateChain(self.chain_type.value),
1003
+ ).staking_state(
857
1004
  service_id=service_id,
858
1005
  staking_contract=staking_contract,
859
1006
  )
@@ -862,72 +1009,15 @@ class _ChainUtil:
862
1009
  self, staking_contract: str, fallback_params: t.Optional[t.Dict] = None
863
1010
  ) -> t.Dict:
864
1011
  """Get agent IDs for the staking contract"""
865
-
866
1012
  if staking_contract is None and fallback_params is not None:
867
1013
  return fallback_params
868
-
869
- cache = _ChainUtil._cache
870
- if staking_contract in cache.setdefault("get_staking_params", {}):
871
- return cache["get_staking_params"][staking_contract]
872
-
873
1014
  self._patch()
874
1015
  staking_manager = StakingManager(
875
- key=self.wallet.key_path,
876
- password=self.wallet.password,
877
- chain_type=self.chain_type,
878
- )
879
- agent_ids = staking_manager.agent_ids(
880
- staking_contract=staking_contract,
1016
+ chain=OperateChain(self.chain_type.value),
881
1017
  )
882
- service_registry = staking_manager.service_registry(
1018
+ return staking_manager.get_staking_params(
883
1019
  staking_contract=staking_contract,
884
1020
  )
885
- staking_token = staking_manager.staking_token(
886
- staking_contract=staking_contract,
887
- )
888
- service_registry_token_utility = staking_manager.service_registry_token_utility(
889
- staking_contract=staking_contract,
890
- )
891
- min_staking_deposit = staking_manager.min_staking_deposit(
892
- staking_contract=staking_contract,
893
- )
894
- activity_checker = staking_manager.activity_checker(
895
- staking_contract=staking_contract,
896
- )
897
-
898
- output = {
899
- "staking_contract": staking_contract,
900
- "agent_ids": agent_ids,
901
- "service_registry": service_registry,
902
- "staking_token": staking_token,
903
- "service_registry_token_utility": service_registry_token_utility,
904
- "min_staking_deposit": min_staking_deposit,
905
- "activity_checker": activity_checker,
906
- "additional_staking_tokens": {},
907
- }
908
- try:
909
- instance = staking_manager.dual_staking_ctr.get_instance(
910
- ledger_api=self.ledger_api,
911
- contract_address=staking_contract,
912
- )
913
- output["additional_staking_tokens"][
914
- instance.functions.secondToken().call()
915
- ] = instance.functions.secondTokenAmount().call()
916
- except Exception: # pylint: disable=broad-except # nosec
917
- # Contract is not a dual staking contract
918
-
919
- # TODO The exception caught here should be ContractLogicError.
920
- # This exception is typically raised when the contract reverts with
921
- # a reason string. However, in some cases, the error message
922
- # does not contain a reason string, which means web3.py raises
923
- # a generic ValueError instead. It should be properly analyzed
924
- # what exceptions might be raised by web3.py in this case. To
925
- # avoid any issues we are simply catching all exceptions.
926
- pass
927
-
928
- cache["get_staking_params"][staking_contract] = output
929
-
930
- return output
931
1021
 
932
1022
 
933
1023
  class OnChainManager(_ChainUtil):
@@ -1115,35 +1205,33 @@ class OnChainManager(_ChainUtil):
1115
1205
  """Stake service."""
1116
1206
  self._patch()
1117
1207
  StakingManager(
1118
- key=self.wallet.key_path,
1119
- password=self.wallet.password,
1120
- chain_type=self.chain_type,
1208
+ chain=OperateChain(self.chain_type.value),
1121
1209
  ).stake(
1122
1210
  service_id=service_id,
1123
1211
  service_registry=service_registry,
1124
1212
  staking_contract=staking_contract,
1213
+ key=self.wallet.key_path,
1214
+ password=self.wallet.password,
1125
1215
  )
1126
1216
 
1127
1217
  def unstake(self, service_id: int, staking_contract: str) -> None:
1128
1218
  """Unstake service."""
1129
1219
  self._patch()
1130
1220
  StakingManager(
1131
- key=self.wallet.key_path,
1132
- password=self.wallet.password,
1133
- chain_type=self.chain_type,
1221
+ chain=OperateChain(self.chain_type.value),
1134
1222
  ).unstake(
1135
1223
  service_id=service_id,
1136
1224
  staking_contract=staking_contract,
1225
+ key=self.wallet.key_path,
1226
+ password=self.wallet.password,
1137
1227
  )
1138
1228
 
1139
1229
  def staking_status(self, service_id: int, staking_contract: str) -> StakingState:
1140
1230
  """Stake the service"""
1141
1231
  self._patch()
1142
1232
  return StakingManager(
1143
- key=self.wallet.key_path,
1144
- password=self.wallet.password,
1145
- chain_type=self.chain_type,
1146
- ).status(
1233
+ chain=OperateChain(self.chain_type.value),
1234
+ ).staking_state(
1147
1235
  service_id=service_id,
1148
1236
  staking_contract=staking_contract,
1149
1237
  )
@@ -1397,6 +1485,182 @@ class EthSafeTxBuilder(_ChainUtil):
1397
1485
  return [deploy_message]
1398
1486
  return [approve_hash_message, deploy_message]
1399
1487
 
1488
+ def get_safe_b_native_transfer_messages( # pylint: disable=too-many-locals
1489
+ self,
1490
+ safe_b_address: str,
1491
+ to: str,
1492
+ amount: int,
1493
+ ) -> t.Tuple[t.Dict, t.Dict]:
1494
+ """
1495
+ Build the two messages (Safe calls) to withdraw native ETH from Safe B via this Safe (owner of Safe B).
1496
+
1497
+ Builds the messages to be settled by this Safe:
1498
+ 1) approveHash(inner_tx_hash)
1499
+ 2) execTransaction(...) to transfer ETH
1500
+ """
1501
+ safe_b_instance = registry_contracts.gnosis_safe.get_instance(
1502
+ ledger_api=self.ledger_api,
1503
+ contract_address=safe_b_address,
1504
+ )
1505
+
1506
+ txs = []
1507
+ txs.append(
1508
+ {
1509
+ "to": to,
1510
+ "data": b"",
1511
+ "operation": MultiSendOperation.CALL,
1512
+ "value": amount,
1513
+ }
1514
+ )
1515
+
1516
+ multisend_address = ContractConfigs.get(MULTISEND_CONTRACT.name).contracts[
1517
+ self.chain_type
1518
+ ]
1519
+ multisend_tx = registry_contracts.multisend.get_multisend_tx(
1520
+ ledger_api=self.ledger_api,
1521
+ contract_address=multisend_address,
1522
+ txs=txs,
1523
+ )
1524
+
1525
+ # Compute inner Safe transaction hash
1526
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
1527
+ ledger_api=self.ledger_api,
1528
+ contract_address=safe_b_address,
1529
+ to_address=multisend_address,
1530
+ value=multisend_tx["value"],
1531
+ data=multisend_tx["data"],
1532
+ operation=SafeOperation.CALL.value,
1533
+ ).get("tx_hash")
1534
+
1535
+ # Build approveHash message
1536
+ approve_hash_data = safe_b_instance.encodeABI(
1537
+ fn_name="approveHash",
1538
+ args=[safe_tx_hash],
1539
+ )
1540
+ approve_hash_message = {
1541
+ "to": safe_b_address,
1542
+ "data": approve_hash_data[2:],
1543
+ "operation": MultiSendOperation.CALL,
1544
+ "value": 0,
1545
+ }
1546
+
1547
+ # Build execTransaction message
1548
+ exec_data = safe_b_instance.encodeABI(
1549
+ fn_name="execTransaction",
1550
+ args=[
1551
+ multisend_address,
1552
+ multisend_tx["value"],
1553
+ multisend_tx["data"],
1554
+ SafeOperation.DELEGATE_CALL.value,
1555
+ 0, # safeTxGas
1556
+ 0, # baseGas
1557
+ 0, # gasPrice
1558
+ ZERO_ADDRESS, # gasToken
1559
+ ZERO_ADDRESS, # refundReceiver
1560
+ get_packed_signature_for_approved_hash(owners=(self.safe,)),
1561
+ ],
1562
+ )
1563
+ exec_message = {
1564
+ "to": safe_b_address,
1565
+ "data": exec_data[2:],
1566
+ "operation": MultiSendOperation.CALL,
1567
+ "value": 0,
1568
+ }
1569
+
1570
+ return approve_hash_message, exec_message
1571
+
1572
+ def get_safe_b_erc20_transfer_messages( # pylint: disable=too-many-locals
1573
+ self,
1574
+ safe_b_address: str,
1575
+ token: str,
1576
+ to: str,
1577
+ amount: int,
1578
+ ) -> t.Tuple[t.Dict, t.Dict]:
1579
+ """
1580
+ Build the two messages (Safe calls) to withdraw ERC20 from Safe B via this Safe (owner of Safe B).
1581
+
1582
+ Builds the messages to be settled by this Safe:
1583
+ 1) approveHash(inner_tx_hash)
1584
+ 2) execTransaction(...) to transfer ERC20 tokens
1585
+ """
1586
+ safe_b_instance = registry_contracts.gnosis_safe.get_instance(
1587
+ ledger_api=self.ledger_api,
1588
+ contract_address=safe_b_address,
1589
+ )
1590
+ erc20_instance = registry_contracts.erc20.get_instance(
1591
+ ledger_api=self.ledger_api,
1592
+ contract_address=token,
1593
+ )
1594
+
1595
+ txs = []
1596
+ txs.append(
1597
+ {
1598
+ "to": token,
1599
+ "data": erc20_instance.encodeABI(
1600
+ fn_name="transfer",
1601
+ args=[to, amount],
1602
+ ),
1603
+ "operation": MultiSendOperation.CALL,
1604
+ "value": 0,
1605
+ }
1606
+ )
1607
+
1608
+ multisend_address = ContractConfigs.get(MULTISEND_CONTRACT.name).contracts[
1609
+ self.chain_type
1610
+ ]
1611
+ multisend_tx = registry_contracts.multisend.get_multisend_tx(
1612
+ ledger_api=self.ledger_api,
1613
+ contract_address=multisend_address,
1614
+ txs=txs,
1615
+ )
1616
+
1617
+ # Compute inner Safe transaction hash
1618
+ safe_tx_hash = registry_contracts.gnosis_safe.get_raw_safe_transaction_hash(
1619
+ ledger_api=self.ledger_api,
1620
+ contract_address=safe_b_address,
1621
+ to_address=multisend_address,
1622
+ value=multisend_tx["value"],
1623
+ data=multisend_tx["data"],
1624
+ operation=SafeOperation.CALL.value,
1625
+ ).get("tx_hash")
1626
+
1627
+ # Build approveHash message
1628
+ approve_hash_data = safe_b_instance.encodeABI(
1629
+ fn_name="approveHash",
1630
+ args=[safe_tx_hash],
1631
+ )
1632
+ approve_hash_message = {
1633
+ "to": safe_b_address,
1634
+ "data": approve_hash_data[2:],
1635
+ "operation": MultiSendOperation.CALL,
1636
+ "value": 0,
1637
+ }
1638
+
1639
+ # Build execTransaction message
1640
+ exec_data = safe_b_instance.encodeABI(
1641
+ fn_name="execTransaction",
1642
+ args=[
1643
+ multisend_address,
1644
+ multisend_tx["value"],
1645
+ multisend_tx["data"],
1646
+ SafeOperation.DELEGATE_CALL.value,
1647
+ 0, # safeTxGas
1648
+ 0, # baseGas
1649
+ 0, # gasPrice
1650
+ ZERO_ADDRESS, # gasToken
1651
+ ZERO_ADDRESS, # refundReceiver
1652
+ get_packed_signature_for_approved_hash(owners=(self.safe,)),
1653
+ ],
1654
+ )
1655
+ exec_message = {
1656
+ "to": safe_b_address,
1657
+ "data": exec_data[2:],
1658
+ "operation": MultiSendOperation.CALL,
1659
+ "value": 0,
1660
+ }
1661
+
1662
+ return approve_hash_message, exec_message
1663
+
1400
1664
  def get_terminate_data(self, service_id: int) -> t.Dict:
1401
1665
  """Get terminate tx data."""
1402
1666
  instance = registry_contracts.service_manager.get_instance(
@@ -1440,9 +1704,7 @@ class EthSafeTxBuilder(_ChainUtil):
1440
1704
  """Get staking approval data"""
1441
1705
  self._patch()
1442
1706
  txd = StakingManager(
1443
- key=self.wallet.key_path,
1444
- password=self.wallet.password,
1445
- chain_type=self.chain_type,
1707
+ chain=OperateChain(self.chain_type.value),
1446
1708
  ).get_stake_approval_tx_data(
1447
1709
  service_id=service_id,
1448
1710
  service_registry=service_registry,
@@ -1464,9 +1726,7 @@ class EthSafeTxBuilder(_ChainUtil):
1464
1726
  """Get staking tx data"""
1465
1727
  self._patch()
1466
1728
  txd = StakingManager(
1467
- key=self.wallet.key_path,
1468
- password=self.wallet.password,
1469
- chain_type=self.chain_type,
1729
+ chain=OperateChain(self.chain_type.value),
1470
1730
  ).get_stake_tx_data(
1471
1731
  service_id=service_id,
1472
1732
  staking_contract=staking_contract,
@@ -1487,9 +1747,7 @@ class EthSafeTxBuilder(_ChainUtil):
1487
1747
  """Get unstaking tx data"""
1488
1748
  self._patch()
1489
1749
  staking_manager = StakingManager(
1490
- key=self.wallet.key_path,
1491
- password=self.wallet.password,
1492
- chain_type=self.chain_type,
1750
+ chain=OperateChain(self.chain_type.value),
1493
1751
  )
1494
1752
  txd = (
1495
1753
  staking_manager.get_forced_unstake_tx_data(
@@ -1517,9 +1775,7 @@ class EthSafeTxBuilder(_ChainUtil):
1517
1775
  """Get claiming tx data"""
1518
1776
  self._patch()
1519
1777
  staking_manager = StakingManager(
1520
- key=self.wallet.key_path,
1521
- password=self.wallet.password,
1522
- chain_type=self.chain_type,
1778
+ chain=OperateChain(self.chain_type.value),
1523
1779
  )
1524
1780
  txd = staking_manager.get_claim_tx_data(
1525
1781
  service_id=service_id,
@@ -1536,9 +1792,7 @@ class EthSafeTxBuilder(_ChainUtil):
1536
1792
  """Stake service."""
1537
1793
  self._patch()
1538
1794
  return StakingManager(
1539
- key=self.wallet.key_path,
1540
- password=self.wallet.password,
1541
- chain_type=self.chain_type,
1795
+ chain=OperateChain(self.chain_type.value),
1542
1796
  ).slots_available(
1543
1797
  staking_contract=staking_contract,
1544
1798
  )
@@ -1548,9 +1802,7 @@ class EthSafeTxBuilder(_ChainUtil):
1548
1802
  self._patch()
1549
1803
  try:
1550
1804
  StakingManager(
1551
- key=self.wallet.key_path,
1552
- password=self.wallet.password,
1553
- chain_type=self.chain_type,
1805
+ chain=OperateChain(self.chain_type.value),
1554
1806
  ).check_if_unstaking_possible(
1555
1807
  service_id=service_id,
1556
1808
  staking_contract=staking_contract,