11import json
22import random
3- from typing import Optional , Dict , Union , TYPE_CHECKING
4-
5- from ..broadcaster import BroadcastResponse , BroadcastFailure , Broadcaster
6- from ..http_client import HttpClient , default_http_client
7-
3+ from typing import Optional , Dict , Union , Any , TYPE_CHECKING
84
95if TYPE_CHECKING :
106 from ..transaction import Transaction
117
8+ from ..broadcaster import BroadcastResponse , BroadcastFailure , Broadcaster
9+ from ..http_client import HttpClient , default_http_client , SyncHttpClient , default_sync_http_client
10+
1211def to_hex (bytes_data ):
1312 return "" .join (f"{ x :02x} " for x in bytes_data )
1413
@@ -19,16 +18,18 @@ def random_hex(length: int) -> str:
1918
2019class ARCConfig :
2120 def __init__ (
22- self ,
23- api_key : Optional [str ] = None ,
24- http_client : Optional [HttpClient ] = None ,
25- deployment_id : Optional [str ] = None ,
26- callback_url : Optional [str ] = None ,
27- callback_token : Optional [str ] = None ,
28- headers : Optional [Dict [str , str ]] = None ,
21+ self ,
22+ api_key : Optional [str ] = None ,
23+ http_client : Optional [HttpClient ] = None ,
24+ sync_http_client : Optional [SyncHttpClient ] = None ,
25+ deployment_id : Optional [str ] = None ,
26+ callback_url : Optional [str ] = None ,
27+ callback_token : Optional [str ] = None ,
28+ headers : Optional [Dict [str , str ]] = None ,
2929 ):
3030 self .api_key = api_key
3131 self .http_client = http_client
32+ self .sync_http_client = sync_http_client
3233 self .deployment_id = deployment_id
3334 self .callback_url = callback_url
3435 self .callback_token = callback_token
@@ -45,6 +46,7 @@ def __init__(self, url: str, config: Union[str, ARCConfig] = None):
4546 if isinstance (config , str ):
4647 self .api_key = config
4748 self .http_client = default_http_client ()
49+ self .sync_http_client = default_sync_http_client ()
4850 self .deployment_id = default_deployment_id ()
4951 self .callback_url = None
5052 self .callback_token = None
@@ -53,6 +55,7 @@ def __init__(self, url: str, config: Union[str, ARCConfig] = None):
5355 config = config or ARCConfig ()
5456 self .api_key = config .api_key
5557 self .http_client = config .http_client or default_http_client ()
58+ self .sync_http_client = config .sync_http_client or default_sync_http_client ()
5659 self .deployment_id = config .deployment_id or default_deployment_id ()
5760 self .callback_url = config .callback_url
5861 self .callback_token = config .callback_token
@@ -75,9 +78,9 @@ async def broadcast(
7578 response = await self .http_client .fetch (
7679 f"{ self .URL } /v1/tx" , request_options
7780 )
78-
81+
7982 response_json = response .json ()
80-
83+
8184 if response .ok and response .status_code >= 200 and response .status_code <= 299 :
8285 data = response_json ["data" ]
8386
@@ -99,7 +102,7 @@ async def broadcast(
99102 code = str (response .status_code ),
100103 description = response_json ["data" ]["detail" ] if "data" in response_json else "Unknown error" ,
101104 )
102-
105+
103106 except Exception as error :
104107 return BroadcastFailure (
105108 status = "failure" ,
@@ -130,3 +133,201 @@ def request_headers(self) -> Dict[str, str]:
130133 headers .update (self .headers )
131134
132135 return headers
136+
137+ def sync_broadcast (
138+ self , tx : 'Transaction' , timeout : int = 30
139+ ) -> Union [BroadcastResponse , BroadcastFailure ]:
140+ """
141+ Synchronously broadcast a transaction
142+
143+ :param tx: Transaction to broadcast
144+ :param timeout: Timeout setting in seconds
145+ :returns: BroadcastResponse or BroadcastFailure
146+ """
147+ # Check if all inputs have source_transaction
148+ has_all_source_txs = all (input .source_transaction is not None for input in tx .inputs )
149+
150+ try :
151+ response = self .sync_http_client .post (
152+ f"{ self .URL } /v1/tx" ,
153+ data = {"rawTx" : tx .to_ef ().hex () if has_all_source_txs else tx .hex ()},
154+ headers = self .request_headers (),
155+ timeout = timeout
156+ )
157+
158+ response_json = response .json ()
159+ data = response_json .get ("data" , {})
160+
161+ if response .ok :
162+ if data .get ("txid" ):
163+ return BroadcastResponse (
164+ status = "success" ,
165+ txid = data .get ("txid" ),
166+ message = f"{ data .get ('txStatus' , '' )} { data .get ('extraInfo' , '' )} " .strip (),
167+ )
168+ else :
169+ return BroadcastFailure (
170+ status = "failure" ,
171+ code = data .get ("status" , "ERR_UNKNOWN" ),
172+ description = data .get ("detail" , "Unknown error" ),
173+ )
174+ else :
175+ # Handle special error cases
176+ if response .status_code == 408 :
177+ return BroadcastFailure (
178+ status = "failure" ,
179+ code = "408" ,
180+ description = f"Transaction broadcast timed out after { timeout } seconds" ,
181+ )
182+
183+ if response .status_code == 503 :
184+ return BroadcastFailure (
185+ status = "failure" ,
186+ code = "503" ,
187+ description = "Failed to connect to ARC service" ,
188+ )
189+
190+ return BroadcastFailure (
191+ status = "failure" ,
192+ code = str (response .status_code ),
193+ description = data .get ("detail" , "Unknown error" ),
194+ )
195+
196+ except Exception as error :
197+ return BroadcastFailure (
198+ status = "failure" ,
199+ code = "500" ,
200+ description = str (error ),
201+ )
202+
203+ def check_transaction_status (self , txid : str , timeout : int = 5 ) -> Dict [str , Any ]:
204+ """
205+ Check transaction status synchronously
206+
207+ :param txid: Transaction ID to check
208+ :param timeout: Timeout setting in seconds
209+ :returns: Dictionary containing transaction status information
210+ """
211+
212+ try :
213+ response = self .sync_http_client .get (
214+ f"{ self .URL } /v1/tx/{ txid } " ,
215+ headers = self .request_headers (),
216+ timeout = timeout
217+ )
218+ response_data = response .json ()
219+ data = response_data .get ("data" , {})
220+
221+ if response .ok :
222+ return {
223+ "txid" : txid ,
224+ "txStatus" : data .get ("txStatus" ),
225+ "blockHash" : data .get ("blockHash" ),
226+ "blockHeight" : data .get ("blockHeight" ),
227+ "merklePath" : data .get ("merklePath" ),
228+ "extraInfo" : data .get ("extraInfo" ),
229+ "competingTxs" : data .get ("competingTxs" ),
230+ "timestamp" : data .get ("timestamp" )
231+ }
232+ else :
233+ # Handle special error cases
234+ if response .status_code == 408 :
235+ return {
236+ "status" : "failure" ,
237+ "code" : 408 ,
238+ "title" : "Request Timeout" ,
239+ "detail" : f"Transaction status check timed out after { timeout } seconds" ,
240+ "txid" : txid ,
241+ "extra_info" : "Consider retrying or increasing timeout value"
242+ }
243+
244+ if response .status_code == 503 :
245+ return {
246+ "status" : "failure" ,
247+ "code" : 503 ,
248+ "title" : "Connection Error" ,
249+ "detail" : "Failed to connect to ARC service" ,
250+ "txid" : txid
251+ }
252+
253+ # Handle general error cases
254+ return {
255+ "status" : "failure" ,
256+ "code" : data .get ("status" , response .status_code ),
257+ "title" : data .get ("title" , "Error" ),
258+ "detail" : data .get ("detail" , "Unknown error" ),
259+ "txid" : data .get ("txid" , txid ),
260+ "extra_info" : data .get ("extraInfo" , "" )
261+ }
262+
263+ except Exception as error :
264+ return {
265+ "status" : "failure" ,
266+ "code" : "500" ,
267+ "title" : "Internal Error" ,
268+ "detail" : str (error ),
269+ "txid" : txid
270+ }
271+
272+ @staticmethod
273+ def categorize_transaction_status (response : Dict [str , Any ]) -> Dict [str , Any ]:
274+ """
275+ Categorize transaction status based on the ARC response
276+
277+ :param response: The transaction status response dictionary from ARC
278+ :returns: Dictionary containing status category and transaction status
279+ """
280+ try :
281+ tx_status = response .get ("txStatus" )
282+
283+ if tx_status :
284+ # Processing transactions - still being handled by the network
285+ if tx_status in [
286+ "UNKNOWN" , "QUEUED" , "RECEIVED" , "STORED" ,
287+ "ANNOUNCED_TO_NETWORK" , "REQUESTED_BY_NETWORK" ,
288+ "SENT_TO_NETWORK" , "ACCEPTED_BY_NETWORK"
289+ ]:
290+ status_category = "progressing"
291+
292+ # Successfully mined transactions
293+ elif tx_status in ["MINED" ]:
294+ status_category = "mined"
295+
296+ # Mined in stale block - needs attention
297+ elif tx_status in ["MINED_IN_STALE_BLOCK" ]:
298+ status_category = "0confirmation"
299+
300+ # Warning status - double spend attempted
301+ elif tx_status in ["DOUBLE_SPEND_ATTEMPTED" ]:
302+ status_category = "warning"
303+
304+ # Seen on network - check for competing transactions
305+ elif tx_status in ["SEEN_ON_NETWORK" ]:
306+ # Check if there are competing transactions in mempool
307+ if response .get ("competingTxs" ):
308+ status_category = "warning"
309+ else :
310+ # Transaction is in mempool without conflicts
311+ status_category = "0confirmation"
312+
313+ # Rejected transactions - failed to process
314+ elif tx_status in ["ERROR" , "REJECTED" , "SEEN_IN_ORPHAN_MEMPOOL" ]:
315+ status_category = "rejected"
316+
317+ else :
318+ status_category = f"unknown_txStatus: { tx_status } "
319+ else :
320+ status_category = "error"
321+ tx_status = "No txStatus"
322+
323+ return {
324+ "status_category" : status_category ,
325+ "tx_status" : tx_status
326+ }
327+
328+ except Exception as e :
329+ return {
330+ "status_category" : "error" ,
331+ "error" : str (e ),
332+ "response" : response
333+ }
0 commit comments