Skip to content

Commit bea3f3e

Browse files
committed
feat(trading): update account data size limit and enhance price calculations
1 parent a095842 commit bea3f3e

File tree

6 files changed

+184
-36
lines changed

6 files changed

+184
-36
lines changed

bots/bot-sniper-1-geyser.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ compute_units:
6161
# Reduces CU cost from 16k to ~128 CU by limiting loaded account data.
6262
# Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead.
6363
# Note: Savings don't show in "consumed CU" but improve tx priority/cost.
64+
# Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue.
6465
# Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit
65-
account_data_size: 512_000 # 512KB limit
66+
account_data_size: 12_500_000
6667

6768
# Filters for token selection
6869
filters:

bots/bot-sniper-2-logs.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ geyser:
2424
# Control trade execution: amount of SOL per trade and acceptable price deviation
2525
trade:
2626
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
27-
buy_slippage: 0.2 # Maximum acceptable price deviation (0.2 = 20%)
27+
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
2828
sell_slippage: 0.3
2929

3030
# Exit strategy configuration
@@ -61,8 +61,9 @@ compute_units:
6161
# Reduces CU cost from 16k to ~128 CU by limiting loaded account data.
6262
# Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead.
6363
# Note: Savings don't show in "consumed CU" but improve tx priority/cost.
64+
# Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue.
6465
# Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit
65-
account_data_size: 512_000 # 512KB limit
66+
account_data_size: 12_500_000
6667

6768
# Filters for token selection
6869
filters:

bots/bot-sniper-3-blocks.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ compute_units:
6161
# Reduces CU cost from 16k to ~128 CU by limiting loaded account data.
6262
# Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead.
6363
# Note: Savings don't show in "consumed CU" but improve tx priority/cost.
64+
# Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue.
6465
# Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit
65-
account_data_size: 512_000 # 512KB limit
66+
account_data_size: 12_500_000
6667

6768
# Filters for token selection
6869
filters:

bots/bot-sniper-4-pp.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ compute_units:
5959
# Reduces CU cost from 16k to ~128 CU by limiting loaded account data.
6060
# Default is 64MB (16k CU). Setting to 512KB significantly reduces overhead.
6161
# Note: Savings don't show in "consumed CU" but improve tx priority/cost.
62+
# Note (Nov 23, 2025): with data size set to 512KB, transactions fail - increasing to 12.5MB resolves the issue.
6263
# Reference: https://www.anza.xyz/blog/cu-optimization-with-setloadedaccountsdatasizelimit
63-
account_data_size: 512_000 # 512KB limit
64+
account_data_size: 12_500_000
6465

6566
# Filters for token selection
6667
filters:

src/core/client.py

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,118 @@ async def get_transaction_token_balance(
284284
Returns:
285285
Token balance (raw amount) after transaction, or None if not found
286286
"""
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+
"""
287399
body = {
288400
"jsonrpc": "2.0",
289401
"id": 1,
@@ -303,20 +415,7 @@ async def get_transaction_token_balance(
303415
if not result or "meta" not in result:
304416
return None
305417

306-
meta = result["meta"]
307-
post_token_balances = meta.get("postTokenBalances", [])
308-
309-
user_str = str(user_pubkey)
310-
mint_str = str(mint)
311-
312-
for balance in post_token_balances:
313-
if balance.get("owner") == user_str and balance.get("mint") == mint_str:
314-
ui_amount = balance.get("uiTokenAmount", {})
315-
amount_str = ui_amount.get("amount")
316-
if amount_str:
317-
return int(amount_str)
318-
319-
return None
418+
return result
320419

321420
async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None:
322421
"""

src/trading/platform_aware.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -131,22 +131,32 @@ async def execute(self, token_info: TokenInfo) -> TradeResult:
131131
if success:
132132
logger.info(f"Buy transaction confirmed: {tx_signature}")
133133

134-
# Fetch actual token amount from transaction to handle slippage
135-
actual_token_balance = await self.client.get_transaction_token_balance(
136-
str(tx_signature), self.wallet.pubkey, token_info.mint
134+
# Fetch actual tokens and SOL spent from transaction
135+
# Uses preBalances/postBalances to get exact amounts
136+
sol_destination = self._get_sol_destination(
137+
token_info, address_provider
138+
)
139+
tokens_raw, sol_spent = await self.client.get_buy_transaction_details(
140+
str(tx_signature), token_info.mint, sol_destination
137141
)
138142

139-
if actual_token_balance is not None:
140-
actual_amount = actual_token_balance / 10**TOKEN_DECIMALS
143+
if tokens_raw is not None and sol_spent is not None:
144+
actual_amount = tokens_raw / 10**TOKEN_DECIMALS
145+
actual_price = (sol_spent / LAMPORTS_PER_SOL) / actual_amount
141146
logger.info(
142147
f"Actual tokens received: {actual_amount:.6f} "
143148
f"(expected: {token_amount:.6f})"
144149
)
150+
logger.info(
151+
f"Actual SOL spent: {sol_spent / LAMPORTS_PER_SOL:.10f} SOL"
152+
)
153+
logger.info(f"Actual price: {actual_price:.10f} SOL/token")
145154
token_amount = actual_amount
155+
token_price_sol = actual_price
146156
else:
147-
logger.warning(
148-
"Could not fetch actual token balance from tx, "
149-
f"using estimated: {token_amount:.6f}"
157+
raise ValueError(
158+
f"Failed to parse transaction details: tokens={tokens_raw}, "
159+
f"sol_spent={sol_spent}"
150160
)
151161

152162
return TradeResult(
@@ -184,6 +194,42 @@ def _get_pool_address(
184194
# Fallback to deriving the address using platform provider
185195
return address_provider.derive_pool_address(token_info.mint)
186196

197+
def _get_sol_destination(
198+
self, token_info: TokenInfo, address_provider: AddressProvider
199+
) -> Pubkey:
200+
"""Get the address where SOL is sent during a buy transaction.
201+
202+
For pump.fun: SOL goes to the bonding curve
203+
For letsbonk: SOL goes to the quote_vault (WSOL vault)
204+
205+
Args:
206+
token_info: Token information
207+
address_provider: Platform-specific address provider
208+
209+
Returns:
210+
Address where SOL is transferred during buy
211+
212+
Raises:
213+
NotImplementedError: If platform SOL destination is not implemented
214+
"""
215+
if token_info.platform == Platform.PUMP_FUN:
216+
# For pump.fun, SOL goes directly to bonding curve
217+
if hasattr(token_info, "bonding_curve") and token_info.bonding_curve:
218+
return token_info.bonding_curve
219+
return address_provider.derive_pool_address(token_info.mint)
220+
elif token_info.platform == Platform.LETS_BONK:
221+
# For letsbonk, SOL goes to quote_vault (WSOL vault)
222+
if hasattr(token_info, "quote_vault") and token_info.quote_vault:
223+
return token_info.quote_vault
224+
# Derive quote_vault if not available
225+
return address_provider.derive_quote_vault(token_info.mint)
226+
227+
raise NotImplementedError(
228+
f"SOL destination not implemented for platform {token_info.platform.value}. "
229+
f"Add platform-specific logic to _get_sol_destination() to specify where "
230+
f"SOL is transferred during buy transactions for this platform."
231+
)
232+
187233
def _get_cu_override(self, operation: str, platform: Platform) -> int | None:
188234
"""Get compute unit override from configuration.
189235
@@ -246,7 +292,7 @@ async def execute(
246292
)
247293
if token_price is None or token_price <= 0:
248294
raise ValueError(
249-
"token_price is required for sell operation. "
295+
"token_price is required for sell operation and must be positive. "
250296
"Pass the price from buy result to avoid RPC delays."
251297
)
252298

@@ -274,20 +320,19 @@ async def execute(
274320
error_message="No tokens to sell",
275321
)
276322

277-
# Calculate expected SOL output
323+
# Calculate expected SOL output with slippage protection
278324
expected_sol_output = token_balance_decimal * token_price_sol
279-
280-
# Calculate minimum SOL output with slippage protection
281-
min_sol_output = int(
282-
(expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL
325+
min_sol_output = max(
326+
1,
327+
int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL),
283328
)
284-
285329
logger.info(
286330
f"Selling {token_balance_decimal} tokens on {token_info.platform.value}"
287331
)
288-
logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL")
332+
logger.info(f"Expected SOL output: {expected_sol_output:.10f} SOL")
289333
logger.info(
290-
f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
334+
f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): "
335+
f"{min_sol_output / LAMPORTS_PER_SOL:.10f} SOL ({min_sol_output} lamports)"
291336
)
292337

293338
# Build sell instructions using platform-specific builder

0 commit comments

Comments
 (0)