opinion-clob-sdk 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opinion-clob-sdk might be problematic. Click here for more details.
- opinion_clob_sdk/__init__.py +1 -1
- opinion_clob_sdk/opinion_clob_sdk/__init__.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contract_caller.py +390 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contracts/conditional_tokens.py +707 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/contracts/erc20.py +111 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/exception.py +11 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/base_builder.py +41 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/exception.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder.py +90 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder_test.py +40 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/constants.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order.py +254 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order_type.py +9 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/sides.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/signatures.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/signer.py +20 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +109 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/constants.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/eip712/__init__.py +176 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/enums.py +6 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/exceptions.py +94 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/multisend.py +347 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe.py +141 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/compatibility_fallback_handler_v1_3_0.py +327 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/multisend_v1_3_0.py +22 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/safe_v1_3_0.py +1035 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/utils.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_signature.py +364 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_test.py +37 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_tx.py +437 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/signatures.py +63 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/typing.py +17 -0
- opinion_clob_sdk/opinion_clob_sdk/chain/safe/utils.py +218 -0
- opinion_clob_sdk/opinion_clob_sdk/config.py +4 -0
- opinion_clob_sdk/opinion_clob_sdk/model.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/__init__.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contract_caller.py +390 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/conditional_tokens.py +707 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/contracts/erc20.py +111 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/exception.py +11 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/base_builder.py +41 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/exception.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder.py +90 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/builders/order_builder_test.py +40 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/constants.py +2 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order.py +254 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/order_type.py +9 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/sides.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/model/signatures.py +8 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/signer.py +20 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/py_order_utils/utils.py +109 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/constants.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/eip712/__init__.py +176 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/enums.py +6 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/exceptions.py +94 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/multisend.py +347 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe.py +141 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/__init__.py +0 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/compatibility_fallback_handler_v1_3_0.py +327 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/multisend_v1_3_0.py +22 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/safe_v1_3_0.py +1035 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_contracts/utils.py +26 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_signature.py +364 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_test.py +37 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/safe_tx.py +437 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/signatures.py +63 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/typing.py +17 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/chain/safe/utils.py +218 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/config.py +4 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/model.py +19 -0
- opinion_clob_sdk/opinion_clob_sdk/opinion_clob_sdk/sdk.py +947 -0
- opinion_clob_sdk/opinion_clob_sdk/sdk.py +947 -0
- opinion_clob_sdk/sdk.py +24 -17
- {opinion_clob_sdk-0.1.1.dist-info → opinion_clob_sdk-0.1.3.dist-info}/METADATA +1 -1
- opinion_clob_sdk-0.1.3.dist-info/RECORD +130 -0
- opinion_clob_sdk-0.1.1.dist-info/RECORD +0 -46
- {opinion_clob_sdk-0.1.1.dist-info → opinion_clob_sdk-0.1.3.dist-info}/WHEEL +0 -0
- {opinion_clob_sdk-0.1.1.dist-info → opinion_clob_sdk-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from typing import Any, Dict, List, NoReturn, Optional, Tuple, Type
|
|
3
|
+
|
|
4
|
+
from eth_account import Account
|
|
5
|
+
from eth_typing import ChecksumAddress
|
|
6
|
+
from hexbytes import HexBytes
|
|
7
|
+
from packaging.version import Version
|
|
8
|
+
from web3 import Web3
|
|
9
|
+
from web3.contract.contract import ContractFunction
|
|
10
|
+
from web3.exceptions import Web3Exception
|
|
11
|
+
from web3.types import BlockIdentifier, Nonce, TxParams, Wei
|
|
12
|
+
|
|
13
|
+
from .constants import NULL_ADDRESS, TxSpeed
|
|
14
|
+
from .safe_contracts.utils import get_safe_contract
|
|
15
|
+
from .eip712 import eip712_encode
|
|
16
|
+
|
|
17
|
+
from .utils import fast_keccak
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
CouldNotFinishInitialization,
|
|
20
|
+
CouldNotPayGasWithEther,
|
|
21
|
+
CouldNotPayGasWithToken,
|
|
22
|
+
HashHasNotBeenApproved,
|
|
23
|
+
InvalidContractSignatureLocation,
|
|
24
|
+
InvalidInternalTx,
|
|
25
|
+
InvalidMultisigTx,
|
|
26
|
+
InvalidOwnerProvided,
|
|
27
|
+
InvalidSignaturesProvided,
|
|
28
|
+
MethodCanOnlyBeCalledFromThisContract,
|
|
29
|
+
ModuleManagerException,
|
|
30
|
+
NotEnoughSafeTransactionGas,
|
|
31
|
+
OnlyOwnersCanApproveAHash,
|
|
32
|
+
OwnerManagerException,
|
|
33
|
+
SafeTransactionFailedWhenGasPriceAndSafeTxGasEmpty,
|
|
34
|
+
SignatureNotProvidedByOwner,
|
|
35
|
+
SignaturesDataTooShort,
|
|
36
|
+
ThresholdNeedsToBeDefined,
|
|
37
|
+
)
|
|
38
|
+
from .safe_signature import SafeSignature
|
|
39
|
+
from .signatures import signature_to_bytes
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SafeTx:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
w3: Web3,
|
|
46
|
+
safe_address: ChecksumAddress,
|
|
47
|
+
to: Optional[ChecksumAddress],
|
|
48
|
+
value: int,
|
|
49
|
+
data: bytes,
|
|
50
|
+
operation: int,
|
|
51
|
+
safe_tx_gas: int,
|
|
52
|
+
base_gas: int,
|
|
53
|
+
gas_price: int,
|
|
54
|
+
gas_token: Optional[ChecksumAddress],
|
|
55
|
+
refund_receiver: Optional[ChecksumAddress],
|
|
56
|
+
signatures: Optional[bytes] = None,
|
|
57
|
+
safe_nonce: Optional[int] = None,
|
|
58
|
+
safe_version: Optional[str] = None,
|
|
59
|
+
chain_id: Optional[int] = None,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
:param w3:
|
|
63
|
+
:param safe_address:
|
|
64
|
+
:param to:
|
|
65
|
+
:param value:
|
|
66
|
+
:param data:
|
|
67
|
+
:param operation:
|
|
68
|
+
:param safe_tx_gas:
|
|
69
|
+
:param base_gas:
|
|
70
|
+
:param gas_price:
|
|
71
|
+
:param gas_token:
|
|
72
|
+
:param refund_receiver:
|
|
73
|
+
:param signatures:
|
|
74
|
+
:param safe_nonce: Current nonce of the Safe. If not provided, it will be retrieved from network
|
|
75
|
+
:param safe_version: Safe version 1.0.0 renamed `baseGas` to `dataGas`. Safe version 1.3.0 added `chainId` to
|
|
76
|
+
the `domainSeparator`. If not provided, it will be retrieved from network
|
|
77
|
+
:param chain_id: Ethereum network chain_id is used in hash calculation for Safes >= 1.3.0. If not provided,
|
|
78
|
+
it will be retrieved from the provided ethereum_client
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
self.w3 = w3
|
|
82
|
+
self.safe_address = safe_address
|
|
83
|
+
self.to = to or NULL_ADDRESS
|
|
84
|
+
self.value = int(value)
|
|
85
|
+
self.data = HexBytes(data) if data else b""
|
|
86
|
+
self.operation = int(operation)
|
|
87
|
+
self.safe_tx_gas = int(safe_tx_gas)
|
|
88
|
+
self.base_gas = int(base_gas)
|
|
89
|
+
self.gas_price = int(gas_price)
|
|
90
|
+
self.gas_token = gas_token or NULL_ADDRESS
|
|
91
|
+
self.refund_receiver = refund_receiver or NULL_ADDRESS
|
|
92
|
+
self.signatures = signatures or b""
|
|
93
|
+
self._safe_nonce = safe_nonce and int(safe_nonce)
|
|
94
|
+
self._safe_version = safe_version
|
|
95
|
+
self._chain_id = chain_id and int(chain_id)
|
|
96
|
+
|
|
97
|
+
self.tx: Optional[TxParams] = None # If executed, `tx` is set
|
|
98
|
+
self.tx_hash: Optional[bytes] = None # If executed, `tx_hash` is set
|
|
99
|
+
|
|
100
|
+
def __str__(self):
|
|
101
|
+
return (
|
|
102
|
+
f"SafeTx - safe={self.safe_address} - to={self.to} - value={self.value} - data={self.data.hex()} - "
|
|
103
|
+
f"operation={self.operation} - safe-tx-gas={self.safe_tx_gas} - base-gas={self.base_gas} - "
|
|
104
|
+
f"gas-price={self.gas_price} - gas-token={self.gas_token} - refund-receiver={self.refund_receiver} - "
|
|
105
|
+
f"signers = {self.signers}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@cached_property
|
|
109
|
+
def contract(self):
|
|
110
|
+
return get_safe_contract(self.w3, address=self.safe_address)
|
|
111
|
+
|
|
112
|
+
@cached_property
|
|
113
|
+
def chain_id(self) -> int:
|
|
114
|
+
if self._chain_id is not None:
|
|
115
|
+
return self._chain_id
|
|
116
|
+
else:
|
|
117
|
+
return self.w3.eth.chain_id
|
|
118
|
+
|
|
119
|
+
@cached_property
|
|
120
|
+
def safe_nonce(self) -> int:
|
|
121
|
+
if self._safe_nonce is not None:
|
|
122
|
+
return self._safe_nonce
|
|
123
|
+
else:
|
|
124
|
+
return self.contract.functions.nonce().call()
|
|
125
|
+
|
|
126
|
+
@cached_property
|
|
127
|
+
def safe_version(self) -> str:
|
|
128
|
+
if self._safe_version is not None:
|
|
129
|
+
return self._safe_version
|
|
130
|
+
else:
|
|
131
|
+
return self.contract.functions.VERSION().call()
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def eip712_structured_data(self) -> Dict[str, Any]:
|
|
135
|
+
safe_version = Version(self.safe_version)
|
|
136
|
+
|
|
137
|
+
# Safes >= 1.0.0 Renamed `baseGas` to `dataGas`
|
|
138
|
+
base_gas_key = "baseGas" if safe_version >= Version("1.0.0") else "dataGas"
|
|
139
|
+
|
|
140
|
+
types = {
|
|
141
|
+
"EIP712Domain": [{"name": "verifyingContract", "type": "address"}],
|
|
142
|
+
"SafeTx": [
|
|
143
|
+
{"name": "to", "type": "address"},
|
|
144
|
+
{"name": "value", "type": "uint256"},
|
|
145
|
+
{"name": "data", "type": "bytes"},
|
|
146
|
+
{"name": "operation", "type": "uint8"},
|
|
147
|
+
{"name": "safeTxGas", "type": "uint256"},
|
|
148
|
+
{"name": base_gas_key, "type": "uint256"},
|
|
149
|
+
{"name": "gasPrice", "type": "uint256"},
|
|
150
|
+
{"name": "gasToken", "type": "address"},
|
|
151
|
+
{"name": "refundReceiver", "type": "address"},
|
|
152
|
+
{"name": "nonce", "type": "uint256"},
|
|
153
|
+
],
|
|
154
|
+
}
|
|
155
|
+
message = {
|
|
156
|
+
"to": self.to,
|
|
157
|
+
"value": self.value,
|
|
158
|
+
"data": self.data,
|
|
159
|
+
"operation": self.operation,
|
|
160
|
+
"safeTxGas": self.safe_tx_gas,
|
|
161
|
+
base_gas_key: self.base_gas,
|
|
162
|
+
"dataGas": self.base_gas,
|
|
163
|
+
"gasPrice": self.gas_price,
|
|
164
|
+
"gasToken": self.gas_token,
|
|
165
|
+
"refundReceiver": self.refund_receiver,
|
|
166
|
+
"nonce": self.safe_nonce,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
payload: Dict[str, Any] = {
|
|
170
|
+
"types": types,
|
|
171
|
+
"primaryType": "SafeTx",
|
|
172
|
+
"domain": {"verifyingContract": self.safe_address},
|
|
173
|
+
"message": message,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Enable chainId from v1.3.0 onwards
|
|
177
|
+
if safe_version >= Version("1.3.0"):
|
|
178
|
+
payload["domain"]["chainId"] = self.chain_id
|
|
179
|
+
types["EIP712Domain"].insert(0, {"name": "chainId", "type": "uint256"})
|
|
180
|
+
|
|
181
|
+
return payload
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def safe_tx_hash_preimage(self) -> HexBytes:
|
|
185
|
+
return HexBytes(b"".join(eip712_encode(self.eip712_structured_data)))
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def safe_tx_hash(self) -> HexBytes:
|
|
189
|
+
return HexBytes(fast_keccak(self.safe_tx_hash_preimage))
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def signers(self) -> List[str]:
|
|
193
|
+
if not self.signatures:
|
|
194
|
+
return []
|
|
195
|
+
else:
|
|
196
|
+
return [
|
|
197
|
+
safe_signature.owner
|
|
198
|
+
for safe_signature in SafeSignature.parse_signature(
|
|
199
|
+
self.signatures, self.safe_tx_hash
|
|
200
|
+
)
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def sorted_signers(self):
|
|
205
|
+
return sorted(self.signers, key=lambda x: int(x, 16))
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def w3_tx(self) -> ContractFunction:
|
|
209
|
+
"""
|
|
210
|
+
:return: Web3 contract tx prepared for `call`, `transact` or `build_transaction`
|
|
211
|
+
"""
|
|
212
|
+
return self.contract.functions.execTransaction(
|
|
213
|
+
self.to,
|
|
214
|
+
self.value,
|
|
215
|
+
self.data,
|
|
216
|
+
self.operation,
|
|
217
|
+
self.safe_tx_gas,
|
|
218
|
+
self.base_gas,
|
|
219
|
+
self.gas_price,
|
|
220
|
+
self.gas_token,
|
|
221
|
+
self.refund_receiver,
|
|
222
|
+
self.signatures,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _raise_safe_vm_exception(self, message: str) -> NoReturn:
|
|
226
|
+
error_with_exception: Dict[str, Type[InvalidMultisigTx]] = {
|
|
227
|
+
# https://github.com/safe-global/safe-contracts/blob/v1.3.0/docs/error_codes.md
|
|
228
|
+
"GS000": CouldNotFinishInitialization,
|
|
229
|
+
"GS001": ThresholdNeedsToBeDefined,
|
|
230
|
+
"Could not pay gas costs with ether": CouldNotPayGasWithEther,
|
|
231
|
+
"GS011": CouldNotPayGasWithEther,
|
|
232
|
+
"Could not pay gas costs with token": CouldNotPayGasWithToken,
|
|
233
|
+
"GS012": CouldNotPayGasWithToken,
|
|
234
|
+
"GS013": SafeTransactionFailedWhenGasPriceAndSafeTxGasEmpty,
|
|
235
|
+
"Hash has not been approved": HashHasNotBeenApproved,
|
|
236
|
+
"Hash not approved": HashHasNotBeenApproved,
|
|
237
|
+
"GS025": HashHasNotBeenApproved,
|
|
238
|
+
"Invalid contract signature location: data not complete": InvalidContractSignatureLocation,
|
|
239
|
+
"GS023": InvalidContractSignatureLocation,
|
|
240
|
+
"Invalid contract signature location: inside static part": InvalidContractSignatureLocation,
|
|
241
|
+
"GS021": InvalidContractSignatureLocation,
|
|
242
|
+
"Invalid contract signature location: length not present": InvalidContractSignatureLocation,
|
|
243
|
+
"GS022": InvalidContractSignatureLocation,
|
|
244
|
+
"Invalid contract signature provided": InvalidContractSignatureLocation,
|
|
245
|
+
"GS024": InvalidContractSignatureLocation,
|
|
246
|
+
"Invalid owner provided": InvalidOwnerProvided,
|
|
247
|
+
"Invalid owner address provided": InvalidOwnerProvided,
|
|
248
|
+
"GS026": InvalidOwnerProvided,
|
|
249
|
+
"Invalid signatures provided": InvalidSignaturesProvided,
|
|
250
|
+
"Not enough gas to execute safe transaction": NotEnoughSafeTransactionGas,
|
|
251
|
+
"GS010": NotEnoughSafeTransactionGas,
|
|
252
|
+
"Only owners can approve a hash": OnlyOwnersCanApproveAHash,
|
|
253
|
+
"GS030": OnlyOwnersCanApproveAHash,
|
|
254
|
+
"GS031": MethodCanOnlyBeCalledFromThisContract,
|
|
255
|
+
"Signature not provided by owner": SignatureNotProvidedByOwner,
|
|
256
|
+
"Signatures data too short": SignaturesDataTooShort,
|
|
257
|
+
"GS020": SignaturesDataTooShort,
|
|
258
|
+
# ModuleManager
|
|
259
|
+
"GS100": ModuleManagerException,
|
|
260
|
+
"Invalid module address provided": ModuleManagerException,
|
|
261
|
+
"GS101": ModuleManagerException,
|
|
262
|
+
"GS102": ModuleManagerException,
|
|
263
|
+
"Invalid prevModule, module pair provided": ModuleManagerException,
|
|
264
|
+
"GS103": ModuleManagerException,
|
|
265
|
+
"Method can only be called from an enabled module": ModuleManagerException,
|
|
266
|
+
"GS104": ModuleManagerException,
|
|
267
|
+
"Module has already been added": ModuleManagerException,
|
|
268
|
+
# OwnerManager
|
|
269
|
+
"Address is already an owner": OwnerManagerException,
|
|
270
|
+
"GS200": OwnerManagerException, # Owners have already been setup
|
|
271
|
+
"GS201": OwnerManagerException, # Threshold cannot exceed owner count
|
|
272
|
+
"GS202": OwnerManagerException, # Invalid owner address provided
|
|
273
|
+
"GS203": OwnerManagerException, # Invalid ower address provided
|
|
274
|
+
"GS204": OwnerManagerException, # Address is already an owner
|
|
275
|
+
"GS205": OwnerManagerException, # Invalid prevOwner, owner pair provided
|
|
276
|
+
"Invalid prevOwner, owner pair provided": OwnerManagerException,
|
|
277
|
+
"New owner count needs to be larger than new threshold": OwnerManagerException,
|
|
278
|
+
"Threshold cannot exceed owner count": OwnerManagerException,
|
|
279
|
+
"Threshold needs to be greater than 0": OwnerManagerException,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for reason, custom_exception in error_with_exception.items():
|
|
283
|
+
if reason in message:
|
|
284
|
+
raise custom_exception(message)
|
|
285
|
+
raise InvalidMultisigTx(message)
|
|
286
|
+
|
|
287
|
+
def call(
|
|
288
|
+
self,
|
|
289
|
+
tx_sender_address: Optional[str] = None,
|
|
290
|
+
tx_gas: Optional[int] = None,
|
|
291
|
+
block_identifier: Optional[BlockIdentifier] = "latest",
|
|
292
|
+
) -> int:
|
|
293
|
+
"""
|
|
294
|
+
:param tx_sender_address:
|
|
295
|
+
:param tx_gas: Force a gas limit
|
|
296
|
+
:param block_identifier:
|
|
297
|
+
:return: `1` if everything ok
|
|
298
|
+
"""
|
|
299
|
+
parameters: TxParams = {
|
|
300
|
+
"from": tx_sender_address if tx_sender_address else self.safe_address
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if tx_gas:
|
|
304
|
+
parameters["gas"] = tx_gas
|
|
305
|
+
try:
|
|
306
|
+
success = self.w3_tx.call(
|
|
307
|
+
parameters,
|
|
308
|
+
block_identifier=block_identifier
|
|
309
|
+
if block_identifier is not None
|
|
310
|
+
else "latest",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if not success:
|
|
314
|
+
raise InvalidInternalTx(
|
|
315
|
+
"Success bit is %d, should be equal to 1" % success
|
|
316
|
+
)
|
|
317
|
+
return success
|
|
318
|
+
except (Web3Exception, ValueError) as exc:
|
|
319
|
+
# e.g. web3.exceptions.ContractLogicError: execution reverted: Invalid owner provided
|
|
320
|
+
return self._raise_safe_vm_exception(str(exc))
|
|
321
|
+
except ValueError as exc: # Parity
|
|
322
|
+
"""
|
|
323
|
+
Parity throws a ValueError, e.g.
|
|
324
|
+
{'code': -32015,
|
|
325
|
+
'message': 'VM execution error.',
|
|
326
|
+
'data': 'Reverted 0x08c379a0000000000000000000000000000000000000000000000000000000000000020000000000000000
|
|
327
|
+
000000000000000000000000000000000000000000000001b496e76616c6964207369676e6174757265732070726f7669
|
|
328
|
+
6465640000000000'
|
|
329
|
+
}
|
|
330
|
+
"""
|
|
331
|
+
error_dict = exc.args[0]
|
|
332
|
+
data = error_dict.get("data")
|
|
333
|
+
if data and isinstance(data, str) and "Reverted " in data:
|
|
334
|
+
# Parity
|
|
335
|
+
result = HexBytes(data.replace("Reverted ", ""))
|
|
336
|
+
return self._raise_safe_vm_exception(str(result))
|
|
337
|
+
else:
|
|
338
|
+
raise exc
|
|
339
|
+
|
|
340
|
+
def recommended_gas(self) -> Wei:
|
|
341
|
+
"""
|
|
342
|
+
:return: Recommended gas to use on the ethereum_tx
|
|
343
|
+
"""
|
|
344
|
+
return Wei(self.base_gas + self.safe_tx_gas + 75000)
|
|
345
|
+
|
|
346
|
+
def execute(
|
|
347
|
+
self,
|
|
348
|
+
tx_sender_private_key: str,
|
|
349
|
+
tx_gas: Optional[int] = None,
|
|
350
|
+
tx_gas_price: Optional[int] = None,
|
|
351
|
+
tx_nonce: Optional[int] = None,
|
|
352
|
+
block_identifier: Optional[BlockIdentifier] = "latest",
|
|
353
|
+
eip1559_speed: Optional[TxSpeed] = None,
|
|
354
|
+
) -> Tuple[HexBytes, TxParams]:
|
|
355
|
+
"""
|
|
356
|
+
Send multisig tx to the Safe
|
|
357
|
+
|
|
358
|
+
:param tx_sender_private_key: Sender private key
|
|
359
|
+
:param tx_gas: Gas for the external tx. If not, `(safe_tx_gas + base_gas) * 2` will be used
|
|
360
|
+
:param tx_gas_price: Gas price of the external tx. If not, `gas_price` will be used
|
|
361
|
+
:param tx_nonce: Force nonce for `tx_sender`
|
|
362
|
+
:param block_identifier: `latest` or `pending`
|
|
363
|
+
:param eip1559_speed: If provided, use EIP1559 transaction
|
|
364
|
+
:return: Tuple(tx_hash, tx)
|
|
365
|
+
:raises: InvalidMultisigTx: If user tx cannot go through the Safe
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
sender_account = Account.from_key(tx_sender_private_key)
|
|
369
|
+
if eip1559_speed and self.ethereum_client.is_eip1559_supported():
|
|
370
|
+
tx_parameters = self.ethereum_client.set_eip1559_fees(
|
|
371
|
+
{
|
|
372
|
+
"from": sender_account.address,
|
|
373
|
+
},
|
|
374
|
+
tx_speed=eip1559_speed,
|
|
375
|
+
)
|
|
376
|
+
else:
|
|
377
|
+
tx_parameters = {
|
|
378
|
+
"from": sender_account.address,
|
|
379
|
+
"gasPrice": Wei(tx_gas_price)
|
|
380
|
+
if tx_gas_price
|
|
381
|
+
else self.w3.eth.gas_price,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if tx_gas:
|
|
385
|
+
tx_parameters["gas"] = tx_gas
|
|
386
|
+
if tx_nonce is not None:
|
|
387
|
+
tx_parameters["nonce"] = Nonce(tx_nonce)
|
|
388
|
+
|
|
389
|
+
self.tx = self.w3_tx.build_transaction(tx_parameters)
|
|
390
|
+
self.tx["gas"] = Wei(
|
|
391
|
+
tx_gas or (max(self.tx["gas"] + 75000, self.recommended_gas()))
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
self.tx_hash = self.ethereum_client.send_unsigned_transaction(
|
|
395
|
+
self.tx,
|
|
396
|
+
private_key=sender_account.key,
|
|
397
|
+
retry=False if tx_nonce is not None else True,
|
|
398
|
+
block_identifier=block_identifier,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Set signatures empty after executing the tx. `Nonce` is increased even if it fails,
|
|
402
|
+
# so signatures are not valid anymore
|
|
403
|
+
self.signatures = b""
|
|
404
|
+
return self.tx_hash, self.tx
|
|
405
|
+
|
|
406
|
+
def sign(self, private_key: str) -> bytes:
|
|
407
|
+
"""
|
|
408
|
+
{bytes32 r}{bytes32 s}{uint8 v}
|
|
409
|
+
:param private_key:
|
|
410
|
+
:return: Signature
|
|
411
|
+
"""
|
|
412
|
+
account = Account.from_key(private_key)
|
|
413
|
+
signature_dict = account.unsafe_sign_hash(self.safe_tx_hash)
|
|
414
|
+
signature = signature_to_bytes(
|
|
415
|
+
signature_dict["v"], signature_dict["r"], signature_dict["s"]
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Insert signature sorted
|
|
419
|
+
if account.address not in self.signers:
|
|
420
|
+
unsorted_signatures = SafeSignature.parse_signature(
|
|
421
|
+
self.signatures + signature, self.safe_tx_hash
|
|
422
|
+
)
|
|
423
|
+
self.signatures = SafeSignature.export_signatures(unsorted_signatures)
|
|
424
|
+
|
|
425
|
+
return signature
|
|
426
|
+
|
|
427
|
+
def unsign(self, address: str) -> bool:
|
|
428
|
+
current_tx_signatures = SafeSignature.parse_signature(
|
|
429
|
+
self.signatures, self.safe_tx_hash
|
|
430
|
+
)
|
|
431
|
+
filtered_tx_signatures = list(
|
|
432
|
+
filter(lambda x: x.owner != address, current_tx_signatures)
|
|
433
|
+
)
|
|
434
|
+
if current_tx_signatures != filtered_tx_signatures:
|
|
435
|
+
self.signatures = SafeSignature.export_signatures(filtered_tx_signatures)
|
|
436
|
+
return True
|
|
437
|
+
return False
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import List, Tuple, Union
|
|
2
|
+
|
|
3
|
+
from eth_keys import keys
|
|
4
|
+
from eth_keys.exceptions import BadSignature
|
|
5
|
+
from hexbytes import HexBytes
|
|
6
|
+
|
|
7
|
+
from .constants import NULL_ADDRESS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def signature_split(
|
|
11
|
+
signatures: Union[bytes, str], pos: int = 0
|
|
12
|
+
) -> Tuple[int, int, int]:
|
|
13
|
+
"""
|
|
14
|
+
:param signatures: signatures in form of {bytes32 r}{bytes32 s}{uint8 v}
|
|
15
|
+
:param pos: position of the signature
|
|
16
|
+
:return: Tuple with v, r, s
|
|
17
|
+
"""
|
|
18
|
+
signatures = HexBytes(signatures)
|
|
19
|
+
signature_pos = 65 * pos
|
|
20
|
+
if len(signatures[signature_pos : signature_pos + 65]) < 65:
|
|
21
|
+
raise ValueError(f"Signature must be at least 65 bytes {signatures.hex()}")
|
|
22
|
+
r = int.from_bytes(signatures[signature_pos : 32 + signature_pos], "big")
|
|
23
|
+
s = int.from_bytes(signatures[32 + signature_pos : 64 + signature_pos], "big")
|
|
24
|
+
v = signatures[64 + signature_pos]
|
|
25
|
+
|
|
26
|
+
return v, r, s
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def signature_to_bytes(v: int, r: int, s: int) -> bytes:
|
|
30
|
+
"""
|
|
31
|
+
Convert ecdsa signature to bytes
|
|
32
|
+
:param v:
|
|
33
|
+
:param r:
|
|
34
|
+
:param s:
|
|
35
|
+
:return: signature in form of {bytes32 r}{bytes32 s}{uint8 v}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
r.to_bytes(32, byteorder="big")
|
|
40
|
+
+ s.to_bytes(32, byteorder="big")
|
|
41
|
+
+ v.to_bytes(1, byteorder="big")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def signatures_to_bytes(signatures: List[Tuple[int, int, int]]) -> bytes:
|
|
46
|
+
"""
|
|
47
|
+
Convert signatures to bytes
|
|
48
|
+
:param signatures: list of tuples(v, r, s)
|
|
49
|
+
:return: 65 bytes per signature
|
|
50
|
+
"""
|
|
51
|
+
return b"".join([signature_to_bytes(v, r, s) for v, r, s in signatures])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_signing_address(signed_hash: bytes, v: int, r: int, s: int) -> str:
|
|
55
|
+
"""
|
|
56
|
+
:return: checksummed ethereum address, for example `0x568c93675A8dEb121700A6FAdDdfE7DFAb66Ae4A`
|
|
57
|
+
:rtype: str or `NULL_ADDRESS` if signature is not valid
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
public_key = keys.ecdsa_recover(signed_hash, keys.Signature(vrs=(v - 27, r, s)))
|
|
61
|
+
return public_key.to_checksum_address()
|
|
62
|
+
except BadSignature:
|
|
63
|
+
return NULL_ADDRESS
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, TypedDict, Union
|
|
2
|
+
|
|
3
|
+
from eth_typing import ChecksumAddress, Hash32, HexStr
|
|
4
|
+
from hexbytes import HexBytes
|
|
5
|
+
from web3.types import LogReceipt
|
|
6
|
+
|
|
7
|
+
EthereumHash = Union[Hash32, HexBytes, HexStr]
|
|
8
|
+
EthereumData = Union[bytes, HexStr]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BalanceDict(TypedDict):
|
|
12
|
+
token_address: Optional[ChecksumAddress]
|
|
13
|
+
balance: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LogReceiptDecoded(LogReceipt):
|
|
17
|
+
args: Dict[str, Any]
|