* fix: /pump holders and sol-price depended on Cloudflare-blocked APIs; honest error for the rest
Continues the pump.fun/PumpSwap edge-case audit. Confirmed live: both
frontend-api-v3.pump.fun and advanced-api-v2.pump.fun return HTTP 403
Cloudflare blocks for every endpoint tried, from this environment — the
same root cause as isGraduated()'s earlier fix. This is a partial slice of
that broader gap covering the parts with a real fix available; the ~19
discovery/listing commands sharing pumpFrontendRequest() with no feasible
on-chain replacement (trending, gainers, search, chart/OHLCV, etc. — all
inherently need an off-chain indexer) are unchanged beyond inheriting a
more honest error message.
- Added getPumpTokenHolders() to pumpapi.ts — real on-chain holder lookup
via connection.getTokenLargestAccounts() + raw SPL account byte parsing
(matches this file's existing convention, e.g. getTokenBalance),
replacing /pump holders' dependency on advanced-api-v2.pump.fun. Also
added creator: PublicKey extraction to parseBondingCurveState/
BondingCurveState (byte 49, 32 bytes — verified against the official
SDK's real BondingCurve type, and against this parser's own pre-existing
isMayhemMode read at byte 81, which only lines up if creator occupies
exactly bytes 49-80), needed for the new isCreator flag on holder
results. handleHolders() now wired to this instead of the blocked API.
- handleSolPrice() now uses getSOLPrice() from src/feeds/crypto/index.ts
(Binance-backed, already used elsewhere in this codebase, not
Cloudflare-gated) instead of pump.fun's own /sol-price endpoint.
- pumpFrontendRequest()'s error message now distinguishes a genuine
Cloudflare block (403 status + non-JSON content-type — live-verified
this returns text/html) from other failures, so a real 404 for an
unknown mint still reports as a 404 instead of being mislabeled. For
the commands with no on-chain alternative, this at least makes the
failure honest and actionable ("pump.fun's data API is blocking this
request... not fixable per-request") instead of a bare, confusing
status code.
Also checked and found already-safe: handleBalance/handleHoldings already
wrap their frontend-API enrichment calls in .catch(() => null) and
degrade gracefully — their core data comes from pumpapi.getTokenBalance/
getUserPumpTokens, both already on-chain.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: bump browserslist to clear a newly-published high-severity GHSA pair (#100)
audit-ci's production-dependency scan started failing on every open PR
(mine and others) with two new browserslist advisories (GHSA-73wf-gq98-2v4g,
GHSA-c83g-rgw3-j3cx — unbounded cache growth / prototype-write crash),
transitive through nyc (@kamino-finance/klend-sdk's farms-sdk) and webpack
(binance). `npm audit fix --production` bumps browserslist 4.28.1 -> 4.28.8
non-breakingly; no allowlist changes needed since the fix is a real upgrade,
not a suppression. Verified: npm audit --production now reports 61
vulnerabilities, all already covered by audit-ci.jsonc's existing allowlist.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
---------
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix: swarm stop-loss/take-profit and 'auto' DEX routing didn't account for graduation
Continues the pump.fun/PumpSwap edge-case audit. Two confirmed, live-verified bugs.
- checkPriceTriggers() (src/solana/pump-swarm.ts) was hitting the same
Cloudflare-blocked pump.fun frontend API isGraduated() was fixed for
earlier this session. On a block, `!response.ok` silently `continue`d
with zero logging — swarm stop-loss/take-profit could stop firing
entirely with no trace. The old raw solReserves/tokenReserves ratio was
also never decimal-adjusted (SOL 9dp vs pump.fun tokens 6dp) — off from
a real per-token price by ~1000x, even when the API call succeeded,
against triggerPrice values documented in real SOL-per-token terms
(e.g. `/swarm stop-loss`'s own usage example of 0.00001).
Fixed by reading the bonding curve directly on-chain via
getBondingCurveState()/calculatePrice() (both already exist in
pumpapi.ts, decimal-correct). A design-review pass then caught a third,
more subtle bug in that fix before it shipped: the bonding curve
account isn't closed on graduation, but its reserves freeze at their
final pre-migration values once trading moves to PumpSwap —
calculatePrice() on a graduated token would have silently returned a
stale/zero price forever, meaning a stop-loss/take-profit surviving a
token's graduation would stop tracking the real price. Live-verified
against the real graduated mint 9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump:
the bonding-curve price resolves to 0 (frozen/zeroed reserves), while
the correct live PumpSwap price is 0.00173 SOL/token. Fixed by branching
on state.complete: non-graduated uses calculatePrice(), graduated
resolves live price from findBestPumpSwapState() (pumpswap.ts) instead.
- getBuilder('auto') (src/solana/swarm-builders.ts) always returned
PumpFunBuilder regardless of whether the token had graduated — 'auto'
didn't do what its name promised, and the deeper pool: 'auto' option
threaded through to buildPumpFunTradeInstructions is treated identically
to pool: 'pump' there too (assertSupportedPumpPool in pumpapi.ts), so
there was no real auto-routing anywhere in this call chain. Reachable in
practice via the `/swarm buy`/`/swarm sell --dex auto` skill flag
(type-allowed, though undocumented in the printed help text). Fixed:
getBuilder is now async, takes optional connection/mint, and checks
getBestPool() when dex === 'auto' to pick PumpFunBuilder vs
PumpSwapBuilder. Both call sites in pump-swarm.ts updated. Live-verified
against both a real active mint (routes to PumpFunBuilder) and the real
graduated mint above (routes to PumpSwapBuilder); the no-context
fallback (no connection/mint available) still safely defaults to
PumpFunBuilder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: bump browserslist to clear a newly-published high-severity GHSA pair (#100)
audit-ci's production-dependency scan started failing on every open PR
(mine and others) with two new browserslist advisories (GHSA-73wf-gq98-2v4g,
GHSA-c83g-rgw3-j3cx — unbounded cache growth / prototype-write crash),
transitive through nyc (@kamino-finance/klend-sdk's farms-sdk) and webpack
(binance). `npm audit fix --production` bumps browserslist 4.28.1 -> 4.28.8
non-breakingly; no allowlist changes needed since the fix is a real upgrade,
not a suppression. Verified: npm audit --production now reports 61
vulnerabilities, all already covered by audit-ci.jsonc's existing allowlist.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
---------
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
audit-ci's production-dependency scan started failing on every open PR
(mine and others) with two new browserslist advisories (GHSA-73wf-gq98-2v4g,
GHSA-c83g-rgw3-j3cx — unbounded cache growth / prototype-write crash),
transitive through nyc (@kamino-finance/klend-sdk's farms-sdk) and webpack
(binance). `npm audit fix --production` bumps browserslist 4.28.1 -> 4.28.8
non-breakingly; no allowlist changes needed since the fix is a real upgrade,
not a suppression. Verified: npm audit --production now reports 61
vulnerabilities, all already covered by audit-ci.jsonc's existing allowlist.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Continues this session's pump.fun/PumpSwap work. isGraduated()'s
pumpswapPool field was always undefined in practice — getTokenInfo() hits
pump.fun's frontend-api-v3.pump.fun endpoint directly, which sits behind
Cloudflare bot-protection that blocks non-browser requests. Confirmed live
against a real, long-graduated token (mint
9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump): its bonding curve correctly
parsed as complete:true — the core graduation-boundary detection itself
was already correct, the account isn't closed on graduation, it persists
on-chain with complete:true — but getTokenInfo() returned null every time
due to the Cloudflare block, so pumpswapPool came back undefined
regardless of whether the token had actually graduated.
Fixed by trying findPumpSwapPools() (from pumpswap.ts, an already-proven
on-chain program-account lookup that file's own trading functions already
rely on) first, falling back to the frontend API only if that comes up
empty. Live-verified end to end: isGraduated() now returns a real pool
address (ECE2mWMQZUa8i8qGchuSAB66QzDN3WUhhtGRMbgNL3L), independently
confirmed to be a real account owned by the actual PumpSwap program
(pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA). getBestPool() automatically
inherits the fix since it just forwards isGraduated()'s result.
Related, non-blocking follow-up not addressed here: cross-checking the
official @pump-fun/pump-sdk's real BondingCurve type shows the hand-rolled
byte-offset parser in parseBondingCurveState() is missing two real
on-chain fields — creator: PublicKey (byte 49) and quoteMint: PublicKey
(later in the struct) — that exist in the actual account layout. The
complete flag and reserve fields it does parse are byte-offset-correct,
but any bonding curve whose quoteMint isn't native SOL (e.g. certain
Mayhem-mode/cashback-coin curves, which the parser already has partial
awareness of via isMayhemMode) would have its virtualSolReserves/
realSolReserves fields silently misinterpreted as SOL when they're
actually a different quote asset.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit of src/solana/drift.ts, part of the ongoing Solana venue-integration
coverage pass. Three confirmed, live-verified bugs.
- OrderType/OrderStatus/MarketType/PositionDirection are Anchor-style
object-shaped enums (e.g. OrderType.LIMIT = { limit: {} }, MarketType.PERP
= { perp: {} }) — verified against the SDK's own types.d.ts. Comparing a
freshly-decoded on-chain value against these with === / !== always fails,
since they're different object references even for the identical
conceptual variant — confirmed both from the type declarations and a
standalone synthetic proof (`{ perp: {} } === MarketType.PERP` is false).
This broke:
- getDriftOrders()'s marketType filter: `order.marketType !== mType`
was always true, so passing a marketType filter always excluded every
order.
- getDriftOrders()'s returned marketType/direction/orderType/status
fields: always mislabeled ('perp' regardless of real type, 'short'
regardless of real direction, 'unknown' for orderType/status always).
- cancelDriftOrder()'s cancel-by-market path: `order.marketType ===
marketType` was always false, so it never found any order to cancel —
always fell through to an empty cancelled list and a no-op txSig.
Fixed with a new enumVariant() helper that reads the decoded object's own
single key (Object.keys(value)[0]) instead of reference-comparing it
against the SDK's static constants.
- getPositions() (used by the liquidation monitor) computed liquidation
price with a hand-rolled formula assuming a flat 5% maintenance margin
and a single isolated position — ignoring the account's real per-market
margin requirements, cross-margining across its other positions, and
open orders. Replaced with the SDK's own user.liquidationPrice
(marketIndex), which the SDK explicitly documents as accounting for all
of that.
- client.getUser() throws ("DriftClient has no user for user id ...")
rather than returning null/undefined for any wallet that has never
initialized a Drift account — confirmed live. getDriftOrders,
getDriftPositions, getDriftBalance, and cancelDriftOrder all called it
unguarded, so any first-time-checking wallet (checking positions/orders/
balance before ever trading on Drift) hit an uncaught exception instead
of the sensible empty/zero result their return types promise. The
liquidation monitor's getPositions()/getAccountHealth() already had
`if (!user) ...` checks, but those were dead code for the same reason —
getUser() throws before ever returning a falsy value. Fixed all 6 call
sites with a client.hasUser() guard, which is the real, correct way to
check first.
Also checked and found clean: confirmed via reading the SDK's own
TransactionConfirmationManager.js that client.placePerpOrder/cancelOrder/
modifyOrder/etc. already check the on-chain confirmation status.err and
throw (via throwTransactionError) on revert — so the "landed but reverted
silently reported as success" bug class already fixed elsewhere this
session does not apply here; no wallet.ts routing needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit of src/solana/kamino.ts, part of the ongoing Solana venue-integration
coverage pass. The most severe finding of this audit series: KaminoMarket
.load()'s real signature is (connection, marketAddress, recentSlotDurationMs:
number, programId?, setupLocalTest?, withReserves?) — verified against the
SDK's own market.d.ts — but every one of the 6 call sites (getKaminoMarkets,
getKaminoObligation, depositToKamino, withdrawFromKamino, borrowFromKamino,
repayToKamino) passed PROGRAM_ID as the third argument (where a number was
expected) and never passed a real programId at all. The `as any` cast on
the destructured import silenced TypeScript's arity check entirely, so this
was invisible to typecheck.
Confirmed live in two layers:
- With the wrong arg order, getKaminoMarkets() always returned [] — first
traced to calculateSupplyAPR()/calculateBorrowAPR() throwing
"[DecimalError] Invalid argument: undefined" for every single reserve,
no exceptions.
- Fixing just that arity (those two methods need (slot, referralFeeBps),
not zero args) surfaced the real, deeper bug: a different error,
"Invalid borrow rate curve, could not identify the interpolation
points," again on every reserve — because recentSlotDurationMs was
receiving a garbage PublicKey object instead of a real millisecond
duration, breaking the SDK's internal rate-curve interpolation
entirely regardless of which reserve.
Fixed by passing the SDK's own DEFAULT_RECENT_SLOT_DURATION_MS (=450) as
the third argument and PROGRAM_ID as the fourth, at all 6 call sites.
After the fix, getKaminoMarkets() returns all 58 real reserves with
realistic live rates (e.g. SOL: 5.05% deposit / 6.6% borrow / 89.9%
utilization / 74% LTV). This bug affected every Kamino lending function in
the file, not just the read path — deposit/withdraw/borrow/repay all load
the market the same way.
Also added a per-reserve try/catch in getKaminoMarkets() (matching the
per-item pattern getKaminoStrategies() below already uses) so one
genuinely malformed reserve can't blank out the whole result the way this
bug did.
Known, non-blocking follow-up not addressed here: getKaminoStrategies/
getKaminoStrategy/getKaminoUserShares (the kliquidity-sdk vault side)
hardcode tvl:'0', apy:0, tokenASymbol:'TokenA', tokenBSymbol:'TokenB', and
every user position amount to '0', despite the SDK exposing real methods
for this data (getStrategyAprApy, getStrategyShareData,
getStrategyTokensHoldings) — confirmed these exist via the SDK's Kamino.d.ts.
Not fixed here since properly wiring up TVL/APY/token-amount computation
across three functions is a separate, larger scope than this fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit of src/solana/raydium.ts, part of the ongoing Solana venue-integration
coverage pass. Two confirmed, live-verified bugs.
- getClmmPositions() had fee/mint fields wrong. The raw on-chain position
layout (verified against @raydium-io/raydium-sdk-v2's PositionInfoLayout
type) has no mintA/mintB at all — those live on the pool, not the
position — and its fee fields are named the opposite of what the old
code assumed: tokenFeesOwedA/B are the actual owed amounts, while
feeGrowthInsideLastX64A/B are internal accounting checkpoints (huge X64
fixed-point cursors, not a real "owed" quantity). The old code set
tokenA/tokenB (meant to be mint addresses) from tokenFeesOwedA/B (fee
amounts) and feeOwedA/feeOwedB from the checkpoint fields — both wrong.
Now batch-fetches pool info for the position set (same pattern
harvestClmmRewards already used) to get real mintA/mintB.address, reads
fees from tokenFeesOwedA/B, and additionally populates rewardInfos
(previously omitted entirely) using pool.rewardDefaultInfos[i].mint.address,
confirmed live.
- listRaydiumPoolsSdk()'s raydium.api.getPoolList() call always returned
{ count: 0, data: [] } regardless of params — confirmed live against the
installed 0.2.32-alpha SDK across every type/sort/order combination
tried. The raw HTTP endpoint (api-v3.raydium.io/pools/info/list), hit
directly with the same param names the sibling listRaydiumPools() (REST
version) already uses successfully, returns real data. Switched
listRaydiumPoolsSdk() to that same proven-working call instead of the
broken SDK wrapper.
Also checked and found clean: fetchPoolById (used by the other CLMM
functions) and getClmmConfigs both return correct, real data live; every
execute() call site in this file already passes { sendAndConfirm: true }
(or { sequentially: true } for the multi-tx harvest path), both of which
route through @solana/web3.js's own sendAndConfirmTransaction internally —
confirmed via the SDK's shipped source — so the "landed but reverted"
silent-success bug class already fixed elsewhere this session does not
apply here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TypeScript's whole-program type resolution for this file kept resolving
the bare '@solana/spl-token' specifier to a stale nested @solana/spl-token@0.1.8
copy (pulled in via this file's other dynamic import of the deprecated
@orca-so/whirlpool-sdk, which pins ^0.1.8) instead of the real installed
0.4.x — tried named import, namespace import, and dynamic import, all
failed identically, only in this file. Sidestepped entirely: orcaTickRangeToPrices
now reads mint decimals via a plain connection.getParsedAccountInfo() call
instead of spl-token's typed getMint(), live-verified to return the correct
decimals (SOL=9, USDC=6) against real mainnet mints.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auditing src/solana/orca.ts turned up four distinct, compounding bug
classes across every function using @orca-so/whirlpools (v2 SDK) — nothing
in that section had ever actually worked:
1. Dependency resolution was broken tree-wide. @orca-so/whirlpools needs
@solana/kit@^5.0.0, but @drift-labs/sdk's gill dependency pinned
@solana/kit@^2.3.0, and with legacy-peer-deps=true npm silently deduped
the whole tree onto the old 2.3.0 — an incompatible major version.
`await import('@orca-so/whirlpools')` crashed immediately with
"SyntaxError: ... does not provide an export named
'sequentialInstructionPlan'" on every call. Fixed with a global
`"@solana/kit": "^5.0.0"` override in package.json, resolved to 5.5.1;
verified this doesn't affect gill/@drift-labs/sdk (our code never
imports either directly). Added @solana/kit, @orca-so/whirlpools-client,
and @orca-so/whirlpools-core as explicit dependencies since orca.ts now
imports them directly rather than relying on transitive resolution.
2. Wrong function names. sdk.openPosition and sdk.increaseLiquidity don't
exist on the real SDK — the actual exports are openConcentratedPosition
and increasePosLiquidity. Only discoverable once fix#1 let the import
succeed at all.
3. Wrong argument types/shapes throughout:
- @solana/kit's `Address` is a plain base58 string, never a legacy
web3.js PublicKey — every `new PublicKey(...)` wrap on an address
argument broke the call (confirmed live, e.g. passing a PublicKey
into fetchPositionsForOwner's first arg threw "rpc.getTokenAccountsBy
Owner is not a function" because it landed in the `rpc` slot).
- The "fetch" family (fetchPositionsForOwner/InWhirlpool/
WhirlpoolsByTokenPair) takes an explicit `rpc` as its first argument,
unlike the "action" family (open/increase/decrease/harvest/close/
create), which gets it implicitly from sdk.setRpc(). Every fetch call
was missing that argument entirely.
- IncreaseLiquidityQuoteParam/DecreaseLiquidityQuoteParam are
discriminated unions accepting exactly one of liquidity/tokenA/tokenB;
the code built objects with multiple keys present. Centralized into
buildOrcaLiquidityParam().
- openConcentratedPosition takes actual (lowerPrice, upperPrice)
numbers, not tick indices, despite the .d.ts appearing tick-shaped.
Converted via @orca-so/whirlpools-core's tickIndexToPrice() using the
pool's real on-chain token decimals, so the tick-based public
interface (already relied on by src/agents/index.ts and
src/skills/bundled/orca/index.ts) didn't need to change.
4. Wrong result shapes and, critically, missing execution:
- fetch results are Account<T> (fields under `.data`), not flat —
confirmed live (a real position's tickLowerIndex/liquidity/feeOwedA
etc. are all under `.data`, and its usable identifier is
`.data.positionMint`, not the account's own `.address`). Centralized
into mapOrcaPositionData()/flattenOrcaPositions() (handles position
bundles too).
- Every write function (open/increase/decrease/harvest/close/create)
returns `{ instructions, quote, callback }` and does NOT send
anything until `.callback()` is called. None of the old code called
it — it read a nonexistent `result.signature` field instead, so these
functions built valid instructions, reported back an empty signature,
and never touched the chain at all. Now every write path calls
`await result.callback()` and returns the real signature.
- harvestPosition's real result shape is `{ feesQuote: { feeOwedA,
feeOwedB }, rewardsQuote: { rewards: [{ rewardsOwed }] } }`, not the
flat fields the old code guessed at.
Verified live end-to-end against real mainnet: fetchWhirlpoolsByTokenPair/
fetchPositionsInWhirlpool/fetchPositionsForOwner now return correct,
populated data (confirmed against a real SOL/USDC Whirlpool and one of its
~20k real positions). openOrcaFullRangePosition/openOrcaConcentratedPosition
now build real instructions and reach actual on-chain simulation (failing
only on AccountNotFound from a deliberately zero-balance throwaway wallet —
proving the full args/shapes/execution path is correct, not guessing).
Left alone, out of scope: executeOrcaWhirlpoolSwap/getOrcaWhirlpoolQuote/
listOrcaWhirlpoolPools still use the older, npm-deprecated
@orca-so/whirlpool-sdk — confirmed still functional live today (unlike the
v2 path above), so not blocking, but a known follow-up to migrate onto the
v2 SDK's swap()/swapInstructions().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every limit-order function in src/solana/jupiter.ts (create, cancel, batch
cancel, list, get, get history, get fee, get trade history, cancel/batch
cancel expired) dynamically imports @jup-ag/limit-order-sdk, which requires
@trpc/client at module load time, which in turn requires @trpc/server as a
peer dependency — never installed in this repo. Confirmed by direct
`require('@jup-ag/limit-order-sdk')`: it threw
"Cannot find module '@trpc/server/observable'" every time, so every one of
those functions rejected immediately, before ever touching Solana. Pinned
@trpc/server to the exact peer version @trpc/client 10.45.4 needs; after
installing it, live calls against the real Jupiter Limit Order program on
mainnet (getJupiterLimitOrderFee, listJupiterLimitOrders) succeed and decode
correctly.
Also fixed two smaller issues found in the same file while verifying live
behavior:
- executeJupiterSwap's return type (JupiterSwapResult) declares top-level
inAmount/outAmount/priceImpactPct/routePlan fields, but the function never
populated them (only nested under `quote`). The /jup and /sol skill swap
commands read those top-level fields directly, so a real swap's summary
output always rendered undefined/N/A/"Direct" for amount, price impact,
and route — only the signature was ever correct. Now populated from the
quote.
- listJupiterLimitOrdersByMint fell back to calling the SDK's getOrders()
with no filters at all when none of owner/inputMint/outputMint were given,
which does an unfiltered scan of every open order on the entire Jupiter
Limit Order program (all users, not just the caller). No current call site
hits this today (both always pass owner), but it's exported and one filter
short of the same unbounded-fetch bug class already fixed in
meteora-dbc.ts's getDbcPools(). Now throws instead of silently fetching
everything.
client.state.getPool()/getPoolByBaseMint()/getPoolsByConfig()/getPoolsByCreator()
in @meteora-ag/dynamic-bonding-curve-sdk all return { poolState: VirtualPool },
not a flat VirtualPool. The existing code guessed at field names via
(pool as any).config || (pool as any).poolConfig-style fallbacks that always
resolved to undefined, so getDbcPoolStatus always reported isMigrated=false,
quoteReserve='0', and empty configAddress/creator regardless of real on-chain
state, and getDbcSwapQuote/V2 called getPoolConfig(undefined) on every
invocation.
Separately, swapQuote()/swapQuote2()'s `virtualPool` param is typed as a flat
VirtualPool in the .d.ts but is actually dereferenced internally as
`virtualPool.poolState.*` (confirmed by reading the shipped dist/index.js) —
the opposite unwrap direction from the pool-status fix. Passing the unwrapped
pool there throws "Cannot read properties of undefined (reading
'quoteReserve')"; passing the untouched wrapper is correct.
Also disabled getDbcPoolConfigs() (unfiltered client.state.getPoolConfigs()
call did not return within 30s live against mainnet — same failure class as
the pre-existing guard on getDbcPools()), pointing callers at
getDbcPoolConfigsByOwner instead.
All fixes verified live against real mainnet DBC pools, including an
end-to-end quote match between the V1 and V2 quote paths on an active
(non-migrated) pool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes out all five items deliberately deferred from the previous audit PR.
1. fetchFeeConfig error swallowing (pumpapi.ts): .catch(() => null) treated
every failure identically, including transient RPC errors, silently
feeding a flat-rate fee fallback into real trade-instruction math.
Added fetchFeeConfigOrNull, which only returns null for Anchor's own
"Account does not exist" error (the genuine "no fee tiers configured"
case, verified against the exact error string Anchor's account fetcher
throws) — any other failure now propagates instead of being silently
swallowed. Live-verified fetchFeeConfig currently succeeds on mainnet
(fee tiers ARE configured), confirming the happy path is unaffected.
2. Deprecated create methods (agents/handlers/solana.ts, pumpfun skill):
migrated createInstruction/createAndBuyInstructions (both @deprecated
in the SDK) to createV2Instruction/createV2AndBuyInstructions. Note the
V2 path always mints under Token-2022 regardless of the mayhemMode flag
(that flag only toggles Mayhem game mechanics) — passing
mayhemMode: false here is a normal, non-Mayhem Token-2022 launch,
matching what pump.fun's own V2 creation flow produces. Live-verified
both createV2Instruction and createV2AndBuyInstructions build correct
instructions against mainnet.
3. metadata_uri validation (same two files): added validateMetadataUri,
checking URL shape, http(s) scheme, reachability, and that the fetched
JSON has the name/symbol fields pump.fun expects — called before any
RPC calls or transaction building, so a bad URI is rejected before
spending real SOL rather than after silently minting a token with
permanently broken metadata. Live-verified all rejection paths
(malformed URL, wrong scheme, unreachable host, valid-but-incomplete
JSON) plus the happy path against a local test server.
4. PumpSwap compute-unit headroom (pumpswap.ts, swarm-builders.ts's
PumpSwapBuilder): raised the hardcoded compute unit limit from 200k to
400k for PumpSwap trades specifically (left the simpler bonding-curve
trade paths at 200k — the worst-case-bundle concern is specific to
PumpSwap's extendAccount + ATA + WSOL wrap/close + boost-account
combinations). Confirmed this doesn't change actual lamports spent on
priority fee: microLamports is derived as
priorityFeeLamports/computeUnitLimit, so the product
(microLamports * computeUnitLimit) stays equal to the caller's
requested priorityFeeLamports regardless of the limit chosen — raising
it only adds headroom, not cost.
5. Blockhash-freshness mismatch (wallet.ts): signAndSendTransaction's
VersionedTransaction branch was fetching a second, newer blockhash
purely to get a lastValidBlockHeight for confirmTransaction, rather
than the height actually corresponding to the blockhash baked into the
already-signed transaction. Added an optional lastValidBlockHeight
parameter threaded through from the two direct pump.fun callers
(executePumpFunTrade, executePumpSwapTrade), which already have the
real value on hand from their own parallelized getLatestBlockhash call.
Falls back to the old fetch-fresh behavior for every other caller of
this shared helper, so this is fully backward compatible.
Live-verified end-to-end: both executePumpFunTrade and executePumpSwapTrade
(with priority fees set, exercising the new CU limits and blockhash
threading together) build and submit correctly, failing only at
on-chain simulation due to the test wallet's zero balance — not at
build time.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Ran a final audit comparing pumpapi.ts/pumpswap.ts/the pump.fun create-claim
handlers against the actual pinned SDK source (node_modules/@pump-fun/*/src,
including @pump-fun/pump-swap-sdk's own test specs) rather than against
memory of what a typical Solana SDK looks like. Three parallel passes
covering trading, PumpSwap, and create/claim all independently converged on
the same critical issue plus one each found a real high-severity bug.
CRITICAL — wallet.ts's shared signAndSendTransaction/
signAndSendVersionedTransaction never checked confirmTransaction's
result.value.err. confirmTransaction only THROWS on expiry/timeout/
nonce-invalidation — a transaction that lands on-chain but whose program
instruction reverts (wrong slippage, insufficient funds, graduated
mid-flight, whatever) resolves normally, with the failure reported solely
via .value.err. Every caller was silently treating "landed" as "succeeded."
This is the single highest-leverage fix in the repo: these two functions
are used by jupiter.ts, kamino.ts, meteora.ts, meteora-dbc.ts, marginfi.ts,
orca.ts, pumpapi.ts, pumpswap.ts, raydium.ts, and solend.ts — every one of
them now gets this check for free. Added the same check inline to the
pump.fun create/claim handlers (agents/handlers/solana.ts,
skills/bundled/pumpfun/index.ts), which had their own duplicated
confirmTransaction calls that didn't go through the shared helper.
Also added a getCreatorVaultBalanceBothPrograms pre-check before claiming —
a zero-balance claim now returns a clear "nothing to claim" message instead
of a cryptic simulation error.
HIGH — getPumpSwapQuote (pumpswap.ts) passed state.pool.coinCreator where
the SDK's own pure quote functions need state.pool.creator — two distinct
Pool fields. This made isPumpPool() evaluate false for virtually every real
pool, silently routing every quote into the wrong (flat, not tiered) fee
branch. Numerically verified: ~2.57% quote error on a real fee schedule.
Confined to the quote path — executePumpSwapTrade/PumpSwapBuilder use the
SDK's own instance methods, which get pool.creator right internally, so
actual trade execution and its on-chain slippage bound were never affected.
Also added the missing virtualQuoteReserves field to the same two calls,
which the SDK's own instance methods pass but the standalone quote calls
were omitting — matters for boost/cashback-enabled pools.
MEDIUM-HIGH — getTokenBalance/getUserPumpTokens (pumpapi.ts) never
detected Token-2022 (Mayhem-mode tokens), always deriving/querying against
classic SPL Token. A Mayhem-mode holding would silently report as a zero
balance or be omitted from the holdings list entirely — inconsistent with
the trading path, which already handles this correctly via
detectTokenProgram. Fixed both to detect/query the real token program.
Known lower-severity findings from the same audit, deliberately left for a
follow-up rather than rushed in under time pressure: fetchFeeConfig()'s
.catch(() => null) doesn't distinguish "no fee tiers configured" from "RPC
call failed" (bounded by slippage, not a fund-loss risk); create/
createAndBuy use SDK methods marked @deprecated in favor of V2 variants
(works today, confirmed against the current IDL, but no compile-time
warning if pump.fun retires the legacy path); metadata_uri is never
validated before spending real SOL on token creation; PumpSwap's
hardcoded 200k compute unit limit could be tight in a worst-case trade
needing extendAccount + ATA creation + WSOL wrap + boost accounts
simultaneously; wallet.ts's VersionedTransaction confirm path fetches a
second, later blockhash purely for lastValidBlockHeight rather than the
one actually baked into the already-signed transaction, which can make
confirmTransaction poll longer than necessary in a genuine-expiry case
(latency cost, not a correctness bug — verified the direction is
fail-safe, not fail-open).
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Measured (not assumed) what actually costs time on the trade-execution
path once PumpPortal was already removed: new PumpSdk() takes ~19.5ms/call
and new OnlinePumpSdk(connection) ~33ms/call — both reconstruct Anchor
Program instances (instruction/account Borsh coders) from the Pump
program's IDL from scratch on every single invocation, which was
happening once per trade. That's ~53ms of pure CPU overhead before any
network call even starts, on a path where every millisecond determines
whether a snipe/buy lands first.
Two fixes:
- new PumpSdk() -> the package's own exported PUMP_SDK singleton (its
constructor takes no connection, so there's no reason not to reuse the
package's instance directly — zero cost after the package's own module
load).
- new OnlinePumpSdk(connection) / new OnlinePumpAmmSdk(connection) ->
cached by connection.rpcEndpoint URL, not object identity.
wallet.getSolanaConnection() returns a fresh Connection object on every
call rather than reusing one, so an object-identity cache would never
hit; keying by the endpoint URL (what actually determines whether two
calls talk to the same node) does.
Also removed redundant sequential RPC round-trips that don't need to be
sequential: fetchGlobal/fetchFeeConfig depend on neither tokenProgram nor
the bonding-curve/pool state fetch (or each other), and
getLatestBlockhash depends on nothing instruction-building computes —
these were awaited one after another purely due to code order, not any
real data dependency. Now issued concurrently via Promise.all wherever
the dependency graph actually allows it, in
buildPumpFunTradeInstructions, getPumpFunQuote, executePumpFunTrade,
PumpFunBuilder, PumpSwapBuilder, and executePumpSwapTrade.
Live-verified against mainnet: cold call (cache empty) 333.8ms, warm call
(cache populated) 48.8ms — a 6.8x speedup — and a third call using a
brand-new Connection object pointed at the same RPC URL still hit the
cache (51.8ms), confirming the endpoint-keyed cache does what
getSolanaConnection()'s per-call Connection construction actually needs.
Quote output verified unchanged/correct after the restructuring.
Also applied the trivial, zero-risk PUMP_SDK swap to the create/claim
handlers (agents/handlers/solana.ts, pumpfun skill) even though token
creation isn't hot-path — free to fix, no reason not to.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Trading (buy/sell/quote) was already migrated off PumpPortal in prior
commits. This finishes the job for every other capability that still
touched pumpportal.fun:
- Token creation (agents/handlers/solana.ts, pumpfun skill): now builds
and signs create/createAndBuy instructions locally via
@pump-fun/pump-sdk's PumpSdk. Metadata hosting (name/symbol/description/
image -> URI) is now the caller's responsibility (metadata_uri param) —
pump.fun's own IPFS upload endpoint is Cloudflare-gated against
server-side/bot requests (verified live, same protection as their
trading frontend API), so there's no reliable official endpoint this
codebase can depend on for that step either.
- Creator fee claiming (same two files): now uses OnlinePumpSdk's
collectCoinCreatorFeeInstructions, which claims fees from both the
bonding-curve program and PumpSwap in one transaction. This is keyed by
creator address, not mint, so the mint parameter these tools used to
require is gone.
- Renamed getPumpPortalQuote/PumpPortalQuote to getPumpFunQuote/
PumpFunQuote (was already 100% local SDK computation, just kept the old
name from the earlier trading migration).
- /pump watch and /pump snipe were fake capabilities — static instructional
text pointing users at PumpPortal's websocket to do it themselves, not
actually implemented. /pump watch is now a real bounded-window
connection.onLogs listener. /pump snipe is honestly marked as not an
auto-trading command in this one-shot request/response tool, pointing
at the pump-swarm skill / copytrade.ts (persistent processes) instead
of fabricating a rushed auto-buy feature.
- Real correctness fix surfaced along the way: PumpPortal's `pool` param
used to route trades across other DEXes entirely (raydium, launchlab,
bonk) via its own aggregation — not just Pump's bonding curve. The
earlier SDK-based rewrite silently ignored that param instead of
erroring, meaning `--pool raydium` would silently execute a Pump.fun
bonding-curve trade instead of the requested route. Added
assertSupportedPumpPool, called from every entry point (single-wallet
trade, swarm builder, quote), which now fails loudly for any pool value
this codebase doesn't actually support instead of silently trading
something different than requested.
- Updated all doc/help text (SKILL.md files, inline help strings, tool
schemas) to match.
Live-verified: assertSupportedPumpPool correctly blocks execution before
any network call for an unsupported pool, and getPumpFunQuote returns
null (not a silently-wrong quote) for the same case.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
pumpswap.ts already quoted PumpSwap trades (the AMM a pump.fun token
graduates to) via the official @pump-fun/pump-swap-sdk, but had no
instruction-building/execute path — a graduated token was not tradeable
through this codebase at all. Adds executePumpSwapTrade, built on the same
SDK (PUMP_AMM_SDK.buyQuoteInput/sellBaseInput), reusing the exact
pool-selection logic (findBestPumpSwapState, extracted from
getPumpSwapQuote) so a quote and the trade it's for route to the same pool.
Native SOL wrapping/unwrapping and ATA creation are handled by the SDK's
own instruction builder.
Also wires PumpSwap into the multi-wallet swarm layer: new PumpSwapBuilder
in swarm-builders.ts (same shape as PumpFunBuilder — buildBuyTransaction/
buildSellTransaction/getQuote), and a new 'pumpswap' DexType value.
Live-verified against mainnet: PUMP's own token (63 real pools) quoted
and built a real buy transaction end-to-end — failed only at simulation
due to the test wallet having zero funds, confirming instruction
construction itself is correct.
Adding 'pumpswap' to the shared DexType union surfaced four separate
files (pump-swarm.ts, swarm-strategies.ts x2, swarm-ai-builder.ts) that
had independently duplicated the same dex/executionMode literal unions
inline instead of importing the shared types — TypeScript's structural
checks caught the resulting incompatibilities immediately. Fixed each to
reference the shared DexType/ExecutionMode types instead of maintaining
a fifth copy.
Co-authored-by: alsk1992 <alsk1992@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>