Skip to content

Commit ba45353

Browse files
committed
fix(Multisend): create multisend as SafeTx directly; add .propose
1 parent f9daa06 commit ba45353

1 file changed

Lines changed: 63 additions & 39 deletions

File tree

ape_safe/multisend.py

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
from io import BytesIO
2-
from typing import TYPE_CHECKING
2+
from typing import TYPE_CHECKING, Any
33

44
from ape import convert
55
from ape.types import AddressType, HexBytes
66
from ape.utils import ManagerAccessMixin, cached_property
77
from eth_abi.packed import encode_packed
88

9-
from .exceptions import UnsupportedChainError, ValueRequired
9+
from ape_safe.client.types import OperationType, SafeTxID
10+
11+
from .accounts import SafeAccount
12+
from .exceptions import ApeSafeException, UnsupportedChainError, ValueRequired
1013
from .packages import MANIFESTS_BY_VERSION, PackageType, get_multisend
1114

1215
if TYPE_CHECKING:
1316
from ape.api import ReceiptAPI, TransactionAPI
1417
from ape.contracts.base import ContractInstance, ContractTransactionHandler
18+
from eip712.common import SafeTx
1519
from packaging.version import Version
1620

1721

@@ -99,7 +103,8 @@ def add(
99103
self,
100104
call,
101105
*args,
102-
value=0,
106+
value: int = 0,
107+
operation: OperationType = OperationType.CALL,
103108
) -> "MultiSend":
104109
"""
105110
Append a call to the MultiSend session object.
@@ -110,30 +115,26 @@ def add(
110115
Args:
111116
call: :class:`ContractMethodHandler` The method to call.
112117
*args: The arguments to invoke the method with.
113-
value: int The amount of ether to forward with the call.
118+
value: int The amount of ether to forward with the call. Defaults to 0.
119+
operation: :enum:`OperationType` The type of the operation. Defaults to 0.
114120
"""
121+
if value < 0:
122+
raise ValueError("`value=` must be positive.")
123+
115124
self.calls.append(
116125
{
117-
"operation": 0,
126+
"operation": OperationType(operation), # NOTE: Raises `ValueError` if invalid
118127
"target": call.contract.address,
119-
"value": value or 0,
128+
"value": value,
120129
"callData": call.encode_input(*args),
121130
}
122131
)
123132
return self
124133

125-
def _validate_calls(self, **txn_kwargs) -> None:
134+
def _validate_safe_tx(self, safe_tx: "SafeTx") -> None:
126135
required_value = sum(call["value"] for call in self.calls)
127-
if required_value > 0:
128-
if "value" not in txn_kwargs:
129-
raise ValueRequired(required_value)
130-
131-
value = self.conversion_manager.convert(txn_kwargs["value"], int)
132-
133-
if required_value < value:
134-
raise ValueRequired(required_value)
135-
136-
# NOTE: Won't fail if `value` is provided otherwise (won't do anything either)
136+
if required_value > safe_tx.value:
137+
raise ValueRequired(required_value)
137138

138139
@property
139140
def encoded_calls(self):
@@ -151,7 +152,47 @@ def encoded_calls(self):
151152
for call in self.calls
152153
]
153154

154-
def __call__(self, **txn_kwargs) -> "ReceiptAPI":
155+
def as_safe_tx(self, safe: SafeAccount, **safe_tx_kwargs) -> "SafeTx":
156+
return safe.safe_tx_def( # type: ignore[call-arg]
157+
to=self.contract.address,
158+
data=self.handler.encode_input(b"".join(self.encoded_calls)),
159+
operation=OperationType.DELEGATECALL,
160+
nonce=safe_tx_kwargs.pop("nonce", None) or safe.new_nonce,
161+
**safe_tx_kwargs,
162+
)
163+
164+
def propose(
165+
self,
166+
safe: SafeAccount,
167+
**safe_tx_kwargs,
168+
) -> SafeTxID:
169+
submitter = safe_tx_kwargs.pop("submitter", None)
170+
safe_tx = self.as_safe_tx(safe, **safe_tx_kwargs)
171+
self._validate_safe_tx(safe_tx)
172+
return safe.propose_safe_tx(safe_tx, submitter=submitter)
173+
174+
def as_transaction(
175+
self, sender: Any = None, impersonate: bool = False, **txn_kwargs
176+
) -> "TransactionAPI":
177+
"""
178+
Encode the MultiSend transaction as a ``TransactionAPI`` object, but do not execute it.
179+
180+
Returns:
181+
:class:`~ape.api.transactions.TransactionAPI`
182+
"""
183+
if not isinstance(sender, SafeAccount):
184+
raise ApeSafeException("Must be a SafeAccount to use Multisend")
185+
186+
safe_tx_kwargs = {}
187+
for field in sender.safe_tx_def.__annotations__:
188+
if field in txn_kwargs:
189+
safe_tx_kwargs[field] = txn_kwargs.pop(field)
190+
191+
safe_tx = self.as_safe_tx(sender, **safe_tx_kwargs)
192+
signatures = {} if impersonate else sender.add_signatures(safe_tx)
193+
return sender.create_execute_transaction(safe_tx, signatures=signatures, **txn_kwargs)
194+
195+
def __call__(self, sender: Any = None, **txn_kwargs) -> "ReceiptAPI":
155196
"""
156197
Execute the MultiSend transaction. The transaction will broadcast again every time
157198
the ``Transaction`` object is called.
@@ -166,27 +207,10 @@ def __call__(self, **txn_kwargs) -> "ReceiptAPI":
166207
Returns:
167208
:class:`~ape.api.transactions.ReceiptAPI`
168209
"""
169-
self._validate_calls(**txn_kwargs)
170-
if "operation" not in txn_kwargs and not txn_kwargs.get("impersonate", False):
171-
txn_kwargs["operation"] = 1
172-
return self.handler(b"".join(self.encoded_calls), **txn_kwargs)
173-
174-
def as_transaction(self, **txn_kwargs) -> "TransactionAPI":
175-
"""
176-
Encode the MultiSend transaction as a ``TransactionAPI`` object, but do not execute it.
177-
178-
Returns:
179-
:class:`~ape.api.transactions.TransactionAPI`
180-
"""
181-
self._validate_calls(**txn_kwargs)
182-
# NOTE: Will fail using `self.handler.as_transaction` because handler
183-
# expects to be called only via delegatecall
184-
if "operation" not in txn_kwargs and not txn_kwargs.get("impersonate", False):
185-
txn_kwargs["operation"] = 1
186-
return self.network_manager.ecosystem.create_transaction(
187-
receiver=self.handler.contract.address,
188-
data=self.handler.encode_input(b"".join(self.encoded_calls)),
189-
**txn_kwargs,
210+
impersonate = txn_kwargs.pop("impersonate", False)
211+
return sender.call(
212+
self.as_transaction(sender=sender, impersonate=impersonate, **txn_kwargs),
213+
impersonate=impersonate,
190214
)
191215

192216
def add_from_calldata(self, calldata: bytes):

0 commit comments

Comments
 (0)