@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 +25 -25
- package/README.zh-CN.md +20 -20
- package/package.json +2 -2
- package/src/chain.js +11 -59
- package/src/fair.js +3 -2
- package/src/gangpad.js +10 -0
- package/src/index.js +51 -79
- package/src/links.js +1 -1
- package/src/logo.js +43 -9
- package/src/notes.js +1 -11
- package/src/v3.js +32 -12
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`
|
|
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
|
|
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/
|
|
57
|
-
gang create
|
|
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/
|
|
94
|
-
gang create --name "My Agent Coin" --symbol MAC --logo-uri
|
|
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
|
|
99
|
-
x.com / twitter.com link (profile, post, community) and stores it as `https://x.com/…`; `--website` takes an
|
|
100
|
-
(`mycoin.xyz` becomes `https://mycoin.xyz/`; no IP address or port). They go into the
|
|
101
|
-
gang.fun
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
as
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
A buy / sell
|
|
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
|
|
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.
|
|
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
|
|
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` 须为
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
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
|
|
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
|
|
97
|
-
|
|
98
|
-
`--
|
|
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
|
|
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
|
-
|
|
116
|
-
buy / sell
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
4
|
-
"description": "Gang launchpad CLI for Binance Agentic Wallet agents:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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) ·
|
|
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
|
|
223
|
-
gang create
|
|
224
|
-
|
|
225
|
-
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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.
|
|
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
|
-
//
|
|
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') && !
|
|
578
|
-
fail('--description only applies
|
|
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
|
|
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
|
-
|
|
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 →
|
|
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:
|
|
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
|
-
|
|
838
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
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:
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|