From b64792951a8618f74535f31c35d8c226af7bba2f Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Wed, 20 May 2026 15:19:39 +0800 Subject: [PATCH 1/8] feat(sunpump): add SunPump read-only commands with mainnet/nile switch - New `sun sunpump` command group covering token info, transactions, klines, portfolio, red-packet, referral, home, campaign and admin endpoints. Default base URL is mainnet (api-v2.sunpump.meme); pass global `--network nile` to hit the Nile testnet (tn-api.sunpump.meme), or `SUNPUMP_API_BASE_URL` for a custom host. - Pretty rendering: dedicated table configs for tokens, holders, txs, klines, portfolio and campaigns; key/value detail view for `token get`; curated price/MCap fallback so missing `trxPriceInUsd` no longer produces `$0`. - Shared helpers: `extractList` recognizes `campaigns`/`banners`; `readPagination` descends into `pageData`/`metadata` and accepts `size` as a `pageSize` alias. - Better error surfacing: HTTP-error messages now include the SunPump `msg` body so failures like an unknown `rankingType` are explicit. - README + docs note the mainnet/nile URLs, valid ranking enums (`MARKET_CAP`/`VOLUME_24H`/`PRICE_CHANGE_24H`) and the server-side 15-row cap on `tx ticker`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 +- README.md | 145 ++++--- package-lock.json | 4 +- package.json | 2 +- src/bin.ts | 2 + src/commands/sunpump.ts | 921 ++++++++++++++++++++++++++++++++++++++++ src/lib/command.ts | 17 +- src/lib/output.ts | 6 + src/lib/sunpump.ts | 389 +++++++++++++++++ 9 files changed, 1432 insertions(+), 57 deletions(-) create mode 100644 src/commands/sunpump.ts create mode 100644 src/lib/sunpump.ts diff --git a/.gitignore b/.gitignore index 46041ee..bfd9d74 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,5 @@ dist # Vite logs files vite.config.js.timestamp-* -vite.config.ts.timestamp-* \ No newline at end of file +vite.config.ts.timestamp-* +docs \ No newline at end of file diff --git a/README.md b/README.md index da27028..e7a58f1 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ - [Liquidity](#liquidity) - [Protocol & History](#protocol--history) - [Generic Contract](#generic-contract) + - [SunPump](#sunpump) - [Global Flags](#global-flags) - [Output Formats](#output-formats) - [Built-In Token Symbols](#built-in-token-symbols) @@ -148,13 +149,13 @@ Swap executed successfully Every command level supports `--help` (or `-h`). Use it to discover options, subcommands, and flag aliases without leaving the terminal. -| Command | What it shows | -| --- | --- | -| `sun --help` | Top-level overview, global flags, full command list | -| `sun --version` | Installed CLI version | -| `sun --help` | Subcommand group help (e.g. `sun pool --help`, `sun liquidity --help`) | -| `sun --help` | Leaf command help with all options (e.g. `sun pool top-apy --help`) | -| `sun help ` | Equivalent to ` --help` | +| Command | What it shows | +| -------------------------- | ---------------------------------------------------------------------- | +| `sun --help` | Top-level overview, global flags, full command list | +| `sun --version` | Installed CLI version | +| `sun --help` | Subcommand group help (e.g. `sun pool --help`, `sun liquidity --help`) | +| `sun --help` | Leaf command help with all options (e.g. `sun pool top-apy --help`) | +| `sun help ` | Equivalent to ` --help` | ```bash sun --help # global flags + command list @@ -202,23 +203,23 @@ Wallets are managed by [`agent-wallet`](https://github.com/BofAI/agent-wallet?ta You can override wallet settings per-invocation with these root flags: -| Flag | Purpose | -| --- | --- | -| `-k, --private-key ` | One-shot private key | -| `-m, --mnemonic ` | One-shot mnemonic | -| `-i, --mnemonic-account-index ` | Mnemonic account index | +| Flag | Purpose | +| ---------------------------------- | -------------------------------- | +| `-k, --private-key ` | One-shot private key | +| `-m, --mnemonic ` | One-shot mnemonic | +| `-i, --mnemonic-account-index ` | Mnemonic account index | | `-p, --agent-wallet-password ` | Override `AGENT_WALLET_PASSWORD` | -| `-d, --agent-wallet-dir ` | Override `AGENT_WALLET_DIR` | +| `-d, --agent-wallet-dir ` | Override `AGENT_WALLET_DIR` | See [`agent-wallet`](https://github.com/BofAI/agent-wallet?tab=readme-ov-file#quick-start) for file formats and the full set of `AGENT_WALLET_*` options. ### Network -| Variable | Purpose | Default | -| --- | --- | --- | -| `TRON_NETWORK` | Target network (`mainnet`, `nile`, …) | `mainnet` | -| `TRONGRID_API_KEY` | TronGrid API key for higher rate limits | — | -| `TRON_RPC_URL` | Custom RPC endpoint | — | +| Variable | Purpose | Default | +| ------------------ | --------------------------------------- | --------- | +| `TRON_NETWORK` | Target network (`mainnet`, `nile`, …) | `mainnet` | +| `TRONGRID_API_KEY` | TronGrid API key for higher rate limits | — | +| `TRON_RPC_URL` | Custom RPC endpoint | — | ```bash export TRON_NETWORK=mainnet @@ -353,25 +354,65 @@ sun contract send transfer --args '["TRecipient","1000000"]' `contract send` returns a `tronscanUrl` on successful broadcast. +### SunPump + +Read-only access to the SunPump API — token launches, trending lists, candlesticks, +holder portfolios, and red-packet/referral data. All endpoints are GET-only; no wallet +required. + +- Mainnet (default): `https://api-v2.sunpump.meme/pump-api` +- Nile testnet: `https://tn-api.sunpump.meme/pump-api` — use the global `--network nile` flag + +```bash +sun sunpump home stats # quick volume/launch counters +sun sunpump token king-of-hill # current king-of-the-hill token +sun sunpump token list --size 20 --sort marketCap,desc +sun sunpump token search --size 10 +sun sunpump token get # token detail +sun sunpump token holders --size 20 +sun sunpump token ranking --type MARKET_CAP --size 10 # also: VOLUME_24H, PRICE_CHANGE_24H + +sun sunpump tx token --size 20 # swap history for a token +sun sunpump tx user --size 20 # swap history for a wallet +sun sunpump tx ticker 50 # latest ticker feed (server caps at ~15 rows regardless of N) + +sun sunpump kline v3 --granularity 1m +sun sunpump portfolio --include-zero + +sun sunpump red-packet get +sun sunpump red-packet remain --user-address T... --ip 1.2.3.4 +sun sunpump home banners 5 +sun sunpump campaign list +``` + +Endpoints requiring a signed message (`favors`, `red-packet by-user`, `referral +rewards|invites`, `quota`) accept `--user-address`, `--signature`, `--signed-message` +flags. Switch to nile testnet with `sun --network nile sunpump ...`, or override the +base URL with `SUNPUMP_API_BASE_URL` for a custom host. + +Full reference (request params, response schemas) is in +[`docs/sunpump-api.md`](docs/sunpump-api.md) and +[`docs/sunpump-api.zh-CN.md`](docs/sunpump-api.zh-CN.md). + --- ## Global Flags Inherited by every subcommand: -| Flag | Description | -| --- | --- | -| `--output ` | Output format: `table`, `json`, `tsv` | -| `--json` | Shortcut for `--output json` | -| `--fields ` | Comma-separated field filter | -| `--network ` | Override `TRON_NETWORK` | -| `-k, --private-key ` | One-shot private key | -| `-m, --mnemonic ` | One-shot mnemonic | -| `-i, --mnemonic-account-index ` | Mnemonic account index | -| `-p, --agent-wallet-password ` | Override `AGENT_WALLET_PASSWORD` | -| `-d, --agent-wallet-dir ` | Override `AGENT_WALLET_DIR` | -| `-y, --yes` | Skip confirmation prompts | -| `--dry-run` | Print intent without sending the write | +| Flag | Description | +| ---------------------------------- | -------------------------------------- | +| `--output ` | Output format: `table`, `json`, `tsv` | +| `--json` | Shortcut for `--output json` | +| `--fields ` | Comma-separated field filter | +| `--network ` | Override `TRON_NETWORK` | +| `-k, --private-key ` | One-shot private key | +| `-m, --mnemonic ` | One-shot mnemonic | +| `-i, --mnemonic-account-index ` | Mnemonic account index | +| `-p, --agent-wallet-password ` | Override `AGENT_WALLET_PASSWORD` | +| `-d, --agent-wallet-dir ` | Override `AGENT_WALLET_DIR` | +| `-y, --yes` | Skip confirmation prompts | +| `--dry-run` | Print intent without sending the write | **Examples:** @@ -387,11 +428,11 @@ sun --dry-run contract send TContract transfer --args '["TRecipient","1000000"]' ## Output Formats -| Mode | When to use | -| --- | --- | -| `table` *(default)* | Human-friendly terminal output | -| `json` | Machine-readable JSON for scripts and agents | -| `tsv` | Tab-separated values for shell pipelines | +| Mode | When to use | +| ------------------- | -------------------------------------------- | +| `table` _(default)_ | Human-friendly terminal output | +| `json` | Machine-readable JSON for scripts and agents | +| `tsv` | Tab-separated values for shell pipelines | ```bash sun pool top-apy --page-size 5 @@ -405,17 +446,17 @@ sun --output tsv token list --protocol V3 Most commands accept these symbols anywhere a token is expected. -| Symbol | Address | Decimals | -| --- | --- | --- | -| `TRX` | `T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb` | 6 | -| `WTRX` | `TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR` | 6 | -| `USDT` | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | -| `USDC` | `TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8` | 6 | -| `USDD` | `TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn` | 18 | -| `SUN` | `TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S` | 18 | -| `JST` | `TCFLL5dx5ZJdKnWuesXxi1VPwjLVmWZZy9` | 18 | -| `BTT` | `TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4` | 18 | -| `WIN` | `TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7` | 6 | +| Symbol | Address | Decimals | +| ------ | ------------------------------------ | -------- | +| `TRX` | `T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb` | 6 | +| `WTRX` | `TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR` | 6 | +| `USDT` | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | +| `USDC` | `TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8` | 6 | +| `USDD` | `TPYmHEhy5n8TCEfYGqW2rPxsghSfzghPDn` | 18 | +| `SUN` | `TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S` | 18 | +| `JST` | `TCFLL5dx5ZJdKnWuesXxi1VPwjLVmWZZy9` | 18 | +| `BTT` | `TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4` | 18 | +| `WIN` | `TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7` | 6 | Symbols and raw addresses are interchangeable: @@ -430,11 +471,11 @@ sun swap T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t 1 Quick lookup for the most common errors: -| Error | Category | Jump to | -| --- | --- | --- | -| `unknown command 'nile'` | CLI parsing | [▸ Flag placement](#error-unknown-command-nile) | -| `No wallet configured` | Wallet setup | [▸ Wallet sources](#error-no-wallet-configured) | -| `Swap failed` | Execution | [▸ Swap diagnostics](#error-swap-failed) | +| Error | Category | Jump to | +| ------------------------ | ------------ | ----------------------------------------------- | +| `unknown command 'nile'` | CLI parsing | [▸ Flag placement](#error-unknown-command-nile) | +| `No wallet configured` | Wallet setup | [▸ Wallet sources](#error-no-wallet-configured) | +| `Swap failed` | Execution | [▸ Swap diagnostics](#error-swap-failed) | ### Error: `unknown command 'nile'` diff --git a/package-lock.json b/package-lock.json index f2d6104..6b052d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bankofai/sun-cli", - "version": "1.0.3", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bankofai/sun-cli", - "version": "1.0.3", + "version": "1.2.0", "license": "MIT", "dependencies": { "@bankofai/agent-wallet": "^2.3.0", diff --git a/package.json b/package.json index e97cdd8..ba25f32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bankofai/sun-cli", - "version": "1.1.0", + "version": "1.2.0", "description": "CLI tool for SUN.IO / SUNSWAP on TRON — for humans and AI agents", "main": "dist/bin.js", "type": "commonjs", diff --git a/src/bin.ts b/src/bin.ts index 0cd753c..dab54b9 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -17,6 +17,7 @@ import { registerPairCommands } from './commands/pair' import { registerFarmCommands } from './commands/farm' import { registerLiquidityCommands } from './commands/liquidity' import { registerContractCommands } from './commands/contract' +import { registerSunpumpCommands } from './commands/sunpump' const { version } = require('../package.json') as { version: string } @@ -79,6 +80,7 @@ registerPairCommands(program) registerFarmCommands(program) registerLiquidityCommands(program) registerContractCommands(program) +registerSunpumpCommands(program) program.parseAsync(process.argv).catch((err) => { process.stderr.write(`${err.message}\n`) diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts new file mode 100644 index 0000000..4395c46 --- /dev/null +++ b/src/commands/sunpump.ts @@ -0,0 +1,921 @@ +import { Command } from 'commander' +import { parseApiResponse } from '../lib/command' +import { + output, + outputError, + withSpinner, + printPaginationFooter, + printKeyValue, + isJsonMode, + formatUsd, + formatTime, + formatAmount, + formatPct, +} from '../lib/output' +import { getSunPump, SunPump, SunPumpNetwork } from '../lib/sunpump' + +// --------------------------------------------------------------------------- +// pumpAction — local mirror of readApiAction but for the SunPump client. +// SunPump responses follow the same {code,msg,data} envelope so we reuse +// parseApiResponse from lib/command.ts. +// --------------------------------------------------------------------------- + +interface PumpActionOpts { + spinnerLabel: string + errorLabel: string + execute: (client: SunPump) => Promise + tableConfig?: { headers: string[]; toRow: (item: any) => string[] } + transform?: (data: any) => unknown + parseEnvelope?: boolean + detailFor?: (data: any) => Record | null +} + +let currentNetwork: SunPumpNetwork = 'mainnet' + +async function pumpAction(opts: PumpActionOpts): Promise { + try { + const client = getSunPump(currentNetwork) + const raw = await withSpinner(opts.spinnerLabel, () => opts.execute(client)) + + let data: unknown = raw + let pagination + if (opts.parseEnvelope !== false) { + const parsed = parseApiResponse(raw) + data = parsed.data + pagination = parsed.pagination + } + const final = opts.transform ? opts.transform(data) : data + if (opts.detailFor && !isJsonMode()) { + const pairs = opts.detailFor(final) + if (pairs) { + printKeyValue(pairs) + if (pagination) printPaginationFooter(pagination) + return + } + } + output(final, opts.tableConfig) + if (pagination) printPaginationFooter(pagination) + } catch (err: any) { + outputError(opts.errorLabel, err) + } +} + +function toIntOrUndef(v: unknown): number | undefined { + if (v === undefined || v === null || v === '') return undefined + const n = Number(v) + return Number.isFinite(n) ? Math.trunc(n) : undefined +} + +function toNumOrUndef(v: unknown): number | undefined { + if (v === undefined || v === null || v === '') return undefined + const n = Number(v) + return Number.isFinite(n) ? n : undefined +} + +// --------------------------------------------------------------------------- +// Shared table configs +// --------------------------------------------------------------------------- + +function tokenPriceUsd(t: any): unknown { + const usd = t.priceInUsd ?? t.tokenPriceUsd ?? t.price + if (usd !== undefined && usd !== null && usd !== '') return usd + const trxPrice = Number(t.priceInTrx) + const trxUsdRaw = t.trxPriceInUsd + if ( + Number.isFinite(trxPrice) && + trxUsdRaw !== null && + trxUsdRaw !== undefined && + trxUsdRaw !== '' + ) { + const trxUsd = Number(trxUsdRaw) + if (Number.isFinite(trxUsd) && trxUsd > 0) return trxPrice * trxUsd + } + const mcap = Number(t.marketCap ?? t.marketCapUsd) + const supply = Number(t.totalSupply) + if (Number.isFinite(mcap) && mcap > 0 && Number.isFinite(supply) && supply > 0) { + return mcap / supply + } + return undefined +} + +const tokenTable = { + headers: ['Symbol', 'Name', 'Address', 'Price', 'MCap', 'Volume24h'], + toRow: (t: any) => [ + t.symbol ?? '-', + t.name ?? '-', + t.contractAddress ?? t.tokenAddress ?? '-', + formatUsd(tokenPriceUsd(t)), + formatUsd(t.marketCap ?? t.mcap ?? t.marketCapUsd), + formatUsd(t.volume24Hr ?? t.volume24h ?? t.volumeUsd1d), + ], +} + +const holderTable = { + headers: ['Holder', 'Type', 'Balance', 'Percent'], + toRow: (h: any) => { + const pctRaw = h.percent ?? h.percentage + let pct: string = '-' + if (pctRaw !== undefined && pctRaw !== null && pctRaw !== '') { + const n = Number(pctRaw) + if (Number.isFinite(n)) { + // API returns percentage values both as fractions (token list endpoint, e.g. 1.95e-6 = 0.000195%) + // and as percent units (holders endpoint, e.g. 38.51 = 38.51%). Use magnitude heuristic. + const asPct = Math.abs(n) <= 1 ? n * 100 : n + pct = `${asPct.toFixed(4)}%` + } + } + return [ + h.holderAddress ?? h.address ?? h.userAddress ?? '-', + h.holderType ?? '-', + h.decimals ? formatAmount(h.amount ?? h.balance, h.decimals) : fmtNum(h.balance ?? h.amount, 2), + pct, + ] + }, +} + +const txTable = { + headers: ['Time', 'Type', 'From → To', 'Volume', 'TxHash'], + toRow: (tx: any) => { + const fromSym = tx.fromTokenSymbol ?? tx.fromSymbol ?? tx.fromTokenAddress ?? '?' + const toSym = tx.toTokenSymbol ?? tx.toSymbol ?? tx.toTokenAddress ?? '?' + return [ + formatTime(tx.txDateTime ?? tx.swapTime ?? tx.timestamp ?? tx.createdAt), + tx.swapTranType ?? tx.txnOrderType ?? tx.type ?? '-', + `${fromSym} → ${toSym}`, + formatUsd(tx.volumeInUsd ?? tx.amountUsd), + tx.txHash ?? tx.transactionHash ?? tx.txnHash ?? '-', + ] + }, +} + +function tokenDetail(t: any): Record | null { + if (!t || typeof t !== 'object') return null + const pairs: Record = {} + const set = (k: string, v: unknown) => { + if (v !== undefined && v !== null && v !== '') pairs[k] = v + } + set('Symbol', t.symbol) + set('Name', t.name) + set('Description', t.description) + const statusBits: string[] = [] + if (t.status) statusBits.push(String(t.status)) + if (t.active === true) statusBits.push('active') + if (t.pumpPercentage !== undefined && t.pumpPercentage !== null) { + statusBits.push(`pump ${Number(t.pumpPercentage).toFixed(2)}%`) + } + if (statusBits.length) set('Status', statusBits.join(' · ')) + set('Contract', t.contractAddress) + set('Owner', t.ownerAddress) + set('Swap Pool', t.swapPoolAddress) + set('Price (USD)', formatUsd(tokenPriceUsd(t))) + set('Price (TRX)', fmtNum(t.priceInTrx, 8)) + if (t.priceChange24Hr !== undefined && t.priceChange24Hr !== null) { + const n = Number(t.priceChange24Hr) + const sign = n > 0 ? '+' : '' + set('24h Change', `${sign}${formatPct(n)}`) + } + set('Market Cap', formatUsd(t.marketCap ?? t.marketCapUsd)) + set('24h Volume', formatUsd(t.volume24Hr ?? t.volume24h ?? t.volumeUsd1d)) + if (t.holders !== undefined && t.holders !== null) set('Holders', t.holders) + if (t.totalSupply !== undefined && t.totalSupply !== null) { + set('Total Supply', Number(t.totalSupply).toLocaleString('en-US')) + } + if (t.trxPriceInUsd !== undefined && t.trxPriceInUsd !== null) { + set('TRX/USD', formatUsd(t.trxPriceInUsd)) + } + set('Created', formatTime(t.tokenCreatedInstant)) + set('Launched', formatTime(t.tokenLaunchedInstant)) + set('Website', t.websiteUrl) + set('Twitter', t.twitterUrl) + set('Telegram', t.telegramUrl) + if (t.listOn && typeof t.listOn === 'object') { + const cex = Object.entries(t.listOn) + .filter(([, url]) => typeof url === 'string' && url !== '') + .map(([name]) => name) + if (cex.length) set('Listed On', cex.join(', ')) + } + return pairs +} + +function fmtNum(v: unknown, digits = 6): string { + if (v === undefined || v === null || v === '') return '-' + const n = Number(v) + if (!Number.isFinite(n)) return '-' + if (n === 0) return '0' + if (Math.abs(n) >= 1) return n.toLocaleString(undefined, { maximumFractionDigits: digits }) + return n.toPrecision(4) +} + +const portfolioTable = { + headers: ['Symbol', 'Address', 'Balance', 'Price (TRX)', 'Value (TRX)', 'Percent'], + toRow: (t: any) => [ + t.symbol ?? '-', + t.address ?? t.contractAddress ?? '-', + fmtNum(t.balance, 4), + fmtNum(t.priceInTrx, 8), + fmtNum(t.valueInTrx, 6), + t.percentage !== undefined && t.percentage !== null + ? `${(Number(t.percentage) * 100).toPrecision(4)}%` + : '-', + ], +} + +function truncate(s: unknown, max: number): string { + if (s === undefined || s === null) return '-' + const str = String(s).replace(/\s+/g, ' ').trim() + if (str.length <= max) return str + return str.slice(0, max - 1) + '…' +} + +function campaignStatus(startMs: unknown, endMs: unknown): string { + const s = Number(startMs) + const e = Number(endMs) + const now = Date.now() + if (Number.isFinite(e) && now > e) return 'expired' + if (Number.isFinite(s) && now < s) return 'upcoming' + if (Number.isFinite(s) && Number.isFinite(e) && now >= s && now <= e) return 'active' + return '-' +} + +const campaignTable = { + headers: ['ID', 'Title', 'Status', 'Start', 'End', 'Link'], + toRow: (c: any) => [ + String(c.id ?? '-'), + truncate(c.title, 40), + campaignStatus(c.startTime, c.endTime), + formatTime(c.startTime), + formatTime(c.endTime), + c.redirectUrl ?? '-', + ], +} + +const klineTable = { + headers: ['Time', 'Open', 'High', 'Low', 'Close', 'Volume'], + toRow: (k: any) => [ + formatTime(k.startTime ?? k.timestamp ?? k.t), + String(k.open ?? k.o ?? '-'), + String(k.high ?? k.h ?? '-'), + String(k.low ?? k.l ?? '-'), + String(k.close ?? k.c ?? '-'), + String(k.volume ?? k.v ?? '-'), + ], +} + +// --------------------------------------------------------------------------- +// Command registration +// --------------------------------------------------------------------------- + +export function registerSunpumpCommands(program: Command) { + const sp = program + .command('sunpump') + .description( + 'SunPump read-only endpoints (mainnet: api-v2.sunpump.meme, nile: tn-api.sunpump.meme). Use global --network nile for testnet.', + ) + .hook('preAction', () => { + const n = program.opts().network ?? process.env.TRON_NETWORK + currentNetwork = n === 'nile' ? 'nile' : 'mainnet' + }) + + // -------------------------- token group ---------------------------------- + const token = sp.command('token').description('Token info, search, holders, ranking') + + token + .command('list') + .description('List tokens with optional filters and pagination') + .option('--contract
', 'Filter by contract address') + .option('--owner
', 'Filter by owner address') + .option('--symbol ', 'Filter by symbol') + .option('--name ', 'Filter by name') + .option('--description ', 'Filter by description') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching tokens...', + errorLabel: 'Failed to fetch tokens', + execute: (c) => + c.listTokens({ + contractAddress: opts.contract, + ownerAddress: opts.owner, + symbol: opts.symbol, + name: opts.name, + description: opts.description, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: tokenTable, + }) + }) + + token + .command('get ') + .description('Get token detail by contract address') + .action(async (contractAddress: string) => { + await pumpAction({ + spinnerLabel: 'Fetching token...', + errorLabel: 'Failed to fetch token', + execute: (c) => c.getToken(contractAddress), + detailFor: tokenDetail, + }) + }) + + token + .command('search ') + .description('Fuzzy search tokens') + .option('--on-sunswap', 'Only tokens listed on SunSwap', false) + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (query: string, opts) => { + await pumpAction({ + spinnerLabel: `Searching tokens for "${query}"...`, + errorLabel: 'Failed to search tokens', + execute: (c) => + c.searchTokens({ + query, + onSunSwap: opts.onSunswap || undefined, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: tokenTable, + }) + }) + + token + .command('search-v2 ') + .description('Fuzzy search tokens (v2 with extra filters)') + .option('--on-sunswap', 'Only tokens listed on SunSwap') + .option('--dlive', 'Filter: DLive showing') + .option('--ai-helper', 'Filter: AI helper') + .option('--twitter-launch', 'Filter: Twitter launch') + .option('--sun-agent-launch', 'Filter: Sun agent launch') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (query: string, opts) => { + await pumpAction({ + spinnerLabel: `Searching tokens (v2) for "${query}"...`, + errorLabel: 'Failed to search tokens', + execute: (c) => + c.searchTokensV2({ + query, + onSunSwap: opts.onSunswap || undefined, + filterDliveShowing: opts.dlive || undefined, + filterAiHelper: opts.aiHelper || undefined, + filterTwitterLaunch: opts.twitterLaunch || undefined, + filterSunAgentLaunch: opts.sunAgentLaunch || undefined, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: tokenTable, + }) + }) + + token + .command('by-owner ') + .description('List tokens created by a wallet') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (ownerAddress: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching tokens by owner...', + errorLabel: 'Failed to fetch tokens', + execute: (c) => + c.tokensByOwner({ + address: ownerAddress, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: tokenTable, + }) + }) + + token + .command('holders ') + .description('List holders of a token') + .option('--include-zero', 'Include zero-balance holders') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (tokenAddress: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching token holders...', + errorLabel: 'Failed to fetch holders', + execute: (c) => + c.tokenHolders({ + address: tokenAddress, + includeZeroBalance: opts.includeZero || undefined, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: holderTable, + }) + }) + + token + .command('holders-v2 ') + .description('List holders of a token (v2)') + .option('--include-zero', 'Include zero-balance holders') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (tokenAddress: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching token holders (v2)...', + errorLabel: 'Failed to fetch holders', + execute: (c) => + c.tokenHoldersV2({ + address: tokenAddress, + includeZeroBalance: opts.includeZero || undefined, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: holderTable, + }) + }) + + token + .command('favors') + .description("List a user's favorite tokens (requires signed message)") + .requiredOption('--user-address
', 'User wallet address') + .requiredOption('--signature ', 'Signature of signed-message') + .requiredOption('--signed-message ', 'Signed message') + .option('--token-address
', 'Filter by token address') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching user favors...', + errorLabel: 'Failed to fetch favors', + execute: (c) => + c.userFavors({ + userAddress: opts.userAddress, + signature: opts.signature, + signedMessage: opts.signedMessage, + tokenAddress: opts.tokenAddress, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + }), + tableConfig: tokenTable, + }) + }) + + token + .command('ranking') + .description('Token ranking by type') + .requiredOption( + '--type ', + 'Ranking type: MARKET_CAP | VOLUME_24H | PRICE_CHANGE_24H', + ) + .option('--size ', 'Number of entries') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching ranking...', + errorLabel: 'Failed to fetch ranking', + execute: (c) => c.ranking({ rankingType: opts.type, size: toIntOrUndef(opts.size) }), + tableConfig: tokenTable, + }) + }) + + token + .command('king-of-hill') + .description('Get the current king-of-the-hill token') + .action(async () => { + await pumpAction({ + spinnerLabel: 'Fetching king of hill...', + errorLabel: 'Failed to fetch king of hill', + execute: (c) => c.kingOfHill(), + tableConfig: tokenTable, + }) + }) + + token + .command('pump-list') + .description('Get the SunPump token list (raw, no envelope)') + .action(async () => { + await pumpAction({ + spinnerLabel: 'Fetching pump token list...', + errorLabel: 'Failed to fetch pump list', + execute: (c) => c.pumpTokenList(), + parseEnvelope: false, + }) + }) + + // -------------------------- tx group ------------------------------------- + const tx = sp.command('tx').description('Swap transactions') + + const addTxFilterOptions = (cmd: Command) => + cmd + .option('--swap-type ', 'Swap type filter (BUY/SELL)') + .option('--pool
', 'Swap pool address') + .option('--tx-hash ', 'Specific tx hash') + .option('--block ', 'Block number') + .option('--start-time ', 'Start time (epoch seconds)') + .option('--end-time ', 'End time (epoch seconds)') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + + addTxFilterOptions( + tx.command('token ').description('Transactions for a token'), + ).action(async (contractAddress: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching token transactions...', + errorLabel: 'Failed to fetch transactions', + execute: (c) => + c.tokenTransactions(contractAddress, { + swapTranType: opts.swapType, + swapPoolAddress: opts.pool, + txHash: opts.txHash, + blockNum: toIntOrUndef(opts.block), + startTime: toIntOrUndef(opts.startTime), + endTime: toIntOrUndef(opts.endTime), + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: txTable, + }) + }) + + addTxFilterOptions( + tx.command('user ').description('Transactions for a wallet'), + ).action(async (ownerAddress: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching user transactions...', + errorLabel: 'Failed to fetch transactions', + execute: (c) => + c.userTransactions(ownerAddress, { + swapTranType: opts.swapType, + swapPoolAddress: opts.pool, + txHash: opts.txHash, + blockNum: toIntOrUndef(opts.block), + startTime: toIntOrUndef(opts.startTime), + endTime: toIntOrUndef(opts.endTime), + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: txTable, + }) + }) + + tx.command('ticker ') + .description( + 'Recent swap transactions (ticker feed). Server hard-caps the response at ~15 rows regardless of ; for larger history use `tx token` or `tx user`.', + ) + .option('--symbol ', 'Filter by symbol') + .option('--min-value ', 'Minimum trade value in USD') + .action(async (numberArg: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching ticker...', + errorLabel: 'Failed to fetch ticker', + execute: (c) => + c.recentTransactions(toIntOrUndef(numberArg) ?? 50, { + symbol: opts.symbol, + valueGreaterEqualTo: toNumOrUndef(opts.minValue), + }), + tableConfig: txTable, + }) + }) + + // -------------------------- kline group ---------------------------------- + const kline = sp.command('kline').description('OHLCV candlestick data') + + const addKlineOptions = (cmd: Command) => + cmd + .option('--granularity ', 'Candle granularity (e.g. 1m, 5m, 1h, 1d)') + .option('--start-time ', 'Start time (epoch seconds)') + .option('--end-time ', 'End time (epoch seconds)') + + addKlineOptions(kline.command('v1
').alias('get').description('Kline v1')).action( + async (address: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching kline...', + errorLabel: 'Failed to fetch kline', + execute: (c) => + c.kline({ + address, + granularity: opts.granularity, + startTime: toIntOrUndef(opts.startTime), + endTime: toIntOrUndef(opts.endTime), + }), + tableConfig: klineTable, + }) + }, + ) + + addKlineOptions(kline.command('v2
').description('Kline v2')).action( + async (address: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching kline v2...', + errorLabel: 'Failed to fetch kline', + execute: (c) => + c.klineV2({ + address, + granularity: opts.granularity, + startTime: toIntOrUndef(opts.startTime), + endTime: toIntOrUndef(opts.endTime), + }), + tableConfig: klineTable, + }) + }, + ) + + addKlineOptions(kline.command('v3
').description('Kline v3 (extra flags)')) + .option('--closest-before-start', 'returnClosestBeforeStartIfEmpty') + .option('--default-frame', 'defaultFrame') + .option('--with-24hr', 'with24HrData') + .option('--usd-value', 'usdValue (price in USD instead of TRX)') + .action(async (address: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching kline v3...', + errorLabel: 'Failed to fetch kline', + execute: (c) => + c.klineV3({ + address, + granularity: opts.granularity, + startTime: toIntOrUndef(opts.startTime), + endTime: toIntOrUndef(opts.endTime), + returnClosestBeforeStartIfEmpty: opts.closestBeforeStart || undefined, + defaultFrame: opts.defaultFrame || undefined, + with24HrData: opts.with24Hr || undefined, + usdValue: opts.usdValue || undefined, + }), + tableConfig: klineTable, + }) + }) + + // -------------------------- portfolio ------------------------------------ + sp.command('portfolio ') + .description('Get tokens held by a wallet (with TRX value filter)') + .option('--include-zero', 'Include zero-balance tokens') + .option('--min-trx ', 'Minimum TRX value') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .option('--sort ', 'Sort field') + .action(async (walletAddress: string, opts) => { + await pumpAction({ + spinnerLabel: 'Fetching portfolio...', + errorLabel: 'Failed to fetch portfolio', + execute: (c) => + c.portfolio(walletAddress, { + includeZeroBalance: opts.includeZero || undefined, + trxAmountMin: toIntOrUndef(opts.minTrx), + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + sort: opts.sort, + }), + tableConfig: portfolioTable, + }) + }) + + // -------------------------- red-packet ----------------------------------- + const rp = sp.command('red-packet').description('Sun Agent red packets') + + rp.command('get ') + .description('Get red packet by id') + .action(async (packetId: string) => { + const id = toIntOrUndef(packetId) + if (id === undefined) { + outputError('Invalid packetId', new Error('packetId must be an integer')) + return + } + await pumpAction({ + spinnerLabel: 'Fetching red packet...', + errorLabel: 'Failed to fetch red packet', + execute: (c) => c.redPacket(id), + }) + }) + + rp.command('remain') + .description('Query remaining red-packet claim quota') + .requiredOption('--user-address
', 'User wallet address') + .requiredOption('--ip ', 'Client IP') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Querying red-packet remain...', + errorLabel: 'Failed to query remain', + execute: (c) => c.redPacketRemain({ userAddress: opts.userAddress, ip: opts.ip }), + }) + }) + + rp.command('by-user') + .description('List red packets by user (signed)') + .requiredOption('--user-address
', 'User wallet address') + .requiredOption('--signature ', 'Signature') + .requiredOption('--signed-message ', 'Signed message') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching red packets...', + errorLabel: 'Failed to fetch red packets', + execute: (c) => + c.redPacketByUser({ + userAddress: opts.userAddress, + signature: opts.signature, + signedMessage: opts.signedMessage, + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + }), + }) + }) + + rp.command('summary') + .description('Red-packet transaction summary by date range') + .requiredOption('--from ', 'From date (YYYY-MM-DD)') + .requiredOption('--to ', 'To date (YYYY-MM-DD)') + .option('--token-flag ', 'Token flag filter') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching summary...', + errorLabel: 'Failed to fetch summary', + execute: (c) => + c.tranSummary({ + fromDate: opts.from, + toDate: opts.to, + tokenFlag: opts.tokenFlag, + }), + }) + }) + + // -------------------------- referral ------------------------------------- + const ref = sp.command('referral').description('Referral rewards and invite details') + + ref + .command('rewards') + .description('Referral rewards paid (signed)') + .requiredOption('--user-address
', 'User wallet address') + .requiredOption('--signature ', 'Signature') + .requiredOption('--signed-message ', 'Signed message') + .option('--page ', 'Page number') + .option('--page-size ', 'Page size') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching referral rewards...', + errorLabel: 'Failed to fetch rewards', + execute: (c) => + c.referralRewards({ + userAddress: opts.userAddress, + signature: opts.signature, + signedMessage: opts.signedMessage, + pageNo: toIntOrUndef(opts.page), + pageSize: toIntOrUndef(opts.pageSize), + }), + }) + }) + + ref + .command('invites') + .description('Referral invite details (signed)') + .requiredOption('--user-address
', 'User wallet address') + .requiredOption('--signature ', 'Signature') + .requiredOption('--signed-message ', 'Signed message') + .option('--start-date ', 'Start date (YYYY-MM-DD)') + .option('--end-date ', 'End date (YYYY-MM-DD)') + .option('--page ', 'Page number') + .option('--page-size ', 'Page size') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching invite details...', + errorLabel: 'Failed to fetch invites', + execute: (c) => + c.referralInvites({ + userAddress: opts.userAddress, + signature: opts.signature, + signedMessage: opts.signedMessage, + startDate: opts.startDate, + endDate: opts.endDate, + pageNo: toIntOrUndef(opts.page), + pageSize: toIntOrUndef(opts.pageSize), + }), + }) + }) + + // -------------------------- home ----------------------------------------- + const home = sp.command('home').description('Home page data and banners') + + home + .command('stats') + .description('Home page statistics') + .action(async () => { + await pumpAction({ + spinnerLabel: 'Fetching home stats...', + errorLabel: 'Failed to fetch stats', + execute: (c) => c.homeStats(), + }) + }) + + home + .command('data') + .description('Overall app data') + .action(async () => { + await pumpAction({ + spinnerLabel: 'Fetching app data...', + errorLabel: 'Failed to fetch app data', + execute: (c) => c.homeAppData(), + }) + }) + + home + .command('banners ') + .description('Top N home banners') + .action(async (count: string) => { + const n = toIntOrUndef(count) + if (n === undefined) { + outputError('Invalid count', new Error('count must be an integer')) + return + } + await pumpAction({ + spinnerLabel: 'Fetching banners...', + errorLabel: 'Failed to fetch banners', + execute: (c) => c.homeBanners(n), + }) + }) + + // -------------------------- campaign ------------------------------------- + const camp = sp.command('campaign').description('Campaigns and banners') + + camp + .command('list') + .description('List campaigns') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching campaigns...', + errorLabel: 'Failed to fetch campaigns', + execute: (c) => + c.campaigns({ + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + }), + tableConfig: campaignTable, + }) + }) + + camp + .command('banners') + .description('List campaign banners') + .option('--page ', 'Page number') + .option('--size ', 'Page size') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching campaign banners...', + errorLabel: 'Failed to fetch banners', + execute: (c) => + c.campaignBanners({ + page: toIntOrUndef(opts.page), + size: toIntOrUndef(opts.size), + }), + tableConfig: campaignTable, + }) + }) + + // -------------------------- admin ---------------------------------------- + sp.command('admin-summary') + .description('Launched-token summary (admin)') + .requiredOption('--password ', 'Admin password') + .requiredOption('--start ', 'Start time string') + .option('--end ', 'End time string') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Fetching admin summary...', + errorLabel: 'Failed to fetch summary', + execute: (c) => + c.adminSummary({ + password: opts.password, + startTimeStr: opts.start, + endTimeStr: opts.end, + }), + }) + }) + + // -------------------------- third-platform quota ------------------------- + sp.command('quota') + .description('Query third-platform token quota (signed)') + .requiredOption('--user-address
', 'Third-platform user address') + .requiredOption('--message ', 'Message that was signed') + .requiredOption('--signature ', 'Signature') + .action(async (opts) => { + await pumpAction({ + spinnerLabel: 'Querying quota...', + errorLabel: 'Failed to query quota', + execute: (c) => + c.thirdPlatQuota({ + thirdPlatUserAddress: opts.userAddress, + message: opts.message, + signature: opts.signature, + }), + }) + }) +} diff --git a/src/lib/command.ts b/src/lib/command.ts index a6a99e3..aadb985 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -121,9 +121,24 @@ function unwrapApiData(raw: unknown): unknown { function readPagination(src: unknown): ApiPagination | undefined { if (!isPlainObject(src)) return undefined + const direct = readPaginationFlat(src) + if (direct) return direct + // Some SunPump endpoints nest pagination under `pageData` or `metadata`. + if (isPlainObject(src.pageData)) { + const nested = readPaginationFlat(src.pageData) + if (nested) return nested + } + if (isPlainObject(src.metadata)) { + const nested = readPaginationFlat(src.metadata) + if (nested) return nested + } + return undefined +} + +function readPaginationFlat(src: Record): ApiPagination | undefined { const total = (src.total ?? src.totalCount) as number | undefined const pageNo = (src.pageNo ?? src.page) as number | undefined - const pageSize = src.pageSize as number | undefined + const pageSize = (src.pageSize ?? src.size) as number | undefined const offset = (src.offset ?? src.next) as string | undefined if ( total === undefined && diff --git a/src/lib/output.ts b/src/lib/output.ts index a0551a0..0e65280 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -175,6 +175,12 @@ export function extractList(data: any): any[] | null { if (Array.isArray(data.data)) return data.data if (Array.isArray(data.tokens)) return data.tokens if (Array.isArray(data.pools)) return data.pools + if (Array.isArray(data.swaps)) return data.swaps + if (Array.isArray(data.holders)) return data.holders + if (Array.isArray(data.klines)) return data.klines + if (Array.isArray(data.items)) return data.items + if (Array.isArray(data.campaigns)) return data.campaigns + if (Array.isArray(data.banners)) return data.banners } return null } diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts new file mode 100644 index 0000000..beb9000 --- /dev/null +++ b/src/lib/sunpump.ts @@ -0,0 +1,389 @@ +/** + * SunPump API client — read-only GET endpoints. + * + * - Mainnet: https://api-v2.sunpump.meme/pump-api + * - Nile testnet: https://tn-api.sunpump.meme/pump-api + * + * Standalone from sun-kit's SunAPI: SunPump is a separate service with its own + * base URL and schema. Uses Node's global fetch (>=18). + * + * Methods mirror the OpenAPI operationIds documented in docs/sunpump-api.md. + */ + +export const SUNPUMP_MAINNET_BASE_URL = 'https://api-v2.sunpump.meme/pump-api' +export const SUNPUMP_NILE_BASE_URL = 'https://tn-api.sunpump.meme/pump-api' +export const SUNPUMP_DEFAULT_BASE_URL = SUNPUMP_MAINNET_BASE_URL + +export type SunPumpNetwork = 'mainnet' | 'nile' + +export function sunPumpBaseUrlFor(network: SunPumpNetwork): string { + return network === 'nile' ? SUNPUMP_NILE_BASE_URL : SUNPUMP_MAINNET_BASE_URL +} + +export interface SunPumpClientOptions { + baseUrl?: string + network?: SunPumpNetwork + fetchImpl?: typeof fetch +} + +type QueryValue = string | number | boolean | null | undefined +export type Query = Record + +export class SunPumpHttpError extends Error { + readonly code = 'SUNPUMP_HTTP_ERROR' + constructor( + message: string, + readonly status: number, + readonly body?: string, + ) { + super(message) + this.name = 'SunPumpHttpError' + } +} + +function buildQueryString(query?: Query): string { + if (!query) return '' + const usp = new URLSearchParams() + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null || value === '') continue + usp.append(key, String(value)) + } + const s = usp.toString() + return s ? `?${s}` : '' +} + +export class SunPump { + private readonly baseUrl: string + private readonly fetchImpl: typeof fetch + + constructor(opts: SunPumpClientOptions = {}) { + const base = + opts.baseUrl ?? + (opts.network ? sunPumpBaseUrlFor(opts.network) : undefined) ?? + process.env.SUNPUMP_API_BASE_URL ?? + SUNPUMP_DEFAULT_BASE_URL + this.baseUrl = base.replace(/\/+$/, '') + this.fetchImpl = opts.fetchImpl ?? fetch + } + + async request(path: string, query?: Query): Promise { + const url = `${this.baseUrl}${path}${buildQueryString(query)}` + const res = await this.fetchImpl(url, { + method: 'GET', + headers: { Accept: 'application/json' }, + }) + const text = await res.text() + if (!res.ok) { + const excerpt = text.length > 500 ? text.slice(0, 500) + '…' : text + let apiMsg: string | undefined + try { + const parsed = JSON.parse(text) + if (parsed && typeof parsed === 'object' && typeof parsed.msg === 'string') { + apiMsg = parsed.msg + } + } catch { + // body not JSON; fall through + } + const suffix = apiMsg ? ` — ${apiMsg}` : '' + throw new SunPumpHttpError( + `SunPump request failed: ${res.status} ${res.statusText} (${path})${suffix}`, + res.status, + excerpt, + ) + } + if (!text) return undefined as unknown as T + try { + return JSON.parse(text) as T + } catch { + throw new SunPumpHttpError(`SunPump returned non-JSON body (${path})`, res.status, text) + } + } + + // --------------------------------------------------------------------------- + // Token Info + // --------------------------------------------------------------------------- + + listTokens( + query: { + contractAddress?: string + ownerAddress?: string + symbol?: string + name?: string + description?: string + page?: number + size?: number + sort?: string + } = {}, + ) { + return this.request('/token', query) + } + + getToken(contractAddress: string) { + return this.request(`/token/${encodeURIComponent(contractAddress)}`) + } + + searchTokens(query: { + query?: string + onSunSwap?: boolean + page?: number + size?: number + sort?: string + }) { + return this.request('/token/search', query) + } + + searchTokensV2(query: { + query?: string + onSunSwap?: boolean + filterDliveShowing?: boolean + filterAiHelper?: boolean + filterTwitterLaunch?: boolean + filterSunAgentLaunch?: boolean + page?: number + size?: number + sort?: string + }) { + return this.request('/token/searchV2', query) + } + + tokensByOwner(query: { address: string; page?: number; size?: number; sort?: string }) { + return this.request('/token/search/by_owner', query) + } + + tokenHolders(query: { + address: string + includeZeroBalance?: boolean + page?: number + size?: number + sort?: string + }) { + return this.request('/token/holders', query) + } + + tokenHoldersV2(query: { + address: string + includeZeroBalance?: boolean + page?: number + size?: number + sort?: string + }) { + return this.request('/token/holdersV2', query) + } + + userFavors(query: { + signature: string + signedMessage: string + userAddress: string + tokenAddress?: string + page?: number + size?: number + }) { + return this.request('/token/getUserFavors', query) + } + + ranking(query: { rankingType: string; size?: number }) { + return this.request('/token/getRanking', query) + } + + kingOfHill() { + return this.request('/token/getKingOfHill') + } + + pumpTokenList() { + return this.request('/token/SunPumpTokenList.json') + } + + // --------------------------------------------------------------------------- + // Transactions + // --------------------------------------------------------------------------- + + tokenTransactions( + contractAddress: string, + query: { + swapTranType?: string + swapPoolAddress?: string + txHash?: string + blockNum?: number + startTime?: number + endTime?: number + page?: number + size?: number + sort?: string + } = {}, + ) { + return this.request(`/transactions/token/${encodeURIComponent(contractAddress)}`, query) + } + + userTransactions( + ownerAddress: string, + query: { + swapTranType?: string + swapPoolAddress?: string + txHash?: string + blockNum?: number + startTime?: number + endTime?: number + page?: number + size?: number + sort?: string + } = {}, + ) { + return this.request(`/transactions/holder/${encodeURIComponent(ownerAddress)}`, query) + } + + recentTransactions( + number: number, + query: { symbol?: string; valueGreaterEqualTo?: number } = {}, + ) { + return this.request(`/transactions/ticker/${number}`, query) + } + + // --------------------------------------------------------------------------- + // Klines + // --------------------------------------------------------------------------- + + kline(query: { address: string; granularity?: string; startTime?: number; endTime?: number }) { + return this.request('/klines', query) + } + + klineV2(query: { address: string; granularity?: string; startTime?: number; endTime?: number }) { + return this.request('/klinesV2', query) + } + + klineV3(query: { + address: string + granularity?: string + startTime?: number + endTime?: number + returnClosestBeforeStartIfEmpty?: boolean + defaultFrame?: boolean + with24HrData?: boolean + usdValue?: boolean + }) { + return this.request('/klinesV3', query) + } + + // --------------------------------------------------------------------------- + // Holder portfolio + // --------------------------------------------------------------------------- + + portfolio( + address: string, + query: { + includeZeroBalance?: boolean + trxAmountMin?: number + page?: number + size?: number + sort?: string + } = {}, + ) { + return this.request(`/holders/${encodeURIComponent(address)}/tokens`, query) + } + + // --------------------------------------------------------------------------- + // Sun Agent (Red Packet) + // --------------------------------------------------------------------------- + + redPacket(packetId: number) { + return this.request(`/sunAgent/redPacket/${packetId}`) + } + + redPacketRemain(query: { userAddress: string; ip: string }) { + return this.request('/sunAgent/redPacket/queryRemain', query) + } + + redPacketByUser(query: { + signature: string + signedMessage: string + userAddress: string + page?: number + size?: number + }) { + return this.request('/sunAgent/redPacket/findByUserAddress', query) + } + + tranSummary(query: { fromDate: string; toDate: string; tokenFlag?: string }) { + return this.request('/sunAgent/queryTranSummary', query) + } + + // --------------------------------------------------------------------------- + // Referral + // --------------------------------------------------------------------------- + + referralRewards(query: { + signature: string + signedMessage: string + userAddress: string + pageNo?: number + pageSize?: number + }) { + return this.request('/referral/getRewardsPaid', query) + } + + referralInvites(query: { + signature: string + signedMessage: string + userAddress: string + startDate?: string + endDate?: string + pageNo?: number + pageSize?: number + }) { + return this.request('/referral/getInviteDetails', query) + } + + // --------------------------------------------------------------------------- + // Home Info + // --------------------------------------------------------------------------- + + homeStats() { + return this.request('/home/statistics') + } + + homeAppData() { + return this.request('/home/system/data') + } + + homeBanners(count: number) { + return this.request(`/home/banner/top/${count}`) + } + + // --------------------------------------------------------------------------- + // Campaign + // --------------------------------------------------------------------------- + + campaigns(query: { page?: number; size?: number } = {}) { + return this.request('/campaign/campaigns', query) + } + + campaignBanners(query: { page?: number; size?: number } = {}) { + return this.request('/campaign/banners', query) + } + + // --------------------------------------------------------------------------- + // Admin + // --------------------------------------------------------------------------- + + adminSummary(query: { password: string; startTimeStr: string; endTimeStr?: string }) { + return this.request('/data/summaryLaunchedTokenInfo', query) + } + + // --------------------------------------------------------------------------- + // Third-platform open API + // --------------------------------------------------------------------------- + + thirdPlatQuota(query: { thirdPlatUserAddress: string; message: string; signature: string }) { + return this.request('/open-api/internal/token/queryConfig', query) + } +} + +const _clients = new Map() + +export function getSunPump(network: SunPumpNetwork = 'mainnet'): SunPump { + let c = _clients.get(network) + if (!c) { + c = new SunPump({ network }) + _clients.set(network, c) + } + return c +} From 6b2bb8647f3e42d496281bbeb0bd03a0e3b3cd2f Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Wed, 20 May 2026 17:38:32 +0800 Subject: [PATCH 2/8] feat(sunpump): add buy/sell/quote/state trading commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New write commands (writeAction → wallet + confirm + dry-run + Tronscan link): - `sunpump buy --trx ` spends TRX on a bonding-curve token - `sunpump sell --amount [--decimals 18]` sells back to TRX - Both accept `--slippage` (default 5%) and `--min-out `. The summary shown before the confirmation prompt includes a live quote. Read commands: - `sunpump state ` prints the on-chain state with named labels (0 NOT_EXIST / 1 TRADING / 2 READY_TO_LAUNCH / 3 LAUNCHED). The sun-kit enum only documents 0-2 but the contract uses 3 for launched-on-DEX, so labels fall back to `UNKNOWN (n)` for anything we don't recognise. - `sunpump quote-buy` / `sunpump quote-sell` preview a trade without sending. Wraps `kit.sunpumpBuy/Sell/QuoteBuy/QuoteSell/getSunPumpTokenState/Info`. Decimal inputs are scaled CLI-side (TRX × 1e6 to Sun; tokens × 10^decimals) so users never have to type raw base units. State enum mapping plus the existing `--network nile` switch route to mainnet/testnet SunPump deployments. README documents the trade commands, decimal handling and slippage default. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 27 +++- src/commands/sunpump.ts | 293 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 316 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e7a58f1..f238f7e 100644 --- a/README.md +++ b/README.md @@ -356,9 +356,10 @@ sun contract send transfer --args '["TRecipient","1000000"]' ### SunPump -Read-only access to the SunPump API — token launches, trending lists, candlesticks, -holder portfolios, and red-packet/referral data. All endpoints are GET-only; no wallet -required. +Access to SunPump — read-only API for discovery (token launches, trending lists, +candlesticks, holder portfolios, red-packet/referral data) plus on-chain trade +commands (`buy`/`sell`/`quote-buy`/`quote-sell`/`state`) that talk to the bonding-curve +contract through `sun-kit`. Read-only API calls need no wallet; trade commands do. - Mainnet (default): `https://api-v2.sunpump.meme/pump-api` - Nile testnet: `https://tn-api.sunpump.meme/pump-api` — use the global `--network nile` flag @@ -385,6 +386,26 @@ sun sunpump home banners 5 sun sunpump campaign list ``` +Trade on the bonding curve (requires a wallet; pre-launch tokens only — once a token +migrates to SunSwap, use `sun swap` instead): + +```bash +sun sunpump state # 0 NOT_EXIST · 1 TRADING · 2 READY_TO_LAUNCH · 3 LAUNCHED +sun sunpump quote-buy --trx 10 # preview, no tx +sun sunpump quote-sell --amount 1000 + +sun sunpump buy --trx 10 # spend 10 TRX +sun sunpump buy --trx 10 --slippage 0.1 # 10% slippage +sun sunpump sell --amount 1000 # sell 1000 tokens (assumes 18 decimals) +sun sunpump sell --amount 1000 --decimals 6 # override token decimals +sun --dry-run sunpump buy --trx 10 # show params without sending +``` + +`--trx` and `--amount` accept decimal values; CLI scales by TRX-Sun (1e6) and token +decimals (default 18) before calling the contract. Default slippage is 5% (meme tokens +move fast); pass `--slippage 0.005` for 0.5% or `--min-out ` for an exact floor in +base units. + Endpoints requiring a signed message (`favors`, `red-packet by-user`, `referral rewards|invites`, `quota`) accept `--user-address`, `--signature`, `--signed-message` flags. Switch to nile testnet with `sun --network nile sunpump ...`, or override the diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 4395c46..e670a86 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -1,5 +1,6 @@ import { Command } from 'commander' -import { parseApiResponse } from '../lib/command' +import { parseApiResponse, writeAction } from '../lib/command' +import { getNetwork, getKit } from '../lib/context' import { output, outputError, @@ -197,6 +198,54 @@ function tokenDetail(t: any): Record | null { return pairs } +// Decimal string (e.g. "10.5") → integer string in base units, scaled by 10^decimals. +function decimalToBaseUnits(value: string, decimals: number): string { + const trimmed = value.trim() + if (!/^-?\d+(\.\d+)?$/.test(trimmed)) { + throw new Error(`Invalid decimal amount: "${value}"`) + } + const negative = trimmed.startsWith('-') + const unsigned = negative ? trimmed.slice(1) : trimmed + const [whole, frac = ''] = unsigned.split('.') + if (frac.length > decimals) { + throw new Error( + `Amount "${value}" has more than ${decimals} decimal places (token precision limit).`, + ) + } + const paddedFrac = (frac + '0'.repeat(decimals)).slice(0, decimals) + const scaled = BigInt(whole || '0') * 10n ** BigInt(decimals) + BigInt(paddedFrac || '0') + return (negative ? -scaled : scaled).toString() +} + +const trxToSun = (trx: string) => decimalToBaseUnits(trx, 6) + +// Inverse: base-unit string → human-readable decimal. +function baseUnitsToDecimal(raw: string | bigint, decimals: number): string { + const n = typeof raw === 'bigint' ? raw : BigInt(raw) + const negative = n < 0n + const abs = negative ? -n : n + const base = 10n ** BigInt(decimals) + const whole = (abs / base).toString() + const fracDigits = (abs % base).toString().padStart(decimals, '0').replace(/0+$/, '') + const out = fracDigits ? `${whole}.${fracDigits}` : whole + return negative ? `-${out}` : out +} + +// SunKit's enum lists 0/1/2 but the contract uses extra states (e.g. 3 = launched-on-DEX). +// Map what we know; surface raw values verbatim otherwise. +const SUNPUMP_STATE_NAME: Record = { + 0: 'NOT_EXIST', + 1: 'TRADING', + 2: 'READY_TO_LAUNCH', + 3: 'LAUNCHED', +} + +function sunpumpStateLabel(state: number | undefined | null): string { + if (state === undefined || state === null) return '-' + const name = SUNPUMP_STATE_NAME[state] + return name ? `${name} (${state})` : `UNKNOWN (${state})` +} + function fmtNum(v: unknown, digits = 6): string { if (v === undefined || v === null || v === '') return '-' const n = Number(v) @@ -900,6 +949,248 @@ export function registerSunpumpCommands(program: Command) { }) }) + // -------------------------- trade (buy/sell/quote/state) ----------------- + sp.command('state ') + .description( + 'Show SunPump token state (0 NOT_EXIST, 1 TRADING, 2 READY_TO_LAUNCH, 3 LAUNCHED)', + ) + .action(async (tokenAddress: string) => { + try { + const network = getNetwork() + const result = await withSpinner('Fetching token state...', async () => { + const kit = await getKit() + const state = await kit.getSunPumpTokenState(tokenAddress, network) + const info = await kit.getSunPumpTokenInfo(tokenAddress, network).catch(() => null) + return { state, info } + }) + if (isJsonMode()) { + output({ ...result, stateName: sunpumpStateLabel(result.state) }) + return + } + const pairs: Record = { + Token: result.info?.tokenAddress ?? tokenAddress, + State: sunpumpStateLabel(result.state), + Launched: result.info?.launched === true ? 'yes' : 'no', + } + if (result.info?.price !== undefined) pairs['Price (raw)'] = String(result.info.price) + if (result.info?.trxReserve !== undefined) { + pairs['TRX Reserve'] = `${baseUnitsToDecimal(result.info.trxReserve, 6)} TRX` + } + if (result.info?.tokenReserve !== undefined) { + pairs['Token Reserve'] = baseUnitsToDecimal(result.info.tokenReserve, 18) + } + printKeyValue(pairs) + } catch (err: any) { + outputError('Failed to fetch state', err) + } + }) + + sp.command('quote-buy ') + .description('Preview a SunPump buy without sending a transaction') + .requiredOption('--trx ', 'TRX to spend (decimal, e.g. 10 or 1.5)') + .action(async (tokenAddress: string, opts) => { + let trxSun: string + try { + trxSun = trxToSun(opts.trx) + } catch (err: any) { + outputError('Invalid --trx', err) + return + } + try { + const network = getNetwork() + const quote: any = await withSpinner('Fetching buy quote...', async () => { + const kit = await getKit() + return kit.sunpumpQuoteBuy(tokenAddress, trxSun, network) + }) + if (isJsonMode()) { + output({ ...quote, tokenAddress, trxSun, trxIn: opts.trx }) + return + } + printKeyValue({ + Token: tokenAddress, + 'TRX In': `${opts.trx} TRX`, + 'Tokens Out (expected)': baseUnitsToDecimal(quote.tokenAmount, 18), + Fee: `${baseUnitsToDecimal(quote.fee, 6)} TRX`, + }) + } catch (err: any) { + outputError('Failed to fetch quote', err) + } + }) + + sp.command('quote-sell ') + .description('Preview a SunPump sell without sending a transaction') + .requiredOption('--amount ', 'Token amount to sell (decimal)') + .option('--decimals ', 'Token decimals (default 18)', '18') + .action(async (tokenAddress: string, opts) => { + const decimals = Number(opts.decimals) || 18 + let tokenRaw: string + try { + tokenRaw = decimalToBaseUnits(opts.amount, decimals) + } catch (err: any) { + outputError('Invalid --amount', err) + return + } + try { + const network = getNetwork() + const quote: any = await withSpinner('Fetching sell quote...', async () => { + const kit = await getKit() + return kit.sunpumpQuoteSell(tokenAddress, tokenRaw, network) + }) + if (isJsonMode()) { + output({ ...quote, tokenAddress, tokenRaw, amountIn: opts.amount }) + return + } + printKeyValue({ + Token: tokenAddress, + 'Tokens In': `${opts.amount}`, + 'TRX Out (expected)': `${baseUnitsToDecimal(quote.trxAmount, 6)} TRX`, + Fee: `${baseUnitsToDecimal(quote.fee, 6)} TRX`, + }) + } catch (err: any) { + outputError('Failed to fetch quote', err) + } + }) + + sp.command('buy ') + .description('Buy a SunPump token with TRX (bonding-curve, pre-launch only)') + .requiredOption('--trx ', 'TRX to spend (decimal, e.g. 10 or 1.5)') + .option('--slippage ', 'Slippage tolerance (decimal, e.g. 0.05 = 5%)', '0.05') + .option('--min-out ', 'Minimum tokens out in raw base units (overrides slippage)') + .action(async (tokenAddress: string, opts) => { + let trxSun: string + try { + trxSun = trxToSun(opts.trx) + } catch (err: any) { + outputError('Invalid --trx', err) + return + } + const slippage = parseFloat(opts.slippage) + const network = getNetwork() + + const summary: Record = { + Token: tokenAddress, + 'TRX In': `${opts.trx} TRX (${trxSun} Sun)`, + Slippage: `${(slippage * 100).toFixed(2)}%`, + Network: network, + } + try { + const quote = await withSpinner('Fetching buy quote...', async () => { + const kit = await getKit() + return kit.sunpumpQuoteBuy(tokenAddress, trxSun, network) + }) + summary['Tokens Out (expected)'] = baseUnitsToDecimal((quote as any).tokenAmount, 18) + summary['Fee'] = `${baseUnitsToDecimal((quote as any).fee, 6)} TRX` + } catch { + // Quote is best-effort; the real call below will surface the error. + } + if (opts.minOut) summary['Min Tokens Out (raw)'] = opts.minOut + + await writeAction({ + title: 'SunPump Buy Preview', + summary, + confirmMsg: 'Execute this buy?', + spinnerLabel: 'Submitting buy...', + errorLabel: 'Buy failed', + execute: (kit) => + kit.sunpumpBuy({ + tokenAddress, + trxAmount: trxSun, + minTokenOut: opts.minOut, + slippage, + network, + }), + onSuccess: async (result: any) => { + if (isJsonMode()) return + const chalk = (await import('chalk')).default + console.log() + console.log(chalk.green('Buy executed')) + if (result.expectedTokens) { + console.log(` Expected: ${baseUnitsToDecimal(result.expectedTokens, 18)} tokens`) + } + if (result.minTokenOut) { + console.log(` Min Out: ${baseUnitsToDecimal(result.minTokenOut, 18)} tokens`) + } + if (result.txResult?.txid) { + console.log(` TxID: ${chalk.bold(result.txResult.txid)}`) + } + if (result.tronscanUrl) { + console.log(` Tronscan: ${chalk.underline(result.tronscanUrl)}`) + } + }, + }) + }) + + sp.command('sell ') + .description('Sell a SunPump token for TRX (bonding-curve, pre-launch only)') + .requiredOption('--amount ', 'Token amount to sell (decimal)') + .option('--decimals ', 'Token decimals (default 18)', '18') + .option('--slippage ', 'Slippage tolerance (decimal, e.g. 0.05 = 5%)', '0.05') + .option('--min-out ', 'Minimum TRX out in Sun (overrides slippage)') + .action(async (tokenAddress: string, opts) => { + const decimals = Number(opts.decimals) || 18 + let tokenRaw: string + try { + tokenRaw = decimalToBaseUnits(opts.amount, decimals) + } catch (err: any) { + outputError('Invalid --amount', err) + return + } + const slippage = parseFloat(opts.slippage) + const network = getNetwork() + + const summary: Record = { + Token: tokenAddress, + 'Tokens In': `${opts.amount} (${tokenRaw} raw)`, + Slippage: `${(slippage * 100).toFixed(2)}%`, + Network: network, + } + try { + const quote = await withSpinner('Fetching sell quote...', async () => { + const kit = await getKit() + return kit.sunpumpQuoteSell(tokenAddress, tokenRaw, network) + }) + summary['TRX Out (expected)'] = `${baseUnitsToDecimal((quote as any).trxAmount, 6)} TRX` + summary['Fee'] = `${baseUnitsToDecimal((quote as any).fee, 6)} TRX` + } catch { + // Best-effort. + } + if (opts.minOut) summary['Min TRX Out (Sun)'] = opts.minOut + + await writeAction({ + title: 'SunPump Sell Preview', + summary, + confirmMsg: 'Execute this sell?', + spinnerLabel: 'Submitting sell...', + errorLabel: 'Sell failed', + execute: (kit) => + kit.sunpumpSell({ + tokenAddress, + tokenAmount: tokenRaw, + minTrxOut: opts.minOut, + slippage, + network, + }), + onSuccess: async (result: any) => { + if (isJsonMode()) return + const chalk = (await import('chalk')).default + console.log() + console.log(chalk.green('Sell executed')) + if (result.expectedTrx) { + console.log(` Expected: ${baseUnitsToDecimal(result.expectedTrx, 6)} TRX`) + } + if (result.minTrxOut) { + console.log(` Min Out: ${baseUnitsToDecimal(result.minTrxOut, 6)} TRX`) + } + if (result.txResult?.txid) { + console.log(` TxID: ${chalk.bold(result.txResult.txid)}`) + } + if (result.tronscanUrl) { + console.log(` Tronscan: ${chalk.underline(result.tronscanUrl)}`) + } + }, + }) + }) + // -------------------------- third-platform quota ------------------------- sp.command('quota') .description('Query third-platform token quota (signed)') From 05905a285aa2dc6ac5a39401610add40dce743ef Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Fri, 22 May 2026 21:19:30 +0800 Subject: [PATCH 3/8] refactor(sunpump): drop home/tx-ticker/kline/red-packet/campaign groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These endpoint groups didn't earn their keep: - `sunpump home` (stats/data/banners) — site-chrome data agents don't need - `sunpump tx ticker` — server hard-caps at 15 rows regardless of N - `sunpump kline` (v1/v2/v3) — three near-identical OHLCV variants - `sunpump red-packet` (get/remain/by-user/summary) — Sun Agent campaign feature, not core trading - `sunpump campaign` (list/banners) — marketing assets, not on-chain data Removes the CLI registrations, the SunPump client methods, the unused table configs and helpers (`klineTable`, `campaignTable`, `truncate`, `campaignStatus`, `toNumOrUndef`), and the matching `extractList` branches in `lib/output.ts`. README examples and the signed-message note in the SunPump section are pruned to match. What stays: `token` (info/search/holders/ranking/king-of-hill/etc), `tx token` + `tx user`, `portfolio`, `referral`, `admin-summary`, `quota`, plus the trading commands `state` / `quote-buy` / `quote-sell` / `buy` / `sell`. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 20 +-- src/commands/sunpump.ts | 283 ---------------------------------------- src/lib/output.ts | 3 - src/lib/sunpump.ts | 86 ------------ 4 files changed, 6 insertions(+), 386 deletions(-) diff --git a/README.md b/README.md index f238f7e..a1d333e 100644 --- a/README.md +++ b/README.md @@ -357,15 +357,14 @@ sun contract send transfer --args '["TRecipient","1000000"]' ### SunPump Access to SunPump — read-only API for discovery (token launches, trending lists, -candlesticks, holder portfolios, red-packet/referral data) plus on-chain trade -commands (`buy`/`sell`/`quote-buy`/`quote-sell`/`state`) that talk to the bonding-curve +holder portfolios, referral data) plus on-chain trade commands +(`buy`/`sell`/`quote-buy`/`quote-sell`/`state`) that talk to the bonding-curve contract through `sun-kit`. Read-only API calls need no wallet; trade commands do. - Mainnet (default): `https://api-v2.sunpump.meme/pump-api` - Nile testnet: `https://tn-api.sunpump.meme/pump-api` — use the global `--network nile` flag ```bash -sun sunpump home stats # quick volume/launch counters sun sunpump token king-of-hill # current king-of-the-hill token sun sunpump token list --size 20 --sort marketCap,desc sun sunpump token search --size 10 @@ -375,15 +374,8 @@ sun sunpump token ranking --type MARKET_CAP --size 10 # also: VOLUME_24H, sun sunpump tx token --size 20 # swap history for a token sun sunpump tx user --size 20 # swap history for a wallet -sun sunpump tx ticker 50 # latest ticker feed (server caps at ~15 rows regardless of N) -sun sunpump kline v3 --granularity 1m sun sunpump portfolio --include-zero - -sun sunpump red-packet get -sun sunpump red-packet remain --user-address T... --ip 1.2.3.4 -sun sunpump home banners 5 -sun sunpump campaign list ``` Trade on the bonding curve (requires a wallet; pre-launch tokens only — once a token @@ -406,10 +398,10 @@ decimals (default 18) before calling the contract. Default slippage is 5% (meme move fast); pass `--slippage 0.005` for 0.5% or `--min-out ` for an exact floor in base units. -Endpoints requiring a signed message (`favors`, `red-packet by-user`, `referral -rewards|invites`, `quota`) accept `--user-address`, `--signature`, `--signed-message` -flags. Switch to nile testnet with `sun --network nile sunpump ...`, or override the -base URL with `SUNPUMP_API_BASE_URL` for a custom host. +Endpoints requiring a signed message (`favors`, `referral rewards|invites`, `quota`) +accept `--user-address`, `--signature`, `--signed-message` flags. Switch to nile +testnet with `sun --network nile sunpump ...`, or override the base URL with +`SUNPUMP_API_BASE_URL` for a custom host. Full reference (request params, response schemas) is in [`docs/sunpump-api.md`](docs/sunpump-api.md) and diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index e670a86..88fe66e 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -67,12 +67,6 @@ function toIntOrUndef(v: unknown): number | undefined { return Number.isFinite(n) ? Math.trunc(n) : undefined } -function toNumOrUndef(v: unknown): number | undefined { - if (v === undefined || v === null || v === '') return undefined - const n = Number(v) - return Number.isFinite(n) ? n : undefined -} - // --------------------------------------------------------------------------- // Shared table configs // --------------------------------------------------------------------------- @@ -269,47 +263,6 @@ const portfolioTable = { ], } -function truncate(s: unknown, max: number): string { - if (s === undefined || s === null) return '-' - const str = String(s).replace(/\s+/g, ' ').trim() - if (str.length <= max) return str - return str.slice(0, max - 1) + '…' -} - -function campaignStatus(startMs: unknown, endMs: unknown): string { - const s = Number(startMs) - const e = Number(endMs) - const now = Date.now() - if (Number.isFinite(e) && now > e) return 'expired' - if (Number.isFinite(s) && now < s) return 'upcoming' - if (Number.isFinite(s) && Number.isFinite(e) && now >= s && now <= e) return 'active' - return '-' -} - -const campaignTable = { - headers: ['ID', 'Title', 'Status', 'Start', 'End', 'Link'], - toRow: (c: any) => [ - String(c.id ?? '-'), - truncate(c.title, 40), - campaignStatus(c.startTime, c.endTime), - formatTime(c.startTime), - formatTime(c.endTime), - c.redirectUrl ?? '-', - ], -} - -const klineTable = { - headers: ['Time', 'Open', 'High', 'Low', 'Close', 'Volume'], - toRow: (k: any) => [ - formatTime(k.startTime ?? k.timestamp ?? k.t), - String(k.open ?? k.o ?? '-'), - String(k.high ?? k.h ?? '-'), - String(k.low ?? k.l ?? '-'), - String(k.close ?? k.c ?? '-'), - String(k.volume ?? k.v ?? '-'), - ], -} - // --------------------------------------------------------------------------- // Command registration // --------------------------------------------------------------------------- @@ -617,92 +570,6 @@ export function registerSunpumpCommands(program: Command) { }) }) - tx.command('ticker ') - .description( - 'Recent swap transactions (ticker feed). Server hard-caps the response at ~15 rows regardless of ; for larger history use `tx token` or `tx user`.', - ) - .option('--symbol ', 'Filter by symbol') - .option('--min-value ', 'Minimum trade value in USD') - .action(async (numberArg: string, opts) => { - await pumpAction({ - spinnerLabel: 'Fetching ticker...', - errorLabel: 'Failed to fetch ticker', - execute: (c) => - c.recentTransactions(toIntOrUndef(numberArg) ?? 50, { - symbol: opts.symbol, - valueGreaterEqualTo: toNumOrUndef(opts.minValue), - }), - tableConfig: txTable, - }) - }) - - // -------------------------- kline group ---------------------------------- - const kline = sp.command('kline').description('OHLCV candlestick data') - - const addKlineOptions = (cmd: Command) => - cmd - .option('--granularity ', 'Candle granularity (e.g. 1m, 5m, 1h, 1d)') - .option('--start-time ', 'Start time (epoch seconds)') - .option('--end-time ', 'End time (epoch seconds)') - - addKlineOptions(kline.command('v1
').alias('get').description('Kline v1')).action( - async (address: string, opts) => { - await pumpAction({ - spinnerLabel: 'Fetching kline...', - errorLabel: 'Failed to fetch kline', - execute: (c) => - c.kline({ - address, - granularity: opts.granularity, - startTime: toIntOrUndef(opts.startTime), - endTime: toIntOrUndef(opts.endTime), - }), - tableConfig: klineTable, - }) - }, - ) - - addKlineOptions(kline.command('v2
').description('Kline v2')).action( - async (address: string, opts) => { - await pumpAction({ - spinnerLabel: 'Fetching kline v2...', - errorLabel: 'Failed to fetch kline', - execute: (c) => - c.klineV2({ - address, - granularity: opts.granularity, - startTime: toIntOrUndef(opts.startTime), - endTime: toIntOrUndef(opts.endTime), - }), - tableConfig: klineTable, - }) - }, - ) - - addKlineOptions(kline.command('v3
').description('Kline v3 (extra flags)')) - .option('--closest-before-start', 'returnClosestBeforeStartIfEmpty') - .option('--default-frame', 'defaultFrame') - .option('--with-24hr', 'with24HrData') - .option('--usd-value', 'usdValue (price in USD instead of TRX)') - .action(async (address: string, opts) => { - await pumpAction({ - spinnerLabel: 'Fetching kline v3...', - errorLabel: 'Failed to fetch kline', - execute: (c) => - c.klineV3({ - address, - granularity: opts.granularity, - startTime: toIntOrUndef(opts.startTime), - endTime: toIntOrUndef(opts.endTime), - returnClosestBeforeStartIfEmpty: opts.closestBeforeStart || undefined, - defaultFrame: opts.defaultFrame || undefined, - with24HrData: opts.with24Hr || undefined, - usdValue: opts.usdValue || undefined, - }), - tableConfig: klineTable, - }) - }) - // -------------------------- portfolio ------------------------------------ sp.command('portfolio ') .description('Get tokens held by a wallet (with TRX value filter)') @@ -727,76 +594,6 @@ export function registerSunpumpCommands(program: Command) { }) }) - // -------------------------- red-packet ----------------------------------- - const rp = sp.command('red-packet').description('Sun Agent red packets') - - rp.command('get ') - .description('Get red packet by id') - .action(async (packetId: string) => { - const id = toIntOrUndef(packetId) - if (id === undefined) { - outputError('Invalid packetId', new Error('packetId must be an integer')) - return - } - await pumpAction({ - spinnerLabel: 'Fetching red packet...', - errorLabel: 'Failed to fetch red packet', - execute: (c) => c.redPacket(id), - }) - }) - - rp.command('remain') - .description('Query remaining red-packet claim quota') - .requiredOption('--user-address
', 'User wallet address') - .requiredOption('--ip ', 'Client IP') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Querying red-packet remain...', - errorLabel: 'Failed to query remain', - execute: (c) => c.redPacketRemain({ userAddress: opts.userAddress, ip: opts.ip }), - }) - }) - - rp.command('by-user') - .description('List red packets by user (signed)') - .requiredOption('--user-address
', 'User wallet address') - .requiredOption('--signature ', 'Signature') - .requiredOption('--signed-message ', 'Signed message') - .option('--page ', 'Page number') - .option('--size ', 'Page size') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching red packets...', - errorLabel: 'Failed to fetch red packets', - execute: (c) => - c.redPacketByUser({ - userAddress: opts.userAddress, - signature: opts.signature, - signedMessage: opts.signedMessage, - page: toIntOrUndef(opts.page), - size: toIntOrUndef(opts.size), - }), - }) - }) - - rp.command('summary') - .description('Red-packet transaction summary by date range') - .requiredOption('--from ', 'From date (YYYY-MM-DD)') - .requiredOption('--to ', 'To date (YYYY-MM-DD)') - .option('--token-flag ', 'Token flag filter') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching summary...', - errorLabel: 'Failed to fetch summary', - execute: (c) => - c.tranSummary({ - fromDate: opts.from, - toDate: opts.to, - tokenFlag: opts.tokenFlag, - }), - }) - }) - // -------------------------- referral ------------------------------------- const ref = sp.command('referral').description('Referral rewards and invite details') @@ -850,86 +647,6 @@ export function registerSunpumpCommands(program: Command) { }) }) - // -------------------------- home ----------------------------------------- - const home = sp.command('home').description('Home page data and banners') - - home - .command('stats') - .description('Home page statistics') - .action(async () => { - await pumpAction({ - spinnerLabel: 'Fetching home stats...', - errorLabel: 'Failed to fetch stats', - execute: (c) => c.homeStats(), - }) - }) - - home - .command('data') - .description('Overall app data') - .action(async () => { - await pumpAction({ - spinnerLabel: 'Fetching app data...', - errorLabel: 'Failed to fetch app data', - execute: (c) => c.homeAppData(), - }) - }) - - home - .command('banners ') - .description('Top N home banners') - .action(async (count: string) => { - const n = toIntOrUndef(count) - if (n === undefined) { - outputError('Invalid count', new Error('count must be an integer')) - return - } - await pumpAction({ - spinnerLabel: 'Fetching banners...', - errorLabel: 'Failed to fetch banners', - execute: (c) => c.homeBanners(n), - }) - }) - - // -------------------------- campaign ------------------------------------- - const camp = sp.command('campaign').description('Campaigns and banners') - - camp - .command('list') - .description('List campaigns') - .option('--page ', 'Page number') - .option('--size ', 'Page size') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching campaigns...', - errorLabel: 'Failed to fetch campaigns', - execute: (c) => - c.campaigns({ - page: toIntOrUndef(opts.page), - size: toIntOrUndef(opts.size), - }), - tableConfig: campaignTable, - }) - }) - - camp - .command('banners') - .description('List campaign banners') - .option('--page ', 'Page number') - .option('--size ', 'Page size') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching campaign banners...', - errorLabel: 'Failed to fetch banners', - execute: (c) => - c.campaignBanners({ - page: toIntOrUndef(opts.page), - size: toIntOrUndef(opts.size), - }), - tableConfig: campaignTable, - }) - }) - // -------------------------- admin ---------------------------------------- sp.command('admin-summary') .description('Launched-token summary (admin)') diff --git a/src/lib/output.ts b/src/lib/output.ts index 0e65280..626f24b 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -177,10 +177,7 @@ export function extractList(data: any): any[] | null { if (Array.isArray(data.pools)) return data.pools if (Array.isArray(data.swaps)) return data.swaps if (Array.isArray(data.holders)) return data.holders - if (Array.isArray(data.klines)) return data.klines if (Array.isArray(data.items)) return data.items - if (Array.isArray(data.campaigns)) return data.campaigns - if (Array.isArray(data.banners)) return data.banners } return null } diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts index beb9000..f9037c1 100644 --- a/src/lib/sunpump.ts +++ b/src/lib/sunpump.ts @@ -231,38 +231,6 @@ export class SunPump { return this.request(`/transactions/holder/${encodeURIComponent(ownerAddress)}`, query) } - recentTransactions( - number: number, - query: { symbol?: string; valueGreaterEqualTo?: number } = {}, - ) { - return this.request(`/transactions/ticker/${number}`, query) - } - - // --------------------------------------------------------------------------- - // Klines - // --------------------------------------------------------------------------- - - kline(query: { address: string; granularity?: string; startTime?: number; endTime?: number }) { - return this.request('/klines', query) - } - - klineV2(query: { address: string; granularity?: string; startTime?: number; endTime?: number }) { - return this.request('/klinesV2', query) - } - - klineV3(query: { - address: string - granularity?: string - startTime?: number - endTime?: number - returnClosestBeforeStartIfEmpty?: boolean - defaultFrame?: boolean - with24HrData?: boolean - usdValue?: boolean - }) { - return this.request('/klinesV3', query) - } - // --------------------------------------------------------------------------- // Holder portfolio // --------------------------------------------------------------------------- @@ -280,32 +248,6 @@ export class SunPump { return this.request(`/holders/${encodeURIComponent(address)}/tokens`, query) } - // --------------------------------------------------------------------------- - // Sun Agent (Red Packet) - // --------------------------------------------------------------------------- - - redPacket(packetId: number) { - return this.request(`/sunAgent/redPacket/${packetId}`) - } - - redPacketRemain(query: { userAddress: string; ip: string }) { - return this.request('/sunAgent/redPacket/queryRemain', query) - } - - redPacketByUser(query: { - signature: string - signedMessage: string - userAddress: string - page?: number - size?: number - }) { - return this.request('/sunAgent/redPacket/findByUserAddress', query) - } - - tranSummary(query: { fromDate: string; toDate: string; tokenFlag?: string }) { - return this.request('/sunAgent/queryTranSummary', query) - } - // --------------------------------------------------------------------------- // Referral // --------------------------------------------------------------------------- @@ -332,34 +274,6 @@ export class SunPump { return this.request('/referral/getInviteDetails', query) } - // --------------------------------------------------------------------------- - // Home Info - // --------------------------------------------------------------------------- - - homeStats() { - return this.request('/home/statistics') - } - - homeAppData() { - return this.request('/home/system/data') - } - - homeBanners(count: number) { - return this.request(`/home/banner/top/${count}`) - } - - // --------------------------------------------------------------------------- - // Campaign - // --------------------------------------------------------------------------- - - campaigns(query: { page?: number; size?: number } = {}) { - return this.request('/campaign/campaigns', query) - } - - campaignBanners(query: { page?: number; size?: number } = {}) { - return this.request('/campaign/banners', query) - } - // --------------------------------------------------------------------------- // Admin // --------------------------------------------------------------------------- From 500f67bc47abd771a7f1817fc21a3de6a1f6ce50 Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Fri, 22 May 2026 21:20:48 +0800 Subject: [PATCH 4/8] refactor(sunpump): drop admin-summary Removes the `sun sunpump admin-summary` command and the matching `adminSummary()` method on the SunPump client. The endpoint (`/data/summaryLaunchedTokenInfo`) requires an admin password and is not appropriate for an end-user CLI. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/sunpump.ts | 19 ------------------- src/lib/sunpump.ts | 8 -------- 2 files changed, 27 deletions(-) diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 88fe66e..949b57b 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -647,25 +647,6 @@ export function registerSunpumpCommands(program: Command) { }) }) - // -------------------------- admin ---------------------------------------- - sp.command('admin-summary') - .description('Launched-token summary (admin)') - .requiredOption('--password ', 'Admin password') - .requiredOption('--start ', 'Start time string') - .option('--end ', 'End time string') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching admin summary...', - errorLabel: 'Failed to fetch summary', - execute: (c) => - c.adminSummary({ - password: opts.password, - startTimeStr: opts.start, - endTimeStr: opts.end, - }), - }) - }) - // -------------------------- trade (buy/sell/quote/state) ----------------- sp.command('state ') .description( diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts index f9037c1..9aac7d1 100644 --- a/src/lib/sunpump.ts +++ b/src/lib/sunpump.ts @@ -274,14 +274,6 @@ export class SunPump { return this.request('/referral/getInviteDetails', query) } - // --------------------------------------------------------------------------- - // Admin - // --------------------------------------------------------------------------- - - adminSummary(query: { password: string; startTimeStr: string; endTimeStr?: string }) { - return this.request('/data/summaryLaunchedTokenInfo', query) - } - // --------------------------------------------------------------------------- // Third-platform open API // --------------------------------------------------------------------------- From 9e51439277ea5577a21c07e66b37153f4dc91498 Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Fri, 22 May 2026 21:22:45 +0800 Subject: [PATCH 5/8] refactor(sunpump): drop referral group Removes `sun sunpump referral rewards` / `sunpump referral invites` and the `referralRewards()` / `referralInvites()` SDK methods. Referral program data is internal-account-management surface, not part of the core trading + discovery loop this CLI is for. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 10 ++++---- src/commands/sunpump.ts | 52 ----------------------------------------- src/lib/sunpump.ts | 26 --------------------- 3 files changed, 5 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index a1d333e..2637673 100644 --- a/README.md +++ b/README.md @@ -357,7 +357,7 @@ sun contract send transfer --args '["TRecipient","1000000"]' ### SunPump Access to SunPump — read-only API for discovery (token launches, trending lists, -holder portfolios, referral data) plus on-chain trade commands +holder portfolios) plus on-chain trade commands (`buy`/`sell`/`quote-buy`/`quote-sell`/`state`) that talk to the bonding-curve contract through `sun-kit`. Read-only API calls need no wallet; trade commands do. @@ -398,10 +398,10 @@ decimals (default 18) before calling the contract. Default slippage is 5% (meme move fast); pass `--slippage 0.005` for 0.5% or `--min-out ` for an exact floor in base units. -Endpoints requiring a signed message (`favors`, `referral rewards|invites`, `quota`) -accept `--user-address`, `--signature`, `--signed-message` flags. Switch to nile -testnet with `sun --network nile sunpump ...`, or override the base URL with -`SUNPUMP_API_BASE_URL` for a custom host. +Endpoints requiring a signed message (`favors`, `quota`) accept `--user-address`, +`--signature`, `--signed-message` flags. Switch to nile testnet with +`sun --network nile sunpump ...`, or override the base URL with `SUNPUMP_API_BASE_URL` +for a custom host. Full reference (request params, response schemas) is in [`docs/sunpump-api.md`](docs/sunpump-api.md) and diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 949b57b..9a52eb1 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -594,58 +594,6 @@ export function registerSunpumpCommands(program: Command) { }) }) - // -------------------------- referral ------------------------------------- - const ref = sp.command('referral').description('Referral rewards and invite details') - - ref - .command('rewards') - .description('Referral rewards paid (signed)') - .requiredOption('--user-address
', 'User wallet address') - .requiredOption('--signature ', 'Signature') - .requiredOption('--signed-message ', 'Signed message') - .option('--page ', 'Page number') - .option('--page-size ', 'Page size') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching referral rewards...', - errorLabel: 'Failed to fetch rewards', - execute: (c) => - c.referralRewards({ - userAddress: opts.userAddress, - signature: opts.signature, - signedMessage: opts.signedMessage, - pageNo: toIntOrUndef(opts.page), - pageSize: toIntOrUndef(opts.pageSize), - }), - }) - }) - - ref - .command('invites') - .description('Referral invite details (signed)') - .requiredOption('--user-address
', 'User wallet address') - .requiredOption('--signature ', 'Signature') - .requiredOption('--signed-message ', 'Signed message') - .option('--start-date ', 'Start date (YYYY-MM-DD)') - .option('--end-date ', 'End date (YYYY-MM-DD)') - .option('--page ', 'Page number') - .option('--page-size ', 'Page size') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Fetching invite details...', - errorLabel: 'Failed to fetch invites', - execute: (c) => - c.referralInvites({ - userAddress: opts.userAddress, - signature: opts.signature, - signedMessage: opts.signedMessage, - startDate: opts.startDate, - endDate: opts.endDate, - pageNo: toIntOrUndef(opts.page), - pageSize: toIntOrUndef(opts.pageSize), - }), - }) - }) // -------------------------- trade (buy/sell/quote/state) ----------------- sp.command('state ') diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts index 9aac7d1..246cd9b 100644 --- a/src/lib/sunpump.ts +++ b/src/lib/sunpump.ts @@ -248,32 +248,6 @@ export class SunPump { return this.request(`/holders/${encodeURIComponent(address)}/tokens`, query) } - // --------------------------------------------------------------------------- - // Referral - // --------------------------------------------------------------------------- - - referralRewards(query: { - signature: string - signedMessage: string - userAddress: string - pageNo?: number - pageSize?: number - }) { - return this.request('/referral/getRewardsPaid', query) - } - - referralInvites(query: { - signature: string - signedMessage: string - userAddress: string - startDate?: string - endDate?: string - pageNo?: number - pageSize?: number - }) { - return this.request('/referral/getInviteDetails', query) - } - // --------------------------------------------------------------------------- // Third-platform open API // --------------------------------------------------------------------------- From a5bdc6ea434bcea723e0d8de0c23a49fbbc43968 Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Fri, 22 May 2026 21:55:15 +0800 Subject: [PATCH 6/8] update readme --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 2637673..87e3a9f 100644 --- a/README.md +++ b/README.md @@ -403,9 +403,6 @@ Endpoints requiring a signed message (`favors`, `quota`) accept `--user-address` `sun --network nile sunpump ...`, or override the base URL with `SUNPUMP_API_BASE_URL` for a custom host. -Full reference (request params, response schemas) is in -[`docs/sunpump-api.md`](docs/sunpump-api.md) and -[`docs/sunpump-api.zh-CN.md`](docs/sunpump-api.zh-CN.md). --- From e0ebf57009aff3b7967b8ef6d4c92a8fa064ecc0 Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Fri, 22 May 2026 21:59:39 +0800 Subject: [PATCH 7/8] refactor(sunpump): drop quota (third-platform open API) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes `sun sunpump quota` and the `thirdPlatQuota()` SDK method. The endpoint queries SunPump's internal third-platform integration quota — not relevant to end-user trading or discovery. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- src/commands/sunpump.ts | 18 ------------------ src/lib/sunpump.ts | 8 -------- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/README.md b/README.md index 87e3a9f..20151f3 100644 --- a/README.md +++ b/README.md @@ -398,7 +398,7 @@ decimals (default 18) before calling the contract. Default slippage is 5% (meme move fast); pass `--slippage 0.005` for 0.5% or `--min-out ` for an exact floor in base units. -Endpoints requiring a signed message (`favors`, `quota`) accept `--user-address`, +Endpoints requiring a signed message (`favors`) accept `--user-address`, `--signature`, `--signed-message` flags. Switch to nile testnet with `sun --network nile sunpump ...`, or override the base URL with `SUNPUMP_API_BASE_URL` for a custom host. diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 9a52eb1..0e22ece 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -837,22 +837,4 @@ export function registerSunpumpCommands(program: Command) { }) }) - // -------------------------- third-platform quota ------------------------- - sp.command('quota') - .description('Query third-platform token quota (signed)') - .requiredOption('--user-address
', 'Third-platform user address') - .requiredOption('--message ', 'Message that was signed') - .requiredOption('--signature ', 'Signature') - .action(async (opts) => { - await pumpAction({ - spinnerLabel: 'Querying quota...', - errorLabel: 'Failed to query quota', - execute: (c) => - c.thirdPlatQuota({ - thirdPlatUserAddress: opts.userAddress, - message: opts.message, - signature: opts.signature, - }), - }) - }) } diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts index 246cd9b..11c0c46 100644 --- a/src/lib/sunpump.ts +++ b/src/lib/sunpump.ts @@ -247,14 +247,6 @@ export class SunPump { ) { return this.request(`/holders/${encodeURIComponent(address)}/tokens`, query) } - - // --------------------------------------------------------------------------- - // Third-platform open API - // --------------------------------------------------------------------------- - - thirdPlatQuota(query: { thirdPlatUserAddress: string; message: string; signature: string }) { - return this.request('/open-api/internal/token/queryConfig', query) - } } const _clients = new Map() From adc1177a03d43d915b423c2ff442321fbc9e1fac Mon Sep 17 00:00:00 2001 From: "Leon.Zhang" Date: Fri, 22 May 2026 22:09:24 +0800 Subject: [PATCH 8/8] refactor(sunpump): drop nile testnet (mainnet only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nile-equivalent host `tn-api.sunpump.meme` is an internal-only service — not publicly reachable. The SunPump bonding-curve contract on Nile is being retired too. Removing nile from every SunPump path: - `lib/sunpump.ts`: drop `SUNPUMP_NILE_BASE_URL`, `SUNPUMP_MAINNET_BASE_URL`, `SunPumpNetwork` type, `sunPumpBaseUrlFor()`, and the network-aware client cache. `getSunPump()` is back to a plain singleton; the constructor resolves the base URL from `baseUrl` arg → env `SUNPUMP_API_BASE_URL` → `SUNPUMP_DEFAULT_BASE_URL` (mainnet). - `commands/sunpump.ts`: replace the `currentNetwork` / preAction network-switching machinery with `assertMainnet()`, which the `sp` preAction hook calls so every sunpump subcommand (read or trade) bails fast on non-mainnet with: SunPump is only available on mainnet (got ""). Drop --network or pass --network mainnet. - README + SunPump section: reflect mainnet-only behaviour; drop the nile URL line and the "switch with --network nile" hint. Behaviour verified locally: `sun sunpump token list` works on mainnet, `sun --network nile sunpump token list` and `sun --network nile sunpump state ...` both fail fast with the new error. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 10 +++++----- src/commands/sunpump.ts | 21 +++++++++++++-------- src/lib/sunpump.ts | 32 +++++++------------------------- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 20151f3..6c58c91 100644 --- a/README.md +++ b/README.md @@ -361,8 +361,9 @@ holder portfolios) plus on-chain trade commands (`buy`/`sell`/`quote-buy`/`quote-sell`/`state`) that talk to the bonding-curve contract through `sun-kit`. Read-only API calls need no wallet; trade commands do. -- Mainnet (default): `https://api-v2.sunpump.meme/pump-api` -- Nile testnet: `https://tn-api.sunpump.meme/pump-api` — use the global `--network nile` flag +SunPump is **mainnet only** — both the API host (`https://api-v2.sunpump.meme/pump-api`) +and the on-chain bonding-curve contract. Passing `--network nile` (or any non-mainnet +value) to a `sunpump` subcommand will fail fast. ```bash sun sunpump token king-of-hill # current king-of-the-hill token @@ -399,9 +400,8 @@ move fast); pass `--slippage 0.005` for 0.5% or `--min-out ` for an exact f base units. Endpoints requiring a signed message (`favors`) accept `--user-address`, -`--signature`, `--signed-message` flags. Switch to nile testnet with -`sun --network nile sunpump ...`, or override the base URL with `SUNPUMP_API_BASE_URL` -for a custom host. +`--signature`, `--signed-message` flags. Override the base URL with +`SUNPUMP_API_BASE_URL` only when you have a custom mainnet-compatible host. --- diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 0e22ece..6b42a62 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -13,7 +13,7 @@ import { formatAmount, formatPct, } from '../lib/output' -import { getSunPump, SunPump, SunPumpNetwork } from '../lib/sunpump' +import { getSunPump, SunPump } from '../lib/sunpump' // --------------------------------------------------------------------------- // pumpAction — local mirror of readApiAction but for the SunPump client. @@ -31,11 +31,19 @@ interface PumpActionOpts { detailFor?: (data: any) => Record | null } -let currentNetwork: SunPumpNetwork = 'mainnet' +function assertMainnet(): void { + const n = getNetwork() + if (n && n !== 'mainnet') { + throw new Error( + `SunPump is only available on mainnet (got "${n}"). Drop --network or pass --network mainnet.`, + ) + } +} async function pumpAction(opts: PumpActionOpts): Promise { try { - const client = getSunPump(currentNetwork) + assertMainnet() + const client = getSunPump() const raw = await withSpinner(opts.spinnerLabel, () => opts.execute(client)) let data: unknown = raw @@ -270,12 +278,9 @@ const portfolioTable = { export function registerSunpumpCommands(program: Command) { const sp = program .command('sunpump') - .description( - 'SunPump read-only endpoints (mainnet: api-v2.sunpump.meme, nile: tn-api.sunpump.meme). Use global --network nile for testnet.', - ) + .description('SunPump endpoints (mainnet only: api-v2.sunpump.meme).') .hook('preAction', () => { - const n = program.opts().network ?? process.env.TRON_NETWORK - currentNetwork = n === 'nile' ? 'nile' : 'mainnet' + assertMainnet() }) // -------------------------- token group ---------------------------------- diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts index 11c0c46..a063c5c 100644 --- a/src/lib/sunpump.ts +++ b/src/lib/sunpump.ts @@ -1,8 +1,7 @@ /** * SunPump API client — read-only GET endpoints. * - * - Mainnet: https://api-v2.sunpump.meme/pump-api - * - Nile testnet: https://tn-api.sunpump.meme/pump-api + * Mainnet only: https://api-v2.sunpump.meme/pump-api * * Standalone from sun-kit's SunAPI: SunPump is a separate service with its own * base URL and schema. Uses Node's global fetch (>=18). @@ -10,19 +9,10 @@ * Methods mirror the OpenAPI operationIds documented in docs/sunpump-api.md. */ -export const SUNPUMP_MAINNET_BASE_URL = 'https://api-v2.sunpump.meme/pump-api' -export const SUNPUMP_NILE_BASE_URL = 'https://tn-api.sunpump.meme/pump-api' -export const SUNPUMP_DEFAULT_BASE_URL = SUNPUMP_MAINNET_BASE_URL - -export type SunPumpNetwork = 'mainnet' | 'nile' - -export function sunPumpBaseUrlFor(network: SunPumpNetwork): string { - return network === 'nile' ? SUNPUMP_NILE_BASE_URL : SUNPUMP_MAINNET_BASE_URL -} +export const SUNPUMP_DEFAULT_BASE_URL = 'https://api-v2.sunpump.meme/pump-api' export interface SunPumpClientOptions { baseUrl?: string - network?: SunPumpNetwork fetchImpl?: typeof fetch } @@ -57,11 +47,7 @@ export class SunPump { private readonly fetchImpl: typeof fetch constructor(opts: SunPumpClientOptions = {}) { - const base = - opts.baseUrl ?? - (opts.network ? sunPumpBaseUrlFor(opts.network) : undefined) ?? - process.env.SUNPUMP_API_BASE_URL ?? - SUNPUMP_DEFAULT_BASE_URL + const base = opts.baseUrl ?? process.env.SUNPUMP_API_BASE_URL ?? SUNPUMP_DEFAULT_BASE_URL this.baseUrl = base.replace(/\/+$/, '') this.fetchImpl = opts.fetchImpl ?? fetch } @@ -249,13 +235,9 @@ export class SunPump { } } -const _clients = new Map() +let _client: SunPump | null = null -export function getSunPump(network: SunPumpNetwork = 'mainnet'): SunPump { - let c = _clients.get(network) - if (!c) { - c = new SunPump({ network }) - _clients.set(network, c) - } - return c +export function getSunPump(): SunPump { + if (!_client) _client = new SunPump() + return _client }