11from io import BytesIO
2- from typing import TYPE_CHECKING
2+ from typing import TYPE_CHECKING , Any
33
44from ape import convert
55from ape .types import AddressType , HexBytes
66from ape .utils import ManagerAccessMixin , cached_property
77from 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
1013from .packages import MANIFESTS_BY_VERSION , PackageType , get_multisend
1114
1215if 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