mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
fix(trading): label quote-denominated output by its actual quote mint (#224)
Prices, exit targets and PnL printed "SOL" for every coin. They now use the quote mint's symbol, promoted to `core.pubkeys.quote_symbol`, and `get_pnl` returns `unrealized_pnl_quote`. Also deletes seven uncalled methods that converted with `LAMPORTS_PER_SOL` or a hardcoded base unit, and adds a verifier to stop them returning. It covers `core/`, `trading/` and `platforms/pumpfun/`; letsbonk still scales prices by a SOL literal, and the verifier records which sites and why it stops there. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
01edbbf8c4
commit
c00cd608e3
@@ -31,6 +31,7 @@ individual scripts for a subset.
|
||||
| `verify_cookbook_arguments.py` | every cookbook script takes its input as a command-line argument |
|
||||
| `verify_documentation_links.py` | no known-dead URL is back; `--live` fetches every one and fails on 4xx/5xx |
|
||||
| `verify_no_rpc_credentials_logged.py` | credentials masked in every log record, including a URL passed as a non-`str` argument, and every site that installs a root handler installs the redaction first |
|
||||
| `verify_pumpfun_prices_without_hardcoded_sol_unit.py` | nothing in `core/`, `trading/` or `platforms/pumpfun/` scales an amount by `LAMPORTS_PER_SOL`; the letsbonk sites left out of scope are still the documented ones |
|
||||
| `verify_pumpswap_account_layout.py` | pump-amm's `pool-v2` account is gated on `coin_creator`, the buyback pair stays last, and base-token decimals are resolved rather than assumed; `--live` re-reads the authorized recipients from `GlobalConfig` |
|
||||
|
||||
Two mainnet simulations, also no funds moved:
|
||||
|
||||
@@ -314,6 +314,18 @@ def resolve_quote_amounts(
|
||||
return resolved
|
||||
|
||||
|
||||
def quote_symbol(quote_mint: Pubkey) -> str:
|
||||
"""Human-readable label for a quote mint, for logging only.
|
||||
|
||||
Returns:
|
||||
"SOL" for wrapped SOL, otherwise a truncated mint address
|
||||
"""
|
||||
if is_sol_paired(quote_mint):
|
||||
return "SOL"
|
||||
mint_str = str(quote_mint)
|
||||
return f"{mint_str[:4]}..{mint_str[-4:]}"
|
||||
|
||||
|
||||
def is_sol_paired(quote_mint: Pubkey | None) -> bool:
|
||||
"""Check whether a coin is SOL-paired (native SOL transfers).
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from spl.token.instructions import create_idempotent_associated_token_account
|
||||
from core.pubkeys import (
|
||||
TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE,
|
||||
TOKEN_ACCOUNT_SIZE,
|
||||
TOKEN_DECIMALS,
|
||||
SystemAddresses,
|
||||
)
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
@@ -152,10 +151,14 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||
), # quote_token_mint
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["base_token_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # base_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["quote_token_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # quote_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
@@ -306,10 +309,14 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||
), # quote_token_mint
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["base_token_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # base_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["quote_token_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # quote_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
@@ -485,28 +492,6 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts
|
||||
)
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int:
|
||||
"""Get the recommended compute unit limit for LetsBonk buy operations.
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from solders.pubkey import Pubkey
|
||||
|
||||
from core.client import SolanaClient
|
||||
from core.pubkeys import (
|
||||
LAMPORTS_PER_SOL,
|
||||
TOKEN_DECIMALS,
|
||||
cached_quote_units,
|
||||
is_sol_paired,
|
||||
@@ -320,43 +319,6 @@ class PumpFunCurveManager(CurveManager):
|
||||
|
||||
return curve_data
|
||||
|
||||
# Additional convenience methods for pump.fun specific operations
|
||||
async def calculate_expected_tokens(
|
||||
self, pool_address: Pubkey, sol_amount: float
|
||||
) -> float:
|
||||
"""Calculate the expected token amount for a given SOL input.
|
||||
|
||||
This is a convenience method that converts between decimal and raw units.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
sol_amount: Amount of SOL to spend (in decimal SOL)
|
||||
|
||||
Returns:
|
||||
Expected token amount (in decimal tokens)
|
||||
"""
|
||||
sol_lamports = int(sol_amount * LAMPORTS_PER_SOL)
|
||||
tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports)
|
||||
return tokens_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
async def calculate_expected_sol(
|
||||
self, pool_address: Pubkey, token_amount: float
|
||||
) -> float:
|
||||
"""Calculate the expected SOL amount for a given token input.
|
||||
|
||||
This is a convenience method that converts between decimal and raw units.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
token_amount: Amount of tokens to sell (in decimal tokens)
|
||||
|
||||
Returns:
|
||||
Expected SOL amount (in decimal SOL)
|
||||
"""
|
||||
tokens_raw = int(token_amount * 10**TOKEN_DECIMALS)
|
||||
sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw)
|
||||
return sol_lamports / LAMPORTS_PER_SOL
|
||||
|
||||
async def is_curve_complete(self, pool_address: Pubkey) -> bool:
|
||||
"""Check if the bonding curve is complete (migrated to Raydium).
|
||||
|
||||
@@ -369,36 +331,6 @@ class PumpFunCurveManager(CurveManager):
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return pool_state.get("complete", False)
|
||||
|
||||
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get bonding curve completion progress information.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Dictionary with progress information
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Calculate progress based on SOL raised vs target
|
||||
# This is approximate since the exact target isn't stored in the curve state
|
||||
sol_raised = pool_state["real_sol_reserves"] / LAMPORTS_PER_SOL
|
||||
|
||||
# Estimate progress based on typical pump.fun graduation requirements
|
||||
# (This could be made more accurate with additional on-chain data)
|
||||
estimated_target_sol = 85.0 # Typical pump.fun graduation target
|
||||
progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
|
||||
|
||||
return {
|
||||
"complete": pool_state.get("complete", False),
|
||||
"sol_raised": sol_raised,
|
||||
"estimated_target_sol": estimated_target_sol,
|
||||
"progress_percentage": progress_percentage,
|
||||
"tokens_available": pool_state["virtual_token_reserves"]
|
||||
/ 10**TOKEN_DECIMALS,
|
||||
"market_cap_sol": sol_raised, # Approximate market cap
|
||||
}
|
||||
|
||||
def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
|
||||
"""Validate that the curve state structure matches IDL expectations.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from solders.instruction import AccountMeta, Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import create_idempotent_associated_token_account
|
||||
|
||||
from core.pubkeys import TOKEN_DECIMALS, is_sol_paired
|
||||
from core.pubkeys import is_sol_paired
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
from utils.idl_parser import IDLParser
|
||||
from utils.logger import get_logger
|
||||
@@ -661,28 +661,6 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
accounts_info["breaking_fee_recipient"],
|
||||
]
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int:
|
||||
"""Get the recommended compute unit limit for pump.fun buy operations.
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ from core.pubkeys import (
|
||||
WSOL_MINT,
|
||||
SystemAddresses,
|
||||
cached_quote_token_program,
|
||||
is_sol_paired,
|
||||
normalize_quote_mint,
|
||||
quote_symbol,
|
||||
quote_units_per_token,
|
||||
)
|
||||
from core.wallet import Wallet
|
||||
@@ -38,18 +38,6 @@ _FAILURE_REASON_FOR = {
|
||||
}
|
||||
|
||||
|
||||
def _quote_symbol(quote_mint: Pubkey) -> str:
|
||||
"""Human-readable label for a quote mint, for logging only.
|
||||
|
||||
Returns:
|
||||
"SOL" for wrapped SOL, otherwise a truncated mint address
|
||||
"""
|
||||
if is_sol_paired(quote_mint):
|
||||
return "SOL"
|
||||
mint_str = str(quote_mint)
|
||||
return f"{mint_str[:4]}..{mint_str[-4:]}"
|
||||
|
||||
|
||||
async def _read_pool_state_with_retry(
|
||||
curve_manager: object,
|
||||
pool_address: Pubkey,
|
||||
@@ -271,7 +259,7 @@ class PlatformAwareBuyer(Trader):
|
||||
)
|
||||
|
||||
quote_unit = quote_units_per_token(quote_mint)
|
||||
quote_label = _quote_symbol(quote_mint)
|
||||
quote_label = quote_symbol(quote_mint)
|
||||
|
||||
# Both branches size the trade from the resolved quote amount:
|
||||
# extreme_fast_mode fixes the token count and back-derives an implied
|
||||
@@ -731,7 +719,7 @@ class PlatformAwareSeller(Trader):
|
||||
)
|
||||
|
||||
quote_unit = quote_units_per_token(quote_mint)
|
||||
quote_label = _quote_symbol(quote_mint)
|
||||
quote_label = quote_symbol(quote_mint)
|
||||
|
||||
# Use pre-known amount and price (no RPC delay)
|
||||
token_balance_decimal = token_amount
|
||||
|
||||
@@ -160,7 +160,7 @@ class Position:
|
||||
"current_price": price_to_use,
|
||||
"price_change": price_change,
|
||||
"price_change_pct": price_change_pct,
|
||||
"unrealized_pnl_sol": unrealized_pnl,
|
||||
"unrealized_pnl_quote": unrealized_pnl,
|
||||
"quantity": self.quantity,
|
||||
}
|
||||
|
||||
@@ -172,4 +172,4 @@ class Position:
|
||||
status = f"CLOSED ({self.exit_reason.value})"
|
||||
else:
|
||||
status = "CLOSED (UNKNOWN)"
|
||||
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} SOL - {status})"
|
||||
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} - {status})"
|
||||
|
||||
@@ -21,6 +21,7 @@ from core.pubkeys import (
|
||||
WSOL_MINT,
|
||||
normalize_quote_mint,
|
||||
quote_decimals,
|
||||
quote_symbol,
|
||||
resolve_quote_amounts,
|
||||
resolve_quote_mint,
|
||||
resolve_quote_token_program,
|
||||
@@ -669,10 +670,15 @@ class UniversalTrader:
|
||||
)
|
||||
|
||||
logger.info(f"Created position: {position}")
|
||||
quote_label = quote_symbol(normalize_quote_mint(token_info.quote_mint))
|
||||
if position.take_profit_price:
|
||||
logger.info(f"Take profit target: {position.take_profit_price:.8f} SOL")
|
||||
logger.info(
|
||||
f"Take profit target: {position.take_profit_price:.8f} {quote_label}"
|
||||
)
|
||||
if position.stop_loss_price:
|
||||
logger.info(f"Stop loss target: {position.stop_loss_price:.8f} SOL")
|
||||
logger.info(
|
||||
f"Stop loss target: {position.stop_loss_price:.8f} {quote_label}"
|
||||
)
|
||||
|
||||
# Monitor position until exit condition is met
|
||||
await self._monitor_position_until_exit(token_info, position)
|
||||
@@ -859,6 +865,9 @@ class UniversalTrader:
|
||||
pool_address = self._get_pool_address(token_info)
|
||||
curve_manager = self.platform_implementations.curve_manager
|
||||
exit_sell_attempts = 0
|
||||
# Every price below is denominated in the coin's quote asset, which is
|
||||
# not always SOL.
|
||||
quote_label = quote_symbol(normalize_quote_mint(token_info.quote_mint))
|
||||
|
||||
# The last price actually read, always positive. A max_hold_time exit
|
||||
# firing with the price feed down still needs a slippage floor, and
|
||||
@@ -880,7 +889,7 @@ class UniversalTrader:
|
||||
|
||||
if should_exit and exit_reason:
|
||||
logger.info(f"Exit condition met: {exit_reason.value}")
|
||||
logger.info(f"Current price: {current_price:.8f} SOL")
|
||||
logger.info(f"Current price: {current_price:.8f} {quote_label}")
|
||||
|
||||
# 0.0 satisfies the stop-loss comparison, so the exit can
|
||||
# fire on a price the sell cannot be floored against.
|
||||
@@ -890,7 +899,8 @@ class UniversalTrader:
|
||||
if exit_price != current_price:
|
||||
logger.warning(
|
||||
f"Curve priced {token_info.symbol} at 0; flooring the "
|
||||
f"exit sell at the last real price {exit_price:.8f} SOL"
|
||||
f"exit sell at the last real price "
|
||||
f"{exit_price:.8f} {quote_label}"
|
||||
)
|
||||
|
||||
exit_sell_attempts += 1
|
||||
@@ -909,7 +919,8 @@ class UniversalTrader:
|
||||
exit_sell_attempts = 0
|
||||
pnl = position.get_pnl(current_price)
|
||||
logger.debug(
|
||||
f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)"
|
||||
f"Position status: {current_price:.8f} {quote_label} "
|
||||
f"({pnl['price_change_pct']:+.2f}%)"
|
||||
)
|
||||
|
||||
# Wait before next price check
|
||||
@@ -925,7 +936,7 @@ class UniversalTrader:
|
||||
logger.warning(
|
||||
f"Max hold time reached for {token_info.symbol} while "
|
||||
f"the price is unreadable - exiting against the last "
|
||||
f"known price {last_known_price:.8f} SOL"
|
||||
f"known price {last_known_price:.8f} {quote_label}"
|
||||
)
|
||||
exit_sell_attempts += 1
|
||||
if await self._run_exit_attempt(
|
||||
@@ -999,7 +1010,9 @@ class UniversalTrader:
|
||||
# Log PnL before exit
|
||||
pnl = position.get_pnl(price)
|
||||
logger.info(
|
||||
f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)"
|
||||
f"Position PnL: {pnl['price_change_pct']:.2f}% "
|
||||
f"({pnl['unrealized_pnl_quote']:.6f} "
|
||||
f"{quote_symbol(normalize_quote_mint(token_info.quote_mint))})"
|
||||
)
|
||||
|
||||
sell_result = await self.seller.execute(
|
||||
@@ -1023,7 +1036,9 @@ class UniversalTrader:
|
||||
|
||||
final_pnl = position.get_pnl()
|
||||
logger.info(
|
||||
f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)"
|
||||
f"Final PnL: {final_pnl['price_change_pct']:.2f}% "
|
||||
f"({final_pnl['unrealized_pnl_quote']:.6f} "
|
||||
f"{quote_symbol(normalize_quote_mint(token_info.quote_mint))})"
|
||||
)
|
||||
await self._cleanup_after_exit(token_info)
|
||||
return ExitSellVerdict.SOLD
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Verify the pump.fun and universal code paths never scale a price by a SOL literal.
|
||||
|
||||
A coin's price is denominated in whatever it trades against, so converting raw
|
||||
reserves to whole units means dividing by *that mint's* unit, not by
|
||||
`LAMPORTS_PER_SOL`. `quote_units_per_token` raising for an unresolved mint keeps
|
||||
that honest wherever the unit is looked up -- but it says nothing about code that
|
||||
writes the divisor in by hand, which is exactly how three uncalled helpers on the
|
||||
pump.fun curve manager came to price everything as SOL.
|
||||
|
||||
Those helpers are gone. This check is what stops a fourth being written.
|
||||
|
||||
Scope: `core/`, `trading/` and `platforms/pumpfun/`.
|
||||
|
||||
`platforms/letsbonk/` is NOT covered, and this is a statement of where the rule
|
||||
holds rather than an exemption granted for convenience. letsbonk genuinely
|
||||
violates it -- `curve_manager.py` prices with
|
||||
`* 10**TOKEN_DECIMALS / LAMPORTS_PER_SOL` and drops the `base_decimals`,
|
||||
`quote_decimals` and `quote_mint` that its `PoolState` carries, so every pool is
|
||||
priced as though it were SOL-quoted. Widening this check to `platforms/` means
|
||||
fixing those two sites first; the runner has no way to express a known failure,
|
||||
so a repo-wide version of this check would be red permanently and would train
|
||||
everyone to stop reading the summary line.
|
||||
|
||||
Offline machine checks, no network and no funds moved:
|
||||
|
||||
A. No module in scope divides or multiplies anything by LAMPORTS_PER_SOL.
|
||||
B. The letsbonk sites this check deliberately does not cover are still the
|
||||
ones named above, so widening it later is a path-filter change against a
|
||||
list that is already written down rather than a fresh investigation.
|
||||
|
||||
Usage:
|
||||
uv run tests/regression/verify_pumpfun_prices_without_hardcoded_sol_unit.py
|
||||
"""
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC = PROJECT_ROOT / "src"
|
||||
|
||||
# Directories this rule covers. letsbonk is out; see the module docstring.
|
||||
IN_SCOPE = ("core", "trading", "platforms/pumpfun")
|
||||
|
||||
SOL_LITERAL = "LAMPORTS_PER_SOL"
|
||||
|
||||
# The letsbonk sites this check does not cover, as (path, symbol). Kept here so
|
||||
# widening the scope starts from a list rather than a search.
|
||||
KNOWN_UNCOVERED = (
|
||||
("src/platforms/letsbonk/curve_manager.py", "calculate_price"),
|
||||
("src/platforms/letsbonk/curve_manager.py", "_decode_pool_state_with_idl"),
|
||||
)
|
||||
|
||||
|
||||
def _in_scope(path: Path) -> bool:
|
||||
"""Whether a module falls under this check's directories.
|
||||
|
||||
Args:
|
||||
path: Module path anywhere under `src/`
|
||||
|
||||
Returns:
|
||||
True if the module is in one of IN_SCOPE
|
||||
"""
|
||||
rel = path.relative_to(SRC).as_posix()
|
||||
return any(rel.startswith(f"{d}/") for d in IN_SCOPE)
|
||||
|
||||
|
||||
def _scales_by_sol_literal(node: ast.AST) -> list[int]:
|
||||
"""Lines under a node that multiply or divide by the SOL literal.
|
||||
|
||||
Args:
|
||||
node: Any AST node to walk -- a module or a single function
|
||||
|
||||
Returns:
|
||||
Line numbers of the offending binary operations
|
||||
"""
|
||||
hits = []
|
||||
for sub in ast.walk(node):
|
||||
if not isinstance(sub, ast.BinOp) or not isinstance(sub.op, ast.Div | ast.Mult):
|
||||
continue
|
||||
for side in (sub.left, sub.right):
|
||||
if isinstance(side, ast.Name) and side.id == SOL_LITERAL:
|
||||
hits.append(sub.lineno)
|
||||
return hits
|
||||
|
||||
|
||||
def check_no_hardcoded_sol_unit_in_pricing() -> None:
|
||||
"""Nothing in scope may scale any amount by LAMPORTS_PER_SOL.
|
||||
|
||||
A flat rule rather than one keyed on function names: the three helpers this
|
||||
replaced included `calculate_expected_tokens`, whose name says nothing about
|
||||
SOL while its body converts with it. Nothing in scope legitimately needs the
|
||||
literal -- amounts are scaled by the quote mint's unit, and the constant
|
||||
itself stays for anything genuinely denominated in SOL, such as a rent
|
||||
reserve or a fee budget, neither of which lives in these directories.
|
||||
"""
|
||||
offenders = []
|
||||
for path in sorted(SRC.rglob("*.py")):
|
||||
if "generated" in path.parts or not _in_scope(path):
|
||||
continue
|
||||
for line in _scales_by_sol_literal(ast.parse(path.read_text())):
|
||||
offenders.append(
|
||||
f"{path.relative_to(PROJECT_ROOT)}:{line}: scales by {SOL_LITERAL}"
|
||||
)
|
||||
|
||||
assert not offenders, (
|
||||
"a price was scaled by a SOL literal instead of the quote mint's unit:\n "
|
||||
+ "\n ".join(offenders)
|
||||
)
|
||||
|
||||
|
||||
def check_uncovered_sites_unchanged() -> None:
|
||||
"""The letsbonk sites left out of scope must still be the documented ones.
|
||||
|
||||
If letsbonk stops violating the rule, this check fails and the scope above
|
||||
should widen. If it grows a third site, the same. Either way the docstring
|
||||
stops being a claim nobody re-reads.
|
||||
"""
|
||||
still_offending = []
|
||||
for rel, symbol in KNOWN_UNCOVERED:
|
||||
path = PROJECT_ROOT / rel
|
||||
if not path.exists():
|
||||
continue
|
||||
for node in ast.walk(ast.parse(path.read_text())):
|
||||
if (
|
||||
isinstance(node, ast.AsyncFunctionDef | ast.FunctionDef)
|
||||
and node.name == symbol
|
||||
and _scales_by_sol_literal(node)
|
||||
):
|
||||
still_offending.append((rel, symbol))
|
||||
|
||||
assert still_offending == list(KNOWN_UNCOVERED), (
|
||||
"the out-of-scope letsbonk sites changed; widen this check's scope or "
|
||||
f"update KNOWN_UNCOVERED.\n documented: {list(KNOWN_UNCOVERED)}\n"
|
||||
f" found: {still_offending}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run every check and report."""
|
||||
print("=" * 72)
|
||||
print("Verifying pump.fun prices are not scaled by a SOL literal")
|
||||
print("=" * 72)
|
||||
|
||||
checks = [
|
||||
("no hardcoded SOL unit in scope", check_no_hardcoded_sol_unit_in_pricing),
|
||||
("the uncovered letsbonk sites are unchanged", check_uncovered_sites_unchanged),
|
||||
]
|
||||
for label, fn in checks:
|
||||
fn()
|
||||
print(f"{label} -> OK")
|
||||
|
||||
print("\nALL CHECKS PASSED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user