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:
Anton Sauchyk
2026-09-24 14:51:15 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 01edbbf8c4
commit c00cd608e3
9 changed files with 211 additions and 143 deletions
+1
View File
@@ -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_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_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_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` | | `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: Two mainnet simulations, also no funds moved:
+12
View File
@@ -314,6 +314,18 @@ def resolve_quote_amounts(
return resolved 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: def is_sol_paired(quote_mint: Pubkey | None) -> bool:
"""Check whether a coin is SOL-paired (native SOL transfers). """Check whether a coin is SOL-paired (native SOL transfers).
+12 -27
View File
@@ -12,7 +12,6 @@ from spl.token.instructions import create_idempotent_associated_token_account
from core.pubkeys import ( from core.pubkeys import (
TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE, TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE,
TOKEN_ACCOUNT_SIZE, TOKEN_ACCOUNT_SIZE,
TOKEN_DECIMALS,
SystemAddresses, SystemAddresses,
) )
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo 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 pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint ), # quote_token_mint
AccountMeta( 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 ), # base_token_program
AccountMeta( 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 ), # quote_token_program
AccountMeta( AccountMeta(
pubkey=accounts_info["event_authority"], pubkey=accounts_info["event_authority"],
@@ -306,10 +309,14 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint ), # quote_token_mint
AccountMeta( 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 ), # base_token_program
AccountMeta( 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 ), # quote_token_program
AccountMeta( AccountMeta(
pubkey=accounts_info["event_authority"], pubkey=accounts_info["event_authority"],
@@ -485,28 +492,6 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts 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: def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int:
"""Get the recommended compute unit limit for LetsBonk buy operations. """Get the recommended compute unit limit for LetsBonk buy operations.
-68
View File
@@ -6,7 +6,6 @@ from solders.pubkey import Pubkey
from core.client import SolanaClient from core.client import SolanaClient
from core.pubkeys import ( from core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS, TOKEN_DECIMALS,
cached_quote_units, cached_quote_units,
is_sol_paired, is_sol_paired,
@@ -320,43 +319,6 @@ class PumpFunCurveManager(CurveManager):
return curve_data 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: async def is_curve_complete(self, pool_address: Pubkey) -> bool:
"""Check if the bonding curve is complete (migrated to Raydium). """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) pool_state = await self.get_pool_state(pool_address)
return pool_state.get("complete", False) 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: def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
"""Validate that the curve state structure matches IDL expectations. """Validate that the curve state structure matches IDL expectations.
+1 -23
View File
@@ -6,7 +6,7 @@ from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from spl.token.instructions import create_idempotent_associated_token_account 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 interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
from utils.idl_parser import IDLParser from utils.idl_parser import IDLParser
from utils.logger import get_logger from utils.logger import get_logger
@@ -661,28 +661,6 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info["breaking_fee_recipient"], 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: def get_buy_compute_unit_limit(self, config_override: int | None = None) -> int:
"""Get the recommended compute unit limit for pump.fun buy operations. """Get the recommended compute unit limit for pump.fun buy operations.
+3 -15
View File
@@ -12,8 +12,8 @@ from core.pubkeys import (
WSOL_MINT, WSOL_MINT,
SystemAddresses, SystemAddresses,
cached_quote_token_program, cached_quote_token_program,
is_sol_paired,
normalize_quote_mint, normalize_quote_mint,
quote_symbol,
quote_units_per_token, quote_units_per_token,
) )
from core.wallet import Wallet 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( async def _read_pool_state_with_retry(
curve_manager: object, curve_manager: object,
pool_address: Pubkey, pool_address: Pubkey,
@@ -271,7 +259,7 @@ class PlatformAwareBuyer(Trader):
) )
quote_unit = quote_units_per_token(quote_mint) 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: # Both branches size the trade from the resolved quote amount:
# extreme_fast_mode fixes the token count and back-derives an implied # 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_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) # Use pre-known amount and price (no RPC delay)
token_balance_decimal = token_amount token_balance_decimal = token_amount
+2 -2
View File
@@ -160,7 +160,7 @@ class Position:
"current_price": price_to_use, "current_price": price_to_use,
"price_change": price_change, "price_change": price_change,
"price_change_pct": price_change_pct, "price_change_pct": price_change_pct,
"unrealized_pnl_sol": unrealized_pnl, "unrealized_pnl_quote": unrealized_pnl,
"quantity": self.quantity, "quantity": self.quantity,
} }
@@ -172,4 +172,4 @@ class Position:
status = f"CLOSED ({self.exit_reason.value})" status = f"CLOSED ({self.exit_reason.value})"
else: else:
status = "CLOSED (UNKNOWN)" 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})"
+23 -8
View File
@@ -21,6 +21,7 @@ from core.pubkeys import (
WSOL_MINT, WSOL_MINT,
normalize_quote_mint, normalize_quote_mint,
quote_decimals, quote_decimals,
quote_symbol,
resolve_quote_amounts, resolve_quote_amounts,
resolve_quote_mint, resolve_quote_mint,
resolve_quote_token_program, resolve_quote_token_program,
@@ -669,10 +670,15 @@ class UniversalTrader:
) )
logger.info(f"Created position: {position}") logger.info(f"Created position: {position}")
quote_label = quote_symbol(normalize_quote_mint(token_info.quote_mint))
if position.take_profit_price: 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: 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 # Monitor position until exit condition is met
await self._monitor_position_until_exit(token_info, position) await self._monitor_position_until_exit(token_info, position)
@@ -859,6 +865,9 @@ class UniversalTrader:
pool_address = self._get_pool_address(token_info) pool_address = self._get_pool_address(token_info)
curve_manager = self.platform_implementations.curve_manager curve_manager = self.platform_implementations.curve_manager
exit_sell_attempts = 0 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 # The last price actually read, always positive. A max_hold_time exit
# firing with the price feed down still needs a slippage floor, and # firing with the price feed down still needs a slippage floor, and
@@ -880,7 +889,7 @@ class UniversalTrader:
if should_exit and exit_reason: if should_exit and exit_reason:
logger.info(f"Exit condition met: {exit_reason.value}") 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 # 0.0 satisfies the stop-loss comparison, so the exit can
# fire on a price the sell cannot be floored against. # fire on a price the sell cannot be floored against.
@@ -890,7 +899,8 @@ class UniversalTrader:
if exit_price != current_price: if exit_price != current_price:
logger.warning( logger.warning(
f"Curve priced {token_info.symbol} at 0; flooring the " 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 exit_sell_attempts += 1
@@ -909,7 +919,8 @@ class UniversalTrader:
exit_sell_attempts = 0 exit_sell_attempts = 0
pnl = position.get_pnl(current_price) pnl = position.get_pnl(current_price)
logger.debug( 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 # Wait before next price check
@@ -925,7 +936,7 @@ class UniversalTrader:
logger.warning( logger.warning(
f"Max hold time reached for {token_info.symbol} while " f"Max hold time reached for {token_info.symbol} while "
f"the price is unreadable - exiting against the last " 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 exit_sell_attempts += 1
if await self._run_exit_attempt( if await self._run_exit_attempt(
@@ -999,7 +1010,9 @@ class UniversalTrader:
# Log PnL before exit # Log PnL before exit
pnl = position.get_pnl(price) pnl = position.get_pnl(price)
logger.info( 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( sell_result = await self.seller.execute(
@@ -1023,7 +1036,9 @@ class UniversalTrader:
final_pnl = position.get_pnl() final_pnl = position.get_pnl()
logger.info( 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) await self._cleanup_after_exit(token_info)
return ExitSellVerdict.SOLD 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())