From 2b4c695ec9f0d41a50a5abad338a27711e852220 Mon Sep 17 00:00:00 2001 From: fubuloubu <3859395+fubuloubu@users.noreply.github.com> Date: Mon, 11 Mar 2024 22:40:11 -0400 Subject: [PATCH] feat(account): add method to propose a SafeTx directly --- ape_safe/accounts.py | 61 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/ape_safe/accounts.py b/ape_safe/accounts.py index 795271f..5f9a0bc 100644 --- a/ape_safe/accounts.py +++ b/ape_safe/accounts.py @@ -2,7 +2,7 @@ import os from collections.abc import Iterable, Iterator, Mapping from pathlib import Path -from typing import Any, Dict, Optional, Union, cast +from typing import Any, Optional, Union, cast from ape.api import AccountAPI, AccountContainerAPI, ReceiptAPI, TransactionAPI from ape.api.address import BaseAddress @@ -181,7 +181,7 @@ def _get_path(self, alias: str) -> Path: def get_signatures( safe_tx_hash: str, signers: Iterable[AccountAPI], -) -> Dict[AddressType, MessageSignature]: +) -> dict[AddressType, MessageSignature]: signatures: Dict[AddressType, MessageSignature] = {} for signer in signers: message = encode_defunct(hexstr=safe_tx_hash) @@ -373,6 +373,48 @@ def create_safe_tx(self, txn: Optional[TransactionAPI] = None, **safe_tx_kwargs) } return self.safe_tx_def(**safe_tx) + def all_delegates(self) -> Iterator[AddressType]: + for delegates in self.client.get_delegates().values(): + yield from delegates + + def propose_safe_tx( + self, + safe_tx: SafeTx, + submitter: Union[AccountAPI, AddressType, str, None] = None, + sigs_by_signer: Optional[dict[AddressType, MessageSignature]] = None, + contractTransactionHash: Optional[SafeTxID] = None, + ) -> SafeTxID: + """ + Propose a transaction to the Safe API client + """ + if not contractTransactionHash: + contractTransactionHash = get_safe_tx_hash(safe_tx) + + if not sigs_by_signer: + sigs_by_signer = {} + + if submitter is not None and not isinstance(submitter, AccountAPI): + submitter = self.load_submitter(submitter) + + if ( + submitter is not None + and submitter.address not in sigs_by_signer + and len(sigs_by_signer) < self.confirmations_required + and (submitter.address in self.signers or submitter.address in self.all_delegates()) + ): + if sig := submitter.sign_message(safe_tx): + sigs_by_signer[submitter.address] = sig + + # NOTE: Signatures don't have to be in order for Safe API post + self.client.post_transaction( + safe_tx, + sigs_by_signer, + sender=submitter.address if submitter else None, + contractTransactionHash=contractTransactionHash, + ) + + return contractTransactionHash + def pending_transactions(self) -> Iterator[tuple[SafeTx, list[SafeTxConfirmation]]]: for executed_tx in self.client.get_transactions(confirmed=False): yield self.create_safe_tx( @@ -544,7 +586,7 @@ def call( # type: ignore[override] return super().call(txn, **call_kwargs) - def get_api_confirmations(self, safe_tx: SafeTx) -> Dict[AddressType, MessageSignature]: + def get_api_confirmations(self, safe_tx: SafeTx) -> dict[AddressType, MessageSignature]: safe_tx_id = get_safe_tx_hash(safe_tx) try: client_confirmations = self.client.get_confirmations(safe_tx_id) @@ -569,7 +611,7 @@ def _contract_approvals(self, safe_tx: SafeTx) -> Mapping[AddressType, MessageSi if self.contract.approvedHashes(signer, safe_tx_hash) > 0 } - def _all_approvals(self, safe_tx: SafeTx) -> Dict[AddressType, MessageSignature]: + def _all_approvals(self, safe_tx: SafeTx) -> dict[AddressType, MessageSignature]: approvals = self.get_api_confirmations(safe_tx) # NOTE: Do this last because it should take precedence @@ -620,7 +662,7 @@ def sign_transaction( submit (bool): The option to submit the transaction. Defaults to ``True``. submitter (Union[``AccountAPI``, ``AddressType``, str, None]): Determine who is submitting the transaction. Defaults to ``None``. - skip (Optional[List[Union[``AccountAPI, `AddressType``, str]]]): + skip (Optional[list[Union[``AccountAPI, `AddressType``, str]]]): Allow bypassing any specified signer. Defaults to ``None``. signatures_required (Optional[int]): The amount of signers required to confirm the transaction. Defaults to ``None``. @@ -720,12 +762,11 @@ def skip_signer(signer: AccountAPI): f"for Safe {self.address}#{safe_tx.nonce}" # TODO: put URI ) - # NOTE: Signatures don't have to be in order for Safe API post - self.client.post_transaction( + self.propose_safe_tx( safe_tx, - sigs_by_signer, + submitter=submitter_account, + sigs_by_signer=sigs_by_signer, contractTransactionHash=safe_tx_hash, - sender=submitter_account.address, ) # Return None so that Ape does not try to submit the transaction. @@ -733,7 +774,7 @@ def skip_signer(signer: AccountAPI): def add_signatures( self, safe_tx: SafeTx, confirmations: Optional[list[SafeTxConfirmation]] = None - ) -> Dict[AddressType, MessageSignature]: + ) -> dict[AddressType, MessageSignature]: confirmations = confirmations or [] if not self.local_signers: raise ApeSafeError("Cannot sign without local signers.")