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

feat: add delegates #41

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

feat: add delegates #41

wants to merge 22 commits into from

Conversation

fubuloubu
Copy link
Member

@fubuloubu fubuloubu commented Mar 11, 2024

What I did

Adds support for delegates to the Client classes (including MockClient), the CLI (via ape safe delegates ...), as well as to the main accounts class (SafeAccount.propose_safe_tx and SafeAccount.propose)

fixes: #16

How I did it

How to verify it

Checklist

  • Passes all linting checks (pre-commit and CI jobs)
  • New test cases have been added and are passing
    - [ ] Documentation has been updated
  • PR title follows Conventional Commit standard (will be automatically included in the changelog)

Copy link
Member

@antazoey antazoey left a comment

Choose a reason for hiding this comment

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

Pretty great PR! I had some nits.

Is there anything particular you want me to verify? I can take it for a spin soon.

ape_safe/_cli/delegates.py Outdated Show resolved Hide resolved
ape_safe/_cli/delegates.py Outdated Show resolved Hide resolved
for delegates in self.client.get_delegates().values():
yield from delegates

def propose_safe_tx(
Copy link
Member

Choose a reason for hiding this comment

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

the propose cli method could probably be refactored to utilize this now

Copy link
Member Author

Choose a reason for hiding this comment

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

Looked into this, but seems more complicated than I'm willing to deal with now

Think I will try to refactor that CLI method later

ape_safe/client/mock.py Show resolved Hide resolved
ape_safe/client/mock.py Outdated Show resolved Hide resolved
ape_safe/client/mock.py Outdated Show resolved Hide resolved
tests/functional/test_delegates.py Show resolved Hide resolved
@fubuloubu
Copy link
Member Author

Is there anything particular you want me to verify? I can take it for a spin soon.

If you can try adding and removing a delegate, I've been doing it and it works for me but curious on the experience (which may improve with ApeWorX/ape#1966)

@antazoey
Copy link
Member

antazoey commented Mar 15, 2024

Thoughts from testing:

  • (future item) I wish there was a way for network detection to be smarter in the safe. I purposely named my safes the same alias as the one generated in the UI, like laughing-sepolia-safe... It'd be cool maybe if could detect sepolia somehow and my default sepolia provider. It feel redundant to have to specify the network. Maybe the network can be included in the safe account data.
  • .ens addresses are not handled in the submitter callback, causing a bug in the add CLI... the callback ends up returning None , and then the line safe.client.add_delegate(delegate, label, signer) passes None for signer and you get an error about it not being a KeyfileAccount (because it is None).
  • Bug: if you have the same account added in multiple plugins, and you pass in an address value for submitter, it should try and pick the KeyfileAccount first, else it fails because it uses the same account from a different plugin and you get the ActionNotPerformedError("Need access to private key for this method.")... work around is to use the alias
  • Similarly to the ens thing above, if you pass any str for delegator that is not an account, it ends up as None and the error doesnt make a ton of sense, could be improved.
  • Good news! I got it to work. The output is nice too:
Signer 0xE3747e6341E0d3430e6Ea9e2346cdDCc2F8a4b5b:
- 0x3a45d3C998476AF7191588332b8a49a8ad8CfbE6

but I am wondering if we would wanto enrich those values, like if they are known accounst, use aliases here?

btw I would fix the second and fourth items and make tickets for the first and third items

@antazoey
Copy link
Member

Related to those, on both add / remove, if you slightly mispell the alias, it just comes back as None and tell you to use a keyfile account again and i was confused for a bit.

i think most of the annoyance stems from the submitter callback

@antazoey
Copy link
Member

ohhh , one more thing i just noticed. When i do:

ape safe delegates list <safe> --network <net>

for a safe with no delegates, it outputs:

ERROR (ape-safe): No delegates for 0x872Df5f6CF2C2Eaa9e7511AB079D263AB8fdc79f (laughing-sepolia-safe)

feedback: Error seems a bit extreme here. Maybe drop it down to warning. no harm in not having delegates, just was checking it got removed.

@fubuloubu
Copy link
Member Author

Maybe the network can be included in the safe account data.

Yes I want to do this too. There are some ways to deploy a safe at the same address on all networks, I have done this for one of our safes but not the other. But still need to detect if it is deployed on the network you wish to use

@fubuloubu
Copy link
Member Author

Bug: if you have the same account added in multiple plugins, and you pass in an address value for submitter, it should try and pick the KeyfileAccount first, else it fails because it uses the same account from a different plugin and you get the ActionNotPerformedError("Need access to private key for this method.")... work around is to use the alias

I am working on a PR for core that'll bypass this issue, and then I'll make it accept a wider range of inputs so you can be more specific with it

@fubuloubu
Copy link
Member Author

feedback: Error seems a bit extreme here. Maybe drop it down to warning. no harm in not having delegates, just was checking it got removed.

Agree, should really just be INFO

@antazoey
Copy link
Member

@fubuloubu the signing features we wanted are in Core now, we should update this and get this out.

@fubuloubu
Copy link
Member Author

ohhh , one more thing i just noticed. When i do:

ape safe delegates list <safe> --network <net>

for a safe with no delegates, it outputs:

ERROR (ape-safe): No delegates for 0x872Df5f6CF2C2Eaa9e7511AB079D263AB8fdc79f (laughing-sepolia-safe)

feedback: Error seems a bit extreme here. Maybe drop it down to warning. no harm in not having delegates, just was checking it got removed.

eddd1f8

@fubuloubu
Copy link
Member Author

@fubuloubu the signing features we wanted are in Core now, we should update this and get this out.

7d8cafd

@fubuloubu
Copy link
Member Author

fubuloubu commented Jun 5, 2024

btw I would fix the second and fourth items and make tickets for the first and third items

2, 3, and 4 solved with 2148cf0, I made issues for the 1st one

@@ -373,6 +373,60 @@ 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]:
Copy link
Contributor

@banteg banteg Jun 12, 2024

Choose a reason for hiding this comment

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

generators are bad ux in interactive sessions.

change to delegates property and make it return a list or a dict of address->label

moreover, since delegates is a client feature, it should only live in SafeClient exclusively, so might as well remove it from SafeAccount.

Copy link
Member

Choose a reason for hiding this comment

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

we can have a method for the iterator such as get_all_delegates() and have the delegates property use the iterator to make a dict

Copy link
Contributor

Choose a reason for hiding this comment

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

my other point is delegates is not a feature of a safe wallet, but rather a feature of a safe backend api, this it should not be in SafeAccount at all, but rather in SafeClient.

it's completely off-chain and only affects the backend accepting txs from non-owners.

@@ -185,6 +188,53 @@ def estimate_gas_cost(
gas = result.get("safeTxGas")
return int(HexBytes(gas).hex(), 16)

def get_delegates(self) -> Dict[AddressType, List[AddressType]]:
Copy link
Contributor

Choose a reason for hiding this comment

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

labels got lost along the way, they are quite useful imo. the current anonymous structure is not immediately clear. maybe it should be a dict of delegates to their metadata like label and delegator. currently delegator is the key, but it's not the primary info here. or it could be a list[dataclass] where str() casts to address.


return delegates

def add_delegate(self, delegate: AddressType, label: str, delegator: AccountAPI):
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of delegator we can say owner, should be less confusing.

"""
Add a delegate for a signer in a Safe
"""
delegate = cli_ctx.conversion_manager.convert(delegate, AddressType)
Copy link
Member

Choose a reason for hiding this comment

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

this could be part of the argument:

@click.argument("delegate", callback=lambda ctx, _, val: ctx.obj.conversion_manager.convert(delegate, AddressType))

@delegates.command(cls=ConnectedProviderCommand)
@safe_cli_ctx()
@safe_option
@click.argument("delegate", type=ChecksumAddress)
Copy link
Member

Choose a reason for hiding this comment

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

type=ChecksumAddress is a bit wrong, since this implicitly handles ENS strings and such.

@delegates.command(cls=ConnectedProviderCommand)
@safe_cli_ctx()
@safe_option
@click.argument("delegate", type=ChecksumAddress)
Copy link
Member

Choose a reason for hiding this comment

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

same here re: the type is incorrect (since ENS domains work because we are converting them?)
this may just all be better with the callback validating the value into an address beforehand.

Copy link
Member

Choose a reason for hiding this comment

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

(and line 60)

@@ -373,6 +373,60 @@ 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]:
Copy link
Member

Choose a reason for hiding this comment

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

we can have a method for the iterator such as get_all_delegates() and have the delegates property use the iterator to make a dict

safe_tx: SafeTx,
submitter: Union[AccountAPI, AddressType, str, None] = None,
sigs_by_signer: Optional[dict[AddressType, MessageSignature]] = None,
contractTransactionHash: Optional[SafeTxID] = None,
Copy link
Member

Choose a reason for hiding this comment

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

a note for why this has to be camelCase would be helpful

@@ -186,6 +189,53 @@ def estimate_gas_cost(
gas = result.get("safeTxGas")
return int(HexBytes(gas).hex(), 16)

def get_delegates(self) -> Dict[AddressType, List[AddressType]]:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def get_delegates(self) -> Dict[AddressType, List[AddressType]]:
def get_delegates(self) -> dict[AddressType, list[AddressType]]:

data = response.json()

for delegate_info in map(DelegateInfo.model_validate, data.get("results", [])):
if delegate_info.delegator not in delegates:
Copy link
Member

Choose a reason for hiding this comment

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

idea: we can use defaultdict again here

"delegate": delegate,
"delegator": delegator.address,
"label": label,
"signature": sig.encode_rsv().hex(),
Copy link
Member

Choose a reason for hiding this comment

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

warning: when we move to web3.py 7.0 series, this will not longer have a 0x prefix. I have been trying to use different ways of going to hex-str now in preparation for this

"delegator": delegator.address,
"signature": sig.encode_rsv().hex(),
}
self._delete(f"delegates/{delegate}", json=payload)
Copy link
Member

Choose a reason for hiding this comment

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

warning: trailing slashes are important i think for all of Safe's APIs

Copy link
Contributor

Choose a reason for hiding this comment

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

true, the api states it needs a trailing slash https://safe-transaction-mainnet.safe.global/

@@ -15,6 +17,21 @@
TESTS_DIR = Path(__file__).parent.absolute()


@pytest.fixture(autouse=True)
def fix_eip712_signing(monkeypatch):
# TODO: `ape_test.TestAccount.sign_message` doesn't support `EIP712Message` yet
Copy link
Member

Choose a reason for hiding this comment

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

it does now!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add delegate support [APE-1293]
3 participants