@@ -271,6 +271,152 @@ async def confirm_transaction(
271271 logger .exception (f"Failed to confirm transaction { signature } " )
272272 return False
273273
274+ async def get_transaction_token_balance (
275+ self , signature : str , user_pubkey : Pubkey , mint : Pubkey
276+ ) -> int | None :
277+ """Get the user's token balance after a transaction from postTokenBalances.
278+
279+ Args:
280+ signature: Transaction signature
281+ user_pubkey: User's wallet public key
282+ mint: Token mint address
283+
284+ Returns:
285+ Token balance (raw amount) after transaction, or None if not found
286+ """
287+ result = await self ._get_transaction_result (signature )
288+ if not result :
289+ return None
290+
291+ meta = result .get ("meta" , {})
292+ post_token_balances = meta .get ("postTokenBalances" , [])
293+
294+ user_str = str (user_pubkey )
295+ mint_str = str (mint )
296+
297+ for balance in post_token_balances :
298+ if balance .get ("owner" ) == user_str and balance .get ("mint" ) == mint_str :
299+ ui_amount = balance .get ("uiTokenAmount" , {})
300+ amount_str = ui_amount .get ("amount" )
301+ if amount_str :
302+ return int (amount_str )
303+
304+ return None
305+
306+ async def get_buy_transaction_details (
307+ self , signature : str , mint : Pubkey , sol_destination : Pubkey
308+ ) -> tuple [int | None , int | None ]:
309+ """Get actual tokens received and SOL spent from a buy transaction.
310+
311+ Uses preBalances/postBalances to find exact SOL transferred to the
312+ pool/curve and pre/post token balance diff to find tokens received.
313+
314+ Args:
315+ signature: Transaction signature
316+ mint: Token mint address
317+ sol_destination: Address where SOL is sent (bonding curve for pump.fun,
318+ quote_vault for letsbonk)
319+
320+ Returns:
321+ Tuple of (tokens_received_raw, sol_spent_lamports), or (None, None)
322+ """
323+ result = await self ._get_transaction_result (signature )
324+ if not result :
325+ return None , None
326+
327+ meta = result .get ("meta" , {})
328+ mint_str = str (mint )
329+
330+ # Get tokens received from pre/post token balance diff
331+ # This works for Token2022 where owner might be different
332+ tokens_received = None
333+ pre_token_balances = meta .get ("preTokenBalances" , [])
334+ post_token_balances = meta .get ("postTokenBalances" , [])
335+
336+ # Build lookup by account index
337+ pre_by_idx = {b .get ("accountIndex" ): b for b in pre_token_balances }
338+ post_by_idx = {b .get ("accountIndex" ): b for b in post_token_balances }
339+
340+ # Find positive token diff for our mint (user receiving tokens)
341+ all_indices = set (pre_by_idx .keys ()) | set (post_by_idx .keys ())
342+ for idx in all_indices :
343+ pre = pre_by_idx .get (idx )
344+ post = post_by_idx .get (idx )
345+
346+ # Check if this is our mint
347+ balance_mint = (post or pre ).get ("mint" , "" )
348+ if balance_mint != mint_str :
349+ continue
350+
351+ pre_amount = (
352+ int (pre .get ("uiTokenAmount" , {}).get ("amount" , 0 )) if pre else 0
353+ )
354+ post_amount = (
355+ int (post .get ("uiTokenAmount" , {}).get ("amount" , 0 )) if post else 0
356+ )
357+ diff = post_amount - pre_amount
358+
359+ # Positive diff means tokens received (not the bonding curve's negative)
360+ if diff > 0 :
361+ tokens_received = diff
362+ logger .info (f"Tokens received from tx: { tokens_received } " )
363+ break
364+
365+ # Get SOL spent from preBalances/postBalances at sol_destination
366+ sol_destination_str = str (sol_destination )
367+ sol_spent = None
368+ pre_balances = meta .get ("preBalances" , [])
369+ post_balances = meta .get ("postBalances" , [])
370+ account_keys = (
371+ result .get ("transaction" , {}).get ("message" , {}).get ("accountKeys" , [])
372+ )
373+
374+ for i , key in enumerate (account_keys ):
375+ key_str = key if isinstance (key , str ) else key .get ("pubkey" , "" )
376+ if key_str == sol_destination_str :
377+ if i < len (pre_balances ) and i < len (post_balances ):
378+ sol_spent = post_balances [i ] - pre_balances [i ]
379+ if sol_spent > 0 :
380+ logger .info (f"SOL to pool/curve: { sol_spent } lamports" )
381+ else :
382+ logger .warning (
383+ f"SOL destination balance change not positive: { sol_spent } "
384+ )
385+ sol_spent = None
386+ break
387+
388+ return tokens_received , sol_spent
389+
390+ async def _get_transaction_result (self , signature : str ) -> dict | None :
391+ """Fetch transaction result from RPC.
392+
393+ Args:
394+ signature: Transaction signature
395+
396+ Returns:
397+ Transaction result dict or None
398+ """
399+ body = {
400+ "jsonrpc" : "2.0" ,
401+ "id" : 1 ,
402+ "method" : "getTransaction" ,
403+ "params" : [
404+ signature ,
405+ {"encoding" : "jsonParsed" , "commitment" : "confirmed" },
406+ ],
407+ }
408+
409+ response = await self .post_rpc (body )
410+ if not response or "result" not in response :
411+ logger .warning (f"Failed to get transaction { signature } " )
412+ return None
413+
414+ result = response ["result" ]
415+ if not result or "meta" not in result :
416+ return None
417+
418+ return result
419+
274420 async def post_rpc (self , body : dict [str , Any ]) -> dict [str , Any ] | None :
275421 """
276422 Send a raw RPC request to the Solana node.
0 commit comments