Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: new flow [APE-1140] #11

Merged
merged 135 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
925f390
refactor: fix Safe API endpoints
fubuloubu Jun 28, 2023
6811528
refactor: make SafeTxID a HexBytes subclass
fubuloubu Jun 28, 2023
549dfbb
feat: add API wrapper for fetching confirmations
fubuloubu Jun 28, 2023
5fab050
feat: add method for fetching pending confirmations to SafeAccount
fubuloubu Jun 28, 2023
e359dbc
feat: add method for fetching confirmations as signature dict
fubuloubu Jun 28, 2023
8af0812
refactor: allow pending txn CLI to submit txns
fubuloubu Jun 28, 2023
9c1ad3d
refactor: add proper exceptions for Safe API client errors
fubuloubu Jun 28, 2023
0906116
refactor: Use ABC for SafeClient to support Mock
fubuloubu Jun 28, 2023
10932e5
feat: add SafeClient mock (for testing)
fubuloubu Jun 28, 2023
f707648
refactor: add client origin string to POST requests
fubuloubu Jun 28, 2023
3c6d3b8
test: use new Ape mock events comparison API
fubuloubu Jun 28, 2023
87ef9f0
fix: wrong signature encoding
fubuloubu Jun 29, 2023
20a9038
refactor: move signature ordering algorithm to utils
fubuloubu Jun 29, 2023
5457a0b
fix: load UnexecutedSafeTxData from SafeTx properly
fubuloubu Jun 29, 2023
e7fe1e8
fix: handle scenario where modules/guard is not supported by safe ver
fubuloubu Jun 29, 2023
79d6924
refactor: add api confirmations to all confs, handle client error
fubuloubu Jun 29, 2023
fe2c762
refactor: extract submitter loader into it's own method
fubuloubu Jun 29, 2023
89e7f47
feat: add utility function for submitting already conf'd SafeTx
fubuloubu Jun 29, 2023
1990cf5
feat: submit to Safe API if not submitting transaction
fubuloubu Jun 29, 2023
14b3318
test: show skipping submit can be executed from mock client
fubuloubu Jun 29, 2023
e94c2d4
Merge branch 'main' into feat/safe-api
dtdang Sep 7, 2023
3b86495
fix: change safe_tx_hash to hex value
dtdang Sep 7, 2023
f1dc4ba
docs: add more documentation
dtdang Sep 8, 2023
ef095b6
fix: empty raises
dtdang Sep 8, 2023
ba27c8e
fix: reivew comments
dtdang Sep 14, 2023
3aef2ad
fix: mock client transaction hash
dtdang Sep 15, 2023
a9316d4
fix: cli pending transactions
dtdang Sep 18, 2023
15fe09b
refactor: cli
antazoey Sep 19, 2023
8ec71d8
refactor: small fixes
antazoey Sep 19, 2023
31b86e6
fix: submit kwarg fix
antazoey Sep 19, 2023
cc416d3
feat: sign on pending
antazoey Sep 19, 2023
07d5c5b
fix: make requests work again
antazoey Sep 19, 2023
3c40b5d
chore: small fixes
antazoey Sep 19, 2023
bd361f9
fix: remove unnecessary confirmation error
dtdang Sep 20, 2023
6f79d35
docs: add service url notes
dtdang Sep 20, 2023
d2f0658
fix: exception handler response
dtdang Sep 20, 2023
1acec78
feat: add safe tx argument
dtdang Sep 20, 2023
fac48e3
fix: change default operation delegatecall
dtdang Sep 20, 2023
fe27eb7
chore: mypy
dtdang Sep 22, 2023
806d811
fix: multisend operation kwarg
dtdang Sep 26, 2023
1578a24
chore: mypy
dtdang Sep 26, 2023
880f433
fix: txn_kwargs impersonate
dtdang Sep 28, 2023
33ab14d
test: multisend operation
dtdang Oct 12, 2023
662d690
chore: mockclient mypy
dtdang Oct 12, 2023
a9f2646
chore: change to union
dtdang Oct 12, 2023
a8c3985
fix: remove extra parenthesis
dtdang Oct 12, 2023
f5b1e4d
fix: network fixture
dtdang Oct 12, 2023
cb47135
fix: foundry and alchemy
dtdang Oct 12, 2023
83f0c6e
fix: test dependencies location
dtdang Oct 25, 2023
af65c12
fix: remove get_args
dtdang Oct 25, 2023
254bfea
fix: confirmations required for signing
dtdang Oct 25, 2023
84b63d1
fix: add network warning
dtdang Oct 25, 2023
ef0bf21
fix: remove comment for url
dtdang Oct 25, 2023
43c06d8
refactor: multisend fixture
dtdang Oct 25, 2023
688d02a
fix: remove version pins
dtdang Oct 25, 2023
d5a98fa
fix: access data using .get
dtdang Oct 25, 2023
4ae4fcc
docs: fix syntax mistake
dtdang Oct 25, 2023
dd50fa0
fix: update call comment
dtdang Oct 25, 2023
d855479
fix: issues with pending
antazoey Oct 31, 2023
d4ed17a
fix: cli fixes
antazoey Oct 31, 2023
0d17dfe
feat: better forked net handling
antazoey Oct 31, 2023
b9a43b2
fix: cli parens
antazoey Oct 31, 2023
5a2197f
docs: add raises doc
antazoey Oct 31, 2023
796f917
docs: add raises doc
antazoey Oct 31, 2023
51d07b6
fix: handle when converted in addresses
antazoey Oct 31, 2023
b085060
docs: say why needed
antazoey Oct 31, 2023
ea5db70
fix: utc now
antazoey Oct 31, 2023
5b79704
feat: snake case ify
antazoey Oct 31, 2023
beb2719
feat: better vm checks
antazoey Oct 31, 2023
16bafe7
refacor: switch to assert
antazoey Oct 31, 2023
ec24330
fix: regression issues
antazoey Oct 31, 2023
940d0eb
refactor: use key
antazoey Oct 31, 2023
d289942
chore: use doc str for short help
antazoey Nov 1, 2023
7728910
chore: lint deps
antazoey Nov 1, 2023
7c7d8b9
refactor: switch to npm dep
antazoey Nov 1, 2023
5c340c7
chore: bump ape
antazoey Nov 3, 2023
dec9009
chore: delete hacks
antazoey Nov 3, 2023
3a8c806
test: call with cls
antazoey Nov 3, 2023
e7617f0
chore: del stuff
antazoey Nov 3, 2023
cd31696
test: del alchemy
antazoey Nov 3, 2023
1ffd543
refactor: more submitter callback
antazoey Nov 3, 2023
b91bac4
feat: wip
antazoey Nov 27, 2023
aded29d
refactor: and fix
antazoey Nov 28, 2023
0b2421f
docs: update docs
antazoey Nov 28, 2023
70840ba
fix: part of hash fix
antazoey Nov 29, 2023
b8d3cbb
feat: execute cli
antazoey Nov 30, 2023
31e04da
chore: print when no tx
antazoey Nov 30, 2023
a512399
refactor: allow x sigs
antazoey Dec 2, 2023
3eff332
chore: highlight problem
antazoey Dec 2, 2023
cc0aeb6
feat: sepolia support
antazoey Dec 4, 2023
7575821
fix: signature
antazoey Dec 4, 2023
10db013
fix: critical trailing slash
antazoey Dec 4, 2023
22d1fed
fix: tx getting and id issue
antazoey Dec 5, 2023
69d582b
feat: way better list cmds
antazoey Dec 5, 2023
a416b2d
fix: get_api_confs fix
antazoey Dec 5, 2023
bc41763
refactor: use eip712 hash calc
antazoey Dec 6, 2023
f7c87b1
fix: title
antazoey Dec 6, 2023
650a05f
feat: allow hashes in cli
antazoey Dec 6, 2023
a06a831
feat: allow approving x
antazoey Dec 6, 2023
ad3935b
fix: figured out how to sign
antazoey Dec 6, 2023
3886024
fix: CLI txn ids and err imprv
antazoey Dec 6, 2023
968c732
fix: issue with submit
antazoey Dec 6, 2023
6fd8a2e
fix: improve tx verbose disp
antazoey Dec 6, 2023
c06f0f2
fix: type issue
antazoey Dec 7, 2023
c6a21ea
feat: propose and reject 'fix'
antazoey Dec 7, 2023
b07d7a9
fix: issues with post_tx
antazoey Dec 7, 2023
a40b42b
fix: txns work now
antazoey Dec 7, 2023
b06b3fb
chore: bump ape
antazoey Dec 11, 2023
aa932f9
fix: metadata setup
antazoey Dec 11, 2023
0b804ba
fix: type fixes
antazoey Dec 11, 2023
d50a2fa
fix: flake8 fix
antazoey Dec 11, 2023
95749d0
fix: more type ignores
antazoey Dec 11, 2023
e7b0340
fix: pydantic 2 issue
antazoey Dec 11, 2023
7d753c2
fix: force to pin pydantic :(
antazoey Dec 11, 2023
7fbff68
chore: upgrade to 0.7
antazoey Dec 20, 2023
adf4e39
feat: complete upgrade
antazoey Dec 20, 2023
377deef
chore: bump ape
antazoey Dec 20, 2023
72b372a
refactor: sigs
antazoey Dec 20, 2023
c7ceb77
chore: more 07 upgrade
antazoey Dec 20, 2023
8f737b1
chore: spacing fix
antazoey Dec 20, 2023
ce487f3
refactor: use a lambda
antazoey Dec 20, 2023
2ac779f
test: add tests for order
antazoey Dec 20, 2023
9f5c939
fix: fixes
antazoey Dec 20, 2023
2a0a883
fix: more tx fix and refactor
antazoey Dec 20, 2023
d2ae0c6
feat: allow nonce in cli
antazoey Dec 21, 2023
d9709cc
fix: more deprecation fix
antazoey Dec 21, 2023
27aa7ef
test: init cli test
antazoey Dec 21, 2023
252208e
fix: issue with 38 39 cli annotated type
antazoey Dec 21, 2023
368a400
feat: default safe config
antazoey Dec 21, 2023
b99c065
fix: remove alias arg instead of opt
antazoey Dec 21, 2023
7a324c6
feat: use skip conf opt
antazoey Dec 21, 2023
b3c8f29
fix: issue with pending when had no safes
antazoey Dec 21, 2023
c1acab2
test: no tx test
antazoey Dec 21, 2023
f590a55
fix: issues with network in list cmd
antazoey Dec 21, 2023
178b4cb
docs: much improve guide
antazoey Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions ape_safe/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,34 @@ def remove(cli_ctx, alias):
cls=NetworkBoundCommand, short_help="See pending transactions for a locally-tracked Safe"
)
@network_option()
@click.option("sign_with_local_signers", "--sign", is_flag=True)
@click.option("--execute", is_flag=False, flag_value=True, default=False)
antazoey marked this conversation as resolved.
Show resolved Hide resolved
@existing_alias_argument(account_type=SafeAccount)
def pending(network, alias):
def pending(network, sign_with_local_signers, execute, alias):
safe = accounts.load(alias)
local_signers = set(signer for signer in accounts if signer.address in safe.signers)
if local_signers:
click.echo("Local Signer(s) detected!")
sign_with_local_signers = click.confirm("Do you want to sign unconfirmed transactions")

else:
sign_with_local_signers = False
if isinstance(execute, str):
antazoey marked this conversation as resolved.
Show resolved Hide resolved
if execute in accounts.aliases:
dtdang marked this conversation as resolved.
Show resolved Hide resolved
submitter = accounts.load(execute)
else:
raise

elif execute is True:
submitter = safe.local_signers[0]
antazoey marked this conversation as resolved.
Show resolved Hide resolved

for txn in safe.client.get_transactions(
starting_nonce=safe.next_nonce,
filter_by_missing_signers=local_signers if sign_with_local_signers else None,
):
click.echo(f"Txn {txn.nonce}: ({len(txn.confirmations)}/{txn.confirmationsRequired})")
for safe_tx, confirmations in safe.pending_transactions():
if sign_with_local_signers and len(confirmations) < safe.confirmations_required:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if executing with a local signer, you don't need all N confirmations to submit, only N - 1

Copy link
Member

@antazoey antazoey Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but if there is 2/3 signers and I still want to sign, I pass --sign but now it won't actually let me sign (with the change), but when the code was like this, it would.

In general we are missing warnings / handling of that case, even in 3/3 signers... so will add something now..
but it is weird to not let me sign when there is room for more signatures and I am specifically requested to sign.

pass # TODO: sign `safe_tx` with local signers not in `confirmations`
antazoey marked this conversation as resolved.
Show resolved Hide resolved

else:
click.echo(f"Txn {safe_tx.nonce}: ({len(confirmations)}/{safe.confirmations_required})")

if execute is not False:
dtdang marked this conversation as resolved.
Show resolved Hide resolved
signatures = safe.get_confirmations(safe_tx)
dtdang marked this conversation as resolved.
Show resolved Hide resolved
if len(signatures) >= safe.confirmations_required and click.confirm(
f"Submit Txn {safe_tx.nonce}"
):
submitter.call(safe.create_execute_transaction(safe_tx, signatures))
dtdang marked this conversation as resolved.
Show resolved Hide resolved


@cli.command(cls=NetworkBoundCommand, short_help="Reject one or more pending transactions")
dtdang marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
123 changes: 84 additions & 39 deletions ape_safe/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@
from ape.utils import ZERO_ADDRESS, cached_property
from ape_ethereum.transactions import TransactionType
from eip712.common import create_safe_tx_def
from eip712.messages import hash_eip712_message
from eth_utils import keccak, to_bytes, to_int
from ethpm_types import ContractType

from .client import SafeClient, SafeTx
from .exceptions import NoLocalSigners, NotASigner, NotEnoughSignatures, handle_safe_logic_error
from .client import BaseSafeClient, MockSafeClient, SafeClient, SafeTx, SafeTxConfirmation
from .exceptions import (
NoLocalSigners,
NotASigner,
NotEnoughSignatures,
SafeClientException,
handle_safe_logic_error,
)
from .utils import order_by_signer
dtdang marked this conversation as resolved.
Show resolved Hide resolved


class AccountContainer(AccountContainerAPI):
Expand Down Expand Up @@ -101,10 +109,13 @@ def fallback_handler(self) -> Optional[ContractInstance]:
return None

@cached_property
def client(self) -> SafeClient:
def client(self) -> BaseSafeClient:
if self.provider.chain_id not in self.account_file["deployed_chain_ids"]:
raise # Not valid on this chain

if self.provider.network.name == "local":
return MockSafeClient(contract=self.contract)

return SafeClient(address=self.address, chain_id=self.provider.chain_id)

@property
Expand Down Expand Up @@ -171,6 +182,10 @@ def create_safe_tx(self, txn: Optional[TransactionAPI] = None, **safe_tx_kwargs)

return self.safe_tx_def(**safe_tx)

def pending_transactions(self) -> Iterator[Tuple[SafeTx, List[SafeTxConfirmation]]]:
for unexec_tx_data in self.client.get_transactions(confirmed=False):
yield self.create_safe_tx(**unexec_tx_data.dict()), unexec_tx_data.confirmations

@property
def local_signers(self) -> List[AccountAPI]:
# NOTE: Is not ordered by signing order
Expand Down Expand Up @@ -199,7 +214,9 @@ def create_execute_transaction(
**txn_options,
) -> TransactionAPI:
exec_args = list(safe_tx._body_["message"].values())[:-1] # NOTE: Skip `nonce`
encoded_signatures = self._encode_signatures(signatures)
encoded_signatures = HexBytes(
b"".join(sig.encode_rsv() for sig in order_by_signer(signatures))
)

# NOTE: executes a `ProviderAPI.prepare_transaction`, which may produce `ContractLogicError`
return self.contract.execTransaction.as_transaction(
Expand All @@ -223,21 +240,32 @@ def compute_prev_signer(self, signer: Union[str, AddressType, BaseAddress]) -> A
# NOTE: SENTINEL_OWNERS is the "previous" address to index 0
return AddressType("0x0000000000000000000000000000000000000001") # type: ignore[arg-type]

def load_submitter(
self,
submitter: Union[AddressType, str, None] = None,
) -> AccountAPI:
if submitter is None:
if len(self.local_signers) == 0:
raise NoLocalSigners()
fubuloubu marked this conversation as resolved.
Show resolved Hide resolved

return self.local_signers[0]

elif (
submitter_address := self.conversion_manager.convert(submitter, AddressType)
in self.account_manager
):
return self.account_manager[submitter_address]

elif isinstance(submitter, str) and submitter in self.account_manager.aliases:
return self.account_manager.load(submitter)

else:
raise # Cannot handle `submitter=type(submitter)`

def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
# NOTE: Need to override `AccountAPI` behavior for balance checks
return self.provider.prepare_transaction(txn)

def _encode_signatures(self, signatures: Dict[AddressType, MessageSignature]) -> HexBytes:
# NOTE: Must order signatures in ascending order of signer address (converted to int)
def addr_to_int(a: AddressType) -> int:
return to_int(hexstr=a)

return HexBytes(
b"".join(
signatures[signer].encode_rsv() for signer in sorted(signatures, key=addr_to_int)
)
)

def _safe_tx_exec_args(self, safe_tx: SafeTx) -> List:
return list(safe_tx._body_["message"].values())

Expand Down Expand Up @@ -281,7 +309,7 @@ def _impersonated_call(self, txn: TransactionAPI, **safe_tx_and_call_kwargs) ->
)
return self.contract.execTransaction(
*safe_tx_exec_args[:-1], # NOTE: Skip nonce
self._encode_signatures(signatures),
HexBytes(b"".join(sig.encode_rsv() for sig in order_by_signer(signatures))),
**safe_tx_and_call_kwargs,
)

Expand All @@ -297,6 +325,20 @@ def call( # type: ignore[override]

dtdang marked this conversation as resolved.
Show resolved Hide resolved
return super().call(txn, **call_kwargs)

def get_api_confirmations(self, safe_tx: SafeTx) -> Dict[AddressType, MessageSignature]:
safe_tx_hash = hash_eip712_message(safe_tx)
try:
client_confs = self.client.get_confirmations(safe_tx_hash)
except SafeClientException:
return {}

return {
conf.owner: MessageSignature(
r=conf.signature[:32], s=conf.signature[32:64], v=conf.signature[64]
)
for conf in client_confs
}

def _contract_approvals(self, safe_tx: SafeTx) -> Dict[AddressType, MessageSignature]:
safe_tx_exec_args = self._safe_tx_exec_args(safe_tx)
safe_tx_hash = self.contract.getTransactionHash(*safe_tx_exec_args)
Expand All @@ -308,8 +350,24 @@ def _contract_approvals(self, safe_tx: SafeTx) -> Dict[AddressType, MessageSigna
}

def _all_approvals(self, safe_tx: SafeTx) -> Dict[AddressType, MessageSignature]:
# TODO: Combine with approvals from SafeAPI
return self._contract_approvals(safe_tx)
approvals = self.get_api_confirmations(safe_tx)
# NOTE: Do this last because it should take precedence
approvals.update(self._contract_approvals(safe_tx))
return approvals

def submit_safe_tx(
self,
safe_tx: SafeTx,
submitter: Union[AccountAPI, AddressType, str, None] = None,
**txn_options,
) -> ReceiptAPI:
signatures = self._all_approvals(safe_tx)
txn = self.create_execute_transaction(safe_tx, signatures, **txn_options)

if not isinstance(submitter, AccountAPI):
submitter = self.load_submitter(submitter)
antazoey marked this conversation as resolved.
Show resolved Hide resolved

return submitter.call(txn)

def sign_transaction(
self,
Expand All @@ -327,25 +385,11 @@ def sign_transaction(
if not submit and submitter:
raise # Cannot specify a submitter if not submitting

elif submit and not submitter:
if len(self.local_signers) == 0:
raise NoLocalSigners()

submitter = self.local_signers[0]
logger.info(f"No submitter specified, so using: {submitter}")

# NOTE: Works whether `submit` is set or not below here
elif (
submitter_address := self.conversion_manager.convert(submitter, AddressType)
in self.account_manager
):
submitter = self.account_manager[submitter_address]

elif isinstance(submitter, str) and submitter in self.account_manager.aliases:
submitter = self.account_manager.load(submitter)

elif not isinstance(submitter, AccountAPI):
raise # Cannot handle `submitter=type(submitter)`
submitter_not_specified = submitter is None
submitter = self.load_submitter(submitter)
antazoey marked this conversation as resolved.
Show resolved Hide resolved
if submitter_not_specified:
logger.info(f"No submitter specified, so using: {submitter}")

# Invariant: `submitter` should be either `AccountAPI` or we are not submitting here
assert isinstance(submitter, AccountAPI) or not submit
Expand Down Expand Up @@ -397,8 +441,7 @@ def skip_signer(signer: AccountAPI):
and len(sigs_by_signer) >= signatures_required
):
# We need to encode the submitter's address for Safe to decode
# NOTE: Should only be triggered if the `submitter` is also a signer
if len(sigs_by_signer) < self.confirmations_required:
if submitter.address in self.signers:
sigs_by_signer[submitter.address] = self._preapproved_signature(submitter)

# Inherit gas args from safe_tx, if set
Expand Down Expand Up @@ -434,5 +477,7 @@ def skip_signer(signer: AccountAPI):
f"Collected {len(sigs_by_signer)}/{self.confirmations_required} signatures "
f"for Safe {self.address}#{safe_tx.nonce}" # TODO: put URI
)
# TODO: Submit safe_tx and sigs to Safe API

# NOTE: Signatures don't have to be in order for Safe API post
self.client.post_transaction(safe_tx, sigs_by_signer)
return None
antazoey marked this conversation as resolved.
Show resolved Hide resolved
Loading