Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <path>` (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 <addr>` — 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 <q>` / `token search-v2 <q>` — fuzzy search
- `token by-owner <addr>` — tokens created by a wallet
- `token holders <addr>` / `token holders-v2 <addr>` — 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 <addr>` / `tx user <addr>` — swap history with filters
- `portfolio <wallet>` — wallet's SunPump positions with TRX value

#### `sun sunpump` — on-chain trading (wallet required)

- `state <addr>` — on-chain state with named label: `0 NOT_EXIST` / `1 TRADING` / `2 READY_TO_LAUNCH` / `3 LAUNCHED`
- `quote-buy <addr> --trx <decimal>` — read-only buy preview
- `quote-sell <addr> --amount <decimal> [--decimals 18]` — read-only sell preview
- `buy <addr> --trx <decimal> [--slippage 0.05] [--min-out <raw>]` — spend TRX, receive tokens
- `sell <addr> --amount <decimal> [--decimals 18] [--slippage 0.05] [--min-out <raw>]` — 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 <raw>` 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
```
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -357,9 +358,10 @@ sun contract send <contractAddress> 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
Expand All @@ -379,6 +381,19 @@ sun sunpump tx user <walletAddress> --size 20 # swap history for a wallet
sun sunpump portfolio <walletAddress> --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 <path>` 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):

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
102 changes: 101 additions & 1 deletion src/commands/sunpump.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,6 +9,7 @@ import {
printPaginationFooter,
printKeyValue,
isJsonMode,
info,
formatUsd,
formatTime,
formatAmount,
Expand Down Expand Up @@ -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 <name>', 'Token name')
.requiredOption('--symbol <symbol>', 'Token symbol')
.requiredOption('--description <text>', 'Token description')
.option('--image <path>', 'Logo image file (read and sent as base64)')
.option('--image-base64 <data>', 'Logo image as a raw base64 string (overrides --image)')
.option('--twitter-url <url>', 'Twitter URL')
.option('--telegram-url <url>', 'Telegram URL')
.option('--website-url <url>', 'Website URL')
.option('--tweet-username <name>', '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<any>(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 <path>.')
}
}
})

// -------------------------- trade (buy/sell/quote/state) -----------------
sp.command('state <tokenAddress>')
.description(
Expand Down
41 changes: 39 additions & 2 deletions src/lib/sunpump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ export interface SunPumpClientOptions {
type QueryValue = string | number | boolean | null | undefined
export type Query = Record<string, QueryValue>

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(
Expand Down Expand Up @@ -53,11 +65,23 @@ export class SunPump {
}

async request<T = unknown>(path: string, query?: Query): Promise<T> {
const url = `${this.baseUrl}${path}${buildQueryString(query)}`
const res = await this.fetchImpl(url, {
return this.send<T>(path, buildQueryString(query), {
method: 'GET',
headers: { Accept: 'application/json' },
})
}

async post<T = unknown>(path: string, body: unknown): Promise<T> {
return this.send<T>(path, '', {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}

private async send<T>(path: string, queryString: string, init: RequestInit): Promise<T> {
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
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
Loading