Skip to content

Commit 3528baa

Browse files
authored
feat: re-release as brownie-safe (#52)
* chore: update deps, rename package * chore: rename module and class * feat: find a working combination of dependencies * feat: update * feat: backport transaction service * feat: use tx service api * refactor: del redundant methods * fix: switch frame to the correct network * nit: cleanup imports * feat!: rebrand
1 parent a1e61a9 commit 3528baa

File tree

11 files changed

+2573
-108
lines changed

11 files changed

+2573
-108
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
dist/
22
_build/
3-
poetry.lock
43
.hypothesis/
54
cache/
65
build/
7-
env/
6+
env/
7+
.python-version

ape_safe.py renamed to brownie_safe.py

Lines changed: 65 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1+
import os
2+
import warnings
13
from copy import copy
2-
from typing import Dict, List, Union, Optional
3-
from urllib.parse import urljoin
4+
from typing import Dict, List, Optional, Union
45

56
import click
6-
import os
7-
import requests
7+
from gnosis.eth import EthereumClient, EthereumNetwork
88
from web3 import Web3 # don't move below brownie import
99
from brownie import Contract, accounts, chain, history, web3
1010
from brownie.convert.datatypes import EthAddress
1111
from brownie.network.account import LocalAccount
1212
from brownie.network.transaction import TransactionReceipt
1313
from eth_abi import encode_abi
1414
from eth_utils import is_address, to_checksum_address
15-
from gnosis.eth import EthereumClient
1615
from gnosis.safe import Safe, SafeOperation
1716
from gnosis.safe.multi_send import MultiSend, MultiSendOperation, MultiSendTx
1817
from gnosis.safe.safe_tx import SafeTx
1918
from gnosis.safe.signatures import signature_split, signature_to_bytes
19+
from gnosis.safe.api import TransactionServiceApi
2020
from hexbytes import HexBytes
21-
from trezorlib import tools, ui, ethereum
21+
from trezorlib import ethereum, tools, ui
2222
from trezorlib.client import TrezorClient
2323
from trezorlib.messages import EthereumSignMessage
2424
from trezorlib.transport import get_transport
25+
from enum import Enum
26+
from gnosis.eth.ethereum_client import EthereumNetworkNotSupported
2527

2628
MULTISEND_CALL_ONLY = '0x40A2aCCbd92BCA938b02010E17A5b8929b49130D'
2729
multisends = {
@@ -30,21 +32,50 @@
3032
288: '0x2Bd65cd56cAAC777f87d7808d13DEAF88e54E0eA',
3133
43114: '0x998739BFdAAdde7C933B942a68053933098f9EDa'
3234
}
33-
transaction_service = {
34-
1: 'https://safe-transaction-mainnet.safe.global',
35-
5: 'https://safe-transaction-goerli.safe.global',
36-
10: 'https://safe-transaction-optimism.safe.global',
37-
56: 'https://safe-transaction-bsc.safe.global',
38-
100: 'https://safe-transaction-gnosis-chain.safe.global',
39-
137: 'https://safe-transaction-polygon.safe.global',
40-
246: 'https://safe-transaction-ewc.safe.global',
41-
250: 'https://safe-txservice.fantom.network',
42-
288: 'https://safe-transaction.mainnet.boba.network',
43-
42161: 'https://safe-transaction-arbitrum.safe.global',
44-
43114: 'https://safe-transaction-avalanche.safe.global',
45-
73799: 'https://safe-transaction-volta.safe.global',
46-
1313161554: 'https://safe-transaction-aurora.safe.global',
47-
}
35+
36+
37+
class EthereumNetworkBackport(Enum):
38+
ARBITRUM_ONE = 42161
39+
AURORA_MAINNET = 1313161554
40+
AVALANCHE_C_CHAIN = 43114
41+
BINANCE_SMART_CHAIN_MAINNET = 56
42+
ENERGY_WEB_CHAIN = 246
43+
GOERLI = 5
44+
MAINNET = 1
45+
POLYGON = 137
46+
OPTIMISM = 10
47+
ENERGY_WEB_VOLTA_TESTNET = 73799
48+
GNOSIS = 100
49+
FANTOM = 250
50+
BOBA_NETWORK = 288
51+
52+
53+
class TransactionServiceBackport(TransactionServiceApi):
54+
URL_BY_NETWORK = {
55+
EthereumNetworkBackport.ARBITRUM_ONE: "https://safe-transaction-arbitrum.safe.global",
56+
EthereumNetworkBackport.AURORA_MAINNET: "https://safe-transaction-aurora.safe.global",
57+
EthereumNetworkBackport.AVALANCHE_C_CHAIN: "https://safe-transaction-avalanche.safe.global",
58+
EthereumNetworkBackport.BINANCE_SMART_CHAIN_MAINNET: "https://safe-transaction-bsc.safe.global",
59+
EthereumNetworkBackport.ENERGY_WEB_CHAIN: "https://safe-transaction-ewc.safe.global",
60+
EthereumNetworkBackport.GOERLI: "https://safe-transaction-goerli.safe.global",
61+
EthereumNetworkBackport.MAINNET: "https://safe-transaction-mainnet.safe.global",
62+
EthereumNetworkBackport.POLYGON: "https://safe-transaction-polygon.safe.global",
63+
EthereumNetworkBackport.OPTIMISM: "https://safe-transaction-optimism.safe.global",
64+
EthereumNetworkBackport.ENERGY_WEB_VOLTA_TESTNET: "https://safe-transaction-volta.safe.global",
65+
EthereumNetworkBackport.GNOSIS: "https://safe-transaction-gnosis-chain.safe.global",
66+
EthereumNetworkBackport.FANTOM: "https://safe-txservice.fantom.network",
67+
EthereumNetworkBackport.BOBA_NETWORK: "https://safe-transaction.mainnet.boba.network",
68+
}
69+
70+
def __init__(self, network: EthereumNetwork, ethereum_client: EthereumClient | None = None, base_url: str | None = None):
71+
self.network = network
72+
self.ethereum_client = ethereum_client
73+
self.base_url = base_url or self.URL_BY_NETWORK.get(EthereumNetworkBackport(network.value))
74+
if not self.base_url:
75+
raise EthereumNetworkNotSupported(network)
76+
77+
78+
warnings.filterwarnings('ignore', 'The function signature for resolver.*')
4879

4980

5081
class ExecutionFailure(Exception):
@@ -55,23 +86,23 @@ class ApiError(Exception):
5586
pass
5687

5788

58-
class ApeSafe(Safe):
89+
class BrownieSafe(Safe):
5990

6091
def __init__(self, address, base_url=None, multisend=None):
6192
"""
62-
Create an ApeSafe from an address or a ENS name and use a default connection.
93+
Create an BrownieSafe from an address or a ENS name and use a default connection.
6394
"""
6495
address = to_checksum_address(address) if is_address(address) else web3.ens.resolve(address)
6596
ethereum_client = EthereumClient(web3.provider.endpoint_uri)
66-
self.base_url = base_url or transaction_service[chain.id]
97+
self.transaction_service = TransactionServiceBackport(ethereum_client.get_network(), ethereum_client, base_url)
6798
self.multisend = multisend or multisends.get(chain.id, MULTISEND_CALL_ONLY)
6899
super().__init__(address, ethereum_client)
69100

70101
def __str__(self):
71102
return EthAddress(self.address)
72103

73104
def __repr__(self):
74-
return f'ApeSafe("{self.address}")'
105+
return f'BrownieSafe("{self.address}")'
75106

76107
@property
77108
def account(self) -> LocalAccount:
@@ -80,21 +111,18 @@ def account(self) -> LocalAccount:
80111
"""
81112
return accounts.at(self.address, force=True)
82113

83-
def contract(self, address=None) -> Contract:
114+
def contract(self, address) -> Contract:
84115
"""
85116
Instantiate a Brownie Contract owned by Safe account.
86117
"""
87-
if address:
88-
address = to_checksum_address(address) if is_address(address) else web3.ens.resolve(address)
89-
return Contract(address, owner=self.account)
90-
return Safe.contract if hasattr(Safe, 'contract') else Safe.get_contract
118+
address = to_checksum_address(address) if is_address(address) else web3.ens.resolve(address)
119+
return Contract(address, owner=self.account)
91120

92121
def pending_nonce(self) -> int:
93122
"""
94123
Subsequent nonce which accounts for pending transactions in the transaction service.
95124
"""
96-
url = urljoin(self.base_url, f'/api/v1/safes/{self.address}/multisig-transactions/')
97-
results = requests.get(url).json()['results']
125+
results = self.transaction_service.get_transactions(self.address)
98126
return results[0]['nonce'] + 1 if results else 0
99127

100128
def tx_from_receipt(self, receipt: TransactionReceipt, operation: SafeOperation = SafeOperation.CALL, safe_nonce: int = None) -> SafeTx:
@@ -230,45 +258,20 @@ def post_transaction(self, safe_tx: SafeTx):
230258
if not safe_tx.sorted_signers:
231259
self.sign_transaction(safe_tx)
232260

233-
sender = safe_tx.sorted_signers[0]
234-
235-
url = urljoin(self.base_url, f'/api/v1/safes/{self.address}/multisig-transactions/')
236-
data = {
237-
'to': safe_tx.to,
238-
'value': safe_tx.value,
239-
'data': safe_tx.data.hex() if safe_tx.data else None,
240-
'operation': safe_tx.operation,
241-
'gasToken': safe_tx.gas_token,
242-
'safeTxGas': safe_tx.safe_tx_gas,
243-
'baseGas': safe_tx.base_gas,
244-
'gasPrice': safe_tx.gas_price,
245-
'refundReceiver': safe_tx.refund_receiver,
246-
'nonce': safe_tx.safe_nonce,
247-
'contractTransactionHash': safe_tx.safe_tx_hash.hex(),
248-
'sender': sender,
249-
'signature': safe_tx.signatures.hex() if safe_tx.signatures else None,
250-
'origin': 'github.com/banteg/ape-safe',
251-
}
252-
response = requests.post(url, json=data)
253-
if not response.ok:
254-
raise ApiError(f'Error posting transaction: {response.text}')
261+
self.transaction_service.post_transaction(safe_tx)
255262

256263
def post_signature(self, safe_tx: SafeTx, signature: bytes):
257264
"""
258265
Submit a confirmation signature to a transaction service.
259266
"""
260-
url = urljoin(self.base_url, f'/api/v1/multisig-transactions/{safe_tx.safe_tx_hash.hex()}/confirmations/')
261-
response = requests.post(url, json={'signature': HexBytes(signature).hex()})
262-
if not response.ok:
263-
raise ApiError(f'Error posting signature: {response.text}')
267+
self.transaction_service.post_signatures(safe_tx.safe_tx_hash, signature)
264268

265269
@property
266270
def pending_transactions(self) -> List[SafeTx]:
267271
"""
268272
Retrieve pending transactions from the transaction service.
269273
"""
270-
url = urljoin(self.base_url, f'/api/v1/safes/{self.address}/transactions/')
271-
results = requests.get(url).json()['results']
274+
results = self.transaction_service._get_request(f'/api/v1/safes/{self.address}/transactions/').json()['results']
272275
nonce = self.retrieve_nonce()
273276
transactions = [
274277
self.build_multisig_tx(
@@ -305,7 +308,7 @@ def estimate_gas(self, safe_tx: SafeTx) -> int:
305308

306309
def preview_tx(self, safe_tx: SafeTx, events=True, call_trace=False) -> TransactionReceipt:
307310
tx = copy(safe_tx)
308-
safe = Contract.from_abi('Gnosis Safe', self.address, self.contract.abi)
311+
safe = Contract.from_abi('Gnosis Safe', self.address, self.get_contract().abi)
309312
# Replace pending nonce with the subsequent nonce, this could change the safe_tx_hash
310313
tx.safe_nonce = safe.nonce()
311314
# Forge signatures from the needed amount of owners, skip the one which submits the tx
@@ -360,6 +363,7 @@ def execute_transaction_with_frame(self, safe_tx: SafeTx, frame_rpc="http://127.
360363
# Requesting accounts triggers a connection prompt
361364
frame = Web3(Web3.HTTPProvider(frame_rpc, {'timeout': 600}))
362365
account = frame.eth.accounts[0]
366+
frame.manager.request_blocking('wallet_switchEthereumChain', [{'chainId': hex(chain.id)}])
363367
payload = safe_tx.w3_tx.buildTransaction()
364368
tx = {
365369
"from": account,
@@ -377,27 +381,3 @@ def preview_pending(self, events=True, call_trace=False):
377381
"""
378382
for safe_tx in self.pending_transactions:
379383
self.preview_tx(safe_tx, events=events, call_trace=call_trace)
380-
381-
@staticmethod
382-
def get_safe_txhash_from_execution_tx(tx: Union[str,TransactionReceipt]) -> str:
383-
"""
384-
Get safe txhash from execution tx.
385-
"""
386-
if isinstance(tx, str):
387-
tx = chain.get_transaction(tx)
388-
return tx.events['ExecutionSuccess']['txHash']
389-
390-
@staticmethod
391-
def get_safe_nonce_from_execution_tx(tx: Union[str,TransactionReceipt]) -> int:
392-
"""
393-
Get safe nonce from execution tx.
394-
"""
395-
safe_txhash = ApeSafe.get_safe_txhash_from_execution_tx(tx)
396-
return ApeSafe.get_safe_nonce_from_safe_tx(safe_txhash)
397-
398-
@staticmethod
399-
def get_safe_nonce_from_safe_tx(safe_txhash: str) -> int:
400-
"""
401-
Get safe nonce from safe txhash.
402-
"""
403-
return requests.get(f"{transaction_service[chain.id]}/api/v1/multisig-transactions/{safe_txhash}").json()['nonce']

docs/ape_safe.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
API docs
22
================
33

4-
.. automodule:: ape_safe
4+
.. automodule:: brownie_safe
55
:members:
66
:undoc-members:
77
:show-inheritance:

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@
5858

5959
html_theme_options = {
6060
'github_user': 'banteg',
61-
'github_repo': 'ape-safe',
61+
'github_repo': 'brownie-safe',
6262
'github_type': 'star',
6363
'extra_nav_links': {
64-
'GitHub': 'https://github.com/banteg/ape-safe',
64+
'GitHub': 'https://github.com/banteg/brownie-safe',
6565
}
6666
}

docs/detailed.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ Play around the same way you would do with a normal account:
2020

2121
.. code-block:: python
2222
23-
>>> from ape_safe import ApeSafe
23+
>>> from brownie_safe import BrownieSafe
2424
2525
# You can specify an ENS name here
2626
# Specify an EthereumClient if you don't run a local node
27-
>>> safe = ApeSafe('ychad.eth')
27+
>>> safe = BrownieSafe('ychad.eth')
2828
2929
# Unlocked account is available as `safe.account`
3030
>>> safe.account

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This tool has been informally known as Chief Multisig Officer at Yearn_ and has
1616
detailed
1717
useful
1818
changelog
19-
ape_safe
19+
brownie_safe
2020

2121

2222
.. _Brownie: https://eth-brownie.readthedocs.io/en/latest/

docs/install.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Then you can simply:
88

99
.. code-block:: bash
1010
11-
pip install -U ape-safe
11+
pip install -U brownie-safe
1212
1313
1414
.. _Brownie: https://eth-brownie.readthedocs.io/en/latest/install.html

docs/quickstart.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ Quickstart
33

44
.. code-block:: bash
55
6-
pip install -U ape-safe
6+
pip install -U brownie-safe
77
brownie console --network mainnet-fork
88
99
1010
.. code-block:: python
1111
12-
from ape_safe import ApeSafe
13-
safe = ApeSafe('ychad.eth')
12+
from brownie_safe import BrownieSafe
13+
safe = BrownieSafe('ychad.eth')
1414
1515
dai = safe.contract('0x6B175474E89094C44Da98b954EedeAC495271d0F')
1616
vault = safe.contract('0x19D3364A399d251E894aC732651be8B0E4e85001')

0 commit comments

Comments
 (0)