diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a04936f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,102 @@ +# Changelog + +All notable changes to `@bankofai/sun-cli` are documented in this file. Format +loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this +project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `sun sunpump launch` — create a token through the SunPump agent endpoint + (`POST /ai/agentTokenLaunch`). Server-side creation: the platform signs and + broadcasts the creation transaction, so no local wallet is needed. Required + `--name`/`--symbol`/`--description`; optional `--image ` (read and + sent as base64) or `--image-base64`, social URLs, `--tweet-username`. + Prints a summary and asks for confirmation (`--yes` skips); honours + `--dry-run`. On success prints the new token's contract address, creation + tx hash and logo URL. Mainnet only. + +## [1.2.0] — 2026-05-22 + +End-to-end SunPump support: read-only discovery for the SunPump meme-token +launchpad, plus on-chain trading against the SunPump bonding-curve contract +via `sun-kit`. Mainnet only. + +### Added + +#### `sun sunpump` — read-only data (no wallet required) + +- `token list` — paginated token list with filters and sort +- `token get ` — token detail (price, market cap, holders, social links, listed CEXes); human mode prints a labelled key/value view, `--json` returns the raw object +- `token search ` / `token search-v2 ` — fuzzy search +- `token by-owner ` — tokens created by a wallet +- `token holders ` / `token holders-v2 ` — top holders with a `Type` column distinguishing pools from users +- `token favors` — signed-message favourites lookup +- `token ranking --type MARKET_CAP|VOLUME_24H|PRICE_CHANGE_24H` +- `token king-of-hill` +- `token pump-list` — raw SunSwap-compatible token list +- `tx token ` / `tx user ` — swap history with filters +- `portfolio ` — wallet's SunPump positions with TRX value + +#### `sun sunpump` — on-chain trading (wallet required) + +- `state ` — on-chain state with named label: `0 NOT_EXIST` / `1 TRADING` / `2 READY_TO_LAUNCH` / `3 LAUNCHED` +- `quote-buy --trx ` — read-only buy preview +- `quote-sell --amount [--decimals 18]` — read-only sell preview +- `buy --trx [--slippage 0.05] [--min-out ]` — spend TRX, receive tokens +- `sell --amount [--decimals 18] [--slippage 0.05] [--min-out ]` — sell tokens for TRX (auto-handles first-time TRC20 approval) + +All write commands go through `writeAction`: wallet check → signed summary → confirmation prompt → broadcast → Tronscan link. `--dry-run` and `--yes` work as expected. Decimal inputs (`--trx 10`, `--amount 1000`) are scaled internally by `1e6` (TRX → Sun) and `10^decimals` (tokens → raw uint256). Buy/sell summaries pre-fetch a quote so the user sees expected output and fee before confirming. + +#### Output & formatting improvements + +- New table configs: `tokenTable` with a `tokenPriceUsd` fallback (no more `$0` rows when the API omits the TRX/USD rate — falls back to `marketCap / totalSupply`); `holderTable` reading the correct `percentage` field with auto-detected fraction/percent units; `portfolioTable`; key/value detail view for `token get`. +- `extractList` recognises `tokens` (alongside the existing `swaps`/`holders`); `readPagination` descends into `pageData` / `metadata` and treats `size` as a `pageSize` alias. +- HTTP errors from SunPump now surface the API's `msg` field, e.g. `SunPump request failed: 400 Bad Request (/token/getRanking) — Validation error: No enum constant ...`. + +### Breaking + +- **Nile testnet removed** for SunPump. The host (`tn-api.sunpump.meme`) is internal-only and the test deployment is being retired. Every `sunpump` subcommand throws on non-mainnet: + + ``` + SunPump is only available on mainnet (got "nile"). + Drop --network or pass --network mainnet. + ``` + + `sun swap`, `sun price`, `sun pool …` and other non-SunPump commands continue to support nile / shasta. + +- **Trimmed API surface.** The following were intentionally removed (not core to trading/discovery): + + | Removed | Reason | + |---|---| + | `sunpump home` (`stats` / `data` / `banners`) | Site-chrome data | + | `sunpump tx ticker` | Server hard-capped at ~15 rows | + | `sunpump kline` (`v1` / `v2` / `v3`) | Three near-identical OHLCV variants | + | `sunpump red-packet` (`get` / `remain` / `by-user` / `summary`) | Sun Agent campaign feature | + | `sunpump campaign` (`list` / `banners`) | Marketing banners | + | `sunpump referral` (`rewards` / `invites`) | Back-office reporting | + | `sunpump admin-summary` | Requires an admin password | + | `sunpump quota` | Third-platform integration, internal | + +### Notes & gotchas + +- **State enum off-by-one.** `sun-kit`'s exported `SunPumpTokenState` lists `LAUNCHED = 2`, but the on-chain contract returns `3` for tokens that have migrated to SunSwap. The CLI re-labels: state `3` prints as `LAUNCHED (3)`. Trust the printed label, not the raw int. +- **Quotes ignore on-chain state.** `quote-buy` returns a price even for `LAUNCHED` tokens (and `quote-sell` may revert with `REVERT opcode executed`). The actual `buy` / `sell` pre-checks state and throws `SUNPUMP_LAUNCHED` cleanly — call `sunpump state` first if you're routing logic. +- **First sell ≠ one transaction.** When the wallet has zero allowance, the SDK auto-sends `approve(SunPump, 2^256-1)` before the sell tx. Only the final sell tx hash is returned in `tronscanUrl`. +- **Default slippage** for bonding-curve trading is `0.05` (5%) — meme tokens are volatile. Tighten with `--slippage 0.005` or pass `--min-out ` for an exact floor. + +### Companion release + +[`sunpump-agent-skill`](https://github.com/BofAI/skills/tree/main/sunpump-agent-skill) +**v1.2.0** ships in parallel — pins this CLI version, documents the new +`buy/sell/quote-*/state` commands as the pre-launch trade path with `sun swap` +as the post-launch path, and updates pre-validation checklists to enforce +`--network mainnet`. + +Install: + +```bash +npm install -g @bankofai/sun-cli@^1.2.0 +npx skills add BofAI/skills +``` diff --git a/README.md b/README.md index 6c58c91..0bafd3d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ - **Read anything** — token prices, pools, farms, positions, transaction history, and protocol metrics - **Quote and route** — best-route quotes across SUNSwap V1/V2/V3/V4 - **Execute on-chain** — swaps, liquidity management (V2/V3/V4), and arbitrary contract writes +- **Meme tokens** — SunPump discovery, one-command token launching, and bonding-curve trading - **Automate** — JSON output, field filters, `--dry-run`, and `--yes` for non-interactive use - **Read-only out of the box** — no wallet required for queries and quotes @@ -357,9 +358,10 @@ sun contract send transfer --args '["TRecipient","1000000"]' ### SunPump Access to SunPump — read-only API for discovery (token launches, trending lists, -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. +holder portfolios), token creation via the agent endpoint (`launch`), and +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 and `launch` +need no wallet; trade commands do. 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 @@ -379,6 +381,19 @@ sun sunpump tx user --size 20 # swap history for a wallet sun sunpump portfolio --include-zero ``` +Launch a new token through the SunPump agent endpoint (server-side creation — +no wallet needed; asks for confirmation, `--yes` to skip, `--dry-run` to preview): + +```bash +sun sunpump launch --name MyToken --symbol MTK \ + --description "my meme token" --image ./logo.png \ + --twitter-url https://x.com/mytoken --website-url https://mytoken.xyz +``` + +`--image ` reads a local file and sends it as base64; pass `--image-base64` +to supply the encoded string directly. On success the CLI prints the new token's +contract address and creation tx hash. + Trade on the bonding curve (requires a wallet; pre-launch tokens only — once a token migrates to SunSwap, use `sun swap` instead): diff --git a/package.json b/package.json index ba25f32..ae1d325 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bankofai/sun-cli", - "version": "1.2.0", + "version": "1.2.1", "description": "CLI tool for SUN.IO / SUNSWAP on TRON — for humans and AI agents", "main": "dist/bin.js", "type": "commonjs", diff --git a/src/commands/sunpump.ts b/src/commands/sunpump.ts index 6b42a62..ec97261 100644 --- a/src/commands/sunpump.ts +++ b/src/commands/sunpump.ts @@ -1,5 +1,6 @@ import { Command } from 'commander' -import { parseApiResponse, writeAction } from '../lib/command' +import { isDryRun, parseApiResponse, writeAction } from '../lib/command' +import { confirm, printSummary } from '../lib/confirm' import { getNetwork, getKit } from '../lib/context' import { output, @@ -8,6 +9,7 @@ import { printPaginationFooter, printKeyValue, isJsonMode, + info, formatUsd, formatTime, formatAmount, @@ -600,6 +602,104 @@ export function registerSunpumpCommands(program: Command) { }) + // -------------------------- launch (agent token launch) ------------------ + sp.command('launch') + .description( + 'Launch a new token via the SunPump agent endpoint (server-side creation, no wallet needed)', + ) + .requiredOption('--name ', 'Token name') + .requiredOption('--symbol ', 'Token symbol') + .requiredOption('--description ', 'Token description') + .option('--image ', 'Logo image file (read and sent as base64)') + .option('--image-base64 ', 'Logo image as a raw base64 string (overrides --image)') + .option('--twitter-url ', 'Twitter URL') + .option('--telegram-url ', 'Telegram URL') + .option('--website-url ', 'Website URL') + .option('--tweet-username ', 'Tweet username to associate with the launch') + .action(async (opts) => { + let imageBase64: string | undefined = opts.imageBase64 + let imageLabel = imageBase64 ? `base64 (${imageBase64.length} chars)` : undefined + if (!imageBase64 && opts.image) { + try { + const { readFile } = await import('fs/promises') + const buf = await readFile(opts.image) + imageBase64 = buf.toString('base64') + imageLabel = `${opts.image} (${buf.length} bytes)` + } catch (err: any) { + outputError('Failed to read --image file', err) + return + } + } + + const params = { + name: opts.name, + symbol: opts.symbol, + description: opts.description, + imageBase64, + twitterUrl: opts.twitterUrl ?? '', + telegramUrl: opts.telegramUrl ?? '', + websiteUrl: opts.websiteUrl ?? '', + tweetUsername: opts.tweetUsername ?? '', + } + + if (isDryRun()) { + output({ + dryRun: true, + action: 'SunPump Agent Token Launch', + params: { ...params, imageBase64: imageLabel }, + }) + return + } + + printSummary('SunPump Agent Token Launch', { + Name: params.name, + Symbol: params.symbol, + Description: params.description, + Image: imageLabel, + Twitter: params.twitterUrl, + Telegram: params.telegramUrl, + Website: params.websiteUrl, + 'Tweet User': params.tweetUsername, + }) + const confirmed = await confirm('Launch this token?') + if (!confirmed) { + if (!isJsonMode()) console.log('Cancelled.') + return + } + + try { + assertMainnet() + const client = getSunPump() + const raw = await withSpinner('Launching token...', () => client.agentTokenLaunch(params)) + const { data } = parseApiResponse(raw) + if (isJsonMode()) { + output(data) + return + } + // Unlike the GET endpoints (plain epoch seconds), the launch endpoint + // serializes *Instant fields as epoch-millis / 1e6 (e.g. 1780476.327). + // Normalize to epoch seconds so tokenDetail/formatTime render correctly. + for (const k of ['tokenCreatedInstant', 'tokenLaunchedInstant', 'firstReachHillInstant']) { + const v = Number(data?.[k]) + if (Number.isFinite(v) && v > 0 && v < 1e8) data[k] = v * 1000 + } + const pairs = tokenDetail(data) ?? {} + if (data?.createTxHash) pairs['Create Tx'] = data.createTxHash + if (data?.logoUrl) pairs['Logo'] = data.logoUrl + const chalk = (await import('chalk')).default + console.log() + console.log(chalk.green('Token launched')) + printKeyValue(pairs) + } catch (err: any) { + outputError('Launch failed', err) + // The server has been seen to return this opaque error when no logo + // image accompanies the launch — point at the likely fix. + if (!imageBase64 && String(err?.message ?? '').includes('Invoke third part error')) { + info('Hint: this error often means the launch lacked a logo — retry with --image .') + } + } + }) + // -------------------------- trade (buy/sell/quote/state) ----------------- sp.command('state ') .description( diff --git a/src/lib/sunpump.ts b/src/lib/sunpump.ts index a063c5c..7044c58 100644 --- a/src/lib/sunpump.ts +++ b/src/lib/sunpump.ts @@ -19,6 +19,18 @@ export interface SunPumpClientOptions { type QueryValue = string | number | boolean | null | undefined export type Query = Record +export interface AgentTokenLaunchParams { + name: string + symbol: string + description: string + /** Logo image content as a base64 string (no data-URI prefix). */ + imageBase64?: string + twitterUrl?: string + telegramUrl?: string + websiteUrl?: string + tweetUsername?: string +} + export class SunPumpHttpError extends Error { readonly code = 'SUNPUMP_HTTP_ERROR' constructor( @@ -53,11 +65,23 @@ export class SunPump { } async request(path: string, query?: Query): Promise { - const url = `${this.baseUrl}${path}${buildQueryString(query)}` - const res = await this.fetchImpl(url, { + return this.send(path, buildQueryString(query), { method: 'GET', headers: { Accept: 'application/json' }, }) + } + + async post(path: string, body: unknown): Promise { + return this.send(path, '', { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + } + + private async send(path: string, queryString: string, init: RequestInit): Promise { + const url = `${this.baseUrl}${path}${queryString}` + const res = await this.fetchImpl(url, init) const text = await res.text() if (!res.ok) { const excerpt = text.length > 500 ? text.slice(0, 500) + '…' : text @@ -217,6 +241,19 @@ export class SunPump { return this.request(`/transactions/holder/${encodeURIComponent(ownerAddress)}`, query) } + // --------------------------------------------------------------------------- + // AI agent — token launch + // --------------------------------------------------------------------------- + + /** + * Launch a new token through the SunPump agent endpoint. The server creates + * the token on-chain itself — no local wallet or signing involved. Returns + * the full token object (contractAddress, createTxHash, …) in the envelope. + */ + agentTokenLaunch(params: AgentTokenLaunchParams) { + return this.post('/ai/agentTokenLaunch', params) + } + // --------------------------------------------------------------------------- // Holder portfolio // ---------------------------------------------------------------------------