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