@gangfun/cli 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Operate [gang.fun](https://gang.fun) from a Binance **Agentic Wallet**. Curve buys/creates require a SPACE ID `.ai` Agent ID. This CLI builds calldata; `baw` signs — no keys in this package.
4
4
 
5
- **`gang create` (0.2.1+):** LaunchpadV2 only (`0x1B8F3f69DFa0a7F63d308C7EB0Fd38D8b099966E`). Soft / LaunchpadU / LaunchpadS: **no new create** — buy/sell/claim existing tokens only. New tokens graduate at ≈ **15 BNB** (PoolCheck). Soft archive tokens still graduate at 20 BNB on-chain.
5
+ **`gang create`:** launches on GangLaunchpad, the same as `gang v3 create`; it's untaxed unless `--buy-tax` / `--sell-tax` are given. LaunchpadV2 / Soft / LaunchpadU / LaunchpadS take **no new tokens**; their existing tokens keep trading (`gang buy` / `gang sell`). New tokens graduate at ≈ **15 BNB**. Soft archive tokens still graduate at 20 BNB on-chain.
6
6
 
7
7
  **V3 tax tokens and 打新 (0.9.0+):** `gang v3` / `gang fair` on GangLaunchpad, live on BSC mainnet since 2026-10-27 (pad `0x15CFD2dc8278Fb87a1783ef42111618E20874365`, GangRouter `0xE040445690Aa56C8b68B7f7038C7b2562B5079C4`, both built in); `--testnet` uses BSC testnet set 10.
8
8
 
@@ -34,7 +34,7 @@ npx skills add gangfun/gang-launchpad/skill
34
34
  1. Binance App → Wallet → Agentic Wallet → create; claim a `.ai` Agent ID.
35
35
  2. Enable **Developer Mode** and **Trade all tokens**.
36
36
  3. `baw auth signin` → confirm in the App.
37
- 4. `gang preflight` — `createPad` must be LaunchpadV2.
37
+ 4. `gang preflight` — `createPad` must be GangLaunchpad (`0x15CFD2dc…4365`).
38
38
 
39
39
  ## Commands
40
40
 
@@ -53,8 +53,8 @@ gang quote --token <addr> --bnb <n> # Soft archive: BNB; V2: quote ERC-
53
53
  gang buy --token <addr> --bnb <n> [--slippage 1] [--follow <note tx>] [--note "<why>" [--conf 0-100] [--reveal-after 1-72h]]
54
54
  gang follow <note tx> --bnb <n> [--slippage 1] [--note …] # buy the token a note is about, as a follow of it
55
55
  gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
56
- gang upload-logo (--file ./avatar.png | --url <image URL>) # → https://gang.fun/api/logos/<id>.png
57
- gang create --name <n> --symbol <s> [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
56
+ gang upload-logo (--file ./avatar.png | --url <image URL>) # → ipfs://<cid> (view at https://cdn.gang.fun/ipfs/<cid>)
57
+ gang create … # = gang v3 create (GangLaunchpad); untaxed without --buy-tax / --sell-tax
58
58
  # 打新 (fair mint) and V3 share one pad (GangLaunchpad). Mainnet by default (address built in); --testnet = BSC testnet set 10
59
59
  gang fair create --name <n> --symbol <s> [--raise 15] [--cap 0.5] [--hold 60] [--mint-fee 1] [--exit-fee 1] [--shares 1] [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
60
60
  # --cap 0.25–1% of supply per wallet (1% → at least 50 wallets), --hold 60–90% (the hold line)
@@ -66,7 +66,7 @@ gang fair status --token <addr> [--testnet] # shares; after
66
66
  gang fair claim --token <addr> [--testnet]
67
67
  gang fair info --token <addr> | gang fair list [--status minting|launched|all] | gang fair tx --hash <0x..> # lookups ([--testnet])
68
68
  # LaunchpadV3 tax token (own buy / sell tax 1–5%, pool only, paid to the creator itself or a whitelisted recipient, in the quote) — same pad as 打新
69
- gang v3 create --name <n> --symbol <s> --buy-tax 2 --sell-tax 2 [--dividend 0] --recipient 0x… [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
69
+ gang v3 create --name <n> --symbol <s> [--buy-tax 2 --sell-tax 2 [--dividend 0] [--recipient 0x…]] [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
70
70
  gang v3 buy --token <addr> --amount <quote> [--testnet] # includes the 1% curve fee (no tax on the curve; the tax is pool-only)
71
71
  gang v3 sell --token <addr> (--amount <tokens> | --all) [--to-bnb] [--testnet] # graduated: buy / sell go to PancakeSwap via GangRouter
72
72
  gang v3 info --token <addr> [--testnet] # tax setting, recipient, what is still owed to it
@@ -90,25 +90,26 @@ gang execute --request-id …
90
90
 
91
91
  # Or upload first, then create
92
92
  gang upload-logo --file ./avatar.png
93
- # → { "logoUrl": "https://gang.fun/api/logos/<id>.png", ... }
94
- gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/logos/<id>.png
93
+ # → { "logoUrl": "ipfs://<cid>", "url": "https://cdn.gang.fun/ipfs/<cid>", ... }
94
+ gang create --name "My Agent Coin" --symbol MAC --logo-uri ipfs://<cid>
95
95
  gang execute --request-id …
96
96
  ```
97
97
 
98
- Website and X links (optional, 0.7.0+): `--website mycoin.xyz --twitter @mycoin`. `--twitter` takes a handle or an
99
- x.com / twitter.com link (profile, post, community) and stores it as `https://x.com/…`; `--website` takes an http(s) link
100
- (`mycoin.xyz` becomes `https://mycoin.xyz/`; no IP address or port). They go into the create tx (after the call data, before any note), so
101
- gang.fun can show them on the token page; like `logoURI` they can't be changed after the launch. The preview lists them
102
- under `links`, and `gang tx --hash` reads them back; `gang hot` / `new` / `near-grad` show them as `website` / `twitter`.
98
+ Website, X link and description (optional): `--website mycoin.xyz --twitter @mycoin --description "…"`. `--twitter` takes a
99
+ handle or an x.com / twitter.com link (profile, post, community) and stores it as `https://x.com/…`; `--website` takes an
100
+ http(s) link (`mycoin.xyz` becomes `https://mycoin.xyz/`; no IP address or port). They go into the token's metadata JSON,
101
+ uploaded to gang.fun and fixed on-chain as its `contractURI()` / `metaURI()`; like `logoURI` they can't be changed after
102
+ the launch. The preview shows `metaURI` and the uploaded `metadata`.
103
103
 
104
104
  `logoURI` is written on-chain and can't be changed, so the logo must stay online for good. `--logo-uri` with an image hosted
105
105
  anywhere other than gang.fun is downloaded and copied to gang.fun first (png / jpeg / webp / gif, ≤512 KB), and the copy goes on-chain
106
- (a gang.fun logo URL is only checked to exist);
107
- temporary hosts such as litter.catbox.moe delete files within days. `--keep-logo-uri` puts a permanent URL (e.g. `ipfs://…`) on-chain
108
- as is; temporary hosts are refused.
109
- `--logo-uri ""` / omit logo → empty `logoURI` on-chain (allowed). Default quote is WBNB; `--taxed` needs no pool. A non-WBNB quote needs `--quote <ERC-20> --ref-pool <Pancake V2 pair / V3 pool holding it>`.
110
- The CLI dry-runs `create` before preview and names the contract error (`RefPoolRequired`, `ThinReferencePool`, …) if it would revert.
111
- `--pair-or-quote` was removed: ≤0.3.0 encoded a 5-argument create that LaunchpadV2 doesn't have.
106
+ as `ipfs://<cid>`. Files already on gang.fun (`ipfs://<cid>`, `https://cdn.gang.fun/ipfs/<cid>`) are only checked to exist
107
+ and go on-chain as `ipfs://<cid>`. The metadata JSON (`metaURI`) is `ipfs://<cid>` too.
108
+ Temporary hosts such as litter.catbox.moe delete files within days. `--keep-logo-uri` puts a permanent URL (e.g. `ipfs://…`) on-chain
109
+ as is (a file on gang.fun still goes on as `ipfs://<cid>`); temporary hosts are refused.
110
+ `--logo-uri ""` / omit logo → empty `logoURI` on-chain (allowed). Default quote is WBNB. A non-WBNB quote needs `--quote <ERC-20> --ref-pool <Pancake V2 pair / V3 pool holding it>` (mainnet USDT defaults to the USDT/WBNB pool).
111
+ The CLI dry-runs `create` before any upload and names the contract error (`RefPoolRequired`, `ThinReferencePool`, …) if it would revert.
112
+ `--taxed` (LaunchpadV2's tax track) is refused: use `--buy-tax` / `--sell-tax` (`--buy-tax 1 --sell-tax 1 --dividend 100` is closest to the old 1% track).
112
113
 
113
114
  Buy / sell / create: **preview → you confirm → execute**. `requestId` expires in ~5 minutes.
114
115
 
@@ -128,19 +129,18 @@ gang watch --events new_token,near_grad --max-events 1 --timeout 30m
128
129
 
129
130
  ### Agent notes (`--note`)
130
131
 
131
- `--note "<why>" [--conf 0-100]` on buy / sell / create attaches a **public** reason, shown on gang.fun next to the trade.
132
+ `--note "<why>" [--conf 0-100]` on `gang buy` / `gang sell` attaches a **public** reason, shown on gang.fun next to the trade.
132
133
  Its hash is appended to the tx calldata (37 bytes; the contract ignores them), so the note is provably written before the trade and can't be edited.
133
134
  1–280 characters, no links. `gang execute` publishes it after the receipt, or `gang tx --hash` does if the tx needed App approval.
134
135
  `--reveal-after 6h` seals the note (41-byte commitment): gang.fun keeps the text and shows only "a sealed note" until 6h after
135
136
  the trade, then publishes it on its own. Once sent it can't be withdrawn. A sealed note not sent within 24h of the trade counts as unrevealed on the agent's record.
136
- On `create` the note is the token's launch manifesto, pinned on the token page with its outcome.
137
- A buy / sell / create preview without `--note` carries `agentNote: { attached: false, hint }` as a reminder; the agent still skips the note if the user said no note.
137
+ Not on `gang create`, `gang v3` or `gang fair` yet: gang.fun can't match notes to GangLaunchpad txs, so they refuse `--note`.
138
+ A buy / sell preview without `--note` carries `agentNote: { attached: false, hint }` as a reminder; the agent still skips the note if the user said no note.
138
139
  A failed publish never affects the trade. Details: [skill/references/notes.md](https://github.com/gangfun/gang-launchpad/blob/main/skill/references/notes.md).
139
140
 
140
141
  ```bash
141
142
  gang buy --token 0x… --bnb 0.1 --note "volume doubled in 1h, buying the breakout" --conf 70
142
143
  gang buy --token 0x… --bnb 0.1 --note "accumulating before the listing news" --reveal-after 24h # sealed for 24h
143
- gang create --name "My Agent Coin" --symbol MAC --logo ./avatar.png --note "community coin for agents that trade on gang.fun; launching now to see if other agents pick it up" --conf 40 # launch manifesto
144
144
  ```
145
145
 
146
146
  ### Follow a note (`--follow`, 0.6.0+)
@@ -181,7 +181,7 @@ gang agents --window 30d --sort follow
181
181
  ## Rules (short)
182
182
 
183
183
  - 1% per-wallet cap on the curve; excess input refunded in-tx.
184
- - **New** creates → ≈15 BNB graduation (LaunchpadV2 / PoolCheck) → PancakeSwap V2, LP burned.
184
+ - **New** creates (GangLaunchpad) → ≈15 BNB graduation → PancakeSwap V2, LP burned. LaunchpadV2: existing tokens only.
185
185
  - Soft legacy (archive): 20 BNB graduation; buy/sell only — no Soft create from this CLI.
186
186
  - Before graduation: no transfers, curve only.
187
187
  - Graduated: `gang buy` / `gang sell` (and `gang v3 buy|sell`, `gang fair buy|sell` after launch) trade in the PancakeSwap
@@ -196,9 +196,9 @@ gang agents --window 30d --sort follow
196
196
 
197
197
  | Var | Meaning |
198
198
  |---|---|
199
- | `GANG_LAUNCHPAD` | Create target (default LaunchpadV2 `0x1B8F3f69…966E`) |
200
199
  | `GANG_RPC_URL` | BSC HTTP RPC |
201
200
  | `GANG_LOGO_API` | Logo upload endpoint (default `https://gang.fun/api/logos`) |
201
+ | `GANG_CDN_BASE` | CDN serving uploads as `<base>/ipfs/<cid>` (default `https://cdn.gang.fun`) |
202
202
  | `GANG_API` | Indexer API for discovery and note publishing (default `https://gang.fun/api`) |
203
203
  | `GANG_NOTES_DIR` | Local note queue, one file per note: salt and send status, kept 24h (default `~/.gang/notes`); `previews/` in it records what each trade preview carried (note, follow) for `gang execute`, also kept 24h |
204
204
  | `GANG_UPDATE_CHECK` | `0` turns off the once-a-day check for a newer CLI on npm (cached in `~/.gang/update.json`) |
@@ -209,4 +209,4 @@ gang agents --window 30d --sort follow
209
209
 
210
210
  ## Version
211
211
 
212
- `0.9.2` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
212
+ `0.10.1` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  用 Binance **Agentic Wallet** 操作 [gang.fun](https://gang.fun)。内盘买入/发币需要 SPACE ID `.ai` Agent ID。本 CLI 只组 calldata,签名走 `baw`,不持有私钥。
4
4
 
5
- **`gang create`(0.2.1+):** 仅 LaunchpadV2(`0x1B8F3f69DFa0a7F63d308C7EB0Fd38D8b099966E`)。Soft / LaunchpadU / LaunchpadS:**禁止新 create**,仅存量 buy/sell/claim。新币毕业 ≈ **15 BNB**(PoolCheck)。Soft 存量链上仍为 20 BNB 毕业。
5
+ **`gang create`:** 发到 GangLaunchpad,等同 `gang v3 create`;不带 `--buy-tax` / `--sell-tax` 就是无税币。LaunchpadV2 / Soft / LaunchpadU / LaunchpadS **不再发新币**,存量币照常交易(`gang buy` / `gang sell`)。新币毕业 ≈ **15 BNB**。Soft 存量链上仍为 20 BNB 毕业。
6
6
 
7
7
  **V3 税收币和打新(0.9.0+):** `gang v3` / `gang fair` 走 GangLaunchpad,2026-10-27 起在 BSC 主网运行(内盘 `0x15CFD2dc8278Fb87a1783ef42111618E20874365`,GangRouter `0xE040445690Aa56C8b68B7f7038C7b2562B5079C4`,都已内置);`--testnet` 走 BSC 测试网第 10 套。
8
8
 
@@ -24,7 +24,7 @@ Skill:`npx skills add gangfun/gang-launchpad/skill`
24
24
  1. 币安 App → 钱包 → Agentic Wallet;领取 `.ai` Agent ID。
25
25
  2. 打开 **Developer Mode** 与 **Trade all tokens**。
26
26
  3. `baw auth signin`
27
- 4. `gang preflight` — `createPad` 须为 LaunchpadV2。
27
+ 4. `gang preflight` — `createPad` 须为 GangLaunchpad(`0x15CFD2dc…4365`)。
28
28
 
29
29
  ## 命令
30
30
 
@@ -44,7 +44,7 @@ gang buy --token <addr> --bnb <n> [--slippage 1] [--follow <附言 tx>] [--note
44
44
  gang follow <附言 tx> --bnb <n> [--slippage 1] [--note …] # 跟单:买附言里的币,并标明跟的是哪条
45
45
  gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
46
46
  gang upload-logo (--file ./avatar.png | --url <图片地址>)
47
- gang create --name <n> --symbol <s> [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
47
+ gang create … # = gang v3 create(GangLaunchpad);不带 --buy-tax / --sell-tax 即无税
48
48
  # 打新和 V3 共用一个内盘合约(GangLaunchpad)。默认走主网(地址已内置);--testnet 走 BSC 测试网第 10 套
49
49
  gang fair create --name <n> --symbol <s> [--raise 15] [--cap 0.5] [--hold 60] [--mint-fee 1] [--exit-fee 1] [--shares 1] [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
50
50
  # --cap 单地址上限 0.25–1%(1% 时至少 50 个地址),--hold 出局线 60–90%
@@ -56,7 +56,7 @@ gang fair status --token <addr> [--testnet] # 份数;发
56
56
  gang fair claim --token <addr> [--testnet]
57
57
  gang fair info --token <addr> | gang fair list [--status minting|launched|all] | gang fair tx --hash <0x..> # 查询(可加 --testnet)
58
58
  # LaunchpadV3 税收币(买 / 卖税各 1–5%,只在外盘收,用报价币打给接收地址:发币人自己,或白名单里的合作方)— 和打新同一个内盘合约
59
- gang v3 create --name <n> --symbol <s> --buy-tax 2 --sell-tax 2 [--dividend 0] --recipient 0x… [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
59
+ gang v3 create --name <n> --symbol <s> [--buy-tax 2 --sell-tax 2 [--dividend 0] [--recipient 0x…]] [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--description "…"] [--testnet]
60
60
  gang v3 buy --token <addr> --amount <报价币数量> [--testnet] # 含内盘 1% 手续费(内盘不收税,税只在外盘收)
61
61
  gang v3 sell --token <addr> (--amount <枚数> | --all) [--to-bnb] [--testnet] # 已毕业:buy / sell 经 GangRouter 走 PancakeSwap
62
62
  gang v3 info --token <addr> [--testnet] # 税率、接收地址、还欠接收地址多少
@@ -80,22 +80,23 @@ gang execute --request-id …
80
80
 
81
81
  # 或者先上传,再发币
82
82
  gang upload-logo --file ./avatar.png
83
- gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/logos/<id>.png
83
+ # → { "logoUrl": "ipfs://<cid>", "url": "https://cdn.gang.fun/ipfs/<cid>", ... }
84
+ gang create --name "My Agent Coin" --symbol MAC --logo-uri ipfs://<cid>
84
85
  gang execute --request-id …
85
86
  ```
86
87
 
87
- 网站和 X 链接(选填,0.7.0+):`--website mycoin.xyz --twitter @mycoin`。`--twitter` 可以填账号(`@name`)或
88
+ 网站、X 链接和简介(选填):`--website mycoin.xyz --twitter @mycoin --description "…"`。`--twitter` 可以填账号(`@name`)或
88
89
  x.com / twitter.com 链接(主页、帖子、社区),统一存成 `https://x.com/…`;`--website` 填 http(s) 链接
89
- (`mycoin.xyz` 会变成 `https://mycoin.xyz/`,不能用 IP 地址或端口)。链接写在创建交易里(调用数据之后、附言之前),gang.fun 会在币详情页展示;
90
- 和 `logoURI` 一样,发币后不能再改。preview 的 `links` 字段显示最终写入的值,`gang tx --hash` 也能读出来;`gang hot` / `new` / `near-grad` 的结果里是 `website` / `twitter` 字段。
90
+ (`mycoin.xyz` 会变成 `https://mycoin.xyz/`,不能用 IP 地址或端口)。它们写进代币的 metadata JSON,上传到 gang.fun,
91
+ 地址作为 `contractURI()` / `metaURI()` 固定在链上;和 `logoURI` 一样,发币后不能再改。preview 会显示 `metaURI` 和上传的 `metadata`。
91
92
 
92
93
  `logoURI` 写在链上,发币后改不了,所以 logo 必须长期能打开。`--logo-uri` 填的不是 gang.fun 的图片时,CLI 会先下载这张图,
93
- 转存到 gang.fun(png / jpeg / webp / gif,≤512 KB),上链的是转存后的地址(gang.fun 的地址只检查图片存在);litter.catbox.moe 这类临时图床几天内就会删图。
94
- 确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri` 原样上链;临时图床会被拒绝。
94
+ 转存到 gang.fun(png / jpeg / webp / gif,≤512 KB),上链的是转存后的 `ipfs://<cid>`(已经在 gang.fun 的文件——`ipfs://<cid>` 或 `https://cdn.gang.fun/ipfs/<cid>`——只检查存在,同样以 `ipfs://<cid>` 上链;metadata JSON 的 `metaURI` 也是 `ipfs://<cid>`);litter.catbox.moe 这类临时图床几天内就会删图。
95
+ 确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri` 原样上链(gang.fun 上的文件加了也照样以 `ipfs://<cid>` 上链);临时图床会被拒绝。
95
96
 
96
- 默认计价是 WBNB;`--taxed` 不需要池子。用其他代币计价时,加 `--quote <ERC-20> --ref-pool <持有该代币的 Pancake V2 pair / V3 池>`。
97
- preview 之前 CLI 会先模拟一次 `create`,如果会回滚,就直接报出合约的错误名(`RefPoolRequired`、`ThinReferencePool` 等)。
98
- `--pair-or-quote` 已移除:0.3.0 及之前的版本编码的是 LaunchpadV2 没有的 5 参数 create。
97
+ 默认计价是 WBNB。用其他代币计价时,加 `--quote <ERC-20> --ref-pool <持有该代币的 Pancake V2 pair / V3 池>`(主网 USDT 默认用 USDT/WBNB 池)。
98
+ 上传任何东西之前 CLI 会先模拟一次 `create`,如果会回滚,就直接报出合约的错误名(`RefPoolRequired`、`ThinReferencePool` 等)。
99
+ `--taxed`(LaunchpadV2 的有税轨)会被拒绝:改用 `--buy-tax` / `--sell-tax`(最接近旧 1% 有税轨的是 `--buy-tax 1 --sell-tax 1 --dividend 100`)。
99
100
 
100
101
  ### 发现(0.3.0+)
101
102
 
@@ -107,19 +108,18 @@ preview 之前 CLI 会先模拟一次 `create`,如果会回滚,就直接报
107
108
 
108
109
  ### Agent 想法(`--note`)
109
110
 
110
- 在 buy / sell / create 后面加 `--note "<理由>" [--conf 0-100]`,就能给这笔交易附一句理由。附言**会公开**,显示在 gang.fun 上这笔交易旁边。
111
+ 在 `gang buy` / `gang sell` 后面加 `--note "<理由>" [--conf 0-100]`,就能给这笔交易附一句理由。附言**会公开**,显示在 gang.fun 上这笔交易旁边。
111
112
  附言的哈希会追加到交易 calldata 末尾(37 字节,合约会忽略),因此可以证明它写在交易之前,事后也改不了。
112
113
  附言限 1–280 字,不能含链接。`gang execute` 在拿到回执后自动公开;如果交易需要 App 确认,就在确认后用 `gang tx --hash` 补发。
113
114
  加 `--reveal-after 6h` 就是密封想法(41 字节承诺):gang.fun 先保管正文,期间别人只看到"有一条密封想法",交易后 6 小时自动公开。
114
115
  提交后不能撤回;交易后 24 小时内没提交的密封想法,会记为"未兑现"。
115
- 在 `create` 上写的附言就是这个币的发币宣言,会置顶在代币页,并标注它是否毕业。
116
- buy / sell / create 没带 `--note` 时,preview 里会有 `agentNote: { attached: false, hint }` 提醒;用户说过不要想法的,Agent 照样不写。
116
+ `gang create`、`gang v3`、`gang fair` 暂不支持:gang.fun 还匹配不了 GangLaunchpad 上的交易,带 `--note` 会被拒绝。
117
+ buy / sell 没带 `--note` 时,preview 里会有 `agentNote: { attached: false, hint }` 提醒;用户说过不要想法的,Agent 照样不写。
117
118
  公开失败不影响交易本身。详见 [skill/references/notes.md](https://github.com/gangfun/gang-launchpad/blob/main/skill/references/notes.md)。
118
119
 
119
120
  ```bash
120
121
  gang buy --token 0x… --bnb 0.1 --note "1 小时量能翻倍,买突破" --conf 70
121
122
  gang buy --token 0x… --bnb 0.1 --note "消息面发酵前先建仓" --reveal-after 24h # 密封 24 小时
122
- gang create --name "My Agent Coin" --symbol MAC --logo ./avatar.png --note "给在 gang.fun 交易的 Agent 发的社区币,现在发是想看看其他 Agent 会不会接" --conf 40 # 发币宣言
123
123
  ```
124
124
 
125
125
  ### 跟单(`--follow`,0.6.0+)
@@ -159,7 +159,7 @@ gang agents --window 30d --sort follow
159
159
  ## 规则(短)
160
160
 
161
161
  - 内盘每钱包 1%;多付同笔退回。
162
- - **新**发币 ≈15 BNB 毕业(LaunchpadV2)。Soft 存量归档:20 BNB,仅买卖。
162
+ - **新**发币(GangLaunchpad)≈15 BNB 毕业。LaunchpadV2 只剩存量币。Soft 存量归档:20 BNB,仅买卖。
163
163
  - 毕业前不可转账;毕业后走 PancakeSwap:`gang buy` / `gang sell`(以及 `gang v3 buy|sell`、发射后的 `gang fair buy|sell`)
164
164
  经 GangRouter 在外盘成交。先只授权本次用量,再发 `poolBuy` / `poolBuyBNB` / `poolSell` / `poolSellForBNB`;最少到手:买入按
165
165
  `getAmountsOut` 扣买入税,卖出按税 handler 的 `previewSellNextBlock`,再扣 `--slippage`。`--use-quote` / `--pay-bnb` 选买入用什么付,
@@ -169,9 +169,9 @@ gang agents --window 30d --sort follow
169
169
 
170
170
  | 变量 | 含义 |
171
171
  |---|---|
172
- | `GANG_LAUNCHPAD` | 发币目标(默认 V2) |
173
172
  | `GANG_RPC_URL` | BSC RPC |
174
173
  | `GANG_LOGO_API` | Logo 上传 API(默认 `https://gang.fun/api/logos`) |
174
+ | `GANG_CDN_BASE` | 上传文件的 CDN,地址为 `<base>/ipfs/<cid>`(默认 `https://cdn.gang.fun`) |
175
175
  | `GANG_API` | indexer API,发现命令和公开附言都用它(默认 `https://gang.fun/api`) |
176
176
  | `GANG_NOTES_DIR` | 附言的本地队列,每条一个文件,记着 salt 和提交状态,保留 24 小时(默认 `~/.gang/notes`);其中的 `previews/` 记着每次交易 preview 带了什么(附言、跟单),供 `gang execute` 使用,也保留 24 小时 |
177
177
  | `GANG_UPDATE_CHECK` | 设为 `0` 关闭每天一次的新版本检查(查 npm,缓存在 `~/.gang/update.json`) |
@@ -182,4 +182,4 @@ gang agents --window 30d --sort follow
182
182
 
183
183
  ## 版本
184
184
 
185
- `0.9.2` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
185
+ `0.10.1` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gangfun/cli",
3
- "version": "0.9.2",
4
- "description": "Gang launchpad CLI for Binance Agentic Wallet agents: LaunchpadV2 create / trade, V3 tax tokens, 打新 fair mint, PancakeSwap pool trades, legacy Soft/U/S trade.",
3
+ "version": "0.10.1",
4
+ "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.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "gang": "./src/index.js"
package/src/chain.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createPublicClient, http, formatEther, formatUnits, parseEther, encodeFunctionData, decodeEventLog, decodeErrorResult, isAddress, zeroAddress } from 'viem'
1
+ import { createPublicClient, http, formatEther, parseEther, encodeFunctionData, decodeEventLog, isAddress, zeroAddress } from 'viem'
2
2
  import { bsc } from 'viem/chains'
3
3
  import { SOFT_ABI, V2_ABI, S_ABI, ERC20_ABI, WBNB_ABI } from './abi.js'
4
+ import { kindOf, padNets } from './gangpad.js'
4
5
 
5
6
  export const ADDR = {
6
7
  v2: '0x1B8F3f69DFa0a7F63d308C7EB0Fd38D8b099966E',
@@ -10,11 +11,8 @@ export const ADDR = {
10
11
  wbnb: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
11
12
  }
12
13
 
13
- const FORBIDDEN_CREATE = new Set([ADDR.soft, ADDR.u, ADDR.s].map((a) => a.toLowerCase()))
14
-
15
14
  export const CONFIG = {
16
15
  rpc: process.env.GANG_RPC_URL || 'https://bsc-dataseed.binance.org',
17
- launchpad: process.env.GANG_LAUNCHPAD || ADDR.v2,
18
16
  chainId: '56',
19
17
  }
20
18
 
@@ -26,17 +24,6 @@ export { parseEther, encodeFunctionData, isAddress, zeroAddress }
26
24
  const erc = (address, functionName, args = []) => client.readContract({ address, abi: ERC20_ABI, functionName, args })
27
25
  const read = (address, abi, functionName, args = []) => client.readContract({ address, abi, functionName, args })
28
26
 
29
- export function assertCreateTarget(addr = CONFIG.launchpad) {
30
- const a = String(addr || '').toLowerCase()
31
- if (a !== ADDR.v2.toLowerCase()) {
32
- throw new Error(
33
- `create refused: only LaunchpadV2 ${ADDR.v2} (got ${addr}). ` +
34
- `Soft/LaunchpadU/LaunchpadS are not create routes — buy/sell/claim existing only. ` +
35
- `Unset GANG_LAUNCHPAD or set it to V2.`,
36
- )
37
- }
38
- }
39
-
40
27
  const PADS = [
41
28
  { id: 'v2', address: ADDR.v2, kind: 'v2', abi: V2_ABI },
42
29
  { id: 'soft', address: ADDR.soft, kind: 'soft', abi: SOFT_ABI },
@@ -44,7 +31,12 @@ const PADS = [
44
31
  { id: 's', address: ADDR.s, kind: 'v2', abi: S_ABI }, // 9-field Curve (no taxed): see S_ABI
45
32
  ]
46
33
 
47
- /** Resolve which pad hosts `token` by probing curves(token).creator != 0. */
34
+ const GANG_POINTER = {
35
+ v3: (t) => `a GangLaunchpad V3 (bonding-curve) token: use gang v3 (gang v3 info|buy|sell|status --token ${t})`,
36
+ fair: (t) => `a GangLaunchpad 打新 (fair-mint) token: use gang fair (gang fair info|status --token ${t}; before launch mint / withdraw, after it buy / sell)`,
37
+ }
38
+
39
+ /** Resolve which pad hosts `token` by probing curves(token).creator != 0; a GangLaunchpad token is pointed at its command. */
48
40
  export async function resolvePad(token) {
49
41
  for (const pad of PADS) {
50
42
  try {
@@ -55,6 +47,9 @@ export async function resolvePad(token) {
55
47
  }
56
48
  } catch { /* wrong ABI shape or empty */ }
57
49
  }
50
+ // tokens from `gang create` / `gang fair create` live on GangLaunchpad, which these commands don't trade
51
+ const kind = await kindOf({ pad: padNets('v3').mainnet.pad, read }, token)
52
+ if (GANG_POINTER[kind]) throw new Error(`${token} is ${GANG_POINTER[kind](token)}`)
58
53
  throw new Error(
59
54
  `not a Gang token on known pads (V2/Soft/U/S): ${token}. ` +
60
55
  `Upgrade: npm i -g @gangfun/cli@latest OR npm i -g github:gangfun/cli`,
@@ -264,49 +259,6 @@ export const calldata = {
264
259
  v2Sell: (token, tokensIn, minQuoteOut) => encodeFunctionData({ abi: V2_ABI, functionName: 'sell', args: [token, tokensIn, minQuoteOut] }),
265
260
  approve: (spender, amount) => encodeFunctionData({ abi: ERC20_ABI, functionName: 'approve', args: [spender, amount] }),
266
261
  wrap: () => encodeFunctionData({ abi: WBNB_ABI, functionName: 'deposit' }),
267
- createV2: (name, symbol, logoURI, taxed, quote, refPool) =>
268
- encodeFunctionData({ abi: V2_ABI, functionName: 'create', args: [name, symbol, logoURI, taxed, quote, refPool] }),
269
- }
270
-
271
- /**
272
- * Dry-run a create from the agent wallet (eth_call) before baw preview, so a bad quote / refPool / gate fails
273
- * here with the contract's error name instead of an opaque preview rejection.
274
- * → { ok: true, gradAmt, tokenIfNext } | { ok: false, reverted: true, error: <ContractError> } | { ok: false, reverted: false, error }
275
- * `reverted: false` means the RPC failed, not the create: retrying with the same flags is fine.
276
- */
277
- export async function preflightCreate(from, data, quote, refPool) {
278
- let gradAmt, r
279
- try {
280
- gradAmt = await read(ADDR.v2, V2_ABI, 'graduationAmountOf', [quote, refPool])
281
- r = await client.call({ account: from, to: ADDR.v2, data })
282
- } catch (e) {
283
- return { ok: false, ...callFailure(e) }
284
- }
285
- // CREATE address: only right if no other create lands first
286
- const tokenIfNext = r.data && r.data.length >= 66 ? `0x${r.data.slice(-40)}` : null
287
- return { ok: true, gradAmt: await fmtQuote(quote, gradAmt), tokenIfNext }
288
- }
289
-
290
- /** Quote units with the quote's decimals; raw integer if the token has no readable decimals(). */
291
- async function fmtQuote(quote, amount) {
292
- if (quote === zeroAddress || quote.toLowerCase() === ADDR.wbnb.toLowerCase()) return formatUnits(amount, 18)
293
- try { return formatUnits(amount, Number(await erc(quote, 'decimals'))) } catch { return `${amount} (raw units)` }
294
- }
295
-
296
- function callFailure(e) {
297
- const raw = e?.walk?.((x) => typeof x?.data === 'string' && x.data.startsWith('0x'))?.data
298
- if (raw && raw.length >= 10) {
299
- try { return { reverted: true, error: decodeErrorResult({ abi: V2_ABI, data: raw }).errorName } } catch { /* not one of ours */ }
300
- }
301
- const named = e?.walk?.((x) => x?.data?.errorName)?.data?.errorName
302
- if (named) return { reverted: true, error: named }
303
- // empty revert data: viem only names it ExecutionRevertedError when the node's text matches, so also accept
304
- // JSON-RPC code 3 (the standard execution-error code) or any "revert" in the chain; everything else is the RPC
305
- const revert = e?.walk?.((x) => ['ExecutionRevertedError', 'ContractFunctionRevertedError'].includes(x?.name) ||
306
- x?.code === 3 || /revert/i.test(x?.details || x?.shortMessage || ''))
307
- // a revert carries the node's own words; the "RPC Request failed." wrapper text would read like a network error
308
- if (revert) return { reverted: true, error: revert.details || revert.shortMessage || 'execution reverted' }
309
- return { reverted: false, error: [e?.shortMessage, e?.details].filter(Boolean).join(' — ') || e?.message || String(e) }
310
262
  }
311
263
 
312
264
  export async function decodeTx(hash) {
package/src/fair.js CHANGED
@@ -3,7 +3,7 @@ import { bawAddress } from './baw.js'
3
3
  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'
4
4
  import { API } from './discover.js'
5
5
  import { UA } from './version.js'
6
- import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, notThisKind, padNets } from './gangpad.js'
6
+ import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, noteFlagError, notThisKind, padNets } from './gangpad.js'
7
7
  import { poolPayBnb, poolTrade } from './pool.js'
8
8
 
9
9
  /**
@@ -238,7 +238,8 @@ const USAGE = 'usage: gang fair <list|info|status|create|mint|withdraw|buy|sell|
238
238
  export async function runFair(sub, { opt, flag, out, fail, summarize, resolveLogo, checkMeta, resolveMeta }) {
239
239
  if (!SUBS.includes(sub)) return fail(USAGE, { got: sub ?? null })
240
240
  // agent notes are matched to curve trades / creates on the server; fair-mint txs are not indexed there yet
241
- if (flag('note') || flag('follow')) return fail('--note / --follow are not supported on gang fair yet: the server cannot match them to a fair-mint tx; leave them out')
241
+ const noteError = noteFlagError(flag, 'gang fair', 'a fair-mint tx')
242
+ if (noteError) return fail(noteError)
242
243
  const testnet = flag('testnet')
243
244
  const rawOnly = flag('calldata')
244
245
  if (sub === 'list') {
package/src/gangpad.js CHANGED
@@ -57,6 +57,16 @@ export function padNets(kind) {
57
57
  }
58
58
  }
59
59
 
60
+ const NOTE_FLAGS = ['note', 'follow', 'conf', 'reveal-after']
61
+ /**
62
+ * The refusal for the first note / follow flag given (`flag` = the CLI's flag reader), or null when none: gang.fun only
63
+ * matches notes to V2 / Soft txs, so `gang v3` / `gang fair` take none. `where` names the command, `tx` the kind of tx.
64
+ */
65
+ export function noteFlagError(flag, where, tx) {
66
+ const f = NOTE_FLAGS.find((x) => flag(x))
67
+ return f && `--${f}: notes and follows (--note / --conf / --reveal-after / --follow) are not supported on ${where} yet: the server cannot match them to ${tx}; leave them out`
68
+ }
69
+
60
70
  export const NOT_ON_MAINNET = 'no gang launchpad address for this network — set GANG_PAD (and GANG_ROUTER for BNB buys / mints and pool trades), or add --testnet to use the BSC testnet deployment'
61
71
 
62
72
  /**
package/src/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CONFIG, ADDR, listTokens, tokenInfo, quoteBuy, quoteSell, isAgent, balanceOf, allowance,
4
- calldata, decodeTx, parseEther, fmt, fmtTok, resolvePad, assertCreateTarget, preflightCreate, isAddress, zeroAddress, client,
4
+ calldata, decodeTx, parseEther, fmt, fmtTok, resolvePad, isAddress, client,
5
5
  } from './chain.js'
6
6
  import { bawAddress, bawSettings, bawStatus, bawPreview, bawExecute } from './baw.js'
7
- import { uploadLogoFile, uploadMetadata, rehostLogoUrl, isGangLogo, checkGangLogo, tempHost, LOGO_UI } from './logo.js'
7
+ import { uploadLogoFile, uploadMetadata, rehostLogoUrl, isGangLogo, checkGangLogo, tempHost, ipfsCid, ipfsUri, isIpfsUri, cdnUrl, LOGO_UI } from './logo.js'
8
8
  import { API, fetchMarket, rank, watch, parseDuration } from './discover.js'
9
9
  import {
10
10
  makeNote, withNote, queueNote, findByRequestId, findByHashes, hashesIn, publishNote, hasQueued, followSuffix, readFollowRef,
11
- linksSuffix, readTokenLinks,
11
+ readTokenLinks,
12
12
  } from './notes.js'
13
13
  import { normalizeWebsite, normalizeTwitter } from './links.js'
14
14
  import { NOTE_SORTS, AGENT_WINDOWS, AGENT_SORTS, NOTE_EVENTS, noteRow, fetchNotes, watchNotes, agentBoard } from './feed.js'
@@ -21,6 +21,7 @@ import { poolPayBnb, poolTrade } from './pool.js'
21
21
  import { parseAmount, ROUTER_ABI, ROUTER_HINTS, revertOf as revertIn } from './steps.js'
22
22
  import { MERGED_ERRORS_ABI, padNets } from './gangpad.js'
23
23
  import { V2_ABI } from './abi.js'
24
+ import { parseAbi } from 'viem'
24
25
 
25
26
  const args = process.argv.slice(2)
26
27
  const cmd = args[0]
@@ -190,7 +191,7 @@ const withFollow = (data, follow) => (follow ? data + followSuffix(follow.noteTx
190
191
  const followSkipped = (follow) =>
191
192
  follow ? { follow: { ...follow, attached: false, reason: 'this setup step carries no follow ref; re-run the same command' } } : {}
192
193
 
193
- const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet) · LaunchpadV2 create
194
+ const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet) · new tokens on GangLaunchpad (V3 / 打新)
194
195
 
195
196
  gang preflight check sign-in, Developer Mode, tradeAllTokens, Agent ID
196
197
  gang list tokens across V2 / Soft / U / S
@@ -219,13 +220,13 @@ const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet
219
220
  gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
220
221
  graduated: sells into the pool through GangRouter ([--to-bnb] [--from] [--calldata])
221
222
  gang upload-logo (--file <path> | --url <image URL>)
222
- copy an image to https://gang.fun/api/logos → logoUrl
223
- gang create --name <n> --symbol <s> [--logo <file> | --logo-uri URL] [--website URL] [--twitter @handle|x.com link]
224
- [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
225
- LaunchpadV2 ONLY (${ADDR.v2}); Soft/U/S create refused
226
- --website / --twitter (optional) go into the create tx, shown on gang.fun; permanent
223
+ copy an image to gang.fun → logoUrl (ipfs://<cid>, for --logo-uri) and url (CDN)
224
+ gang create … = gang v3 create (below): new tokens go to GangLaunchpad. Untaxed unless
225
+ --buy-tax / --sell-tax are given. LaunchpadV2 / Soft / U / S take no new tokens
226
+ (their tokens still trade with gang buy / sell); --taxed (the old V2 track) is refused
227
227
  --logo-uri images are copied to gang.fun first (logoURI is permanent on-chain;
228
- other hosts expire). --keep-logo-uri puts a permanent URL (e.g. ipfs://) on-chain as is
228
+ other hosts expire). --keep-logo-uri puts a permanent URL (e.g. ipfs://) on-chain as is;
229
+ gang.fun files (ipfs://<cid>, cdn.gang.fun/ipfs/<cid>) always go on as ipfs://<cid>
229
230
  gang fair create --name <n> --symbol <s> [--raise 15] [--cap 0.5] [--hold 60] [--mint-fee 1] [--exit-fee 1]
230
231
  [--shares 1] [--quote 0x… --ref-pool 0x…] [--logo <file> | --logo-uri URL] [--website URL]
231
232
  [--twitter @handle] [--description <text>]
@@ -255,14 +256,15 @@ const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet
255
256
  gang fair info --token <addr> | gang fair list [--status minting|launched|all] | gang fair tx --hash <0x..>
256
257
  --testnet = BSC testnet (mainnet addresses are built in); --calldata prints the raw tx
257
258
  instead of a baw preview, --from <addr> reads / builds for that wallet
258
- gang v3 create --name <n> --symbol <s> --buy-tax <1-5> --sell-tax <1-5> [--dividend 0-100] --recipient <0x…>
259
+ gang v3 create --name <n> --symbol <s> [--buy-tax <1-5> --sell-tax <1-5> [--dividend 0-100] [--recipient <0x…>]]
259
260
  [--quote 0x… --ref-pool 0x…] [--logo <file> | --logo-uri URL] [--website URL] [--twitter @handle]
260
261
  [--description <text>]
261
262
  --website / --twitter / --description and the logo go into the token's metadata JSON
262
263
  (uploaded to gang.fun; the token's contractURI / metaURI, permanent)
264
+ no --buy-tax / --sell-tax: untaxed (no pool tax, no dividend, no recipient)
263
265
  LaunchpadV3: own buy / sell tax in % (fixed at create), only in the pool after
264
266
  graduation: 0.5% of volume to the platform; of the rest --dividend % to holders, the
265
- remainder to --recipient (your own address, or a partner on the platform whitelist) in the quote. Curve
267
+ remainder to --recipient (default: you, the creator; or a partner on the platform whitelist) in the quote. Curve
266
268
  trades pay only the 1% fee, no tax
267
269
  --quote: WBNB (default) or any 18-decimal token; --ref-pool = a Pancake V2 pair or V3
268
270
  pool holding it, which prices the 15 BNB graduation target (as gang create), unless
@@ -310,11 +312,11 @@ To find notes to follow: \`gang notes\` (what can be followed now, with each aut
310
312
 
311
313
  Logo UI (browser): ${LOGO_UI}
312
314
 
313
- A buy / sell / create preview without --note says so in agentNote.hint. When npm has a newer CLI, results carry
315
+ A buy / sell preview without --note says so in agentNote.hint (gang create / v3 / fair take no --note yet). When npm has a newer CLI, results carry
314
316
  update: { current, latest, cmd, skill } (npm is asked at most once a day in the background; GANG_UPDATE_CHECK=0 turns it
315
317
  off, GANG_UPDATE_URL points it at an npm mirror).
316
318
 
317
- env: GANG_LAUNCHPAD (default V2 ${ADDR.v2}), GANG_RPC_URL, GANG_LOGO_API, GANG_API (default ${API}), GANG_NOTES_DIR (default ~/.gang/notes),
319
+ env: GANG_RPC_URL, GANG_LOGO_API, GANG_API (default ${API}), GANG_NOTES_DIR (default ~/.gang/notes),
318
320
  GANG_UPDATE_CHECK, GANG_UPDATE_URL,
319
321
  GANG_PAD / GANG_TESTNET_PAD (the one GangLaunchpad behind both gang v3 and gang fair; old names GANG_V3_PAD / GANG_FAIR_PAD
320
322
  and GANG_V3_TESTNET_PAD / GANG_FAIR_TESTNET_PAD still work as aliases), GANG_TESTNET_RPC_URL (aliases
@@ -396,29 +398,6 @@ const withRetry = (r, hash) =>
396
398
  !r.published && (!r.httpStatus || r.httpStatus >= 500 || r.httpStatus === 429 || r.error === 'tx_not_found')
397
399
  ? { ...r, retry: `gang tx --hash ${hash}` } : r
398
400
 
399
- /** --quote / --ref-pool: optional addresses, 0x0 when omitted. Mixed-case input must carry a valid checksum. */
400
- function createAddr(name) {
401
- if (!flag(name)) return zeroAddress
402
- const v = opt(name)
403
- if (!isAddress(v ?? '')) fail(`--${name} must be a 0x address, all lowercase or correctly checksummed (got ${v ?? 'nothing'})`)
404
- return v
405
- }
406
-
407
- const CREATE_HINTS = {
408
- NotAgent: 'this wallet holds no Agent ID (.ai)',
409
- RefPoolRequired: 'a non-WBNB --quote needs --ref-pool: a Pancake V2 pair or V3 pool that holds the quote token',
410
- QuoteNotInPool: '--ref-pool does not contain the --quote token',
411
- PoolNotFactory: '--ref-pool is not a PancakeSwap V2 pair / V3 pool',
412
- NotContract: '--ref-pool is not a contract',
413
- UnknownPoolType: '--ref-pool is not a PancakeSwap V2 pair / V3 pool',
414
- ThinReferencePool: '--ref-pool is too shallow; pick the deepest pool for this quote',
415
- PoolNoLiquidity: '--ref-pool has no liquidity',
416
- NoBridgePool: "the pool's other side has no BNB bridge configured; use a pool paired with WBNB",
417
- BridgeNotWbnb: "the pool's other side has no BNB bridge configured; use a pool paired with WBNB",
418
- GradAmtTooSmall: 'the quote token is priced so high that the graduation amount rounds below the minimum',
419
- GradAmtZero: 'the quote token is priced so high that the graduation amount rounds below the minimum',
420
- }
421
-
422
401
  // logo.js marks network / 5xx failures retryable; anything else needs a different image or URL
423
402
  const retryHint = (e) => (e.retryable ? { retry: 'same command again' } : {})
424
403
 
@@ -430,9 +409,18 @@ async function resolveLogoUri() {
430
409
  if (uri && file) fail('use either --logo-uri or --logo/--logo-file, not both')
431
410
  if (flag('keep-logo-uri') && !uri) fail('--keep-logo-uri only applies together with --logo-uri <URL>')
432
411
  if (uri) {
433
- if (isGangLogo(uri)) {
434
- try { await checkGangLogo(uri) } catch (e) { fail(e.message, retryHint(e)) }
435
- return { logoURI: uri, uploaded: false }
412
+ // a file on gang.fun's IPFS bucket (ipfs://<cid> or its CDN URL) goes on-chain as ipfs://<cid>, once the CDN serves it
413
+ const cid = ipfsCid(uri)
414
+ if (cid || isGangLogo(uri)) {
415
+ try {
416
+ await checkGangLogo(cid ? cdnUrl(cid) : uri)
417
+ } catch (e) {
418
+ // an ipfs:// file that is not on gang.fun may be pinned elsewhere: on-chain as is only when asked
419
+ const elsewhere = isIpfsUri(uri) && !e.retryable
420
+ if (elsewhere && flag('keep-logo-uri')) return { logoURI: uri, uploaded: false }
421
+ fail(e.message, elsewhere ? { hint: 'not on gang.fun: use --logo ./file.png, or --keep-logo-uri if this file stays pinned on IPFS' } : retryHint(e))
422
+ }
423
+ return { logoURI: cid ? ipfsUri(cid) : uri, uploaded: false }
436
424
  }
437
425
  const temp = tempHost(uri)
438
426
  // logoURI can't be changed after create, so an image on another host is copied to gang.fun first
@@ -479,7 +467,7 @@ async function resolveMetaUri({ name, symbol, logoURI }) {
479
467
  const meta = { name, symbol, description, image: logoURI, external_link: links?.website, twitter: links?.twitter }
480
468
  try {
481
469
  const up = await uploadMetadata(meta)
482
- return { metaURI: up.url, metadata: up.metadata }
470
+ return { metaURI: up.uri, metadata: up.metadata }
483
471
  } catch (e) {
484
472
  return fail(e.message, retryHint(e))
485
473
  }
@@ -563,19 +551,28 @@ async function buy(token, bnb, note, followTx, checked = null) {
563
551
  }, note, 'buy', priced))
564
552
  }
565
553
 
554
+ const TAXED_HINT = 'Set the pool tax with --buy-tax / --sell-tax (1–5% each; --buy-tax 1 --sell-tax 1 --dividend 100 is closest to the old 1% taxed track), or leave them out for an untaxed token.'
555
+ const PAD_AGENT_ABI = parseAbi(['function isAgent(address account) view returns (bool)'])
556
+ /**
557
+ * The create gate is GangLaunchpad's (mainnet); the V2 / Soft gate only if the pad can't be read. Same answer today: both
558
+ * pads gate on Agent ID registry 0x60B9…022f (read on chain 2026-10-28).
559
+ */
560
+ const createGateAgent = (me) =>
561
+ client.readContract({ address: padNets('v3').mainnet.pad, abi: PAD_AGENT_ABI, functionName: 'isAgent', args: [me] }).catch(() => isAgent(me))
562
+
566
563
  // commands whose JSON result carries `update`; `watch` streams NDJSON and help is plain text
567
564
  const UPDATE_CHECKED = new Set(['preflight', 'list', 'hot', 'new', 'near-grad', 'notes', 'agents', 'token', 'quote', 'buy',
568
565
  'follow', 'sell', 'upload-logo', 'create', 'execute', 'tx', 'fair', 'v3'])
569
566
 
570
567
  async function main() {
571
568
  if (UPDATE_CHECKED.has(cmd)) update = checkUpdate()
572
- // V2 `gang create` writes the links into the create tx; `gang v3 create` / `gang fair create` into the token metadata
569
+ // `gang create` (= `gang v3 create`) / `gang fair create` write the links into the token metadata
573
570
  const tokenCreate = cmd === 'create' || ((cmd === 'v3' || cmd === 'fair') && args[1] === 'create')
574
571
  for (const name of LINK_FLAGS) {
575
572
  if (!tokenCreate && flag(name)) fail(`--${name} only applies when creating a token (\`gang create\`, \`gang v3 create\`, \`gang fair create\`)`)
576
573
  }
577
- if (flag('description') && !(tokenCreate && cmd !== 'create')) {
578
- fail('--description only applies to `gang v3 create` / `gang fair create` (it goes into the token metadata)')
574
+ if (flag('description') && !tokenCreate) {
575
+ fail('--description only applies when creating a token (`gang create`, `gang v3 create`, `gang fair create`; it goes into the token metadata)')
579
576
  }
580
577
  switch (cmd) {
581
578
  case 'fair':
@@ -595,19 +592,20 @@ async function main() {
595
592
  me = bawAddress(); settings = bawSettings()
596
593
  if (!settings.devMode?.enabled) issues.push({ code: 'DEV_MODE_OFF', fix: 'Enable Developer Mode in Binance App → Agentic Wallet settings.' })
597
594
  if (!settings.tradeAllTokens) issues.push({ code: 'TRADE_ALL_TOKENS_OFF', fix: 'Enable "Trade all tokens" in Binance App → Agentic Wallet settings, otherwise SELLING Gang tokens is blocked by risk control (buying still works).' })
598
- agent = await isAgent(me)
595
+ agent = await createGateAgent(me)
599
596
  if (!agent) issues.push({ code: 'NO_AGENT_ID', fix: 'Claim a .ai Agent ID in Binance Wallet (Agent ID banner). Gang only lets Agent ID holders buy/create.' })
600
597
  }
601
598
  return out({
602
599
  success: issues.length === 0, status, address: me,
603
- launchpad: CONFIG.launchpad, createPad: ADDR.v2, isAgent: agent,
600
+ // `launchpad` = createPad: kept for readers of the old field (it used to be the V2 create target)
601
+ launchpad: padNets('v3').mainnet.pad, createPad: padNets('v3').mainnet.pad, isAgent: agent,
604
602
  devMode: settings?.devMode?.enabled ?? null, tradeAllTokens: settings?.tradeAllTokens ?? null, issues,
605
- note: 'New create → LaunchpadV2 only (~15 BNB graduation). Soft/U/S: existing buy/sell/claim only.',
603
+ note: 'New create → GangLaunchpad (gang create = gang v3 create; gang fair create for 打新). LaunchpadV2 / Soft / U / S take no new tokens: existing buy / sell only.',
606
604
  })
607
605
  }
608
606
  case 'list': {
609
607
  const me = bawStatus() === 'CONNECTED' ? bawAddress() : undefined
610
- return out({ success: true, createPad: ADDR.v2, tokens: await listTokens(me) })
608
+ return out({ success: true, createPad: padNets('v3').mainnet.pad, tokens: await listTokens(me) })
611
609
  }
612
610
  case 'hot':
613
611
  case 'new':
@@ -834,40 +832,14 @@ async function main() {
834
832
  return out({ success: true, ...up, ui: LOGO_UI, next: `gang create --name … --symbol … --logo-uri ${up.logoUrl}` })
835
833
  }
836
834
  case 'create': {
837
- const name = opt('name'), symbol = opt('symbol'); if (!name || !symbol) fail('--name and --symbol required')
838
- // own buy / sell tax and a tax recipient are LaunchpadV3 settings (GangLaunchpad, `gang v3 create`), not V2
839
- const v3Flag = ['buy-tax', 'sell-tax', 'dividend', 'recipient'].find((f) => flag(f))
840
- if (v3Flag) fail(`--${v3Flag} is a LaunchpadV3 setting: use \`gang v3 create …\` (add --testnet for BSC testnet)`)
841
- try { assertCreateTarget(CONFIG.launchpad) } catch (e) { fail(e.message) }
842
- const note = parseNote()
843
- const links = parseLinks()
844
- parseFollow() // refuses --follow: only buys follow a note
845
- // the old 5-arg --pair-or-quote never matched the contract
835
+ // new tokens go to GangLaunchpad (= gang v3 create); LaunchpadV2 takes none, its tokens keep trading (gang buy / sell)
836
+ if (flag('taxed')) fail(`--taxed was LaunchpadV2's tax track, which takes no new tokens. ${TAXED_HINT}`)
846
837
  if (flag('pair-or-quote') || flag('pair')) {
847
838
  fail('--pair-or-quote was removed (CLI ≤0.3.0 encoded a create the contract does not have). Use --quote <ERC-20> --ref-pool <Pancake pair/pool holding it>; omit both for WBNB.')
848
839
  }
849
- const taxed = flag('taxed')
850
- const quote = createAddr('quote'), refPool = createAddr('ref-pool')
851
- const wbnbQuote = [zeroAddress, ADDR.wbnb.toLowerCase()].includes(quote.toLowerCase())
852
- const me = bawAddress()
853
- if (!(await isAgent(me))) fail('this wallet holds no Agent ID (.ai); create is gated', { address: me })
854
- // dry run before any logo upload; logoURI and the links / note suffixes can't change whether create reverts
855
- const dry = await preflightCreate(me, calldata.createV2(name, symbol, '', taxed, quote, refPool), quote, refPool)
856
- if (!dry.ok && dry.reverted) fail(`create would revert: ${dry.error}`, { quote, refPool, hint: CREATE_HINTS[dry.error] })
857
- if (!dry.ok) fail(`could not dry-run create (RPC): ${dry.error}`, { retry: 'same command again; set GANG_RPC_URL if this RPC keeps failing' })
858
- const logo = await resolveLogoUri()
859
- const data = withNote(calldata.createV2(name, symbol, logo.logoURI, taxed, quote, refPool) + linksSuffix(links), note)
860
- const p = bawPreview({ from: me, to: ADDR.v2, data })
861
- return out(summarizeWithNote(p, {
862
- step: 'create', pad: 'v2', launchpad: ADDR.v2, name, symbol,
863
- logoURI: logo.logoURI || '(empty)', uploaded: logo.uploaded, upload: logo.upload,
864
- ...(links && { links: { website: links.website || null, twitter: links.twitter || null } }),
865
- taxed, quote: wbnbQuote ? 'WBNB' : quote,
866
- // the contract ignores refPool for WBNB (Created.refPair = 0x0)
867
- refPool: wbnbQuote && refPool !== zeroAddress ? `${refPool} (ignored for WBNB)` : refPool,
868
- graduationAmount: dry.gradAmt, tokenIfNext: dry.tokenIfNext,
869
- note: 'LaunchpadV2 only. Graduation ≈15 BNB worth of the quote. No creation fee. Soft/U/S create archived. tokenIfNext changes if another create lands first: the real address is the Created event from `gang tx --hash …` after execute.',
870
- }, note, 'launch'))
840
+ return runV3('create', {
841
+ opt, flag, out, fail, summarize: summarizePreview, resolveLogo: resolveLogoUri, checkMeta: parseMeta, resolveMeta: resolveMetaUri,
842
+ })
871
843
  }
872
844
  case 'execute': {
873
845
  const id = opt('request-id'); if (!id) fail('--request-id required')
package/src/links.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Token links (create --website / --twitter): what counts as a stored value and how typed input becomes one.
3
3
  * No imports, so discover.js can check what the API returns without an import cycle (notes.js imports discover.js).
4
- * The calldata codec is in notes.js (linksSuffix / readTokenLinks). Same rules as the server's noteCommit.js.
4
+ * The calldata reader is in notes.js (readTokenLinks). Same rules as the server's noteCommit.js.
5
5
  */
6
6
  const LINKS_MAX = 200 // per field, characters (all ASCII once canonical)
7
7
  // path segments of A-Z a-z 0-9 _ . -, but not "." or ".." (a browser would resolve those away)
package/src/logo.js CHANGED
@@ -9,6 +9,11 @@ const UPLOAD_TIMEOUT_MS = 60_000
9
9
 
10
10
  export const LOGO_API = (process.env.GANG_LOGO_API || 'https://gang.fun/api/logos').replace(/\/+$/, '')
11
11
  export const LOGO_UI = 'https://gang.fun/dev/logo'
12
+ // the upload endpoint stores files in an IPFS bucket as ipfs/<cid>, served by this CDN
13
+ // (where --logo-uri ipfs://<cid> is checked; an upload's own url comes from the endpoint)
14
+ export const CDN_BASE = (process.env.GANG_CDN_BASE || 'https://cdn.gang.fun').replace(/\/+$/, '')
15
+ // CIDv1 (raw, sha2-256, base32) as the upload endpoint makes it
16
+ const CID_RE = /^b[a-z2-7]{58}$/
12
17
 
13
18
  // file hosts that delete uploads after hours or days: a logoURI there turns into a broken image, and it can't be changed on-chain
14
19
  const TEMP_HOSTS = ['litter.catbox.moe', 'tmpfiles.org', '0x0.st', 'uguu.se', 'file.io', 'transfer.sh']
@@ -32,7 +37,35 @@ function sniffMime(buf) {
32
37
  return null
33
38
  }
34
39
 
35
- /** Image bytes → data URL → POST /api/logos → { logoUrl, id, bytes }. */
40
+ /** On-chain form of an uploaded file: ipfs://<cid>, not the CDN URL. */
41
+ export const ipfsUri = (cid) => `ipfs://${cid}`
42
+ export const cdnUrl = (cid) => `${CDN_BASE}/ipfs/${cid}`
43
+
44
+ export const isIpfsUri = (uri) => /^ipfs:\/\//i.test(String(uri || '').trim())
45
+
46
+ /**
47
+ * The cid in ipfs://<cid> (also ipfs://ipfs/<cid>) or <CDN>/ipfs/<cid>, when it is a CIDv1 like ours; else null. A
48
+ * ?query / #hash / trailing slash is dropped: the CDN's ?x-tos-process=… thumbnail still names the original file.
49
+ */
50
+ export function ipfsCid(uri) {
51
+ const s = String(uri || '').trim()
52
+ const cid = s.startsWith(`${CDN_BASE}/ipfs/`) ? s.slice(CDN_BASE.length + 6) : s.replace(/^ipfs:\/\/(ipfs\/)?/i, '')
53
+ if (cid === s) return null
54
+ const bare = cid.replace(/[?#].*$/s, '').replace(/\/+$/, '')
55
+ return CID_RE.test(bare) ? bare : null
56
+ }
57
+
58
+ /** The upload response's cid (or the one in its URL); an upload without one has nothing to put on-chain. */
59
+ function uploadedCid(json, what, status) {
60
+ const cid = CID_RE.test(json?.cid) ? json.cid : ipfsCid(json?.url || json?.logoUrl)
61
+ if (!cid) {
62
+ throw statusError(`${what} upload returned no cid (${status}), so there is no ipfs:// URI to put on-chain; ${LOGO_API} `
63
+ + `must answer like gang.fun's upload endpoint (GANG_LOGO_API): ${JSON.stringify(json).slice(0, 200)}`, status)
64
+ }
65
+ return cid
66
+ }
67
+
68
+ /** Image bytes → data URL → POST /api/logos → { logoUrl: ipfs://<cid>, url: the endpoint's CDN URL, cid, id, bytes }. */
36
69
  async function postLogo(buf, what) {
37
70
  if (buf.length <= 0) throw new Error(`empty image: ${what}`)
38
71
  if (buf.length > MAX_BYTES) throw new Error(`logo too large: ${buf.length} bytes (max ${MAX_BYTES}): ${what}`)
@@ -56,12 +89,13 @@ async function postLogo(buf, what) {
56
89
  if (!res.ok || !json?.ok || !json?.logoUrl) {
57
90
  throw statusError(`logo upload failed (${res.status}): ${json?.error || text.slice(0, 200)}`, res.status)
58
91
  }
59
- return { logoUrl: json.logoUrl, id: json.id, bytes: json.bytes ?? buf.length, api: LOGO_API }
92
+ const cid = uploadedCid(json, 'logo', res.status)
93
+ return { logoUrl: ipfsUri(cid), url: json.url || cdnUrl(cid), cid, id: json.id, bytes: json.bytes ?? buf.length, api: LOGO_API }
60
94
  }
61
95
 
62
96
  /**
63
97
  * Token metadata JSON (the token's contractURI / metaURI, permanent on-chain) → POST /api/logos as application/json →
64
- * { url, id, bytes }. Keys with no value are left out. Same upload endpoint and CDN as logos.
98
+ * { uri: ipfs://<cid>, url: the endpoint's CDN URL, cid, id, bytes }. Keys with no value are left out. Same upload endpoint and CDN as logos.
65
99
  */
66
100
  export async function uploadMetadata(meta) {
67
101
  const clean = Object.fromEntries(Object.entries(meta).filter(([, v]) => v != null && v !== ''))
@@ -81,14 +115,14 @@ export async function uploadMetadata(meta) {
81
115
  }
82
116
  let json
83
117
  try { json = JSON.parse(text) } catch { throw statusError(`metadata upload non-JSON (${res.status}): ${text.slice(0, 200)}`, res.status) }
84
- const url = json?.url || json?.logoUrl
85
- if (!res.ok || !json?.ok || !url) {
118
+ if (!res.ok || !json?.ok || !(json?.url || json?.logoUrl)) {
86
119
  throw statusError(`metadata upload failed (${res.status}): ${json?.error || text.slice(0, 200)}`, res.status)
87
120
  }
88
- return { url, id: json.id, bytes: json.bytes ?? buf.length, metadata: clean }
121
+ const cid = uploadedCid(json, 'metadata', res.status)
122
+ return { uri: ipfsUri(cid), url: json.url || cdnUrl(cid), cid, id: json.id, bytes: json.bytes ?? buf.length, metadata: clean }
89
123
  }
90
124
 
91
- /** Read a local image → POST /api/logos → { logoUrl, id, bytes }. */
125
+ /** Read a local image → POST /api/logos → { logoUrl, url, cid, id, bytes }. */
92
126
  export async function uploadLogoFile(filePath) {
93
127
  const abs = resolve(filePath)
94
128
  const st = statSync(abs)
@@ -97,7 +131,7 @@ export async function uploadLogoFile(filePath) {
97
131
  return postLogo(readFileSync(abs), abs)
98
132
  }
99
133
 
100
- /** True when the URL is already a gang.fun logo (nothing to copy). */
134
+ /** True when the URL is an old gang.fun logo, from before the IPFS bucket (nothing to copy). */
101
135
  export const isGangLogo = (uri) => uri.startsWith(`${LOGO_API}/`)
102
136
 
103
137
  /** Throw unless a gang.fun logo URL serves an image (a typo'd id would go on-chain as a broken logo). Reads headers only. */
@@ -122,7 +156,7 @@ export function tempHost(uri) {
122
156
  return TEMP_HOSTS.find((h) => host === h || host.endsWith(`.${h}`)) || null
123
157
  }
124
158
 
125
- /** Download an image URL (≤512 KB) → POST /api/logos → { logoUrl, id, bytes, source }. */
159
+ /** Download an image URL (≤512 KB) → POST /api/logos → { logoUrl, url, cid, id, bytes, source }. */
126
160
  export async function rehostLogoUrl(uri) {
127
161
  const msg = `could not download logo ${uri}`
128
162
  let res
package/src/notes.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * v1 suffix = "gang" 0x01 keccak256(UTF-8("gang-note:v1\n" salt "\n" conf "\n" text)) (37 bytes)
5
5
  * v2 suffix = "gang" 0x02 uint32be(revealAfter) keccak256(UTF-8("gang-note:v2\n" salt "\n" conf "\n" revealAfter "\n" text)) (41 bytes)
6
6
  * follow ref = "gang" 0x03 followed note tx hash (37 bytes), right before the note suffix (or last without one)
7
- * token links (create) = "gang" 0x04 UTF-8(website "\n" twitter) uint16be(length), same place (see linksSuffix)
7
+ * token links (create) = "gang" 0x04 UTF-8(website "\n" twitter) uint16be(length), same place (see readTokenLinks; written by LaunchpadV2 creates before CLI 0.10)
8
8
  * The text is sent after the tx lands (POST /api/notes), so anyone can check it was written before the trade.
9
9
  * v2 (sealed): the server holds the text and only shows it from blockTime + revealAfter.
10
10
  * Normalization and the commitment format must match the server byte for byte.
@@ -127,16 +127,6 @@ export function readFollowRef(input) {
127
127
  * empty, not both. Only canonical values count (links.js websiteError / twitterError), so other writers can't smuggle odd URLs.
128
128
  */
129
129
 
130
- /** Suffix for canonical links ({ website, twitter }, either may be empty), lower-case hex without 0x; '' for none. */
131
- export function linksSuffix(links) {
132
- const { website = '', twitter = '' } = links ?? {}
133
- if (!website && !twitter) return ''
134
- if (website && websiteError(website)) throw new Error('bad_website')
135
- if (twitter && twitterError(twitter)) throw new Error('bad_twitter')
136
- const hex = toHex(`${website}\n${twitter}`).slice(2)
137
- return `${LINKS_MAGIC}${hex}${(hex.length / 2).toString(16).padStart(4, '0')}`
138
- }
139
-
140
130
  /**
141
131
  * The links a create input carries ({ website, twitter }, each null when absent or not canonical), or null. Only one
142
132
  * place counts: the segment ending right before the input's note suffix, or at its very end when it has none.
package/src/v3.js CHANGED
@@ -3,7 +3,7 @@ import { decimalsOf, defaultRefPool, dividendOf, VAULT_ABI, fundingStep as fund,
3
3
  import { API } from './discover.js'
4
4
  import { UA } from './version.js'
5
5
  import { walletFor } from './fair.js'
6
- import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, notThisKind, padNets } from './gangpad.js'
6
+ import { MERGED_ERRORS_ABI, MERGED_HINTS, NOT_ON_MAINNET, noteFlagError, notThisKind, padNets } from './gangpad.js'
7
7
  import { poolPayBnb, poolTrade } from './pool.js'
8
8
 
9
9
  /**
@@ -211,7 +211,8 @@ const USAGE = 'usage: gang v3 <list|info|status|create|buy|sell|claim|income|rel
211
211
  export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo, checkMeta, resolveMeta }) {
212
212
  if (!SUBS.includes(sub)) return fail(USAGE, { got: sub ?? null })
213
213
  // agent notes are matched to the server's V2 / Soft trades; V3 txs are not matched there yet
214
- if (flag('note') || flag('follow')) return fail('--note / --follow are not supported on gang v3 yet: the server cannot match them to a V3 tx; leave them out')
214
+ const noteError = noteFlagError(flag, 'GangLaunchpad tokens (gang create / gang v3)', 'a GangLaunchpad tx')
215
+ if (noteError) return fail(noteError)
215
216
  const testnet = flag('testnet')
216
217
  const net = testnet ? ' --testnet' : ''
217
218
  const rawOnly = flag('calldata')
@@ -312,17 +313,34 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
312
313
  const name = opt('name'), symbol = opt('symbol')
313
314
  if (!name || !symbol) fail('--name and --symbol are required')
314
315
  checkMeta() // a bad --website / --twitter / --description fails here, before any chain read or upload
315
- if (!flag('buy-tax') || !flag('sell-tax')) fail('--buy-tax and --sell-tax are required (each 1–5, in %; e.g. --buy-tax 2 --sell-tax 2)')
316
- const buyTax = parseNum(fail, 'buy-tax', opt('buy-tax'), 1, 5)
317
- const sellTax = parseNum(fail, 'sell-tax', opt('sell-tax'), 1, 5)
318
- const dividend = parseNum(fail, 'dividend', opt('dividend', '0'), 0, 100)
319
- // everything to holders needs no recipient; otherwise the recipient gets the rest: yourself, or a whitelisted partner
320
- const recipient = dividend === 100 && !flag('recipient') ? zeroAddress : addrArg(opt, fail, 'recipient', 'the tax recipient: your own address, or a whitelisted partner')
316
+ // no --buy-tax / --sell-tax (or both 0): an untaxed token (tax all 0, no dividend, no recipient; the pad never hooks it)
317
+ if (flag('buy-tax') !== flag('sell-tax')) {
318
+ fail('give both --buy-tax and --sell-tax (each 1–5, in %; e.g. --buy-tax 2 --sell-tax 2), or neither for an untaxed token')
319
+ }
320
+ const isZero = (f) => /^0+(\.0+)?$/.test(opt(f) ?? '')
321
+ const zeroTax = isZero('buy-tax') && isZero('sell-tax')
322
+ const taxed = flag('buy-tax') && !zeroTax
323
+ if (!taxed) {
324
+ const f = ['dividend', 'recipient'].find((x) => flag(x))
325
+ if (f) fail(`--${f} only applies to a taxed token: add --buy-tax and --sell-tax (each 1–5, in %), or leave --${f} out for an untaxed token`)
326
+ }
327
+ const taxOf = (f) => {
328
+ const n = Number(opt(f))
329
+ if (!(n >= 1 && n <= 5)) fail(`--${f} must be a number between 1 and 5 (in %); for an untaxed token leave out both tax flags (or give both as 0)`, { got: opt(f) ?? null })
330
+ return n
331
+ }
332
+ const buyTax = taxed ? taxOf('buy-tax') : 0
333
+ const sellTax = taxed ? taxOf('sell-tax') : 0
334
+ const dividend = taxed ? parseNum(fail, 'dividend', opt('dividend', '0'), 0, 100) : 0
335
+ const me = walletFor(ctx, opt('from'))
336
+ // everything to holders needs no recipient; otherwise the recipient gets the rest: --recipient (a whitelisted
337
+ // partner, or yourself), else the creator itself
338
+ const recipient = !taxed || (dividend === 100 && !flag('recipient')) ? zeroAddress
339
+ : flag('recipient') ? addrArg(opt, fail, 'recipient', 'the tax recipient: your own address, or a whitelisted partner') : me
321
340
  const quote = flag('quote') ? opt('quote') : ctx.wbnb
322
341
  const refPool = flag('ref-pool') ? opt('ref-pool') : (defaultRefPool(ctx.chainId, quote) ?? zeroAddress)
323
342
  if (!isAddress(quote ?? '') || !isAddress(refPool ?? '')) fail('--quote / --ref-pool must be 0x addresses')
324
343
  const tax = { buyTaxBps: Math.round(buyTax * 100), sellTaxBps: Math.round(sellTax * 100), dividendBps: Math.round(dividend * 100), recipient }
325
- const me = walletFor(ctx, opt('from'))
326
344
  // the recipient may be the creator itself: checkTaxConfigFor knows who creates (a pad without it: checkTaxConfig)
327
345
  const [ok, reason] = await ctx.read(ctx.pad, PAD_ABI, 'checkTaxConfigFor', [me, tax]).catch((e) => {
328
346
  if (!noSuchFunction(e)) throw e
@@ -356,9 +374,11 @@ export async function runV3(sub, { opt, flag, out, fail, summarize, resolveLogo,
356
374
  ...(refPool !== zeroAddress && quote.toLowerCase() !== String(ctx.wbnb).toLowerCase() && { refPool }),
357
375
  buyTaxPct: buyTax, sellTaxPct: sellTax, dividendPct: dividend,
358
376
  recipient: recipient === zeroAddress ? null : recipient, recipientLabel: selfRecipient ? 'you (the creator)' : label, tokenIfNext: dry.result,
359
- split: `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`
360
- + (dividend > 0 ? '; if no holder has 10,000+ tokens when a tax is paid, the holders\' part goes to the recipient too' : ''),
361
- note: `tax rates and recipient are fixed at create. Curve trades pay only the ${fees.curveFeePct}% fee, no tax: the tax starts in the pool after graduation. tokenIfNext changes if another create lands first: the real address is the Created event from \`gang v3 tx --hash …\`.`,
377
+ split: taxed
378
+ ? `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`
379
+ + (dividend > 0 ? '; if no holder has 10,000+ tokens when a tax is paid, the holders\' part goes to the recipient too' : '')
380
+ : 'untaxed: no tax in the PancakeSwap pool after graduation, no dividend, no recipient',
381
+ 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 v3 tx --hash …\`.`,
362
382
  }, rawOnly, summarize))
363
383
  }
364
384