11import json
2+ import os
23from collections .abc import Iterator
34from datetime import datetime
45from functools import reduce
2627from ape_safe .exceptions import (
2728 ActionNotPerformedError ,
2829 ClientResponseError ,
29- ClientUnsupportedChainError ,
3030 MultisigTransactionNotFoundError ,
3131)
3232from ape_safe .utils import get_safe_tx_hash , order_by_signer
3333
3434if TYPE_CHECKING :
3535 from ape .api import AccountAPI
36+ from requests import Response
3637
3738APE_SAFE_VERSION = get_package_version (__name__ )
3839APE_SAFE_USER_AGENT = f"Ape-Safe/{ APE_SAFE_VERSION } { USER_AGENT } "
3940# NOTE: Origin must be a string, but can be json that contains url & name fields
4041ORIGIN = json .dumps (dict (url = "https://apeworx.io" , name = "Ape Safe" , ua = APE_SAFE_USER_AGENT ))
4142assert len (ORIGIN ) <= 200 # NOTE: Must be less than 200 chars
4243
43- TRANSACTION_SERVICE_URL = {
44- # NOTE: If URLs need to be updated, a list of available service URLs can be found at
45- # https://docs.safe.global/safe-core-api/available-services.
46- # NOTE: There should be no trailing slashes at the end of the URL.
47- 1 : "https://safe-transaction-mainnet.safe.global" ,
48- 10 : "https://safe-transaction-optimism.safe.global" ,
49- 56 : "https://safe-transaction-bsc.safe.global" ,
50- 100 : "https://safe-transaction-gnosis-chain.safe.global" ,
51- 137 : "https://safe-transaction-polygon.safe.global" ,
52- 250 : "https://safe-txservice.fantom.network" ,
53- 288 : "https://safe-transaction.mainnet.boba.network" ,
54- 8453 : "https://safe-transaction-base.safe.global" ,
55- 42161 : "https://safe-transaction-arbitrum.safe.global" ,
56- 43114 : "https://safe-transaction-avalanche.safe.global" ,
57- 84531 : "https://safe-transaction-base-testnet.safe.global" ,
58- 11155111 : "https://safe-transaction-sepolia.safe.global" ,
59- 81457 : "https://transaction.blast-safe.io" ,
44+ # URL for the multichain client gateway
45+ SAFE_CLIENT_GATEWAY_URL = "https://api.safe.global/tx-service"
46+ GATEWAY_API_KEY = os .environ .get ("APE_SAFE_GATEWAY_API_KEY" )
47+ EIP3770_BLOCKCHAIN_NAMES_BY_CHAIN_ID = {
48+ 1 : "eth" , # Ethereum Mainnet
49+ 11155111 : "sep" , # Ethereum Sepolia
50+ 10 : "oeth" , # Optimism Mainnet
51+ 42161 : "arb1" , # Arbitrum One Mainnet
52+ 56 : "bnb" , # Binance Smart Chain
53+ 146 : "sonic" ,
54+ 5000 : "mantle" ,
55+ 43114 : "avax" ,
56+ 1313161554 : "aurora" ,
57+ 8453 : "base" , # Base Mainnet
58+ 84532 : "basesep" , # Base Sepolia
59+ 42220 : "celo" , # Celo Mainnet
60+ 100 : "gno" , # Gnosis Chain
61+ 59144 : "linea" , # Linea Mainnet
62+ 137 : "pol" , # Polygon
63+ 534352 : "scr" , # Scroll
64+ 130 : "unichain" , # Unichain Mainnet
65+ 480 : "wc" , # Worldchain Mainnet
66+ 324 : "zksync" , # zkSync Mainnet
67+ 57073 : "ink" , # Ink Mainnet
68+ 800094 : "berachain" , # Berachain Mainnet
6069}
6170
6271
@@ -68,24 +77,34 @@ def __init__(
6877 chain_id : Optional [int ] = None ,
6978 ) -> None :
7079 self .address = address
80+ self .chain_id = chain_id
7181
7282 if override_url :
73- tx_service_url = override_url
74-
83+ base_url = override_url
7584 elif chain_id :
76- if chain_id not in TRANSACTION_SERVICE_URL :
77- raise ClientUnsupportedChainError ( chain_id )
85+ if chain_id not in EIP3770_BLOCKCHAIN_NAMES_BY_CHAIN_ID :
86+ raise ValueError ( f"Chain ID { chain_id } is not a supported chain." )
7887
79- tx_service_url = TRANSACTION_SERVICE_URL [chain_id ]
88+ elif not GATEWAY_API_KEY :
89+ raise ValueError ("Must provide API key via 'APE_SAFE_GATEWAY_API_KEY='." )
8090
91+ base_url = (
92+ f"{ SAFE_CLIENT_GATEWAY_URL } /{ EIP3770_BLOCKCHAIN_NAMES_BY_CHAIN_ID [chain_id ]} /api"
93+ )
8194 else :
8295 raise ValueError ("Must provide one of chain_id or override_url." )
8396
84- super ().__init__ (tx_service_url )
97+ super ().__init__ (base_url )
98+
99+ def _request (self , method : str , url : str , json : Optional [dict ] = None , ** kwargs ) -> "Response" :
100+ # NOTE: Add authorization header
101+ headers = kwargs .pop ("headers" , {})
102+ headers .update (dict (Authorization = f"Bearer { GATEWAY_API_KEY } " ))
103+ return super ()._request (method , url , json = json , headers = headers , ** kwargs )
85104
86105 @property
87106 def safe_details (self ) -> SafeDetails :
88- response = self ._get (f"safes/{ self .address } / " )
107+ response = self ._get (f"/ safes/{ self .address } " )
89108 return SafeDetails .model_validate (response .json ())
90109
91110 def get_next_nonce (self ) -> int :
@@ -96,7 +115,7 @@ def _all_transactions(self) -> Iterator[SafeApiTxData]:
96115 Get all transactions from safe, both confirmed and unconfirmed
97116 """
98117
99- url = f"safes/{ self .address } /all -transactions/ "
118+ url = f"/ safes/{ self .address } /multisig -transactions"
100119 while url :
101120 response = self ._get (url )
102121 data = response .json ()
@@ -115,12 +134,9 @@ def _all_transactions(self) -> Iterator[SafeApiTxData]:
115134 url = data .get ("next" )
116135
117136 def get_confirmations (self , safe_tx_hash : SafeTxID ) -> Iterator [SafeTxConfirmation ]:
118- url = f"multisig-transactions/{ str (safe_tx_hash )} /confirmations/"
119- while url :
120- response = self ._get (url )
121- data = response .json ()
122- yield from map (SafeTxConfirmation .model_validate , data .get ("results" ))
123- url = data .get ("next" )
137+ response = self ._get (f"/multisig-transactions/{ safe_tx_hash } /raw" )
138+ data = response .json ()
139+ yield from map (SafeTxConfirmation .model_validate , data .get ("confirmations" , []))
124140
125141 def post_transaction (
126142 self , safe_tx : SafeTx , signatures : dict [AddressType , MessageSignature ], ** kwargs
@@ -153,7 +169,7 @@ def post_transaction(
153169 # Signature handled above.
154170 post_dict .pop ("signatures" )
155171
156- url = f"safes/ { tx_data .safe } /multisig-transactions/ "
172+ url = f"/transactions/ { tx_data .safe } /propose "
157173 response = self ._post (url , json = post_dict )
158174 return response
159175
@@ -169,7 +185,7 @@ def post_signatures(
169185 safe_tx_hash = safe_tx_or_hash
170186
171187 safe_tx_hash = cast (SafeTxID , to_hex (HexBytes (safe_tx_hash )))
172- url = f"multisig- transactions/{ safe_tx_hash } /confirmations/ "
188+ url = f"/ transactions/{ safe_tx_hash } /confirmations"
173189 signature = to_hex (
174190 HexBytes (b"" .join ([x .encode_rsv () for x in order_by_signer (signatures )]))
175191 )
@@ -184,7 +200,7 @@ def post_signatures(
184200 def estimate_gas_cost (
185201 self , receiver : AddressType , value : int , data : bytes , operation : int = 0
186202 ) -> Optional [int ]:
187- url = f"safes/{ self .address } /multisig-transactions/estimations/ "
203+ url = f"/ safes/{ self .address } /multisig-transactions/estimations"
188204 request : dict = {
189205 "to" : receiver ,
190206 "value" : value ,
@@ -196,7 +212,7 @@ def estimate_gas_cost(
196212 return int (to_hex (HexBytes (gas )), 16 )
197213
198214 def get_delegates (self ) -> dict ["AddressType" , list ["AddressType" ]]:
199- url = "delegates/ "
215+ url = "/delegates "
200216 delegates : dict [AddressType , list [AddressType ]] = {}
201217
202218 while url :
@@ -227,7 +243,7 @@ def add_delegate(self, delegate: "AddressType", label: str, delegator: "AccountA
227243 "label" : label ,
228244 "signature" : sig .encode_rsv ().hex (),
229245 }
230- self ._post ("delegates/ " , json = payload )
246+ self ._post ("/delegates " , json = payload )
231247
232248 def remove_delegate (self , delegate : "AddressType" , delegator : "AccountAPI" ):
233249 msg_hash = self .create_delegate_message (delegate )
@@ -240,7 +256,7 @@ def remove_delegate(self, delegate: "AddressType", delegator: "AccountAPI"):
240256 "delegator" : delegator .address ,
241257 "signature" : sig .encode_rsv ().hex (),
242258 }
243- self ._delete (f"delegates/{ delegate } / " , json = payload )
259+ self ._delete (f"/ delegates/{ delegate } " , json = payload )
244260
245261
246262__all__ = [
0 commit comments