@nirholas/pump-sdk
Advanced tools
| /** | ||
| * Tests for the CLI's rendering layer. | ||
| * | ||
| * `--json` is a contract: scripts parse it. The two bugs guarded here both shipped | ||
| * silently once and produced output that looked fine to a human and was wrong to a | ||
| * machine: BN values leaking out in hexadecimal, and the SDK's scaled spot price | ||
| * being printed as if it were SOL per token (off by 1000). | ||
| */ | ||
| import BN from "bn.js"; | ||
| import { PublicKey } from "@solana/web3.js"; | ||
| import { | ||
| formatBps, | ||
| formatCompact, | ||
| formatScaledPrice, | ||
| formatSol, | ||
| formatTokens, | ||
| lamportsToSol, | ||
| meter, | ||
| normalizeForJson, | ||
| rawToTokens, | ||
| setColorEnabled, | ||
| shortAddress, | ||
| solToLamports, | ||
| toJson, | ||
| tokensToRaw, | ||
| } from "../cli/format"; | ||
| beforeAll(() => { | ||
| // Colour codes would make every string assertion below unreadable. | ||
| setColorEnabled(false); | ||
| }); | ||
| describe("cli/format", () => { | ||
| describe("normalizeForJson", () => { | ||
| it("renders BN in base 10, not the hex that BN.toJSON would emit", () => { | ||
| // BN.prototype.toJSON returns a padded hex string, so a naive | ||
| // JSON.stringify replacer never sees the BN at all. | ||
| expect(normalizeForJson(new BN(0))).toBe("0"); | ||
| expect(normalizeForJson(new BN(255))).toBe("255"); | ||
| expect(normalizeForJson(new BN("1000000000000000"))).toBe( | ||
| "1000000000000000", | ||
| ); | ||
| }); | ||
| it("renders public keys as base58", () => { | ||
| const key = PublicKey.default; | ||
| expect(normalizeForJson(key)).toBe(key.toBase58()); | ||
| }); | ||
| it("walks nested objects and arrays", () => { | ||
| const value = normalizeForJson({ | ||
| outer: { inner: new BN(42), list: [new BN(1), new BN(2)] }, | ||
| }) as { outer: { inner: string; list: string[] } }; | ||
| expect(value.outer.inner).toBe("42"); | ||
| expect(value.outer.list).toEqual(["1", "2"]); | ||
| }); | ||
| it("passes plain scalars through untouched", () => { | ||
| expect(normalizeForJson(7)).toBe(7); | ||
| expect(normalizeForJson("hello")).toBe("hello"); | ||
| expect(normalizeForJson(true)).toBe(true); | ||
| expect(normalizeForJson(undefined)).toBeNull(); | ||
| }); | ||
| it("survives a full round trip through toJson", () => { | ||
| const parsed: unknown = JSON.parse( | ||
| toJson({ marketCap: new BN("1234500000000"), mint: PublicKey.default }), | ||
| ); | ||
| expect(parsed).toEqual({ | ||
| marketCap: "1234500000000", | ||
| mint: PublicKey.default.toBase58(), | ||
| }); | ||
| }); | ||
| }); | ||
| describe("formatScaledPrice", () => { | ||
| it("divides the scaled spot price down to SOL per whole token", () => { | ||
| // 1e12 scaled units is exactly 1 SOL per token. | ||
| expect(formatScaledPrice(new BN("1000000000000"))).toBe("1 SOL"); | ||
| expect(formatScaledPrice(new BN("500000000000"))).toBe("0.5 SOL"); | ||
| }); | ||
| it("does not agree with formatSol, which would be the 1000x bug", () => { | ||
| const scaled = new BN("93703"); | ||
| expect(formatScaledPrice(scaled)).not.toBe(formatSol(scaled)); | ||
| }); | ||
| it("renders zero cleanly", () => { | ||
| expect(formatScaledPrice(new BN(0))).toBe("0 SOL"); | ||
| }); | ||
| }); | ||
| describe("lamport and token conversions", () => { | ||
| it("round-trips SOL through lamports", () => { | ||
| expect(lamportsToSol(solToLamports(1.5))).toBeCloseTo(1.5, 9); | ||
| expect(solToLamports(1).toString()).toBe("1000000000"); | ||
| }); | ||
| it("round-trips whole tokens through raw units", () => { | ||
| expect(rawToTokens(tokensToRaw(1234.5))).toBeCloseTo(1234.5, 6); | ||
| expect(tokensToRaw(1).toString()).toBe("1000000"); | ||
| }); | ||
| it("formats SOL without trailing zero noise", () => { | ||
| expect(formatSol(new BN("1500000000"))).toBe("1.5 SOL"); | ||
| expect(formatSol(new BN(0))).toBe("0 SOL"); | ||
| }); | ||
| it("uses exponent notation rather than rounding tiny amounts to zero", () => { | ||
| expect(formatSol(new BN(1))).toContain("e-"); | ||
| }); | ||
| }); | ||
| describe("compact numbers", () => { | ||
| it("abbreviates by magnitude", () => { | ||
| expect(formatCompact(1_500)).toBe("1.5K"); | ||
| expect(formatCompact(2_400_000)).toBe("2.4M"); | ||
| expect(formatCompact(3_000_000_000)).toBe("3B"); | ||
| expect(formatCompact(1_000_000_000_000)).toBe("1T"); | ||
| }); | ||
| it("formats a one billion token supply as 1B", () => { | ||
| expect(formatTokens(new BN("1000000000000000"))).toBe("1B"); | ||
| }); | ||
| }); | ||
| describe("formatBps", () => { | ||
| it("converts basis points to a percentage", () => { | ||
| expect(formatBps(10_000)).toBe("100%"); | ||
| expect(formatBps(125)).toBe("1.25%"); | ||
| expect(formatBps(0)).toBe("0%"); | ||
| }); | ||
| }); | ||
| describe("meter", () => { | ||
| it("clamps out-of-range fractions instead of drawing a broken bar", () => { | ||
| expect(meter(-1, 10)).toContain("0.00%"); | ||
| expect(meter(2, 10)).toContain("100.00%"); | ||
| expect(meter(0.5, 10)).toContain("50.00%"); | ||
| }); | ||
| it("draws exactly the requested width", () => { | ||
| const bar = meter(0.5, 20).split(" ")[0] ?? ""; | ||
| expect([...bar]).toHaveLength(20); | ||
| }); | ||
| }); | ||
| describe("shortAddress", () => { | ||
| it("elides the middle of a long address", () => { | ||
| const short = shortAddress("FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump"); | ||
| expect(short.startsWith("FeMbDo")).toBe(true); | ||
| expect(short.endsWith("JVJpump")).toBe(false); | ||
| expect(short).toContain("…"); | ||
| }); | ||
| it("leaves short strings alone", () => { | ||
| expect(shortAddress("abc")).toBe("abc"); | ||
| }); | ||
| }); | ||
| }); |
| /** | ||
| * Regression tests for reading a token whose bonding curve has migrated. | ||
| * | ||
| * When a token graduates to PumpAMM the program zeroes every reserve field on | ||
| * the bonding curve account. Before this was handled, every analytics helper | ||
| * that divides by `virtualTokenReserves` threw "Division by zero" for the | ||
| * entire population of graduated tokens, which is the majority of tokens anyone | ||
| * looks up by the time they are worth looking up. | ||
| */ | ||
| import BN from "bn.js"; | ||
| import { getBondingCurveSummary, getTokenPrice } from "../analytics"; | ||
| import { computeFeesBps } from "../fees"; | ||
| import { | ||
| makeFeeConfig, | ||
| makeGlobal, | ||
| makeMigratedBondingCurve, | ||
| } from "./fixtures"; | ||
| const global = makeGlobal(); | ||
| const feeConfig = makeFeeConfig(); | ||
| const migrated = makeMigratedBondingCurve(); | ||
| const mintSupply = global.tokenTotalSupply; | ||
| describe("migrated bonding curves", () => { | ||
| describe("getTokenPrice", () => { | ||
| it("reports zeros instead of throwing on a zeroed curve", () => { | ||
| const price = getTokenPrice({ | ||
| global, | ||
| feeConfig, | ||
| mintSupply, | ||
| bondingCurve: migrated, | ||
| }); | ||
| expect(price.isGraduated).toBe(true); | ||
| expect(price.marketCap.isZero()).toBe(true); | ||
| expect(price.buyPricePerToken.isZero()).toBe(true); | ||
| expect(price.sellPricePerToken.isZero()).toBe(true); | ||
| }); | ||
| it("works with a null fee config too", () => { | ||
| expect(() => | ||
| getTokenPrice({ | ||
| global, | ||
| feeConfig: null, | ||
| mintSupply, | ||
| bondingCurve: migrated, | ||
| }), | ||
| ).not.toThrow(); | ||
| }); | ||
| }); | ||
| describe("computeFeesBps", () => { | ||
| it("falls back to the base tier rather than dividing by zero", () => { | ||
| const fees = computeFeesBps({ | ||
| global, | ||
| feeConfig, | ||
| mintSupply, | ||
| virtualSolReserves: migrated.virtualSolReserves, | ||
| virtualTokenReserves: migrated.virtualTokenReserves, | ||
| }); | ||
| expect(fees.protocolFeeBps).toBeInstanceOf(BN); | ||
| expect(fees.protocolFeeBps.isNeg()).toBe(false); | ||
| expect(fees.creatorFeeBps.isNeg()).toBe(false); | ||
| }); | ||
| }); | ||
| describe("getBondingCurveSummary", () => { | ||
| it("summarises a migrated curve as graduated at 100%", () => { | ||
| const summary = getBondingCurveSummary({ | ||
| global, | ||
| feeConfig, | ||
| mintSupply, | ||
| bondingCurve: migrated, | ||
| }); | ||
| expect(summary.isGraduated).toBe(true); | ||
| expect(summary.progressBps).toBe(10_000); | ||
| expect(summary.solNeededToGraduate.isZero()).toBe(true); | ||
| expect(summary.marketCap.isZero()).toBe(true); | ||
| }); | ||
| it("still prices a live curve normally", () => { | ||
| const summary = getBondingCurveSummary({ | ||
| global, | ||
| feeConfig, | ||
| mintSupply, | ||
| bondingCurve: liveCurve(), | ||
| }); | ||
| expect(summary.isGraduated).toBe(false); | ||
| expect(summary.marketCap.gt(new BN(0))).toBe(true); | ||
| expect(summary.buyPricePerToken.gt(new BN(0))).toBe(true); | ||
| }); | ||
| }); | ||
| }); | ||
| /** A curve with live reserves, to prove the zero-guard did not swallow them. */ | ||
| function liveCurve() { | ||
| return { | ||
| ...makeMigratedBondingCurve(), | ||
| virtualTokenReserves: new BN("1073000000000000"), | ||
| virtualSolReserves: new BN("30000000000"), | ||
| realTokenReserves: new BN("793100000000000"), | ||
| realSolReserves: new BN(0), | ||
| complete: false, | ||
| }; | ||
| } |
+1
-1
| { | ||
| "name": "@nirholas/pump-sdk", | ||
| "version": "1.35.3", | ||
| "version": "1.36.0", | ||
| "description": "TypeScript SDK for the Pump protocol on Solana: token creation, bonding curves, AMM pools, fee sharing, and volume rewards", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+59
-0
@@ -29,2 +29,3 @@ <p align="center"> | ||
| - [Quick Start](#-quick-start) | ||
| - [The `pump` CLI](#-the-pump-cli) | ||
| - [50 Runnable Examples](#-50-runnable-examples) | ||
@@ -140,2 +141,60 @@ - [Usage Examples](#-usage-examples) | ||
| ## 💻 The `pump` CLI | ||
| The SDK ships with a binary. No wallet, no API key, and no configuration are needed for anything that reads the chain. | ||
| ```bash | ||
| npm install -g @nirholas/pump-sdk | ||
| pump curve <mint> # price, market cap, graduation meter, fee tier | ||
| pump quote buy <mint> --sol 1 # what 1 SOL really buys, after fees and impact | ||
| pump watch <mint> # live dashboard | ||
| pump doctor # check your RPC, wallet, and balance in one shot | ||
| ``` | ||
| Or without installing anything: `npx -p @nirholas/pump-sdk pump curve <mint>`. | ||
| ``` | ||
| FeMbDoX7R1Psc4GEcvJdsbNbZA3bfztcyDCatJVJpump live on the bonding curve | ||
| ──────────────────────────────────────────────────────────── | ||
| Market cap 93.705347 SOL | ||
| Buy price 0.000000096 SOL per token | ||
| Sell price 0.000000091 SOL per token | ||
| Trading fee 1.25% 0.95% protocol + 0.3% creator | ||
| Graduation ███████████████░░░░░░░░░ 61.39% | ||
| SOL to graduate 60.684661 SOL | ||
| SOL in curve 24.921536 SOL | ||
| Tokens left 306.21M of 793.1M | ||
| ``` | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `pump curve` / `price` / `pool` / `global` | Read curve, price, AMM pool, and protocol state | | ||
| | `pump quote buy\|sell` | Price a trade offline: output, fees, effective price, impact | | ||
| | `pump buy` / `sell` | Trade, auto-routing between the bonding curve and PumpAMM | | ||
| | `pump create` | Launch a token, optionally with an atomic first buy | | ||
| | `pump vanity` | Grind a `...pump` mint address | | ||
| | `pump fees` / `incentives` | Check and claim creator fees and volume rewards | | ||
| | `pump watch` | Live-refreshing dashboard, or a JSON price feed | | ||
| | `pump events` | Decode the Pump events in any transaction | | ||
| | `pump pda` | Derive any Pump program address | | ||
| | `pump doctor` / `config` | Diagnose and configure | | ||
| **Safe by construction.** Reads never load a keypair. Every trade is simulated before anything is sent, prints exactly what is about to happen, and waits for an explicit yes. `--simulate` sends nothing; `--yes` is the scripting escape hatch; a piped command with neither refuses rather than spending funds unattended. | ||
| **Scriptable.** Every command takes `--json` and writes nothing else to stdout in that mode: | ||
| ```bash | ||
| pump curve <mint> --json | jq .marketCapSol | ||
| pump watch <mint> --json --interval 10 | jq -r '"\(.at) \(.marketCapSol) SOL"' | ||
| pump events <sig> --json | jq '.events[] | select(.type=="trade")' | ||
| ``` | ||
| Full reference: **[docs/cli.md](docs/cli.md)**. Ten-minute walkthrough: **[tutorial 45](tutorials/45-cli-quickstart.md)**. | ||
| --- | ||
| ## 🧪 50 Runnable Examples | ||
@@ -142,0 +201,0 @@ |
@@ -15,3 +15,3 @@ /** | ||
| import BN from "bn.js"; | ||
| import { Keypair, PublicKey } from "@solana/web3.js"; | ||
| import { Keypair, type PublicKey } from "@solana/web3.js"; | ||
@@ -18,0 +18,0 @@ import { calculateBuyPriceImpact, calculateSellPriceImpact } from "../../analytics"; |
@@ -14,3 +14,3 @@ /** | ||
| import type { Command } from "commander"; | ||
| import { Keypair } from "@solana/web3.js"; | ||
| import type { Keypair } from "@solana/web3.js"; | ||
@@ -17,0 +17,0 @@ import { |
+10
-9
@@ -94,13 +94,14 @@ #!/usr/bin/env node | ||
| /** | ||
| * Read the package version from the manifest rather than hardcoding it, so a | ||
| * release bump never leaves `pump --version` lying. | ||
| * The package version, injected by tsup at build time (see `tsup.config.ts`). | ||
| * | ||
| * Declared rather than imported so a release bump can never leave | ||
| * `pump --version` lying, and so the binary does not read a file to answer it. | ||
| * The fallback covers running the TypeScript source directly, e.g. under `tsx`. | ||
| */ | ||
| declare const __PUMP_CLI_VERSION__: string | undefined; | ||
| 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"; | ||
| } | ||
| return typeof __PUMP_CLI_VERSION__ === "string" | ||
| ? __PUMP_CLI_VERSION__ | ||
| : "0.0.0-dev"; | ||
| } | ||
@@ -107,0 +108,0 @@ |
+3
-3
@@ -13,6 +13,6 @@ /** | ||
| ComputeBudgetProgram, | ||
| Keypair, | ||
| PublicKey, | ||
| type Keypair, | ||
| type PublicKey, | ||
| Transaction, | ||
| TransactionInstruction, | ||
| type TransactionInstruction, | ||
| VersionedTransaction, | ||
@@ -19,0 +19,0 @@ TransactionMessage, |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
6691562
0.1%58
3.57%153872
0.05%579
11.35%