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
12 changes: 12 additions & 0 deletions DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ Each command prints one JSON object on stdout. The exceptions: `gang watch` prin

New fields can appear at any time, so ignore what you don't know. Other fields can change with a minor version: read `gang capabilities` and test before you upgrade.

### Pad 3.0 token addresses

After the owner enables the `abcd` factory on Pad 3.0, the CLI automatically searches a CREATE2 salt for every curve,
fair-mint and fly launch, including vault and promoter variants. The commands and flags stay the same. The create preview
includes `addressSuffix: "abcd"` and `tokenIfNext`: this address is fixed for that transaction, including its final metadata,
and does not change when another creator launches first. Confirm the deployed token from the `Created` event.

Direct integrations read `vanityDeployer()` on the pad. A nonzero result requires wrapping the original create calldata in
`createWithSalt(bytes32 salt, bytes data)`. Obtain the exact `tokenInitCodeHash` or `mintInitCodeHash` from that factory and
search with the effective salt `keccak256(abi.encode(creator, salt))`. The address must end in `abcd` (hex, ignoring checksum
capitalization). Existing tokens keep their addresses. A suffix alone does not prove a token was launched on Gang.

### Failure codes

| `code` | Meaning |
Expand Down
10 changes: 10 additions & 0 deletions DEVELOPERS.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ gang execute --request-id <id> 发送

随时可能增加新字段,不认识的忽略即可。其他字段可能随小版本变化:升级前看 `gang capabilities` 并先测试。

### Pad 3.0 代币地址

Pad 3.0 的 owner 启用 `abcd` 部署器后,CLI 会为曲线、打新、铸币飞轮发币自动搜索 CREATE2 salt,金库和推广员版本也包含在内。
命令和参数不变。发币预览增加 `addressSuffix: "abcd"` 和 `tokenIfNext`:地址按这笔交易的最终元数据确定,其他人先发币也不会改变它。
实际部署结果仍以 `Created` 事件为准。

直接对接合约时,先读 pad 的 `vanityDeployer()`。返回非零地址就必须用 `createWithSalt(bytes32 salt, bytes data)` 包住原来的发币 calldata。
从该部署器读取准确的 `tokenInitCodeHash` 或 `mintInitCodeHash`,按有效 salt `keccak256(abi.encode(creator, salt))` 搜索地址。
十六进制地址后四位必须为 `abcd`,忽略校验和带来的大小写。旧币地址不变;仅凭后缀不能认定是 Gang 发行的币。

### 失败代码

| `code` | 含义 |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Operate [gang.fun](https://gang.fun) from a Binance **Agentic Wallet**. Curve bu

**One command per action (0.12.0+):** `gang buy` / `sell` / `info` / `status` / `claim` / `mint` / `withdraw` / `release` work on every token: the CLI reads the token's kind and runs the V3, 打新 or V2 / Soft code. `gang v3 …` / `gang fair …` stay as aliases.

**Pad 3.0 address suffix (0.38.0+):** after the owner activates the `abcd` deployer, all new curve, fair-mint and flywheel tokens end in `abcd` (ignoring checksum capitalization). The CLI automatically finds the salt after metadata is finalized, including `--grad`, `--vault` and `--promote` launches, and shows the predicted address in `tokenIfNext`. Update the CLI before activation: old direct create calls are then refused. Existing tokens keep their addresses and trade normally. Before activation the new CLI uses the existing create flow.

**铸币飞轮 / Mint Flywheel (0.18.0+):** `gang create --fly` starts a sale (kind 3): shares with a quota per wallet (invites add quota), a pool at the mint price once it fills, and minting that goes on after launch. The unified commands detect these tokens (`kind: "fly"`); `gang fly …` is the alias. Live on BSC mainnet since 2026-10-30 (V1 pad `0x631e66560F4FAE4e486B65349edE48189E48B029`); from 0.21.0 new tokens went to the V2 pad `0x281889ad915c1A95ffc4922235c6314b3Cbd8a5a`, and from 0.30.0 they go to GangLaunchpad 3.0 (below). Every other command follows the token's own pad, so V1 and V2 tokens keep working (`padVersion` in `gang info`: 1, 2 or 3). `--testnet` uses BSC testnet (V1 set 2, V2 `0x7069…E3d6`, 3.0). Rules: `skill/references/fly.md`.

**V3 tax tokens and 打新 (0.9.0+):** on GangLaunchpad, live on BSC mainnet since 2026-10-27 (1.0: pad `0x15CFD2dc8278Fb87a1783ef42111618E20874365`, GangRouter `0xE040445690Aa56C8b68B7f7038C7b2562B5079C4`; 2.0 since 2026-10-31: pad `0x770331a9accdb53ACb4a45B6c5B7D151fADCe557`, GangRouter `0xa83209d6bfC2B220cD86410cCDDC71FEb354844a`; new tokens go to 3.0 since CLI 0.30.0, see below; all built in); `--testnet` uses BSC testnet (3.0 for new tokens; 2.0 and set 10 = 1.0 for their tokens).
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Gang CLI(`gang`)

**Pad 3.0 地址后缀(0.38.0+):** owner 启用 `abcd` 部署器后,曲线、打新和铸币飞轮新币地址统一以 `abcd` 结尾(忽略校验和大小写)。CLI 会在最终元数据确定后自动计算 salt,`--grad`、`--vault`、`--promote` 发币也包含在内,`tokenIfNext` 显示确定的预测地址。请在启用前升级 CLI:启用后旧的直接发币调用会被拒绝。旧币地址和交易不变;启用前,新 CLI 仍走原有发币流程。

用 Binance **Agentic Wallet** 操作 [gang.fun](https://gang.fun)。内盘买入/发币需要 SPACE ID `.ai` Agent ID。本 CLI 只组 calldata,签名走 `baw`,不持有私钥。

**`gang create`:** 在 GangLaunchpad 上发 V3 币(`gang create --fair` 发打新);不带 `--buy-tax` / `--sell-tax` 就是无税币。LaunchpadV2 / Soft / LaunchpadU / LaunchpadS **不再发新币**,存量币照常交易(`gang buy` / `gang sell`)。V3 新币在 GangLaunchpad 1.0 上 **15 BNB** 毕业;2.0 和 3.0 上由 creator 用 `--grad` 选 **6 / 9 / 12 / 15 BNB**(默认 9)。Soft 存量链上仍为 20 BNB 毕业。
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gangfun/cli",
"version": "0.37.0",
"version": "0.38.0",
"mcpName": "io.github.gangfun/gang",
"description": "Gang launchpad CLI for Binance Agentic Wallet agents: create on GangLaunchpad (untaxed or tax tokens), 打新 fair mint, PancakeSwap pool trades, LaunchpadV2 and legacy Soft/U/S trade.",
"type": "module",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
"url": "https://github.com/gangfun/cli",
"source": "github"
},
"version": "0.37.0",
"version": "0.38.0",
"packages": [
{
"registryType": "npm",
"identifier": "@gangfun/cli",
"version": "0.37.0",
"version": "0.38.0",
"transport": {
"type": "stdio"
},
Expand Down
15 changes: 11 additions & 4 deletions src/fair.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createPublicClient, parseAbi, encodeFunctionData, decodeEventLog, formatUnits, parseEther, parseUnits, isAddress, zeroAddress } from 'viem'
import { VANITY_ABI, vanityCreate, vanityDeployer } from './vanity.js'
import { bawAddress } from './baw.js'
import { defaultRefPool, dividendOf, fundingStep as fund, parseAmount, parseIntIn, parseNum, poolSellQuote, POOL_SELL_HINT, revertOf as revertIn, ROUTER_ABI, ROUTER_HINTS, routerDeadline, step, swapBackState, units, viaRouter, GAS_RESERVE } from './steps.js'
import { API } from './discover.js'
Expand Down Expand Up @@ -125,7 +126,7 @@ export function walletFor(ctx, from) {
}

/** Revert name (+ BadConfig reason) of a failed eth_call, or null when it was the RPC. */
const revertOf = (e) => revertIn(e, [PAD_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })
const revertOf = (e) => revertIn(e, [PAD_ABI, VANITY_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })

/** Dry-run `data` from `from`; null when it would pass. */
async function dryRun(ctx, from, to, data, value = 0n) {
Expand Down Expand Up @@ -329,15 +330,21 @@ export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLog
const createData = (logoURI, metaURI) => encodeFunctionData({
abi: PAD_ABI, functionName: 'create', args: [name, symbol, logoURI, metaURI, quote, refPool, cfg, BigInt(devShares)],
})
const vanity = await vanityDeployer(ctx)
const handler = vanity ? await ctx.read(ctx.pad, PAD_ABI, 'taxHandler') : zeroAddress
const prepareCreate = (logoURI, metaURI) => vanityCreate(ctx, me, createData(logoURI, metaURI), {
args: [name, symbol, metaURI, handler, 100, 100, true],
})
// the logo / metadata strings never decide a revert: dry-run without them, so a create that would fail uploads nothing
const dry = await dryRun(ctx, me, ctx.pad, createData('', ''))
const dry = await dryRun(ctx, me, ctx.pad, (await prepareCreate('', '')).data)
if (dry) return out({ success: false, step: 'create', ...dry })
const logo = await resolveLogo()
const meta = await resolveMeta({ name, symbol, logoURI: logo.logoURI })
const data = createData(logo.logoURI, meta.metaURI)
return out(notes.step(ctx, { from: me, to: ctx.pad, data }, {
const launch = await prepareCreate(logo.logoURI, meta.metaURI)
return out(notes.step(ctx, { from: me, to: ctx.pad, data: launch.data }, {
step: 'create', chainId: Number(ctx.chainId), name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }),
metaURI: meta.metaURI, metadata: meta.metadata, quote: sym || quote,
...(launch.token && { tokenIfNext: launch.token, addressSuffix: 'abcd' }),
raise: await units(ctx, quote, raiseQuote), sharePrice: await units(ctx, quote, sharePrice),
capShares: cfg.capBps * 2, minWallets: Math.ceil(10_000 / (cfg.capBps * 2)), holdPct, mintFeePct, exitFeePct, devShares,
devCost: await units(ctx, quote, need),
Expand Down
19 changes: 14 additions & 5 deletions src/fly.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createPublicClient, parseAbi, encodeFunctionData, decodeEventLog, formatUnits, parseEther, parseUnits, isAddress, zeroAddress } from 'viem'
import { VANITY_ABI, vanityCreate, vanityDeployer } from './vanity.js'
import { defaultRefPool, dividendOf, VAULT_ABI, fundingStep as fund, parseAmount, parseIntIn, parseNum, poolSellQuote, POOL_SELL_HINT, revertOf as revertIn, roomyGas, ROUTER_ABI, ROUTER_HINTS, step, swapBackState, units, GAS_RESERVE } from './steps.js'
import { API, get as apiGet } from './discover.js'
import { walletFor } from './fair.js'
Expand Down Expand Up @@ -191,7 +192,7 @@ const HINTS = {
FeeOnTransferQuote: 'the quote token takes a fee on transfer; it cannot be used',
RouterNeedsGate: 'this sale has no Agent gate, so router entries are off',
}
const revertOf = (e) => revertIn(e, [FLY_PAD_ABI, FLY_HANDLER_ABI, TOKEN_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })
const revertOf = (e) => revertIn(e, [FLY_PAD_ABI, VANITY_ABI, FLY_HANDLER_ABI, TOKEN_ABI, ROUTER_ABI, MERGED_ERRORS_ABI], { ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })

/**
* Network settings: GangLaunchpad's chain, RPCs, WBNB and GangRouter (pool trades), with the fly pads. ctx.pad /
Expand Down Expand Up @@ -827,16 +828,24 @@ export async function runFly(sub, { opt, flag, out, fail, summarize, resolveLogo
const sym = await symbolOf(ctx, quote)
const pre = await fund(ctx, { me: who, token: quote, need, spender: ctx.pad, sym }, rawOnly, summarize)
if (pre) return out(pre)
const createData = (logoURI, metaURI) => tagged(encodeFunctionData({
const createData = (logoURI, metaURI) => encodeFunctionData({
abi: padAbi, functionName: 'create', args: [[name, symbol, logoURI, metaURI], quote, refPool, cfg, BigInt(devShares)],
}))
const dry = await dryRun(ctx, who, ctx.pad, createData('', ''))
})
const vanity = await vanityDeployer(ctx)
const handler = vanity ? await handlerOf() : zeroAddress
const maxSupply = parseEther('2000000000') * BigInt(cfg.postMint ? cfg.supplyCapMul : 1)
const prepareCreate = (logoURI, metaURI) => vanityCreate(ctx, who, createData(logoURI, metaURI), {
mint: true, args: [name, symbol, metaURI, handler, 100, cfg.sellTaxBps, 0n, maxSupply],
})
const dry = await dryRun(ctx, who, ctx.pad, tagged((await prepareCreate('', '')).data))
if (dry) return out({ success: false, step: 'create', ...dry })
const logo = await resolveLogo()
const meta = await resolveMeta({ name, symbol, logoURI: logo.logoURI })
return out(write({ from: who, to: ctx.pad, data: createData(logo.logoURI, meta.metaURI) }, {
const launch = await prepareCreate(logo.logoURI, meta.metaURI)
return out(write({ from: who, to: ctx.pad, data: tagged(launch.data) }, {
step: 'create', kind: 'fly', padVersion: ctx.version, ...(ctx.version === 3 && { padGen: PAD3_GEN }), pad: ctx.pad, name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }),
metaURI: meta.metaURI, metadata: meta.metadata, quote: sym || quote, preset: presetName, config: cfgView(cfg),
...(launch.token && { tokenIfNext: launch.token, addressSuffix: 'abcd' }),
sharePrice: await units(ctx, quote, quoteWbnb ? price : (price * 100n) / 102n), devShares, devCost: await units(ctx, quote, need),
...(v2 && v.postMint && {
guardNote: cfg.guardBps
Expand Down
20 changes: 15 additions & 5 deletions src/v3.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { splitOf } from './split.js'
import { lpOf } from './lp.js'
import { comboOf, decoyWarning } from './combo.js'
import { cappedOf, decoyWarning as cappedDecoyWarning } from './capped.js'
import { VANITY_ABI, vanityCreate, vanityDeployer } from './vanity.js'

/**
* `gang v3 …` — LaunchpadV3 (tax recipient): a token with its own buy / sell tax (1–5% each), a dividend share and a
Expand Down Expand Up @@ -160,7 +161,7 @@ const VIRTUAL_TOKEN = 1_066_666_666_666_666_666_666_666_667n
const ceilDiv = (a, b) => (a + b - 1n) / b

/** Revert name (+ BadTaxConfig reason) of a failed call, or null when it was the RPC. */
const revertOf = (e) => revertIn(e, [PAD_ABI, PROMOTE_PAD_ABI, CREATOR_FEE_ABI, GRAD_TARGET_ABI, TAX_ROUTER_ABI, GANG_ROUTER_ABI, MERGED_ERRORS_ABI], { BadPromoteTerms: PROMOTE_HINTS.BadPromoteTerms, TraderNotOrigin: PROMOTE_HINTS.TraderNotOrigin, ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })
const revertOf = (e) => revertIn(e, [PAD_ABI, VANITY_ABI, PROMOTE_PAD_ABI, CREATOR_FEE_ABI, GRAD_TARGET_ABI, TAX_ROUTER_ABI, GANG_ROUTER_ABI, MERGED_ERRORS_ABI], { BadPromoteTerms: PROMOTE_HINTS.BadPromoteTerms, TraderNotOrigin: PROMOTE_HINTS.TraderNotOrigin, ...MERGED_HINTS, ...ROUTER_HINTS, ...HINTS })

/** eth_call `data` from `from`: { result } when it passes, { failed } with the revert (or RPC error) otherwise. */
async function simulate(ctx, from, to, abi, functionName, args, value = 0n) {
Expand Down Expand Up @@ -557,11 +558,19 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
const argsOf = (logoURI, metaURI) => [name, symbol, logoURI, metaURI, quote, refPool, tax,
...(vault ? [parseUnits(String(targetBnb), 18), vault] : promote ? [parseUnits(String(targetBnb), 18), promote.shareBps, promote.window] : grad !== null ? [parseUnits(String(grad), 18)] : [])]
const createData = (logoURI, metaURI) => encodeFunctionData({ abi: PAD_ABI, functionName: fn, args: argsOf(logoURI, metaURI) })
const vanity = await vanityDeployer(ctx)
const handler = vanity && taxed ? (await taxContracts(ctx)).handler : zeroAddress
const prepareCreate = (logoURI, metaURI) => vanityCreate(ctx, me, createData(logoURI, metaURI), {
args: [name, symbol, metaURI, handler, tax.buyTaxBps, tax.sellTaxBps, false],
})
// the logo / metadata strings never decide a revert: dry-run without them, so a create that would fail uploads nothing
const dry = await simulate(ctx, me, ctx.pad, PAD_ABI, fn, argsOf('', ''))
const probe = await prepareCreate('', '')
const dry = await simulate(ctx, me, ctx.pad, probe.salt ? VANITY_ABI : PAD_ABI,
probe.salt ? 'createWithSalt' : fn, probe.salt ? [probe.salt, createData('', '')] : argsOf('', ''))
if (dry.failed) return out({ success: false, step: 'create', ...dry.failed })
const logo = await resolveLogo()
const meta = await resolveMeta({ name, symbol, logoURI: logo.logoURI })
const launch = await prepareCreate(logo.logoURI, meta.metaURI)
let label = null
const selfRecipient = recipient !== zeroAddress && recipient.toLowerCase() === String(me).toLowerCase()
if (recipient !== zeroAddress && !selfRecipient) {
Expand All @@ -572,14 +581,15 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
const baseCreatorFee = await createNote(ctx, net)
const creatorFee = baseCreatorFee && promote ? `${baseCreatorFee} Less the ${promote.shareBps / 100}% of it you give promoters on their bound wallets' trades.` : baseCreatorFee
const poolModule = promote && !!launchPoolModule
return out(notes.step(ctx, { from: me, to: ctx.pad, data: createData(logo.logoURI, meta.metaURI) }, {
return out(notes.step(ctx, { from: me, to: ctx.pad, data: launch.data }, {
step: 'create', chainId: Number(ctx.chainId), name, symbol, logoURI: logo.logoURI, ...(logo.uploaded && { uploaded: true }),
metaURI: meta.metaURI, metadata: meta.metadata,
quote: (await symbolOf(ctx, quote)) || quote, graduationTargetBnb: targetBnb, graduationAt: await units(ctx, quote, gradAmt),
graduation: `${gradLine(targetBnb, quote.toLowerCase() === String(ctx.wbnb).toLowerCase() ? null : `${await units(ctx, quote, gradAmt)} ${(await symbolOf(ctx, quote)) || 'of the quote'}`)}${tiers ? `; ${grad !== null ? 'picked with --grad' : 'the default'}, --grad picks ${tiers.join(' / ')}` : ''}`,
...(refPool !== zeroAddress && quote.toLowerCase() !== String(ctx.wbnb).toLowerCase() && { refPool }),
buyTaxPct: buyTax, sellTaxPct: sellTax, dividendPct: dividend,
recipient: recipient === zeroAddress ? null : recipient, recipientLabel: selfRecipient ? 'you (the creator)' : label, tokenIfNext: dry.result,
recipient: recipient === zeroAddress ? null : recipient, recipientLabel: selfRecipient ? 'you (the creator)' : label, tokenIfNext: launch.token ?? dry.result,
...(launch.token && { addressSuffix: 'abcd' }),
split: taxed
? `of each ${buyTax}% buy / ${sellTax}% sell tax in the PancakeSwap pool (after graduation; the curve charges no tax): ${fees.platformTaxPct}% of volume to the platform, then ${dividend}% of the rest to holders, ${rest}% to the recipient in the quote`
+ (dividend > 0 ? '; if no holder has 10,000+ tokens when a tax is paid, the holders\' part goes to the recipient too' : '')
Expand All @@ -597,7 +607,7 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
padGen: ctx.gen, pad: ctx.pad, vault,
vaultNote: 'the token is bound to this vault for good. Until graduation the token moves only through the pad: the vault locks a holder\'s tokens (counted in that holder\'s 1% cap) and releases them through the pad, for the holder that sends the tx and holds an Agent ID; after graduation it is a plain token. This create uses up the wallet\'s naming for the vault: one token per naming. A vault-bound token takes no GangBond (gang bond post refuses it): a lock moves tokens out of the creator\'s wallet, which would forfeit a bond',
}),
note: `${taxed ? 'tax rates and recipient are' : 'untaxed is'} fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax${taxed ? ': the tax starts in the pool after graduation' : ''}. tokenIfNext changes if another create lands first: the real address is the Created event from \`gang tx --hash …\`.`,
note: `${taxed ? 'tax rates and recipient are' : 'untaxed is'} fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax${taxed ? ': the tax starts in the pool after graduation' : ''}. ${launch.token ? 'The CREATE2 address ends in abcd and is fixed for this transaction.' : 'tokenIfNext changes if another create lands first: the real address is the Created event from `gang tx --hash …`.'}`,
}, 'create'))
}

Expand Down
Loading
Loading