Sign In

@nirholas/pump-sdk

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nirholas/pump-sdk - npm Package Compare versions

Comparing version
1.35.2
to
1.35.3
+1
dist/cli/index.d.ts
#!/usr/bin/env node

Sorry, the diff of this file is too big to display

/**
* `pump pda` — derive any Pump program address from the command line.
*
* Deriving a PDA by hand means knowing the seed bytes and the program id, and
* getting either wrong yields a valid-looking address that simply does not
* exist. This exposes the SDK's derivations directly so a shell script, an
* explorer search, or a bug report can name the exact account.
*/
import type { Command } from "commander";
import {
AMM_FEE_CONFIG_PDA,
AMM_GLOBAL_CONFIG_PDA,
AMM_GLOBAL_PDA,
AMM_GLOBAL_VOLUME_ACCUMULATOR_PDA,
GLOBAL_PDA,
GLOBAL_VOLUME_ACCUMULATOR_PDA,
PUMP_EVENT_AUTHORITY_PDA,
PUMP_FEE_CONFIG_PDA,
ammCreatorVaultPda,
ammUserVolumeAccumulatorPda,
bondingCurvePda,
canonicalPumpPoolPda,
creatorVaultPda,
feeSharingConfigPda,
pumpPoolAuthorityPda,
userVolumeAccumulatorPda,
} from "../../pda";
import { PUMP_AMM_PROGRAM_ID, PUMP_FEE_PROGRAM_ID, PUMP_PROGRAM_ID } from "../../sdk";
import type { CliContext } from "../context";
import { CliError, parsePublicKey } from "../context";
import { c, heading, keyValue, solscanAccount, toJson } from "../format";
/** Derivations that take a single address argument. */
const WITH_ARGUMENT: Record<string, { describe: string; derive: (key: string) => string }> = {
"bonding-curve": {
describe: "Bonding curve account for a mint",
derive: (mint) => bondingCurvePda(parsePublicKey(mint, "mint")).toBase58(),
},
pool: {
describe: "Canonical PumpAMM pool for a mint",
derive: (mint) => canonicalPumpPoolPda(parsePublicKey(mint, "mint")).toBase58(),
},
"pool-authority": {
describe: "Pool authority for a mint",
derive: (mint) => pumpPoolAuthorityPda(parsePublicKey(mint, "mint")).toBase58(),
},
"creator-vault": {
describe: "Bonding-curve creator fee vault",
derive: (creator) => creatorVaultPda(parsePublicKey(creator, "creator")).toBase58(),
},
"amm-creator-vault": {
describe: "PumpAMM creator fee vault",
derive: (creator) => ammCreatorVaultPda(parsePublicKey(creator, "creator")).toBase58(),
},
"user-volume": {
describe: "Volume accumulator for a user",
derive: (user) => userVolumeAccumulatorPda(parsePublicKey(user, "user")).toBase58(),
},
"amm-user-volume": {
describe: "PumpAMM volume accumulator for a user",
derive: (user) => ammUserVolumeAccumulatorPda(parsePublicKey(user, "user")).toBase58(),
},
"fee-sharing": {
describe: "Fee sharing config for a mint",
derive: (mint) => feeSharingConfigPda(parsePublicKey(mint, "mint")).toBase58(),
},
};
/** Fixed protocol accounts, no argument needed. */
const CONSTANTS: Record<string, { describe: string; address: string }> = {
global: { describe: "Pump global config", address: GLOBAL_PDA.toBase58() },
"amm-global": { describe: "PumpAMM global", address: AMM_GLOBAL_PDA.toBase58() },
"amm-global-config": {
describe: "PumpAMM global config",
address: AMM_GLOBAL_CONFIG_PDA.toBase58(),
},
"fee-config": { describe: "Fee program config", address: PUMP_FEE_CONFIG_PDA.toBase58() },
"amm-fee-config": { describe: "PumpAMM fee config", address: AMM_FEE_CONFIG_PDA.toBase58() },
"global-volume": {
describe: "Global volume accumulator",
address: GLOBAL_VOLUME_ACCUMULATOR_PDA.toBase58(),
},
"amm-global-volume": {
describe: "PumpAMM global volume accumulator",
address: AMM_GLOBAL_VOLUME_ACCUMULATOR_PDA.toBase58(),
},
"event-authority": {
describe: "Pump event authority",
address: PUMP_EVENT_AUTHORITY_PDA.toBase58(),
},
"program-pump": { describe: "Pump program id", address: PUMP_PROGRAM_ID.toBase58() },
"program-amm": { describe: "PumpAMM program id", address: PUMP_AMM_PROGRAM_ID.toBase58() },
"program-fees": { describe: "Fee program id", address: PUMP_FEE_PROGRAM_ID.toBase58() },
};
export function registerPdaCommand(
program: Command,
getContext: () => CliContext,
): void {
program
.command("pda [kind] [address]")
.description("Derive a Pump program address (run with no arguments to list every kind)")
.action((kind: string | undefined, address: string | undefined) => {
runPda(getContext(), kind, address);
});
}
function runPda(
ctx: CliContext,
kind: string | undefined,
address: string | undefined,
): void {
if (kind === undefined) {
listKinds(ctx);
return;
}
const constant = CONSTANTS[kind];
if (constant !== undefined) {
emit(ctx, kind, constant.address, constant.describe);
return;
}
const derivation = WITH_ARGUMENT[kind];
if (derivation === undefined) {
throw new CliError(
`Unknown PDA kind "${kind}"`,
`Run \`pump pda\` with no arguments to list them. Closest matches: ${suggest(kind).join(", ")}`,
);
}
if (address === undefined) {
throw new CliError(
`\`pump pda ${kind}\` needs an address`,
`${derivation.describe}. Usage: pump pda ${kind} <address>`,
);
}
emit(ctx, kind, derivation.derive(address), derivation.describe);
}
function emit(
ctx: CliContext,
kind: string,
address: string,
describe: string,
): void {
if (ctx.json) {
process.stdout.write(`${toJson({ kind, address, description: describe })}\n`);
return;
}
process.stdout.write(
`${[
"",
keyValue([
{ label: describe, value: c.bold(address) },
{ label: "Solscan", value: c.dim(solscanAccount(address)) },
]),
"",
].join("\n")}\n`,
);
}
function listKinds(ctx: CliContext): void {
if (ctx.json) {
process.stdout.write(
`${toJson({
constants: Object.entries(CONSTANTS).map(([kind, value]) => ({
kind,
description: value.describe,
address: value.address,
})),
derived: Object.entries(WITH_ARGUMENT).map(([kind, value]) => ({
kind,
description: value.describe,
usage: `pump pda ${kind} <address>`,
})),
})}\n`,
);
return;
}
process.stdout.write(
`${[
heading("Derived from an address"),
"",
keyValue(
Object.entries(WITH_ARGUMENT).map(([kind, value]) => ({
label: kind,
value: value.describe,
})),
),
heading("Fixed protocol accounts"),
"",
keyValue(
Object.entries(CONSTANTS).map(([kind, value]) => ({
label: kind,
value: value.describe,
})),
),
"",
c.dim(" Example: pump pda bonding-curve <mint>"),
"",
].join("\n")}\n`,
);
}
/** Cheap edit-distance-free suggestion: shared prefix or substring. */
function suggest(input: string): string[] {
const all = [...Object.keys(WITH_ARGUMENT), ...Object.keys(CONSTANTS)];
const matches = all.filter(
(kind) => kind.includes(input) || input.includes(kind.split("-")[0] ?? kind),
);
return matches.length > 0 ? matches.slice(0, 3) : all.slice(0, 3);
}
/**
* `pump config` and `pump doctor` — the two commands people run when something
* is wrong.
*
* `doctor` exists because the failure modes of a Solana CLI are boring and
* identical every time: a dead RPC, a rate-limited public endpoint, a missing
* keypair, an unfunded wallet, or a clock-skewed node. Diagnosing those by
* reading a stack trace is a waste of everyone's afternoon.
*/
import type { Command } from "commander";
import BN from "bn.js";
import { GLOBAL_PDA } from "../../pda";
import {
CONFIG_KEYS,
configPath,
loadConfig,
saveConfig,
type CliConfig,
} from "../config";
import type { CliContext } from "../context";
import { CliError } from "../context";
import {
c,
failure,
formatSol,
heading,
info,
keyValue,
success,
toJson,
warn,
} from "../format";
export function registerSetupCommands(
program: Command,
getContext: () => CliContext,
): void {
const config = program
.command("config")
.description("Read and write the saved CLI configuration");
config
.command("list", { isDefault: true })
.description("Show the current configuration and where it came from")
.action(() => {
runConfigList(getContext());
});
config
.command("get <key>")
.description("Print a single configuration value")
.action((key: string) => {
runConfigGet(getContext(), key);
});
config
.command("set <key> <value>")
.description(`Set a value (${Object.keys(CONFIG_KEYS).join(", ")})`)
.action((key: string, value: string) => {
runConfigSet(getContext(), key, value);
});
config
.command("unset <key>")
.description("Remove a value and fall back to the default")
.action((key: string) => {
runConfigUnset(getContext(), key);
});
config
.command("path")
.description("Print the config file path")
.action(() => {
process.stdout.write(`${configPath()}\n`);
});
program
.command("doctor")
.description("Check the RPC endpoint, the wallet, and protocol reachability")
.action(async () => {
await runDoctor(getContext());
});
}
function assertKnownKey(key: string): keyof CliConfig {
if (!(key in CONFIG_KEYS)) {
throw new CliError(
`Unknown config key "${key}"`,
`Valid keys: ${Object.keys(CONFIG_KEYS).join(", ")}`,
);
}
return key as keyof CliConfig;
}
function runConfigList(ctx: CliContext): void {
const config = loadConfig();
if (ctx.json) {
process.stdout.write(
`${toJson({
path: configPath(),
saved: config,
effective: {
rpcUrls: ctx.endpoints,
keypair: ctx.walletPath,
slippage: ctx.slippage,
priorityFee: ctx.priorityFee,
computeUnitLimit: ctx.computeUnitLimit,
},
})}\n`,
);
return;
}
process.stdout.write(
`${[
heading("Effective configuration", configPath()),
"",
keyValue([
{ label: "RPC", value: ctx.primaryEndpoint },
...(ctx.endpoints.length > 1
? [{ label: "Fallbacks", value: ctx.endpoints.slice(1).join(", ") }]
: []),
{ label: "Keypair", value: ctx.walletPath },
{ label: "Slippage", value: `${ctx.slippage}%` },
{
label: "Priority fee",
value:
ctx.priorityFee > 0
? `${ctx.priorityFee} micro-lamports/CU`
: c.dim("none"),
},
{ label: "Compute limit", value: String(ctx.computeUnitLimit) },
]),
"",
Object.keys(config).length === 0
? ` ${c.dim("Nothing is saved yet, everything above is a default. Set one with `pump config set rpcUrl <url>`.")}`
: ` ${c.dim(`Saved keys: ${Object.keys(config).join(", ")}`)}`,
"",
].join("\n")}\n`,
);
}
function runConfigGet(ctx: CliContext, key: string): void {
const typed = assertKnownKey(key);
const value = loadConfig()[typed];
if (ctx.json) {
process.stdout.write(`${toJson({ key, value: value ?? null })}\n`);
return;
}
process.stdout.write(`${value === undefined ? "" : String(value)}\n`);
}
function runConfigSet(ctx: CliContext, key: string, value: string): void {
const typed = assertKnownKey(key);
const config = loadConfig();
const parsed = CONFIG_KEYS[typed](value);
const next = { ...config, [typed]: parsed };
const path = saveConfig(next);
if (ctx.json) {
process.stdout.write(`${toJson({ key, value: parsed, path })}\n`);
return;
}
process.stdout.write(`${success(`${key} = ${JSON.stringify(parsed)}`)}\n`);
}
function runConfigUnset(ctx: CliContext, key: string): void {
const typed = assertKnownKey(key);
const config = loadConfig();
delete config[typed];
const path = saveConfig(config);
if (ctx.json) {
process.stdout.write(`${toJson({ key, value: null, path })}\n`);
return;
}
process.stdout.write(`${success(`${key} unset`)}\n`);
}
interface Check {
name: string;
ok: boolean;
detail: string;
hint?: string;
}
async function runDoctor(ctx: CliContext): Promise<void> {
const checks: Check[] = [];
checks.push(await checkRpc(ctx));
checks.push(await checkProtocol(ctx));
const walletCheck = checkWallet(ctx);
checks.push(walletCheck);
if (walletCheck.ok) checks.push(await checkBalance(ctx));
const failures = checks.filter((check) => !check.ok);
if (ctx.json) {
process.stdout.write(`${toJson({ checks, healthy: failures.length === 0 })}\n`);
process.exitCode = failures.length === 0 ? 0 : 1;
return;
}
const lines = checks.map((check) => {
const status = check.ok ? success(check.name) : failure(check.name);
const hint =
check.hint === undefined ? "" : `\n ${c.yellow(check.hint)}`;
return ` ${status}\n ${c.dim(check.detail)}${hint}`;
});
process.stdout.write(
`${[
heading("pump doctor"),
"",
lines.join("\n\n"),
"",
failures.length === 0
? ` ${success("Everything checks out.")}`
: ` ${warn(`${failures.length} check${failures.length === 1 ? "" : "s"} failed.`)}`,
"",
].join("\n")}\n`,
);
process.exitCode = failures.length === 0 ? 0 : 1;
}
async function checkRpc(ctx: CliContext): Promise<Check> {
const started = Date.now();
try {
const slot = await ctx.connection.getSlot("confirmed");
const latency = Date.now() - started;
return {
name: `RPC reachable (${latency} ms)`,
ok: true,
detail: `${ctx.primaryEndpoint} at slot ${slot.toLocaleString()}`,
hint:
latency > 2000
? "That endpoint is slow. A dedicated RPC makes every command noticeably faster."
: undefined,
};
} catch (error) {
return {
name: "RPC reachable",
ok: false,
detail: `${ctx.primaryEndpoint}: ${(error as Error).message}`,
hint: "Set a working endpoint with `pump config set rpcUrl <url>`, or export PUMP_RPC_URL.",
};
}
}
async function checkProtocol(ctx: CliContext): Promise<Check> {
try {
const global = await ctx.sdk.fetchGlobal();
return {
name: "Pump protocol readable",
ok: true,
detail: `global ${GLOBAL_PDA.toBase58()} decoded, authority ${global.authority.toBase58()}`,
};
} catch (error) {
return {
name: "Pump protocol readable",
ok: false,
detail: (error as Error).message,
hint: "The endpoint answered but could not serve the Pump global account. Devnet and testnet endpoints do not host the Pump programs: use a mainnet RPC.",
};
}
}
function checkWallet(ctx: CliContext): Check {
try {
const signer = ctx.requireSigner();
return {
name: "Wallet loaded",
ok: true,
detail: `${signer.publicKey.toBase58()} from ${ctx.walletPath}`,
};
} catch (error) {
return {
name: "Wallet loaded",
ok: false,
detail: (error as Error).message,
hint: "Read commands work without a wallet. To trade, run `solana-keygen new` and then `pump config set keypair ~/.config/solana/id.json`.",
};
}
}
async function checkBalance(ctx: CliContext): Promise<Check> {
const signer = ctx.requireSigner();
try {
const lamports = await ctx.connection.getBalance(signer.publicKey, "confirmed");
const funded = lamports > 5_000_000;
return {
name: funded ? "Wallet funded" : "Wallet nearly empty",
ok: funded,
detail: `${formatSol(new BN(lamports))} available`,
hint: funded
? undefined
: "Trades need SOL for the buy, the network fee, and rent for a new token account. Around 0.02 SOL is a workable floor.",
};
} catch (error) {
return {
name: "Wallet funded",
ok: false,
detail: (error as Error).message,
};
}
}
/** Shown by `pump` with no arguments: the shortest path to a first result. */
export function quickstart(): string {
return [
heading("pump", "the Pump protocol from your terminal"),
"",
` ${c.dim("Inspect a token (no wallet needed)")}`,
` ${c.cyan("pump curve")} <mint> price, market cap, graduation progress`,
` ${c.cyan("pump quote buy")} <mint> --sol 1 what 1 SOL buys, fees and impact included`,
` ${c.cyan("pump watch")} <mint> live dashboard, refreshes every 5s`,
"",
` ${c.dim("Trade (needs a wallet, always asks before sending)")}`,
` ${c.cyan("pump buy")} <mint> --sol 0.5`,
` ${c.cyan("pump sell")} <mint> --percent 50`,
` ${c.cyan("pump sell")} <mint> --all sells out and reclaims the rent`,
"",
` ${c.dim("Launch")}`,
` ${c.cyan("pump vanity")} --suffix pump grind a ...pump mint address`,
` ${c.cyan("pump create")} --name "My Token" --symbol MTK --uri https://...`,
"",
` ${c.dim("Earnings")}`,
` ${c.cyan("pump fees")} unclaimed creator fees`,
` ${c.cyan("pump incentives")} unclaimed volume rewards`,
"",
` ${info("First run? `pump doctor` checks your RPC and wallet in one shot.")}`,
` ${c.dim("Every command takes --json. Full list: pump --help")}`,
"",
].join("\n");
}
#!/usr/bin/env node
/**
* `pump` — the Pump protocol from a terminal.
*
* Built on the same offline instruction builders the SDK exports, so anything
* the CLI does is something a script can do with three lines of TypeScript. The
* CLI is the fastest way to answer a question about a token; the SDK is how you
* put that answer in a product.
*
* Design rules this file enforces:
* - Read commands never require a wallet or any configuration.
* - Every command supports `--json` and prints nothing else to stdout in that
* mode, so `| jq` always works.
* - Anything that spends funds simulates first and asks before sending.
* - Errors name the fix, not just the failure.
*/
import { Command } from "commander";
import { registerEarningsCommands } from "./commands/earnings";
import { registerEventsCommand } from "./commands/events";
import { registerInspectCommands } from "./commands/inspect";
import { registerPdaCommand } from "./commands/pda";
import { registerQuoteCommand } from "./commands/quote";
import { registerSetupCommands, quickstart } from "./commands/setup";
import { registerTradeCommands } from "./commands/trade";
import { registerVanityCommand } from "./commands/vanity";
import { registerWatchCommand } from "./commands/watch";
import { CliContext, CliError, type GlobalOptions } from "./context";
import { c, failure, setColorEnabled } from "./format";
// Resolved lazily and cached: building a Connection on every command
// registration would open a socket even for `pump --help`.
let context: CliContext | undefined;
function getContext(): CliContext {
if (context === undefined) {
context = new CliContext(program.opts<GlobalOptions>());
}
return context;
}
const program = new Command();
program
.name("pump")
.description(
"Inspect, trade, and launch tokens on the Pump protocol.\n" +
"Read commands need no wallet. Trades simulate first and always ask before sending.",
)
.version(readVersion(), "-v, --version", "Print the CLI version")
.option("-r, --rpc <url>", "RPC endpoint, or a comma-separated failover list")
.option("-k, --keypair <path>", "Signer keypair (JSON byte array or base58 secret)")
.option("--json", "Emit machine-readable JSON on stdout and nothing else")
.option("--no-color", "Disable colour output")
.option("--slippage <percent>", "Slippage tolerance in percent", Number)
.option("--priority-fee <microLamports>", "Priority fee per compute unit", Number)
.option("--compute-unit-limit <units>", "Compute unit limit for trades", Number)
.option("-y, --yes", "Skip the confirmation prompt (for scripts)")
.option("--simulate", "Simulate and report, never send")
.addHelpText(
"after",
[
"",
"Examples:",
" pump curve <mint> Inspect a bonding curve",
" pump quote buy <mint> --sol 1 Price a 1 SOL buy",
" pump buy <mint> --sol 0.5 Buy, with a confirmation prompt",
" pump sell <mint> --all Exit a position and reclaim rent",
" pump watch <mint> --interval 3 Live dashboard",
" pump vanity --suffix pump Grind a ...pump mint",
" pump fees Unclaimed creator fees",
" pump curve <mint> --json | jq .marketCapSol",
"",
"Configuration:",
" Flags beat environment variables beat ~/.pump/config.json beat defaults.",
" PUMP_RPC_URL, PUMP_KEYPAIR, PUMP_SLIPPAGE, PUMP_PRIORITY_FEE are all read.",
"",
"Docs: https://sdk.pumpk.it Source: https://github.com/nirholas/pump-fun-sdk",
].join("\n"),
);
registerInspectCommands(program, getContext);
registerQuoteCommand(program, getContext);
registerTradeCommands(program, getContext);
registerEarningsCommands(program, getContext);
registerVanityCommand(program, getContext);
registerEventsCommand(program, getContext);
registerWatchCommand(program, getContext);
registerPdaCommand(program, getContext);
registerSetupCommands(program, getContext);
/**
* Read the package version from the manifest rather than hardcoding it, so a
* release bump never leaves `pump --version` lying.
*/
function readVersion(): string {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require("../../package.json") as { version?: string };
return pkg.version ?? "0.0.0";
} catch {
return "0.0.0";
}
}
/**
* Render an error the way a person can act on it.
*
* A `CliError` carries a hint written for whoever hit it. Anything else is a
* bug or an RPC failure, so the raw message is shown and `--json` still emits
* a parseable object rather than a half-written stream.
*/
function reportError(error: unknown, asJson: boolean): void {
if (asJson) {
const payload = {
error:
error instanceof Error ? error.message : String(error),
hint: error instanceof CliError ? (error.hint ?? null) : null,
};
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
return;
}
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`\n${failure(message)}\n`);
if (error instanceof CliError && error.hint !== undefined) {
process.stderr.write(` ${c.yellow(error.hint)}\n`);
}
process.stderr.write("\n");
}
async function main(): Promise<void> {
// `pump` alone should teach, not dump a wall of flags.
if (process.argv.length <= 2) {
setColorEnabled(process.stdout.isTTY === true && process.env.NO_COLOR === undefined);
process.stdout.write(`${quickstart()}\n`);
return;
}
await program.parseAsync(process.argv);
}
main().catch((error: unknown) => {
const asJson = process.argv.includes("--json");
reportError(error, asJson);
process.exitCode = 1;
});
+18
-4
{
"name": "@nirholas/pump-sdk",
"version": "1.35.2",
"version": "1.35.3",
"description": "TypeScript SDK for the Pump protocol on Solana: token creation, bonding curves, AMM pools, fee sharing, and volume rewards",

@@ -15,3 +15,8 @@ "keywords": [

"typescript",
"mcp"
"mcp",
"cli",
"pump-fun-cli",
"command-line",
"solana-cli",
"terminal"
],

@@ -43,2 +48,6 @@ "homepage": "https://sdk.pumpk.it",

"import": "./dist/*/index.js"
},
"./cli": {
"types": "./dist/cli/index.d.ts",
"require": "./dist/cli/index.js"
}

@@ -70,3 +79,6 @@ },

"@solana/web3.js": "^1.98.4",
"bn.js": "^5.2.2"
"bn.js": "^5.2.3",
"bs58": "^6.0.0",
"commander": "^12.1.0",
"picocolors": "^1.1.1"
},

@@ -79,3 +91,2 @@ "devDependencies": {

"@types/node": "^20.0.0",
"bs58": "^6.0.0",
"cz-conventional-changelog": "^3.3.0",

@@ -133,3 +144,6 @@ "eslint-config-flat-gitignore": "^2.1.0",

]
},
"bin": {
"pump": "./dist/cli/index.js"
}
}

@@ -107,2 +107,19 @@ /**

/**
* True when the curve no longer prices the token.
*
* On migration to PumpAMM the program zeroes the bonding curve's reserves, so
* every constant-product formula over them divides by zero. That is the normal
* end state of a successful launch, not bad input, so the analytics helpers
* report it instead of throwing. Note that `complete` alone is not enough: a
* curve can be flagged complete in the same slot it is drained, and a curve
* with zeroed reserves is unpriceable whether or not the flag is set yet.
*/
function isCurveRetired(bondingCurve: BondingCurve): boolean {
return (
bondingCurve.virtualTokenReserves.isZero() ||
bondingCurve.virtualSolReserves.isZero()
);
}
// ── Price Impact ──────────────────────────────────────────────────────

@@ -328,2 +345,8 @@

*
* A graduated curve reports zero prices and a zero market cap rather than
* throwing: when a token migrates to PumpAMM the program zeroes every reserve
* field on the bonding curve account, so the curve genuinely no longer prices
* the token. Read the pool instead (`OnlinePumpSdk.fetchPool`). Callers can
* branch on the returned `isGraduated`.
*
* @param global - Pump global state

@@ -346,2 +369,11 @@ * @param feeConfig - Fee config (null for legacy)

}): TokenPriceInfo {
if (isCurveRetired(bondingCurve)) {
return {
buyPricePerToken: new BN(0),
sellPricePerToken: new BN(0),
marketCap: new BN(0),
isGraduated: true,
};
}
// Cost to buy 1 whole token (1e6 raw units)

@@ -348,0 +380,0 @@ const buyPricePerToken = getBuySolAmountFromTokenAmount({

+1
-1

@@ -145,3 +145,3 @@ /**

default:
return genericRows(event.data as Record<string, unknown>);
return genericRows(event.data as unknown as Record<string, unknown>);
}

@@ -148,0 +148,0 @@ }

@@ -11,2 +11,3 @@ /**

import BN from "bn.js";
import type { PublicKey } from "@solana/web3.js";

@@ -78,2 +79,7 @@ import { computeFeesBps } from "../../fees";

// Migration zeroes the curve's reserves, so a graduated token prices at zero
// here and only the AMM pool knows what it is worth. Showing "0 SOL" would be
// technically true and completely useless, so read the pool instead.
const live = curve.complete ? await fetchPoolPricing(ctx, mint) : undefined;
const fees = computeFeesBps({

@@ -99,5 +105,14 @@ global,

marketCapLamports: summary.marketCap,
marketCapSol: lamportsToSol(summary.marketCap),
marketCapSol: live?.marketCapSol ?? lamportsToSol(summary.marketCap),
buyPricePerTokenLamports: summary.buyPricePerToken,
sellPricePerTokenLamports: summary.sellPricePerToken,
pool:
live === undefined
? null
: {
address: live.pool,
liquiditySol: live.liquiditySol,
baseTokens: live.baseTokens,
pricePerTokenSol: live.pricePerTokenSol,
},
graduation: {

@@ -132,2 +147,26 @@ progressBps: progress.progressBps,

const priceRows =
live === undefined
? [
{ label: "Market cap", value: c.bold(formatSol(summary.marketCap)) },
{ label: "Buy price", value: formatSol(summary.buyPricePerToken, 9), note: "per token" },
{ label: "Sell price", value: formatSol(summary.sellPricePerToken, 9), note: "per token" },
]
: [
{
label: "Market cap",
value: c.bold(`${formatCompact(live.marketCapSol)} SOL`),
note: "from the AMM pool",
},
{
label: "Price",
value: `${live.pricePerTokenSol.toPrecision(6)} SOL`,
note: "per token, pool spot",
},
{
label: "Pool liquidity",
value: `${formatCompact(live.liquiditySol)} SOL / ${formatCompact(live.baseTokens)} tokens`,
},
];
const lines = [

@@ -137,29 +176,42 @@ heading(mintArg, status),

keyValue([
{ label: "Market cap", value: c.bold(formatSol(summary.marketCap)) },
{ label: "Buy price", value: formatSol(summary.buyPricePerToken, 9), note: "per token" },
{ label: "Sell price", value: formatSol(summary.sellPricePerToken, 9), note: "per token" },
...priceRows,
{ label: "Trading fee", value: formatBps(totalFeeBps), note: `${formatBps(fees.protocolFeeBps.toNumber())} protocol + ${formatBps(fees.creatorFeeBps.toNumber())} creator` },
]),
"",
` ${c.dim("Graduation")} ${meter(progress.progressBps / 10_000)}`,
...(live === undefined
? [
` ${c.dim("Graduation")} ${meter(progress.progressBps / 10_000)}`,
"",
keyValue([
{
label: "SOL to graduate",
value: formatSol(progress.solNeededToGraduate),
},
{ label: "SOL in curve", value: formatSol(progress.solAccumulated) },
{
label: "Tokens left",
value: formatTokens(progress.tokensRemaining),
note: `of ${formatTokens(progress.tokensTotal)}`,
},
{
label: "Virtual reserves",
value: `${formatSol(curve.virtualSolReserves)} / ${formatTokens(curve.virtualTokenReserves)} tokens`,
},
{
label: "Real reserves",
value: `${formatSol(curve.realSolReserves)} / ${formatTokens(curve.realTokenReserves)} tokens`,
},
{ label: "Total supply", value: formatTokens(curve.tokenTotalSupply) },
]),
]
: [
keyValue([
{ label: "Total supply", value: formatTokens(curve.tokenTotalSupply) },
{ label: "Pool", value: live.pool },
]),
"",
` ${c.dim(`The bonding curve is retired and reads zero on chain. Run \`pump pool ${mintArg}\` for the full pool state.`)}`,
]),
"",
keyValue([
{
label: "SOL to graduate",
value: progress.isGraduated
? c.green("already graduated")
: formatSol(progress.solNeededToGraduate),
},
{ label: "SOL in curve", value: formatSol(progress.solAccumulated) },
{
label: "Tokens left",
value: formatTokens(progress.tokensRemaining),
note: `of ${formatTokens(progress.tokensTotal)}`,
},
{ label: "Virtual reserves", value: `${formatSol(curve.virtualSolReserves)} / ${formatTokens(curve.virtualTokenReserves)} tokens` },
{ label: "Real reserves", value: `${formatSol(curve.realSolReserves)} / ${formatTokens(curve.realTokenReserves)} tokens` },
{ label: "Total supply", value: formatTokens(curve.tokenTotalSupply) },
]),
"",
keyValue([
{ label: "Creator", value: curve.creator.toBase58() },

@@ -359,2 +411,54 @@ { label: "Curve PDA", value: bondingCurvePda(mint).toBase58() },

interface PoolPricing {
pool: string;
liquiditySol: number;
baseTokens: number;
pricePerTokenSol: number;
marketCapSol: number;
}
/**
* Spot-price a graduated token from its AMM pool reserves.
*
* Returns undefined rather than throwing when the pool is unreadable: a curve
* flagged complete whose pool has not been created yet is a real, transient
* state during migration, and it should degrade to the curve view rather than
* failing the whole command.
*/
async function fetchPoolPricing(
ctx: CliContext,
mint: PublicKey,
): Promise<PoolPricing | undefined> {
try {
const pool = await ctx.sdk.fetchPool(mint);
const [baseBalance, quoteBalance] = await Promise.all([
ctx.connection.getTokenAccountBalance(pool.poolBaseTokenAccount),
ctx.connection.getTokenAccountBalance(pool.poolQuoteTokenAccount),
]);
const baseTokens = Number(baseBalance.value.uiAmountString ?? 0);
const liquiditySol = Number(quoteBalance.value.uiAmountString ?? 0);
if (baseTokens <= 0) return undefined;
const pricePerTokenSol = liquiditySol / baseTokens;
// Pump mints a fixed one billion supply, so market cap is price * supply.
const supply = rawToTokens(await fetchMintSupply(ctx, mint));
return {
pool: canonicalPumpPoolPda(mint).toBase58(),
liquiditySol,
baseTokens,
pricePerTokenSol,
marketCapSol: pricePerTokenSol * supply,
};
} catch {
return undefined;
}
}
async function fetchMintSupply(ctx: CliContext, mint: PublicKey): Promise<BN> {
const supply = await ctx.connection.getTokenSupply(mint);
return new BN(supply.value.amount);
}
/** Turn the raw Anchor "account does not exist" into an actionable message. */

@@ -361,0 +465,0 @@ function rethrowMissingCurve(mint: string) {

@@ -26,2 +26,3 @@ /**

formatImpact,
formatScaledPrice,
formatSol,

@@ -163,4 +164,4 @@ formatTokens,

{ label: "Price impact", value: formatImpact(impact.impactBps) },
{ label: "Price before", value: formatSol(impact.priceBefore, 9) },
{ label: "Price after", value: formatSol(impact.priceAfter, 9) },
{ label: "Price before", value: formatScaledPrice(impact.priceBefore), note: "per token" },
{ label: "Price after", value: formatScaledPrice(impact.priceAfter), note: "per token" },
]),

@@ -268,4 +269,4 @@ ...(impact.impactBps >= 500

{ label: "Price impact", value: formatImpact(impact.impactBps) },
{ label: "Price before", value: formatSol(impact.priceBefore, 9) },
{ label: "Price after", value: formatSol(impact.priceAfter, 9) },
{ label: "Price before", value: formatScaledPrice(impact.priceBefore), note: "per token" },
{ label: "Price after", value: formatScaledPrice(impact.priceAfter), note: "per token" },
]),

@@ -272,0 +273,0 @@ ...(willOverflow

@@ -109,3 +109,5 @@ /**

onProgress: ({ attempts, attemptsPerSecond }) => {
if (ctx.json) return;
// Carriage-return progress only makes sense on a terminal. Piped to a
// file or a log it just concatenates every update into one long line.
if (ctx.json || process.stderr.isTTY !== true) return;
const percent = Math.min(99, (attempts / expected) * 100);

@@ -112,0 +114,0 @@ process.stderr.write(

@@ -17,2 +17,3 @@ /**

import {
DEFAULT_RPC_URL,
resolveComputeUnitLimit,

@@ -51,2 +52,4 @@ resolveKeypairPath,

readonly endpoints: string[];
/** The endpoint every command reports as "the" RPC. Never undefined. */
readonly primaryEndpoint: string;
readonly connection: Connection;

@@ -66,6 +69,7 @@ readonly sdk: OnlinePumpSdk;

this.endpoints = resolveRpcUrls(options.rpc);
this.primaryEndpoint = this.endpoints[0] ?? DEFAULT_RPC_URL;
this.connection =
this.endpoints.length > 1
? createFallbackConnection(this.endpoints, { commitment: "confirmed" })
: new Connection(this.endpoints[0], "confirmed");
: new Connection(this.primaryEndpoint, "confirmed");
this.sdk = new OnlinePumpSdk(this.connection);

@@ -72,0 +76,0 @@ this.json = options.json === true;

@@ -73,2 +73,19 @@ /**

/**
* Render one of the SDK's scaled spot prices as SOL per whole token.
*
* `PriceImpactResult.priceBefore` / `priceAfter` are lamports per *raw* token
* unit multiplied by 1e9 for integer precision. A whole token is 1e6 raw units,
* so SOL per token is `scaled * 1e6 / 1e9 / 1e9`, i.e. `scaled / 1e12`. Passing
* one of these straight to `formatSol` reports a price 1000x too high, which is
* exactly the kind of quiet unit bug that makes a quote untrustworthy.
*/
export function formatScaledPrice(scaled: BN, digits = 12): string {
const sol = Number(scaled.toString()) / 1e12;
if (sol !== 0 && Math.abs(sol) < 10 ** -digits) {
return `${sol.toExponential(2)} SOL`;
}
return `${trimZeros(sol.toFixed(digits))} SOL`;
}
/** `12.4M` style compact numbers for supply and token counts. */

@@ -165,23 +182,35 @@ export function formatCompact(value: number): string {

*
* BN and PublicKey both stringify to something useless by default (`{negative:
* 0, words: [...]}` and `{_bn: ...}`), which is exactly the kind of output that
* makes a CLI unusable from a script. Convert them to decimal and base58
* strings so `jq` sees real values.
* BN and PublicKey both serialize to something useless by default, which is
* exactly the kind of output that makes a CLI unusable from a script. Worse, a
* `JSON.stringify` replacer cannot fix it: `BN.prototype.toJSON` runs first and
* hands the replacer a zero-padded *hexadecimal* string, so `new BN(0)` arrives
* as `"00"` and a market cap comes out in base 16. The values must therefore be
* converted by walking the structure before stringify ever sees it.
*/
export function toJson(value: unknown): string {
return JSON.stringify(value, jsonReplacer, 2);
return JSON.stringify(normalizeForJson(value), null, 2);
}
function jsonReplacer(_key: string, value: unknown): unknown {
if (BN.isBN(value)) return value.toString();
/** Recursively replace BN, PublicKey, bigint, and Buffer with plain values. */
export function normalizeForJson(value: unknown): unknown {
if (value === null || value === undefined) return null;
if (BN.isBN(value)) return value.toString(10);
if (typeof value === "bigint") return value.toString();
if (
typeof value === "object" &&
value !== null &&
"toBase58" in value &&
typeof (value as { toBase58: unknown }).toBase58 === "function"
) {
return (value as { toBase58: () => string }).toBase58();
if (Buffer.isBuffer(value)) return value.toString("base64");
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map(normalizeForJson);
if (typeof value === "object") {
if (
"toBase58" in value &&
typeof (value as { toBase58: unknown }).toBase58 === "function"
) {
return (value as { toBase58: () => string }).toBase58();
}
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, inner]) => [
key,
normalizeForJson(inner),
]),
);
}
if (Buffer.isBuffer(value)) return value.toString("base64");
return value;

@@ -188,0 +217,0 @@ }

@@ -74,7 +74,13 @@ import {

if (feeConfig != null) {
const marketCap = bondingCurveMarketCap({
mintSupply,
virtualSolReserves,
virtualTokenReserves,
});
// A graduated curve has zeroed reserves, so its market cap is undefined
// rather than invalid. Price it at zero, which selects the base fee tier,
// instead of letting `bondingCurveMarketCap` throw on the division. Without
// this, every fee lookup for a migrated token fails.
const marketCap = virtualTokenReserves.isZero()
? new BN(0)
: bondingCurveMarketCap({
mintSupply,
virtualSolReserves,
virtualTokenReserves,
});

@@ -81,0 +87,0 @@ return calculateFeeTier({

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display