|
1 | 1 | import os |
2 | 2 | from abc import abstractmethod |
3 | 3 | from collections.abc import Iterator |
| 4 | +from contextlib import contextmanager |
4 | 5 | from functools import cached_property |
5 | 6 | from pathlib import Path |
6 | 7 | from typing import TYPE_CHECKING, Any, Optional, Union |
|
18 | 19 | from ape.exceptions import ( |
19 | 20 | AccountsError, |
20 | 21 | AliasAlreadyInUseError, |
| 22 | + APINotImplementedError, |
21 | 23 | ConversionError, |
22 | 24 | MethodNonPayableError, |
23 | 25 | MissingDeploymentBytecodeError, |
@@ -60,9 +62,14 @@ def __dir__(self) -> list[str]: |
60 | 62 | self.__class__.call.__name__, |
61 | 63 | self.__class__.deploy.__name__, |
62 | 64 | self.__class__.prepare_transaction.__name__, |
| 65 | + self.__class__.sign_authorization.__name__, |
63 | 66 | self.__class__.sign_message.__name__, |
64 | 67 | self.__class__.sign_transaction.__name__, |
65 | 68 | self.__class__.transfer.__name__, |
| 69 | + self.__class__.delegate.fget.__name__, # type: ignore[attr-defined] |
| 70 | + self.__class__.set_delegate.__name__, |
| 71 | + self.__class__.remove_delegate.__name__, |
| 72 | + self.__class__.delegate_to.__name__, |
66 | 73 | ] |
67 | 74 |
|
68 | 75 | @property |
@@ -95,16 +102,49 @@ def sign_raw_msghash(self, msghash: "HexBytes") -> Optional[MessageSignature]: |
95 | 102 | Args: |
96 | 103 | msghash (:class:`~eth_pydantic_types.HexBytes`): |
97 | 104 | The message hash to sign. Plugins may or may not support this operation. |
98 | | - Default implementation is to raise ``NotImplementedError``. |
| 105 | + Default implementation is to raise ``APINotImplementedError``. |
99 | 106 |
|
100 | 107 | Returns: |
101 | 108 | :class:`~ape.types.signatures.MessageSignature` (optional): |
102 | 109 | The signature corresponding to the message. |
103 | 110 | """ |
104 | | - raise NotImplementedError( |
| 111 | + raise APINotImplementedError( |
105 | 112 | f"Raw message signing is not supported by '{self.__class__.__name__}'" |
106 | 113 | ) |
107 | 114 |
|
| 115 | + def sign_authorization( |
| 116 | + self, |
| 117 | + address: Any, |
| 118 | + chain_id: Optional[int] = None, |
| 119 | + nonce: Optional[int] = None, |
| 120 | + ) -> Optional[MessageSignature]: |
| 121 | + """ |
| 122 | + Sign an `EIP-7702 <https://eips.ethereum.org/EIPS/eip-7702>`__ Authorization. |
| 123 | +
|
| 124 | + Args: |
| 125 | + address (Any): A delegate address to sign the authorization for. |
| 126 | + chain_id (Optional[int]): |
| 127 | + The chain ID that the authorization should be valid for. |
| 128 | + A value of ``0`` means that the authorization is valid for **any chain**. |
| 129 | + Default tells implementation to use the currently connected network's ``chain_id``. |
| 130 | + nonce (Optional[int]): |
| 131 | + The nonce to use to sign authorization with. Defaults to account's current nonce. |
| 132 | +
|
| 133 | + Returns: |
| 134 | + :class:`~ape.types.signatures.MessageSignature` (optional): |
| 135 | + The signature corresponding to the message. |
| 136 | +
|
| 137 | + ```{caution} |
| 138 | + This action has the capability to be extremely destructive to the signer, and might lead to |
| 139 | + full account compromise. All implementations are recommended to ensure that the signer be |
| 140 | + made aware of the severity and impact of this action through some callout. |
| 141 | + ``` |
| 142 | + """ |
| 143 | + |
| 144 | + raise APINotImplementedError( |
| 145 | + f"Authorization signing is not supported by '{self.__class__.__name__}'" |
| 146 | + ) |
| 147 | + |
108 | 148 | @abstractmethod |
109 | 149 | def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]: |
110 | 150 | """ |
@@ -438,6 +478,107 @@ def get_deployment_address(self, nonce: Optional[int] = None) -> AddressType: |
438 | 478 | nonce = self.nonce if nonce is None else nonce |
439 | 479 | return ecosystem.get_deployment_address(self.address, nonce) |
440 | 480 |
|
| 481 | + def set_delegate(self, contract: Union[BaseAddress, AddressType, str], **txn_kwargs): |
| 482 | + """ |
| 483 | + Have the account class override the value of its ``delegate``. For plugins that support |
| 484 | + this feature, the way they choose to handle it can vary. For example, it could be a call to |
| 485 | + upgrade itself using some built-in method for a smart wallet (with default txn args) e.g. |
| 486 | + the Safe smart wallet (https://github.com/ApeWorX/ape-safe), or it could be to use an EIP- |
| 487 | + 7702-like feature available on the network to set a delegate for that account. However if a |
| 488 | + plugin chooses to handle it, the resulting action (if successful) should make sure that the |
| 489 | + value that ``self.delegate`` returns is the same as ``contract`` after it is completed. |
| 490 | +
|
| 491 | + By default, this method raises ``APINotImplementedError`` signaling that support is not |
| 492 | + available for this feature. Calling this may result in other errors if implemented. |
| 493 | +
|
| 494 | + Args: |
| 495 | + contract (`:class:~ape.contracts.ContractInstance`): |
| 496 | + The contract instance to override the delegate with. |
| 497 | + **txn_kwargs: Additional transaction kwargs passed to |
| 498 | + :meth:`~ape.api.networks.EcosystemAPI.create_transaction`, such as ``gas`` |
| 499 | + ``max_fee``, or ``max_priority_fee``. For a list of available transaction |
| 500 | + kwargs, see :class:`~ape.api.transactions.TransactionAPI`. |
| 501 | + """ |
| 502 | + raise APINotImplementedError |
| 503 | + |
| 504 | + def remove_delegate(self, **txn_kwargs): |
| 505 | + """ |
| 506 | + Has the account class remove the override for the value of its ``delegate``. For plugins |
| 507 | + that support this feature, the way they choose to handle it can vary. For example, on a |
| 508 | + network using an EIP7702-like feature available it will reset the delegate to empty. |
| 509 | + However, if a plugin chooses to handle it, the resulting action (if successful) should |
| 510 | + make sure that the value that ``self.delegate`` returns ``None`` after it is completed. |
| 511 | +
|
| 512 | + By default, this method raises ``APINotImplementedError`` signaling that support is not |
| 513 | + available for this feature. Calling this may result in other errors if implemented. |
| 514 | +
|
| 515 | + Args: |
| 516 | + **txn_kwargs: Additional transaction kwargs passed to |
| 517 | + :meth:`~ape.api.networks.EcosystemAPI.create_transaction`, such as ``gas`` |
| 518 | + ``max_fee``, or ``max_priority_fee``. For a list of available transaction |
| 519 | + kwargs, see :class:`~ape.api.transactions.TransactionAPI`. |
| 520 | + """ |
| 521 | + raise APINotImplementedError |
| 522 | + |
| 523 | + @contextmanager |
| 524 | + def delegate_to( |
| 525 | + self, |
| 526 | + new_delegate: Union[BaseAddress, AddressType, str], |
| 527 | + set_txn_kwargs: Optional[dict] = None, |
| 528 | + reset_txn_kwargs: Optional[dict] = None, |
| 529 | + **txn_kwargs, |
| 530 | + ) -> Iterator[BaseAddress]: |
| 531 | + """ |
| 532 | + Temporarily override the value of ``delegate`` for the account inside of a context manager, |
| 533 | + and yields a contract instance object whose interface matches that of ``new_delegate``. |
| 534 | + This is useful for ensuring that delegation is only temporarily extended to an account when |
| 535 | + doing a critical action temporarily, such as using an EIP7702 delegate module. |
| 536 | +
|
| 537 | + Args: |
| 538 | + new_delegate (`:class:~ape.contracts.ContractInstance`): |
| 539 | + The contract instance to override the `delegate` with. |
| 540 | + set_txn_kwargs (dict | None): Additional transaction kwargs passed to |
| 541 | + :meth:`~ape.api.networks.EcosystemAPI.create_transaction` for the |
| 542 | + :meth:`AccountAPI.set_delegate` method, such as ``gas``, ``max_fee``, or |
| 543 | + ``max_priority_fee``. Overrides the values provided via ``txn_kwargs``. For a list of |
| 544 | + available transaction kwargs, see :class:`~ape.api.transactions.TransactionAPI`. |
| 545 | + reset_txn_kwargs (dict | None): Additional transaction kwargs passed to |
| 546 | + :meth:`~ape.api.networks.EcosystemAPI.create_transaction` for the |
| 547 | + :meth:`AccountAPI.remove_delegate` method, such as ``gas``, ``max_fee``, or |
| 548 | + ``max_priority_fee``. Overrides the values provided via ``txn_kwargs``. For a list of |
| 549 | + available transaction kwargs, see :class:`~ape.api.transactions.TransactionAPI`. |
| 550 | + **txn_kwargs: Additional transaction kwargs passed to |
| 551 | + :meth:`~ape.api.networks.EcosystemAPI.create_transaction`, such as ``gas`` |
| 552 | + ``max_fee``, or ``max_priority_fee``. For a list of available transaction |
| 553 | + kwargs, see :class:`~ape.api.transactions.TransactionAPI`. |
| 554 | +
|
| 555 | + Returns: |
| 556 | + `:class:~ape.contracts.ContractInstance`: |
| 557 | + The contract instance of this account with the interface of `contract`. |
| 558 | + """ |
| 559 | + set_txn_kwargs = {**txn_kwargs, **(set_txn_kwargs or {})} |
| 560 | + existing_delegate = self.delegate |
| 561 | + |
| 562 | + self.set_delegate(new_delegate, **set_txn_kwargs) |
| 563 | + |
| 564 | + # NOTE: Do not cache this type as it is temporary |
| 565 | + from ape.contracts import ContractInstance |
| 566 | + |
| 567 | + # This is helpful for using it immediately to send things as self |
| 568 | + with self.account_manager.use_sender(self): |
| 569 | + if isinstance(new_delegate, ContractInstance): |
| 570 | + # NOTE: Do not cache this |
| 571 | + yield ContractInstance(self.address, contract_type=new_delegate.contract_type) |
| 572 | + |
| 573 | + else: |
| 574 | + yield self |
| 575 | + |
| 576 | + reset_txn_kwargs = {**txn_kwargs, **(reset_txn_kwargs or {})} |
| 577 | + if existing_delegate: |
| 578 | + self.set_delegate(existing_delegate, **reset_txn_kwargs) |
| 579 | + else: |
| 580 | + self.remove_delegate(**reset_txn_kwargs) |
| 581 | + |
441 | 582 |
|
442 | 583 | class AccountContainerAPI(BaseInterfaceModel): |
443 | 584 | """ |
@@ -534,7 +675,7 @@ def append(self, account: AccountAPI): |
534 | 675 | self.__setitem__(account.address, account) |
535 | 676 |
|
536 | 677 | def __setitem__(self, address: AddressType, account: AccountAPI): |
537 | | - raise NotImplementedError("Must define this method to use `container.append(acct)`.") |
| 678 | + raise APINotImplementedError("Must define this method to use `container.append(acct)`.") |
538 | 679 |
|
539 | 680 | def remove(self, account: AccountAPI): |
540 | 681 | """ |
@@ -563,7 +704,7 @@ def __delitem__(self, address: AddressType): |
563 | 704 | Args: |
564 | 705 | address (:class:`~ape.types.address.AddressType`): The address of the account to delete. |
565 | 706 | """ |
566 | | - raise NotImplementedError("Must define this method to use `container.remove(acct)`.") |
| 707 | + raise APINotImplementedError("Must define this method to use `container.remove(acct)`.") |
567 | 708 |
|
568 | 709 | def __contains__(self, address: AddressType) -> bool: |
569 | 710 | """ |
@@ -689,7 +830,7 @@ def address(self) -> AddressType: |
689 | 830 | return self.raw_address |
690 | 831 |
|
691 | 832 | def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]: |
692 | | - raise NotImplementedError("This account cannot sign messages") |
| 833 | + raise APINotImplementedError("This account cannot sign messages") |
693 | 834 |
|
694 | 835 | def sign_transaction(self, txn: TransactionAPI, **signer_options) -> Optional[TransactionAPI]: |
695 | 836 | # Returns input transaction unsigned (since it doesn't have access to the key) |
|
0 commit comments