1111from ape .contracts import ContractCall
1212from ape .exceptions import ContractNotFoundError , ProviderNotConnectedError
1313from ape .logging import logger
14- from ape .types import AddressType , HexBytes , MessageSignature
14+ from ape .types import AddressType , MessageSignature
1515from ape .utils import ZERO_ADDRESS , cached_property
1616from ape_ethereum .proxies import ProxyInfo , ProxyType
1717from ape_ethereum .transactions import TransactionType
1818from eip712 .common import SafeTxV1 , SafeTxV2 , create_safe_tx_def
19- from eth_utils import keccak , to_bytes , to_int
19+ from eth_utils import keccak , to_bytes , to_canonical_address , to_int
2020from ethpm_types import ContractType
2121from ethpm_types .abi import ABIType , MethodABI
2222from packaging .version import Version
2323
24- from .client import (
25- BaseSafeClient ,
26- MockSafeClient ,
27- SafeClient ,
28- SafeTx ,
29- SafeTxConfirmation ,
30- SafeTxID ,
31- )
24+ from ape_safe .client .types import OperationType
25+
26+ from .client import BaseSafeClient , MockSafeClient , SafeClient , SafeTx , SafeTxConfirmation , SafeTxID
3227from .config import SafeConfig
3328from .exceptions import (
3429 ApeSafeError ,
4338from .modules import SafeModuleManager
4439from .packages import PackageType
4540from .types import SafeCacheData
46- from .utils import get_safe_tx_hash , order_by_signer
41+ from .utils import encode_signatures , get_safe_tx_hash
4742
4843if TYPE_CHECKING :
4944 from ape .api .address import BaseAddress
@@ -190,8 +185,7 @@ def get_signatures(
190185) -> dict [AddressType , MessageSignature ]:
191186 signatures : dict [AddressType , MessageSignature ] = {}
192187 for signer in signers :
193- signature = signer .sign_message (safe_tx )
194- if signature :
188+ if signature := signer .sign_message (safe_tx ):
195189 signatures [signer .address ] = signature
196190
197191 return signatures
@@ -417,27 +411,39 @@ def create_safe_tx(self, txn: Optional[TransactionAPI] = None, **safe_tx_kwargs)
417411
418412 Args:
419413 txn (Optional[``TransactionAPI``]): The transaction
420- **safe_tx_kwargs: The safe transactions specifications, such as ``submitter ``.
414+ **safe_tx_kwargs: The safe transaction specifications, such as ``operation ``.
421415
422416 Returns:
423417 :class:`~ape_safe.client.SafeTx`: The Safe Transaction to be used.
424418 """
425- safe_tx = {
426- "to" : txn .receiver if txn else self .address , # Self-call, e.g. rejection
427- "value" : txn .value if txn else 0 ,
428- "data" : (txn .data or b"" ) if txn else b"" ,
429- "nonce" : self .new_nonce if txn is None or txn .nonce is None else txn .nonce ,
430- "operation" : 0 ,
431- "safeTxGas" : 0 ,
432- "gasPrice" : 0 ,
433- "gasToken" : ZERO_ADDRESS ,
434- "refundReceiver" : ZERO_ADDRESS ,
435- }
436- safe_tx = {
437- ** safe_tx ,
438- ** {k : v for k , v in safe_tx_kwargs .items () if k in safe_tx and v is not None },
439- }
440- return self .safe_tx_def (** safe_tx )
419+ return self .safe_tx_def ( # type: ignore[call-arg]
420+ # NOTE: Use `txn` only for these 3 commonly-used params
421+ to = (
422+ txn .receiver
423+ if txn
424+ else self .conversion_manager .convert (
425+ safe_tx_kwargs .get ("to" , self .address ), AddressType
426+ )
427+ ),
428+ value = (
429+ txn .value
430+ if txn
431+ else self .conversion_manager .convert (safe_tx_kwargs .get ("value" , 0 ), int )
432+ ),
433+ data = (txn .data or b"" ) if txn else safe_tx_kwargs .get ("data" , b"" ),
434+ # ...then use only kwargs for the rest
435+ # NOTE: Use `.new_nonce` to get latest nonce in off-chain txn queue
436+ nonce = safe_tx_kwargs .get ("nonce" , self .new_nonce ),
437+ operation = safe_tx_kwargs .get ("operation" , OperationType .CALL ),
438+ safeTxGas = self .conversion_manager .convert (safe_tx_kwargs .get ("safeTxGas" , 0 ), int ),
439+ gasPrice = self .conversion_manager .convert (safe_tx_kwargs .get ("gasPrice" , 0 ), int ),
440+ gasToken = self .conversion_manager .convert (
441+ safe_tx_kwargs .get ("gasToken" , ZERO_ADDRESS ), AddressType
442+ ),
443+ refundReceiver = self .conversion_manager .convert (
444+ safe_tx_kwargs .get ("refundReceiver" , ZERO_ADDRESS ), AddressType
445+ ),
446+ )
441447
442448 def create_batch (self ) -> "MultiSend" :
443449 from ape_safe .multisend import MultiSend
@@ -530,24 +536,69 @@ def local_signers(self) -> list[AccountAPI]:
530536
531537 return list (filter (lambda a : a in signers , accounts ))
532538
539+ def _preapproved_signature (
540+ self , signer : Union [AddressType , "BaseAddress" , str ]
541+ ) -> MessageSignature :
542+ # Get the Safe-style "preapproval" signature type, which is a sentinel value used to denote
543+ # when a signer approved via some other method, such as `approveHash` or being `msg.sender`
544+ # TODO: Link documentation for this
545+ return MessageSignature (
546+ v = 1 , # Approved hash (e.g. submitter is approved)
547+ r = (
548+ b"\x00 " * 12
549+ + to_canonical_address (self .conversion_manager .convert (signer , AddressType ))
550+ ),
551+ s = b"\x00 " * 32 ,
552+ )
553+
554+ def _impersonate_approvals (self , safe_tx : SafeTx ) -> dict [AddressType , MessageSignature ]:
555+ safe_tx_hash = self .contract .getTransactionHash (* safe_tx )
556+
557+ # Bypass signature collection logic and attempt to submit by impersonation
558+ # NOTE: Only works for fork and local network providers that support `set_storage`
559+ signatures = dict ()
560+ for signer_address in self .signers [: self .confirmations_required ]:
561+ # NOTE: `approvedHashes` is `address => safe_tx_hash => num_confs` @ slot 8
562+ # TODO: Use native ape slot indexing, once available
563+ # e.g. `self.contract.approvedHashes[signer_address][safe_tx_hash] = 1`
564+ address_bytes32 = to_bytes (hexstr = signer_address )
565+ address_bytes32 = b"\x00 " * (32 - len (address_bytes32 )) + address_bytes32
566+ key_hash = keccak (address_bytes32 + b"\x00 " * 31 + to_bytes (8 ))
567+ slot = to_int (keccak (safe_tx_hash + key_hash ))
568+ self .provider .set_storage (self .address , slot , b"\x00 " * 31 + b"\x01 " )
569+
570+ signatures [signer_address ] = self ._preapproved_signature (signer_address )
571+
572+ return signatures
573+
533574 @handle_safe_logic_error ()
534575 def create_execute_transaction (
535576 self ,
536577 safe_tx : SafeTx ,
537578 signatures : Mapping [AddressType , MessageSignature ],
579+ submitter : Union [AccountAPI , AddressType , str , None ] = None ,
580+ impersonate : bool = False ,
538581 ** txn_options ,
539582 ) -> TransactionAPI :
540- exec_args = list (safe_tx ._body_ ["message" ].values ())[:- 1 ] # NOTE: Skip `nonce`
541- encoded_signatures = HexBytes (
542- b"" .join (
543- sig .encode_rsv () if isinstance (sig , MessageSignature ) else sig
544- for sig in order_by_signer (signatures )
545- )
546- )
583+ if impersonate :
584+ signatures = self ._impersonate_approvals (safe_tx )
585+
586+ elif len (signatures ) < self .confirmations_required :
587+ raise NotEnoughSignatures (self .confirmations_required , len (signatures ))
588+
589+ if not isinstance (submitter , AccountAPI ):
590+ submitter = self .load_submitter (submitter )
591+ assert isinstance (submitter , AccountAPI ) # NOTE: mypy happy
592+
593+ exec_args = _safe_tx_exec_args (safe_tx )[:- 1 ] # NOTE: Skip `nonce`
594+ encoded_signatures = encode_signatures (signatures )
547595
548596 # NOTE: executes a `ProviderAPI.prepare_transaction`, which may produce `ContractLogicError`
549597 return self .contract .execTransaction .as_transaction (
550- * exec_args , encoded_signatures , ** txn_options
598+ * exec_args ,
599+ encoded_signatures ,
600+ ** txn_options ,
601+ sender = submitter ,
551602 )
552603
553604 def compute_prev_signer (self , signer : Union [str , AddressType , "BaseAddress" ]) -> AddressType :
@@ -606,56 +657,6 @@ def estimate_gas_cost(self, **kwargs) -> int:
606657 or 0
607658 )
608659
609- def _preapproved_signature (
610- self , signer : Union [AddressType , "BaseAddress" , str ]
611- ) -> MessageSignature :
612- # Get the Safe-style "preapproval" signature type, which is a sentinel value used to denote
613- # when a signer approved via some other method, such as `approveHash` or being `msg.sender`
614- # TODO: Link documentation for this
615- return MessageSignature (
616- v = 1 , # Approved hash (e.g. submitter is approved)
617- r = b"\x00 " * 12 + to_bytes (hexstr = self .conversion_manager .convert (signer , AddressType )),
618- s = b"\x00 " * 32 ,
619- )
620-
621- @handle_safe_logic_error ()
622- def _impersonated_call (self , txn : TransactionAPI , ** safe_tx_and_call_kwargs ) -> ReceiptAPI :
623- safe_tx = self .create_safe_tx (txn , ** safe_tx_and_call_kwargs )
624- safe_tx_exec_args = _safe_tx_exec_args (safe_tx )
625- signatures = {}
626-
627- # Bypass signature collection logic and attempt to submit by impersonation
628- # NOTE: Only works for fork and local network providers that support `set_storage`
629- safe_tx_hash = self .contract .getTransactionHash (* safe_tx_exec_args )
630- signer_address = None
631- for signer_address in self .signers [: self .confirmations_required ]:
632- # NOTE: `approvedHashes` is `address => safe_tx_hash => num_confs` @ slot 8
633- # TODO: Use native ape slot indexing, once available
634- address_bytes32 = to_bytes (hexstr = signer_address )
635- address_bytes32 = b"\x00 " * (32 - len (address_bytes32 )) + address_bytes32
636- key_hash = keccak (address_bytes32 + b"\x00 " * 31 + to_bytes (8 ))
637- slot = to_int (keccak (safe_tx_hash + key_hash ))
638- self .provider .set_storage (self .address , slot , to_bytes (1 ))
639-
640- signatures [signer_address ] = self ._preapproved_signature (signer_address )
641-
642- # NOTE: Could raise a `SafeContractError`
643- safe_tx_and_call_kwargs ["sender" ] = safe_tx_and_call_kwargs .get (
644- "submitter" ,
645- # NOTE: Use whatever the last signer was if no `submitter`
646- self .account_manager .test_accounts [signer_address ],
647- )
648- return self .contract .execTransaction (
649- * safe_tx_exec_args [:- 1 ], # NOTE: Skip nonce
650- HexBytes (
651- b"" .join (
652- sig .encode_rsv () if isinstance (sig , MessageSignature ) else sig
653- for sig in order_by_signer (signatures )
654- )
655- ),
656- ** safe_tx_and_call_kwargs ,
657- )
658-
659660 @handle_safe_logic_error ()
660661 def call ( # type: ignore[override]
661662 self ,
@@ -665,13 +666,11 @@ def call( # type: ignore[override]
665666 ) -> ReceiptAPI :
666667 # NOTE: This handles if given `submit=None'.
667668 default_submit = not impersonate
668- submit = (
669+ call_kwargs [ " submit" ] = (
669670 call_kwargs .pop ("submit_transaction" , call_kwargs .pop ("submit" , default_submit ))
670671 or not default_submit
671672 )
672- call_kwargs ["submit" ] = submit
673- if impersonate :
674- return self ._impersonated_call (txn , ** call_kwargs )
673+ call_kwargs ["impersonate" ] = impersonate
675674
676675 return super ().call (txn , ** call_kwargs )
677676
@@ -699,8 +698,7 @@ def get_api_confirmations(
699698 }
700699
701700 def _contract_approvals (self , safe_tx : SafeTx ) -> Mapping [AddressType , MessageSignature ]:
702- safe_tx_exec_args = _safe_tx_exec_args (safe_tx )
703- safe_tx_hash = self .contract .getTransactionHash (* safe_tx_exec_args )
701+ safe_tx_hash = self .contract .getTransactionHash (* safe_tx )
704702
705703 return {
706704 signer : self ._preapproved_signature (signer )
@@ -715,10 +713,11 @@ def _all_approvals(self, safe_tx: SafeTx) -> dict[AddressType, MessageSignature]
715713 approvals .update (self ._contract_approvals (safe_tx ))
716714 return approvals
717715
716+ @handle_safe_logic_error ()
718717 def submit_safe_tx (
719718 self ,
720719 safe_tx : Union [SafeTx , SafeTxID ],
721- submitter : Union [ AccountAPI , AddressType , str , None ] = None ,
720+ impersonate : bool = False ,
722721 ** txn_options ,
723722 ) -> ReceiptAPI :
724723 """
@@ -740,16 +739,22 @@ def submit_safe_tx(
740739
741740 else :
742741 safe_tx_id = get_safe_tx_hash (safe_tx )
743-
744742 assert isinstance (safe_tx , (SafeTxV1 , SafeTxV2 ))
745- signatures = self ._all_approvals (safe_tx )
746- txn = self .create_execute_transaction (safe_tx , signatures , ** txn_options )
747743
748- if not isinstance (submitter , AccountAPI ):
749- submitter = self .load_submitter (submitter )
750- assert isinstance (submitter , AccountAPI ) # NOTE: mypy happy
744+ if impersonate :
745+ signatures = {}
751746
752- return submitter .call (txn )
747+ elif len (signatures := self ._all_approvals (safe_tx )) < self .confirmations_required :
748+ signatures = self .add_signatures (safe_tx )
749+
750+ txn = self .create_execute_transaction (
751+ safe_tx ,
752+ signatures ,
753+ impersonate = impersonate ,
754+ ** txn_options ,
755+ )
756+
757+ return self .provider .send_transaction (txn )
753758
754759 def sign_transaction (
755760 self ,
@@ -856,13 +861,8 @@ def skip_signer(signer: AccountAPI):
856861 safe_tx ,
857862 sigs_by_signer ,
858863 ** gas_args ,
859- nonce = submitter_account .nonce ,
860- # NOTE: Because of `ape_ethereum.transactions.BaseTransaction.serialize_transaction`
861- # doing a recovered signer check, and we have to make sure the address matches
862- # the recovered address of the signed transaction.
863- sender = submitter_account .address ,
864+ submitter = submitter_account ,
864865 )
865- # NOTE: If `submitter` does not sign, this returns `None` which raises downstream
866866 return submitter_account .sign_transaction (exec_transaction , ** signer_options )
867867
868868 elif submit :
0 commit comments