14
14
15
15
# Constants specific to Monad testnet
16
16
MONAD_BASE_GAS_PRICE = 50 # gwei - hardcoded for testnet
17
+ MONAD_CHAIN_ID = 10143
18
+ MONAD_SCANNER_URL = "testnet.monadexplorer.com"
19
+ ZERO_EX_API_URL = "https://api.0x.org/swap/v2"
17
20
18
21
class MonadConnectionError (Exception ):
19
22
"""Base exception for Monad connection errors"""
@@ -30,8 +33,8 @@ def __init__(self, config: Dict[str, Any]):
30
33
if not self .rpc_url :
31
34
raise ValueError ("RPC URL must be provided in config" )
32
35
33
- self .scanner_url = "testnet.monadexplorer.com"
34
- self .chain_id = config . get ( "chain_id" , 10143 )
36
+ self .scanner_url = MONAD_SCANNER_URL
37
+ self .chain_id = MONAD_CHAIN_ID
35
38
36
39
super ().__init__ (config )
37
40
self ._initialize_web3 ()
@@ -111,6 +114,13 @@ def register_actions(self) -> None:
111
114
)
112
115
}
113
116
117
+ def _get_current_account (self ) -> 'LocalAccount' :
118
+ """Get current account from private key"""
119
+ private_key = os .getenv ('MONAD_PRIVATE_KEY' )
120
+ if not private_key :
121
+ raise MonadConnectionError ("No wallet private key configured" )
122
+ return self ._web3 .eth .account .from_key (private_key )
123
+
114
124
def configure (self ) -> bool :
115
125
"""Sets up Monad wallet"""
116
126
logger .info ("\n ⛓️ MONAD SETUP" )
@@ -164,41 +174,38 @@ def is_configured(self, verbose: bool = False) -> bool:
164
174
load_dotenv ()
165
175
166
176
if not os .getenv ('MONAD_PRIVATE_KEY' ):
167
- logger .error ("Missing MONAD_PRIVATE_KEY in .env" )
177
+ if verbose :
178
+ logger .error ("Missing MONAD_PRIVATE_KEY in .env" )
168
179
return False
169
180
170
181
if not self ._web3 .is_connected ():
171
- logger .error ("Not connected to Monad network" )
182
+ if verbose :
183
+ logger .error ("Not connected to Monad network" )
172
184
return False
173
185
174
186
# Test account access
175
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
176
- account = self ._web3 .eth .account .from_key (private_key )
187
+ account = self ._get_current_account ()
177
188
balance = self ._web3 .eth .get_balance (account .address )
178
189
179
190
return True
180
191
181
192
except Exception as e :
182
- logger .error (f"Configuration check failed: { e } " )
193
+ if verbose :
194
+ logger .error (f"Configuration check failed: { e } " )
183
195
return False
184
196
185
197
def get_address (self ) -> str :
186
198
"""Get the wallet address"""
187
199
try :
188
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
189
- account = self ._web3 .eth .account .from_key (private_key )
200
+ account = self ._get_current_account ()
190
201
return f"Your Monad address: { account .address } "
191
202
except Exception as e :
192
203
return f"Failed to get address: { str (e )} "
193
204
194
205
def get_balance (self , token_address : Optional [str ] = None ) -> float :
195
206
"""Get native or token balance for the configured wallet"""
196
207
try :
197
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
198
- if not private_key :
199
- return "No wallet private key configured in .env"
200
-
201
- account = self ._web3 .eth .account .from_key (private_key )
208
+ account = self ._get_current_account ()
202
209
203
210
if token_address is None or token_address .lower () == self .NATIVE_TOKEN .lower ():
204
211
raw_balance = self ._web3 .eth .get_balance (account .address )
@@ -224,8 +231,7 @@ def _prepare_transfer_tx(
224
231
) -> Dict [str , Any ]:
225
232
"""Prepare transfer transaction with Monad-specific gas handling"""
226
233
try :
227
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
228
- account = self ._web3 .eth .account .from_key (private_key )
234
+ account = self ._get_current_account ()
229
235
230
236
# Get latest nonce
231
237
nonce = self ._web3 .eth .get_transaction_count (account .address )
@@ -258,7 +264,7 @@ def _prepare_transfer_tx(
258
264
'nonce' : nonce ,
259
265
'to' : Web3 .to_checksum_address (to_address ),
260
266
'value' : self ._web3 .to_wei (amount , 'ether' ),
261
- 'gas' : 21000 ,
267
+ 'gas' : 21000 , # Standard ETH transfer gas
262
268
'gasPrice' : gas_price ,
263
269
'chainId' : self .chain_id
264
270
}
@@ -277,6 +283,8 @@ def transfer(
277
283
) -> str :
278
284
"""Transfer tokens with Monad-specific balance validation"""
279
285
try :
286
+ account = self ._get_current_account ()
287
+
280
288
# Check balance including gas cost since Monad charges on gas limit
281
289
gas_cost = Web3 .to_wei (MONAD_BASE_GAS_PRICE * 21000 , 'gwei' )
282
290
gas_cost_eth = float (self ._web3 .from_wei (gas_cost , 'ether' ))
@@ -290,9 +298,6 @@ def transfer(
290
298
291
299
# Prepare and send transaction
292
300
tx = self ._prepare_transfer_tx (to_address , amount , token_address )
293
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
294
- account = self ._web3 .eth .account .from_key (private_key )
295
-
296
301
signed = account .sign_transaction (tx )
297
302
tx_hash = self ._web3 .eth .send_raw_transaction (signed .rawTransaction )
298
303
@@ -310,9 +315,10 @@ def _get_swap_quote(
310
315
amount : float ,
311
316
sender : str
312
317
) -> Dict :
313
- """Get swap quote from 0x API"""
318
+ """Get swap quote from 0x API using v2 endpoints """
314
319
try :
315
320
load_dotenv ()
321
+
316
322
# Convert amount to raw value with proper decimals
317
323
if token_in .lower () == self .NATIVE_TOKEN .lower ():
318
324
amount_raw = self ._web3 .to_wei (amount , 'ether' )
@@ -324,27 +330,30 @@ def _get_swap_quote(
324
330
decimals = token_contract .functions .decimals ().call ()
325
331
amount_raw = int (amount * (10 ** decimals ))
326
332
327
- # The URL should match exactly
328
- url = "https://api.0x.org/swap/permit2/quote"
329
-
330
- # Headers should match exactly
333
+ # Set up API request according to v2 spec
331
334
headers = {
332
335
"0x-api-key" : os .getenv ('ZEROEX_API_KEY' ),
333
336
"0x-version" : "v2"
334
337
}
335
338
336
- # Parameters should match exactly
337
339
params = {
338
340
"sellToken" : token_in ,
339
341
"buyToken" : token_out ,
340
342
"sellAmount" : str (amount_raw ),
341
- "chainId" : str (self .chain_id ), # Convert to string to match exact format
342
- "taker" : sender
343
+ "chainId" : str (self .chain_id ),
344
+ "taker" : sender ,
345
+ "gasPrice" : str (Web3 .to_wei (MONAD_BASE_GAS_PRICE , 'gwei' ))
343
346
}
344
347
345
- response = requests .get (url , headers = headers , params = params )
348
+ response = requests .get (
349
+ f"{ ZERO_EX_API_URL } /permit2/quote" ,
350
+ headers = headers ,
351
+ params = params
352
+ )
346
353
response .raise_for_status ()
347
- return response .json ()
354
+
355
+ data = response .json ()
356
+ return data
348
357
349
358
except Exception as e :
350
359
logger .error (f"Failed to get swap quote: { str (e )} " )
@@ -358,8 +367,7 @@ def _handle_token_approval(
358
367
) -> Optional [str ]:
359
368
"""Handle token approval for spender, returns tx hash if approval needed"""
360
369
try :
361
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
362
- account = self ._web3 .eth .account .from_key (private_key )
370
+ account = self ._get_current_account ()
363
371
364
372
token_contract = self ._web3 .eth .contract (
365
373
address = Web3 .to_checksum_address (token_address ),
@@ -411,10 +419,9 @@ def swap(
411
419
amount : float ,
412
420
slippage : float = 0.5
413
421
) -> str :
414
- """Execute token swap using 0x API"""
422
+ """Execute token swap using 0x API with Monad-specific handling """
415
423
try :
416
- private_key = os .getenv ('MONAD_PRIVATE_KEY' )
417
- account = self ._web3 .eth .account .from_key (private_key )
424
+ account = self ._get_current_account ()
418
425
419
426
# Validate balance including potential gas costs
420
427
current_balance = self .get_balance (
@@ -423,7 +430,7 @@ def swap(
423
430
if current_balance < amount :
424
431
raise ValueError (f"Insufficient balance. Required: { amount } , Available: { current_balance } " )
425
432
426
- # Get swap quote
433
+ # Get swap quote with v2 API
427
434
quote_data = self ._get_swap_quote (
428
435
token_in ,
429
436
token_out ,
@@ -433,12 +440,12 @@ def swap(
433
440
434
441
logger .debug (f"Quote data received: { quote_data } " )
435
442
436
- # Extract transaction data from the nested structure
443
+ # Extract transaction data from quote
437
444
transaction = quote_data .get ("transaction" )
438
445
if not transaction or not transaction .get ("to" ) or not transaction .get ("data" ):
439
446
raise ValueError ("Invalid transaction data in quote" )
440
447
441
- # Handle token approval if needed
448
+ # Handle token approval if needed for non-native tokens
442
449
if token_in .lower () != self .NATIVE_TOKEN .lower ():
443
450
spender_address = quote_data .get ("allowanceTarget" )
444
451
amount_raw = int (quote_data .get ("sellAmount" ))
@@ -450,7 +457,7 @@ def swap(
450
457
# Wait for approval confirmation
451
458
self ._web3 .eth .wait_for_transaction_receipt (approval_hash )
452
459
453
- # Prepare swap transaction using the nested transaction data
460
+ # Prepare swap transaction using quote data
454
461
tx = {
455
462
'from' : account .address ,
456
463
'to' : Web3 .to_checksum_address (transaction ["to" ]),
@@ -459,9 +466,15 @@ def swap(
459
466
'nonce' : self ._web3 .eth .get_transaction_count (account .address ),
460
467
'gasPrice' : Web3 .to_wei (MONAD_BASE_GAS_PRICE , 'gwei' ),
461
468
'chainId' : self .chain_id ,
462
- 'gas' : int (transaction .get ("gas" , 500000 )) # Use gas from quote or fallback to 500000
463
469
}
464
470
471
+ # Estimate gas or use quote's gas estimate
472
+ try :
473
+ tx ['gas' ] = int (transaction .get ("gas" , 0 )) or self ._web3 .eth .estimate_gas (tx )
474
+ except Exception as e :
475
+ logger .warning (f"Gas estimation failed: { e } , using default gas limit" )
476
+ tx ['gas' ] = 500000 # Default gas limit for swaps
477
+
465
478
# Sign and send transaction
466
479
signed_tx = account .sign_transaction (tx )
467
480
tx_hash = self ._web3 .eth .send_raw_transaction (signed_tx .rawTransaction )
@@ -490,4 +503,9 @@ def perform_action(self, action_name: str, kwargs: Dict[str, Any]) -> Any:
490
503
491
504
method_name = action_name .replace ('-' , '_' )
492
505
method = getattr (self , method_name )
493
- return method (** kwargs )
506
+
507
+ try :
508
+ return method (** kwargs )
509
+ except Exception as e :
510
+ logger .error (f"Action { action_name } failed: { str (e )} " )
511
+ raise
0 commit comments